
微服务分布式SpringBootVueSpringCloud的超市购物采购库存管理系统——这是一类很典型的“听起来高大上、落地全是坑”的项目我之所以想专门写一篇总结是因为这套系统几乎把微服务开发里最常见的几个硬骨头都踩了一遍服务怎么拆分、远程调用怎么管、库存扣减怎么保证不超卖、分布式事务到底用不用得上以及前端怎么配合后端推着走。说实话外面能找到的教程很多但真正把“超市采购库存管理”这个业务场景和微服务技术结合起来、而且能跑通的完整复盘并不多。如果你是准备做毕业设计、或者想在公司内部搭建一套类似的中小型微服务管理系统这篇内容会非常对口。我先放一个结论这不是一个“必须微服务”的项目但只要你决定用了微服务架构核心难点就不在CRUD上而在拆分边界、数据一致性和库存并发这三件事上。我会按自己实际开发这套系统的顺序来写前半部分讲架构思路中间部分讲核心业务实现后半部分全是踩坑实录尽量让你看完直接能上手复现。1. 项目整体设计与思路拆解1.1 先搞清楚超市采购库存系统为什么要走微服务很多人的第一个疑问是一个超市购物、采购、库存管理系统数据量也不至于到海量并发为什么非要用SpringCloud做大堆微服务我的看法是——不要为了微服务而微服务但如果你想学微服务、或者公司技术栈已经有微服务规划这个业务域是很好的练兵场。拆开标题看这个系统可以分成三个大业务域购物前台销售/购物车/订单处理、采购供应商/采购单/入库审核、库存库存台账/盘点/预警/出库扣减。这三个域天然有数据交互比如购物下单要扣库存库存不足要触发采购建议采购入库又要回写库存数量。这种业务结构在单体时代就是一个大数据库里的几个模块在微服务时代却必须彻底分家否则所谓微服务就是改了个名。我当时给这套系统的定位是“按业务域拆服务按流程拉协作”拆出来的服务清单非常常规但每一处拆分都有明确理由用户/权限服务负责登录、角色权限。这个是几乎所有管理系统的底座独立出来能避免其他服务都来查用户表。商品服务商品基本信息、分类、品牌、上下架、价格策略。商品数据是购物和库存的基础所以它的数据要被多个服务读取。订单/购物车服务前台购物车的增删改查、订单创建、订单状态流转。订单与用户、商品、库存都有耦合是最容易出问题的服务。采购服务供应商管理、采购计划的生成、采购单审核、采购入库单。采购服务和库存服务的交互非常频繁。库存服务商品库存台账、出入库记录、库存锁定与扣减、库存预警、盘点单。这是整个系统的核心也是并发压力最大的服务。网关服务统一入口负责路由转发、鉴权、限流。前端所有请求打到这里再分发到各个业务服务。这一拆就发现了两件很重要的事第一服务拆分后的“分布式查询”才是日常开发的主体工作量。比如前端商品列表页要显示商品名称、库存数量、当前售价这在单体系统里一条SQL关联三张表就出来了微服务里要分别调商品服务、库存服务然后自己组装。你得学会接受这种“慢且繁琐”的组装过程并在设计接口时提前考虑怎么减少跨服务调用的次数。第二拆分后数据一致性责任完全变了。原来的事务由数据库保证现在每个服务有独立的库单据流转、库存扣减这些跨库操作只能靠接口调用来协作一旦某个环节失败你需要自己回答“怎么回滚”或者“怎么补偿”。这是后面分布式事务那一节要解决的重点。1.2 技术选型时的核心考量版本适配比功能多更重要技术栈看起来是不需要思考的后端SpringBootSpringCloud前端Vue数据库MySQL缓存Redis。但真正开始搭工程我发现这套组合最容易出问题的不是业务代码而是版本兼容性。我建议的直接参考搭配后端基础框架SpringBoot 2.7.x SpringCloud Hoxton.SR12或与SpringBoot版本严格配套的Cloud版本注册中心与配置中心Nacos 2.x用作服务发现配置管理可以少部署一个服务服务调用OpenFeign Ribbon负载均衡SpringCloud 2020以后Ribbon被Spring Cloud LoadBalancer替代但用旧版本组合更稳网关SpringCloud Gateway不要用Zuul维护性和性能都差不少缓存与分布式锁Redis 6.x Redisson客户端数据库每个微服务独立库MySQL 8.x消息中间件ActiveMQ如果暂时不想引入Kafka/RabbitMQActiveMQ就足够处理采购入库、库存变更通知这类削峰需求前端Vue 2.x Vue Router Vuex ElementUI请求封装用Axios这么一套配置下来可以保证在Win/Mac本机开发时不会再出现“SpringCloud组件启动秒挂”这种常见问题。因为SpringCloud和SpringBoot的版本绑定关系非常强不同大版本的API差别很大网上很多教程用的是旧版的依赖照抄到高版本项目里就会报各种各样的兼容错误。版本适配最稳的办法就是直接用Spring官方提供的Spring Initializr生成基础工程再对照SpringCloud官方版本说明选择对应的Cloud版本号。版本的坑我后面专门用一节来说这里先给个提醒不要看到新版本就升级SpringCloud这个生态要的是整套兼容不是单个组件最新。2. 微服务架构下的核心实现从拆分到部署2.1 服务清单与数据库边界划分标准微服务的第一原则就是数据独立所以每个服务必须独占自己的数据库或至少独占表。开发中最常见的设计错误就是把所有库表都放在同一个MySQL实例下然后号称“分了微服务”这种做法会让服务间随时可以跨库join等于没有拆分。我当时设计的库表边界如下服务名称独立数据库核心表用户权限服务db_usersys_user、sys_role、sys_permission、sys_user_role商品服务db_productproduct_info、product_category、product_brand、product_sku订单服务db_ordercart_info、order_info、order_item、order_status_log采购服务db_purchasesupplier_info、purchase_order、purchase_order_item、purchase_instock库存服务db_stockstock_info、stock_record、stock_lock、stock_warning、stock_check这里有个非常细节但很关键的问题商品服务有product_info表库存服务有stock_info表两边都有“商品ID”但商品名称、价格等属性只在商品库里。库存服务接收采购入库单时只知道要管哪个商品ID的库存并不需要商品名称所以设计接口时库存服务的入参只需传商品ID、数量、批次等库存域数据避免跨服务拿商品详情。但前端展示库存列表的时候又需要商品名称怎么办我的方案是页面的库存列表接口由网关聚合或前端并行请求库存服务返回“商品ID库存量”前端查完再批量调商品服务获取名称和规格信息。这种“数据冗余不冗余、接口怎么聚合”的设计比代码本身更考验架构功底。2.2 网关路由与统一鉴权怎么把服务串起来SpringCloud Gateway是整个系统对外的唯一入口所有请求统一先到服务网关再路由到对应的微服务。虽然服务间调用用OpenFeign是直接走服务名的但对外暴露的接口必须通过网关转发。好处是你可以把鉴权、日志、限流都集中在网关做而不用每个服务重复实现。网关这块我的配置核心思路有两点第一路由规则要按服务拆分不要按页面拆。比如所有 /api/user/** 转发到用户服务/api/product/** 转发到商品服务/api/stock/** 转发到库存服务。前端封装axios时统一baseURL指向网管地址目录结构按模块分方便维护。第二Token校验别放在网关内部直接查数据库而是用Redis。用户登录成功后用户服务生成JWT并把用户信息写入Redis网关从请求头取出Token先去Redis校验登录态再通过Token里的userId、role信息做路径级权限校验。这样做的好处是网关不依赖用户服务的接口避免了高频鉴权请求全部打到用户服务导致的服务过载。路径权限这块我建议用如下规则接口URL中带上角色前缀或逻辑前缀比如/api/admin/**和/api/stock/**分开网关里配置一个简单的路径-角色映射表。大型项目可以用Shiro或SpringSecurity做成细粒度权限但超市管理系统的角色数量有限Admin、采购员、仓库管理员、收银员用网关层做粗粒度校验、服务内部做细粒度数据隔离就够用了。2.3 前端Vue如何配合微服务的多模块布局前端Vue和微服务后端配合时最大的变化就是页面视角从“单系统”变成了“多模块协作”。前端不再是直接请求业务服务而是统一请求网关。Vue侧应该先把接口封装层做清晰。我在前端做的目录结构参考src/ api/ // 统一接口封装 user.js product.js order.js purchase.js stock.js router/ index.js // 路由配置含动态路由逻辑 permission.js // 路由守卫 store/ modules/ user.js // 用户状态 app.js // 应用状态 views/ login/ dashboard/ product/ purchase/ stock/ order/ utils/ request.js // axios封装带token、错误拦截Vue的路由配置有一个非常值得注意的点动态路由。传统后端管理系统的路由表是写死在Vue代码里的但微服务多模块下用户权限不同你希望登录后只显示他有权限看的菜单和页面所以要用路由守卫动态挂载。我的做法是基础路由只放登录页和404页面其余页面全部在登录成功后根据后端返回的菜单权限数组动态生成路由用router.addRoutes()挂载。菜单权限数组由用户权限服务下发给前端比如仓库管理员返回的菜单只有“库存管理”、“采购入库”、“库存预警”采购员返回“采购管理”、“供应商管理”。这样前端菜单不再是一刀切配合网关的接口权限校验实现了“前端隐藏菜单后端拒绝请求”的双重保障。很多初学者只做前端路由隐藏后端接口默认全开放结果接口被人直接构造请求就能访问这是很严重的安全漏洞。2.4 服务间调用的坑OpenFeign超时与熔断服务间同步调用用的是OpenFeign但用上之后你会发现它没你想的那么省心。最大的坑是默认的超时时间非常短而跨服务调用经常因为网络抖动、数据库慢查询等原因耗时超过默认超时时间结果订单服务调库存服务超时整个下单流程直接失败。我的Feign核心配置经验如下ribbon: ReadTimeout: 5000 ConnectTimeout: 3000这组配置的意思是连接超时3秒读取超时5秒。但更要紧的是给Feign加熔断降级。库存服务如果宕机了订单服务不能一直干等着报错而应该快速失败并给用户提示“库存服务暂不可用”。实现方式是FeignClient的fallback类里返回一个兜底结果比如扣减库存失败时订单直接标记为待重试状态。我建议你在设计接口时把Feign调用模块单独抽出来做一个包把fallback集中管理避免精力分散导致某个服务漏掉降级策略。3. 核心业务实现采购、购物、库存的完整闭环3.1 采购流程设计与数据流超市采购比较好理解库存低于预警值就需要发起采购申请采购员联系供应商下单供应商送货后仓库管理员做入库操作最后库存台账更新。这个流程的难点不在单张表上的CRUD而在跨服务的数据流转。我设计的采购流程大概是采购服务根据库存预警消息生成采购计划 - 采购员提交采购单 - 采购单通过审批后采购服务生成“待入库”状态的采购入库单 - 仓库管理员在采购服务里确认入库采购服务立刻调库存服务的“入库”接口库存服务更新库存台账并记录流水。这里面最需要注意的一点是不要用订单服务和采购服务直接操作库存表所有库存变动必须走库存服务提供的接口。原本可能觉得“反正都是自己项目的代码直接改库存表多简单”但这样会把库存的核心逻辑散落到多个服务中后续排查和涨库时会非常痛苦。3.2 购物下单与库存扣减为什么必须用分布式锁购物下单是库存系统的高频场景。用户在收银台下单时系统需要先锁定库存再创建订单最后扣减库存。如果在高并发下两个用户同时购买了同一个商品就可能导致超卖。传统单体系统用数据库行锁或悲观锁能解决但微服务环境下库存扣减发生在库存服务里订单创建发生在订单服务里任何一方操作数据库都只能锁住自己服务的那张表。这里就是分布式锁登场的地方。我用了Redisson来实现因为Redisson对Redis分布式锁的封装已经很完善天然支持看门狗机制自动续期不需要自己再写复杂的续期逻辑。扣减库存的核心代码片段大概长这样RLock lock redissonClient.getLock(stock:lock: skuId); try { // 尝试获取锁最多等3秒锁过期自动释放时间设30秒 if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 先查库存是否充足 int stock stockMapper.selectStockBySkuId(skuId); if (stock deductNum) { throw new BusinessException(库存不足); } // 扣减库存并更新库存台账 stockMapper.deductStock(skuId, deductNum); stockRecordMapper.insert(new StockRecord(skuId, deductNum, SALE)); } else { throw new BusinessException(系统繁忙); } } finally { lock.unlock(); }我特别想强调几个不能忽略的地方锁的粒度一定要细到SKU级别而不是整个库存服务的锁。如果锁的是全局库存那这个超市的收银系统性能会差到离谱“A商品扣库存”会把“B商品扣库存”也阻塞住。事务必须包含锁内操作。一开始我只把“查询库存”放进了锁里扣减操作在事务里提交结果发现线程A查询库存10件、线程B也查询库存10件然后A扣完B再扣照样超卖。所以查询和更新必须在同一把锁内完成。如果不用Redisson手动写Redis的SETNX要注意设置过期时间否则锁释放不了就死锁了。很多老项目踩过这个坑Java新手尤其容易漏掉。3.3 库存预警与采购建议业务闭环的触发点这套系统里我个人最喜欢的一个功能是“库存预警自动触发采购建议”因为它在微服务架构下把多个服务串了起来充分体现了分布式系统的协作能力。实现思路是这样的库存表里维护每个SKU的库存上限和下限。每天定时任务扫描库存台账凡是库存量低于下限的SKU生成预警记录。库存预警记录发到消息中间件ActiveMQ采购服务监听这个队列自动生成采购计划草稿里面带上商品ID、建议补货数量、当前库存量、建议供应商。采购员在采购模块看到自动生成的采购计划草稿修改确认后提交审批。这个流程的亮点在于库存服务和采购服务之间完全解耦了。库存服务不用关心采购服务怎么处理预警采购服务也不用关心库存服务是怎么判断预警的。以后如果要把ActiveMQ换成Kafka或者RabbitMQ只需要改消费者端上游逻辑一点不用动。使用ActiveMQ时我建议开启手动ACK机制。默认自动ACK下消费者拿到消息但还没处理完程序就崩了消息已经被确认丢失预警就永远没了。手动ACK下处理完业务逻辑再确认消息遇到异常可以不确认或重新入队保证消息不丢。3.4 分布式事务超市系统真的需要Seata吗分布式事务这块我先说结论这套系统我用的是“可靠消息最终一致性”的方案没有引入Seata因为业务场景根本不需要强一致性。很多人一看到跨库操作就条件反射地说“上Seata”但Seata的AT模式会带来明显的性能损耗和复杂度上升如果业务场景能容忍最终一致就不该用它。每一次购物下单的流程都涉及订单库和库存库如果订单创建了但库存扣减失败怎么办我的方案是订单服务先创建订单状态设为“待扣库存”。异步调用库存服务扣库存扣减成功再更新订单状态为“已付款/待发货”。如果扣减失败订单留在“待扣库存”状态通过定时任务扫描并重试扣减。扣减成功的标志是库存记录里生成流水号订单表里也记录这个流水号实现幂等。这就是典型的本地消息表定时重试的最终一致性方案。虽然订单在极端情况下会有短暂的状态不一致可能用户看到“处理中”但在这个系统里完全可以接受因为超市的下单流程本来就不是毫秒级强一致的场景。如果非要用Seata我建议只在采购入库这种强一致的场景下使用比如“更新采购入库单状态增加库存”必须同时成功因为一旦不一致账实就会对不上。但即便用也建议把Seata的全局事务范围控制到最小避免把好几个服务拉进一个大事务里导致锁范围和事务时间都失控。4. 分布式系统下常见问题与排查实录4.1 库存扣减与订单创建的“超级大坑”还不是分布式锁那么简单很多人觉得库存扣减只要加了分布式锁就万事大吉但实际还遇到过一个非常隐蔽的问题下单接口是“先查库存再锁库存再扣库存”但在事务提交之前锁就已经释放会怎样虽然锁是加在了方法上但如果你用的是Transactional放在外层、锁代码放在内层事务还没有提交锁就释放了另一个线程进来读到的是旧库存数据一样会超卖。解决的办法是把事务和锁放到同一个层次里或者用编程式事务手动控制提交点。我的经验是对于库存扣减这种小事务干脆不要依赖注解事务直接用编程式事务监控提交状态确保“锁内查询-扣减-事务提交”一气呵成。还有一个细节如果扣减库存和生成库存流水记录在同一个事务里那你必须在锁里把两个写操作都做完。不要为了缩短锁持有时间只把扣库存放锁里流水记录放到锁外面因为一旦流水插入失败库存扣减已经提交数据就不完整了。4.2 服务调用超时与Feign降级导致的“幽灵订单”我在联调阶段遇到过最诡异的问题用户在前台提交了订单但订单服务显示“扣库存失败订单已取消”可库存服务那边却显示扣了库存。查到最后发现库存服务其实成功扣了库存但响应返回到订单服务时因为网络延迟超过了Feign的读取超时时间订单服务瓜分降级逻辑把订单取消了但此时库存已经扣了。这个问题特别典型直接暴露了同步调用在分布式环境下的不确定性。后来我做了两处处理库存服务的扣减接口改为先更新库存状态扣减成功后直接返回明确结果保持响应速度在可控范围。订单服务收到降级结果后不直接取消订单而是把订单置为“待扣库存待确认”状态通过补偿任务去查库存服务确认实际扣减结果然后根据真实结果流转订单状态。这种“先标记、后确认”的思想比“一刀切失败回滚”要可靠得多因为分布式环境下的失败并不代表真的失败可能只是响应丢失得通过查询和幂等重试来消除不确定性。4.3 并发扣减时数据库行锁与Redis锁叠加性能反而下降我在压测阶段发现库存服务的QPS上不去用参数分析后发现每个请求都要先抢Redis锁抢到锁后进入数据库事务MySQL又会对库存行加行锁。也就是说Redis分布式锁和数据库行锁是双重锁请求量一大Redis锁排队的线程也排队数据库行锁也排队整个扣减接口的吞吐量被拉低了很多。后来我对库存扣减做了优化去掉数据库行锁的依赖把“乐观锁”用起来。也就是在库存表的更新语句里直接加上库存数量条件UPDATE stock_info SET stock stock - #{num} WHERE sku_id #{skuId} AND stock #{num}如果影响行数为0说明库存不足或版本冲突再走分布式锁重试一次。这样的好处是绝大多数请求可以在不排队的情况下直接更新成功只有极少数并发冲突才会用到分布式锁做重试。Redis分布式锁变成了兜底方案而不是每笔扣减都要走这个重量级路径。这个优化让扣减接口的TPS提升了将近一倍顺便也减少了数据库行锁持有时间。想把系统跑顺并发控制一定要分清层级不要把每个请求都往重型锁里塞。4.4 网关内网端口暴露问题一次偶然的“越权访问”事故曾经有一次团队里有人为了图方便把商品服务的端口直接暴露在防火墙规则里然后前端联调时直接访问商品服务IP端口绕过了网关。这么做最直接的影响是网关的鉴权和限流完全失效任何知道这个IP端口的人都能直接访问商品服务内部接口如果商品服务里有一两个“管理端接口”没做好权限校验就很容易出现越权风险。微服务架构下业务服务端口必须只在内网可见对外只能暴露Gateway端口。我在部署时用防火墙或安全组规则限制了各微服务端口的访问来源只允许Servlet容器所在网段互相访问而公网只放行网关端口。这个经验看似基础但在实际开发中很容易被合作同事打破。4.5 常见问题排查速查表这一章节做个小结把我在开发中遇到的最值得记录的固定问题整理成一个速查表现象可能原因排查命令或手段解决方案服务启动后注册不到NacosNacos版本与SpringCloud版本不匹配或配置文件namespace不对查看Nacos控制台服务列表检查bootstrap.yml统一版本配套显式指定namespace和groupFeign调用报Connection refused服务未启动或服务名大小写不敏感或负载均衡未生效curl http://服务名/actuator/health查Feign的日志确认服务健康检查通过配置正确服务名下单总是显示“库存不足”缓存中的库存数据和数据库不一致对比Redis缓存和MySQL库存看是否有订单未回滚缓存删除或主动刷新核对扣减接口的幂等性分布式锁明明加了还是有超卖锁粒度不是SKU级锁未覆盖事务提交抓取日志看锁key分析AOP执行顺序锁key加SKU锁内手动控制事务提交采购预警重复生成消费者收到消息后ACK失败导致消息重复消费看消息日志中是否有重复记录检查消费端幂等消费端加消息唯一键去重采用数据库唯一索引前端菜单不显示权限接口返回的菜单code与前端路由name不一致打印权限接口返回值检查路由表name统一前后端菜单编码规范库存服务一个SQL慢导致整套订单接口慢库存表数据量增大且没有索引同时锁等待严重使用慢SQL日志查看执行计划加索引拆大事务读写分离5. 实操经验从开发到部署的完整过程复盘5.1 本地开发环境搭建的细节开发微服务项目第一步就是搞定本机运行环境。有些细节不处理清楚后面会反复在启动上报错。Nacos必须配持久化不然重启掉配置。我当时因为嫌麻烦直接用了Nacos默认的内嵌数据库结果改了几次配置后服务重启配置文件丢了一部分排查了半天。建议直接把Nacos切换到MySQL持久化。统一用Maven多模块工程。一个父pom管理所有微服务模块每个服务是一个子module公共的entity、dto、utils抽成一个common模块。JAVA开发中Maven的必要性不用多说父工程一定要锁定SpringCloud和SpringBoot版本子模块不要再写死版本不然就等着解决依赖冲突吧。本地联调时Feign调用报DNS错误。 可能是Nacos服务列表里注册的是容器IP或虚拟机IP配置注册IP时要指定为本机局域网IP。这个在application.yml里加一行配置就可以解决。5.2 数据库初始化与初始化数据管理的技巧微服务拆分后数据库初始化变成了一件麻烦事。单体系统一个init.sql搞定全部表微服务就得每个服务建自己的库。我建议使用Flyway来管理每个微服务的数据库脚本每个服务模块的resources/db/migration目录放各自的SQL脚本。这样在新环境部署时直接启动服务就能自动执行建表脚本不用再手工去跑SQL。迁移脚本命名要有纪律性比如V1.0__init_schema.sql、V1.1__add_stock_record_index.sql否则团队协作时会产生大量冲突。5.3 一次全链路联调的大教训服务调用链的日志追踪微服务下查问题非常依赖日志和链路追踪。我第一次联调时因为每个服务打印日志格式不一样用户反馈“提交订单报错”后查了整整半天才定位到是库存服务里一个空指针。后来才意识到担心是不该省的钱必须引入TraceId。做法很简单网关在入口生成一个UUID作为traceId放到请求头里各个服务在拦截器里取出这个traceId然后写入日志的MDC上下文。这样查日志时用同一个traceId一搜就能把整个调用链看全。如果不想引入SkyWalking这类重链路系统至少要把日志格式里的traceId统一做好。后来我发现这种方式对于排查超时、降级、幂等重复这类问题几乎是“救命级”的强烈建议所有人做微服务前就先考虑好。5.4 部署方式选择要Docker吗如果只是本地演示或者毕业设计不部署Docker也可以用java -jar逐个启动也能跑。但如果你想体验真正的分布式部署我建议用Docker Compose把Nacos、Redis、MySQL、ActiveMQ以及各个微服务编排起来在一台机器上模拟多节点。一个需要注意的点是SpringBoot服务镜像的构建要特别注意时区和JVM参数。国内环境下不加时区配置日志时间会差8小时。我在Dockerfile里固定加上ENV TZAsia/Shanghai以及JVM启动参数里设置堆大小和GC参数避免容器默认配置导致OOM。真正的生产部署还会涉及环境变量管理、CI/CD流程但如果你只是做到Docker化这一步已经比90%的课程设计有说服力了。6. 关于这个项目的一些个人体会这个系统开发下来我最大的体会是做微服务项目真正的技术难点不是那些看上去很炫的组件而是“边界”和“状态”这两个极其朴素的词。边界指的是模块怎么分、库表怎么分、哪些数据归哪个服务管状态指的是订单、库存、采购单这些核心数据的流转要设计好在任何步骤失败时都能处理。如果你正在准备做类似项目我建议你先把核心业务用户、商品、订单、库存、采购的建模和状态图画清楚再开始写代码。架构图也好、SpringCloud组件也罢都是你理解了业务之后才能用好的工具而不是反过来让工具拖着业务走。另外记住一点这套系统不一定需要微服务但既然选了微服务就必须把服务间的数据一致性、分布式锁、超时与降级这些真实的工程问题处理好。处理好了它就是一份叫得响的完整项目处理不好它就是一个披着微服务外衣的CRUD系统面试时一问就会露馅。希望这篇博文能帮你少走一些我已经走过的弯路。