ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微服务架构实战:SpringBoot+Vue智慧食堂系统设计与踩坑全记录

微服务架构实战:SpringBoot+Vue智慧食堂系统设计与踩坑全记录 咱们直接聊一个我最近完整走通的项目机关智慧食堂后勤管理系统技术栈锁定在 SpringBoot Vue SpringCloud 微服务分布式这套组合上。这段时间经常有人问我机关单位食堂这种“看似不复杂”的业务到底有没有必要上微服务上了微服务之后分布式事务、分布式锁、文件存储这些坑又该怎么填这篇文章我把整个项目的拆解思路、技术选型理由、核心代码实现以及踩坑记录一次性说清楚希望能给正在做后勤类管理系统或者打算把单体系统升级成微服务架构的朋友提供一份可以直接参考的实操手册。这个系统面向的是机关内部食堂的完整后勤管理场景覆盖员工档案、菜品菜品管理、档口管理、在线订餐、支付对账、食材采购、库存管理、供应商结算、设备报修、数据统计等一整套流程。之所以选择微服务架构是因为这些模块虽然从业务上看都在一个食堂体系里但它们的用户角色、访问频率、扩展方向和对稳定性的要求差异很大。比如菜品信息和点餐服务面向的是全体员工访问量集中在用餐高峰期而供应商管理和采购库存则是后勤部门的工作台数据量不大但对事务一致性要求很高。如果全部塞进一个 SpringBoot 单体应用里后期只要点餐高峰一来整个系统都可能被拖垮。这一点我在项目启动前结合历史单体版本的数据想得很清楚。从开发到上线我前后花了两个多月中间经历了 Nacos 注册不上去、Vue 打包后路由白屏、MinIO 文件上传跨域、Seata 分布式事务回滚失效这类典型问题。为了避免大家在同样的地方浪费时间我会把每一步的关键决策和实验过程都写出来。以下是正文。1. 项目整体设计与微服务拆分思路1.1 为什么机关食堂系统也要拆微服务很多人觉得机关食堂后勤管理就是一个内部系统用户量撑死几百人用单体足够。这个观点在工作量不饱和、并发量很低、业务人员也比较少的场景下是成立的。但我这次接到的需求并不是只做一个点餐系统它要对接食堂台账、供应商、财务报销、库房进出、设备维保还要考虑后续可能的集团食堂统一管理。单体应用最麻烦的地方在于所有模块共用一套数据库、一套缓存、一套定时任务调度器。比如点餐模块需要 Redis 做高峰期的菜品缓存如果菜品管理和员工管理也在同一个进程里那么任何一次的全局缓存清理都会影响到点餐体验。更致命的是部署层面每次改一个菜品上下架的接口都要把整个系统重新编译打包一旦启动失败点餐、支付、财务、库房全链路瘫痪。微服务拆分之后每个服务独立部署、独立扩缩容至少能保证核心业务的故障隔离。我在设计时把系统拆成了七个服务用户认证与权限服务负责员工账号、角色权限、登录态、单点登录。菜品与档口服务菜品的增删改查、上下架、档口分类、营养标签。点餐与活动服务在线订餐、取餐码、智慧餐台、用餐规则。订单与支付服务订单状态流转、支付渠道对接、退款处理。库存与采购服务食材库存、采购单、供应商供货、入库出库。后勤事务服务设备报修、食堂公告、满意度评价、巡检记录。数据聚合服务报表统计、经营分析、大屏展示数据接口。这套拆分不是按照技术功能来切而是按照“业务领域 独立生命周期”来切。比如点餐服务和订单服务虽然调用关系密切但点餐服务是 C 端高频操作订单支付服务则涉及资金安全两者必须隔离便于针对性地做限流和风控。1.2 技术选型为什么不选 Dubbo 而是 SpringCloud这个问题我在做方案设计时被领导专门问过。Dubbo 是高性能 RPC 框架在国内很多传统企业项目中非常常见但它本身的定位更偏向于服务治理和远程调用并没有像 SpringCloud 那样形成一套完整的“全家桶”生态。我们这个项目不仅仅需要服务间调用还需要网关路由、熔断降级、配置中心、分布式事务方案等一堆配套设施。SpringCloud 最大的优势就是与 SpringBoot 的结合足够自然开发人员的学习曲线比较平滑。项目里我选用了 SpringCloud 的 2023 版本搭配 SpringBoot 2.7。这套组合在实际使用中非常稳定。Alibaba 那套组件我用了 Nacos 做注册中心和配置中心OpenFeign 做服务间调用Sentinel 做接口限流和熔断Seata 做分布式事务再配合 MinIO 做文件存储Redis 做缓存和分布式锁。因为项目里所有服务都是 Java 生态RPC 选择了 OpenFeign HTTP 的默认方案并没有引入 Dubbo。通过 Nacos 做服务发现Feign 会根据服务名自动负载均衡到对应实例这套方案在几百 QPS 的压力下表现得完全够用。如果你所在团队对性能有极端要求可以再考虑 Dubbo但这大概率不是后勤管理系统的瓶颈所在。1.3 数据库拆分与分布式事务的取舍微服务拆分之后数据库不能继续放在一个 MySQL 实例里否则服务隔离就没有意义。每个服务拥有自己的独立数据库并通过唯一的服务入口操作自己的数据。比如订单表只由订单服务操作库存表只由库存服务操作用户表和角色表只由认证服务操作。这样拆分后最大的麻烦就是跨服务的数据一致性。以点餐下单为例这个流程涉及订单服务创建订单、库存服务锁定食材库存、用户服务扣减餐补余额三个操作如果分属三个数据库传统数据库事务就完全失效了。我在实际处理时没有把每一步都强制放进 Seata 全局事务里因为食堂点餐的流程并不需要强一致最终一致就能满足业务需求。具体做法是本地事务 消息队列。订单服务在自己的库里写入订单记录并发送一条 MQ 消息库存服务和用户服务各自监听消息并完成本地事务如果任何一个环节失败则通过消息重试和人工补偿机制处理。只有在供应商结算、财务对账这种金额敏感场景才单独启用 Seata 的 AT 模式确保入库、结算、财务入账三个本地事务要么全部提交要么全部回滚。2. 核心业务模块与数据库设计2.1 用户认证服务从单点登录到权限控制机关食堂系统最麻烦的不是点餐功能而是人员身份体系。员工入职、调岗、离职都会影响食堂用餐权限不同部门可能还有不同的补贴标准和用餐时段。我把用户服务做成了完全独立的模块不仅管登录还把组织架构、部门层级、用餐补贴规则全部纳进来。在登录方案上我没有采用传统的 Session 共享而是直接使用 JWT 令牌由网关统一校验并透传用户上下文。JWT 的优势在微服务架构里非常明显服务间调用不需要每次都回查认证中心令牌本身携带用户 ID、角色列表、失效时间接收方解码验证签名即可。为了避免 JWT 泄露带来的风险我设置了较短的过期时间两小时并结合 Redis 做登录态管理用户退出或换设备登录时强制刷新令牌。权限控制方面用的是 RBAC 模型。用户表、角色表、菜单权限表、用户角色关联表、角色权限关联表这五张核心表支撑了整个后台管理端和员工端的权限体系。前端根据用户角色动态生成路由菜单后端接口通过 Spring Security 自定义注解完成细粒度校验比如食堂管理员可以看见“菜品管理”“供应商结算”普通员工只能看见“点餐”“订单记录”“餐补查询”。2.2 菜品与档口服务不只是简单的 CRUD菜品和档口服务看似最简单无非是菜品名称、图片、价格、分类几个字段的增删改查。但在这个项目里它承担着整个智慧食堂的“商品中心”职责所有其他服务的数据都依赖它所以做的时候绝对不能用简单的 CRUD 思路糊弄。我在菜品表里设计了一些关键字段菜品编码、菜品名称、分类 ID、档口 ID、售卖价格、成本价格、营养标签、忌口标签、上下架状态、菜品图片链接、创建时间、更新时间。其中成本价格专门用于财务毛利核算不让普通员工在前端看到。菜品图片采用的是 MinIO 对象存储图片链接保存的是 MinIO 地址前端通过网关代理访问避免把存储节点直接暴露到公网。考虑到菜品的展示需要应对每日菜单变化我将菜品表分成了基础菜品表和每日菜单表两个层次。基础菜品表保存的是食堂做过并有完整资料的菜品每日菜单表则记录某一天在某个档口实际售卖了哪些菜品包含当日售价和售卖份数。这个设计能兼顾重复使用的菜品资料和每天菜单的动态变化还能为后面做“智能备餐量预测”积累数据。2.3 订单与支付服务订单状态机与金额计算订单服务是整个系统里最核心的服务。食堂点餐有两种模式一种是在线预订某个档口套餐另一种是线下刷卡或扫码支付。在线预订流程是这样员工登录系统选择档口选择菜品或者套餐生成订单后从餐补余额中扣款然后生成取餐二维码凭码到窗口核销领餐。为了保证订单状态清晰可靠我在代码里明确实现了一套订单状态机。状态包括待支付、已支付、待取餐、已取餐、已退款、退款中、已完成、已关闭。每一个状态变更都对应一个具体的操作事件禁止跨状态跳转。比如退款操作只允许发生在“已支付”和“待取餐”状态如果订单已经“已取餐”则只能走售后流程。整套状态机用枚举 状态流转表来管理方便记录日志和排查问题。金额计算这块也有不少细节需要注意。订单的金额分为菜品金额、打包费、优惠减免、实际支付金额四个字段。优惠减免可能来自食堂的套餐活动也可能是员工不同等级的补贴。为了方便财务对账订单表还保存了原始流水号和支付平台单号每笔订单在支付成功后都会给支付服务发送一条消息由支付服务异步更新订单状态同时记录资金流水。3. 微服务分布式核心问题与解决方案3.1 分布式 ID订单号为什么不能自增微服务架构下订单表如果继续使用 MySQL 自增主键会引发大问题。多个服务实例同时操作同一个订单表时会生成重复主键订单号还会暴露当天订单量等数据。我在项目里没有用 UUID而是采用了基于 Redis 的号段模式每次从 Redis 领取一批 ID比如一次领取 1000 个号段起始值 100001步长 1000然后在本地内存中依次分配。这样做的原因是UUID 虽然全球唯一但它是无序字符串订单号如果使用 UUID数据库索引的 B 树插入会因为节点分裂而频繁产生碎片数据量大了之后写入性能会明显下降。号段模式生成的订单 ID 是一个长整型数字保证全局唯一、趋势递增、存储高效还能嵌入业务含义比如前两位代表数据中心中间代表业务类型末尾是序列号。3.2 分布式锁库存扣减为什么不能用悲观锁点餐高峰期最大的并发压力来自“售罄”这个操作。假如疫苗菜品只有 100 份但 1000 个员工同时提交订单库存服务必须保证不会超卖。最初我直接用 MySQL 的SELECT ... FOR UPDATE来锁库存也就是悲观锁方案。这么做的确能防止超卖但代价是数据库连接会大量占用一旦有订单事务处理慢连接池很快耗尽反而拖垮整个服务。后来我改成了 Redis 分布式锁加库存预扣方案。下单时库存服务先尝试在 Redis 里扣减一个dish:stock:{dishId}的健值利用 Redis 单线程执行指令的原子性保证并发安全。如果扣减成功再创建订单如果扣减失败直接返回“菜品已售罄”。订单失效或超时取消时再通过 Lua 脚本把库存加回去。分布式锁的实现我选择了 Redisson 框架而不是手动用 setnx 命令。Redisson 封装好了看门狗机制可以自动续期避免业务执行时间过长导致锁提前过期引发重复操作。Redisson 分布式锁的原生代码大概长这样// 使用 Redisson 做分布式锁 RLock lock redissonClient.getLock(dish:stock:lock: dishId); boolean locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (!locked) { throw new BusinessException(系统繁忙请稍后重试); } try { // Redis 原子扣减库存 String script return redis.call(DECRBY, KEYS[1], ARGV[1]); Long result redisTemplate.execute( new DefaultRedisScript(script, Long.class), Arrays.asList(dish:stock: dishId), quantity ); if (result null || result 0) { throw new BusinessException(菜品库存不足); } } finally { lock.unlock(); }用 Redis 做库存扣减真正把数据库的压力降了下来订单高峰期即使有大量并发请求数据库只需要在支付完成后做一个最终扣减即可。这是我在这个项目里做得最值的一个优化点。3.3 分布式事务Seata AT 模式与消息最终一致性虽然前面我说大部分场景使用消息队列做最终一致性但供应商结算这个场景不能这么设计。供应商供货后要生成入库单入库单要生成采购结算单采购结算单要同步财务系统的应付账款这三步如果哪一步失败而前面已经提交财务就会对不上账。因为涉及钱所以这里必须使用分布式事务强一致方案。我在项目里引入了 Seata选择了 AT 模式。AT 模式是 Seata 最省心的方案它通过代理数据源自动记录事务的 undo_log业务代码不需要改 SQL只需要在业务方法上加上GlobalTransactional注解。当全局事务结束时如果有一个参与事务的服务提交失败Seata 会通过 undo_log 把之前其他分支事务的数据反向回滚恢复到操作之前的快照。Seata 的部署本身不算复杂需要启动一个 TC 服务器业务服务接入时只需要配置 Nacos 地址和事务组名。配置好之后我的入库和结算代码大概是这种结构GlobalTransactional(name create-purchase-settle, timeoutMills 30000) public void createSettleOrder(PurchaseOrder order) { // 本地事务 1生成入库记录 stockService.createStockIn(order); // 远程调用库存服务锁定供应商货款 supplierService.lockPayable(order); // 本地事务 2生成财务应付记录 financeService.createPayable(order); }需要特别注意的是AT 模式的事务分支不能嵌套调用多个远程服务时使用普通连接必须让所有参与事务的分支都走 Seata 的代理数据源并保证全局事务 ID 能通过 OpenFeign 的请求头传递下去。我在排查 Seata 回滚失效的问题时就发现是因为 Feign 拦截器没有显式传递TX_XID导致下游服务根本不知道自己在全局事务里。4. SpringCloud 基础设施搭建实战4.1 Nacos 注册中心与配置中心从启动到踩坑Nacos 是整个微服务架构的“地址簿”和“配置中心”所有服务启动后都要在 Nacos 上注册其他服务才能通过服务名找到它。我的部署方式是Nacos 单独部署在一台 4G 内存的服务器上使用 MySQL 持久化存储配置和服务注册信息避免 Nacos 重启后服务列表丢失。SpringBoot 服务接入 Nacos 时我建议直接用最新稳定版的对应的 starter 版本。这里有一个非常重要的坑SpringBoot 2.4 以上版本的配置文件加载方式发生了改动不再默认读取--spring.profiles.active来判断 profile 指定而是必须使用优先配置方式。比如我要给订单服务用 Nacos 中心配置需要在 bootstrap.yml 里设置spring: application: name: order-service cloud: nacos: server-addr: 192.168.1.100:8848 username: nacos password: nacos config: file-extension: yaml namespace: dev group: FOOD_SERVICE shared-configs: ->spring: cloud: gateway: routes: - id: user-web uri: lb://user-service predicates: - Path/api/user/** - id: order-web uri: lb://order-service predicates: - Path/api/order/**网关上的全局过滤器统一处理 JWT 令牌校验。校验通过后过滤器把用户 ID 和角色信息放入请求头再转发给下游服务下游服务直接从请求头里取用户信息不再重复校验。整个链路下来认证服务只负责发放令牌和管理用户状态订单服务、菜品服务等都变成了无状态服务扩展起来很轻松。跨域问题也是在这个层面一次性解决。开发环境前端跑在 8080 端口服务跑在 8081 等端口如果不处理跨域浏览器一定会拦截请求。我在网关加了一个 CorsWebFilter设置允许来源、请求头和方法同时放行预检请求。上线后前后端部署在同一台 Nginx 下其实跨域影响不大但开发阶段没有这一步基本寸步难行。4.3 MinIO 文件存储菜品图片与M3U8视频的私有化存储食堂系统需要处理两类多媒体文件一类是菜品图片另一类厨房操作间的监控视频片段。图片还好说直接传到 MinIO 的某个 bucket 即可但视频我特别说一下。食堂的明厨亮灶监控要求视频可以被授权的前端播放而浏览器原生播放视频文件一直是一个很麻烦的问题特别是监控视频基本都是切片输出的 M3U8 格式。MinIO 存储 M3U8 切片文件十分方便。我把每个片段的 ts 文件和 m3u8 索引文件都放在同一个目录上传完成后生成一个对应的访问地址。前端播放时我先通过网关生成一个带签名和过期时间的播放凭证这个凭证本质上是一个有效期十分钟的 MinIO 直链地址前端拿到地址后交给 hls.js 播放。免安装播放器的方案我试过很多最后还是选了 hls.js它在浏览器上播放 M3U8 的能力非常成熟不需要用户安装任何软件。接入 MinIO 后代码并不复杂核心是用 Java SDK 上传和生成签名 URLAutowired private MinioClient minioClient; public String uploadFile(MultipartFile file, String bucketName) { String fileName UUID.randomUUID() - file.getOriginalFilename(); // 判断 bucket 存在与否 boolean exists minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (!exists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } // 上传文件 minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return fileName; } public String generateSignedUrl(String bucketName, String objectName, int expiresSeconds) { return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(objectName) .expiry(expiresSeconds) .method(Method.GET) .build()); }在接入 MinIO 的过程中我踩过一个大坑网关上配置了全局跨域过滤器后浏览器直接请求 MinIO 签名地址时还是会报跨域错误。原因是签名地址是 MinIO 自己的域名的端口并非网关域名浏览器认为这是跨域请求。解决办法是在 MinIO 服务端设置跨域规则在 MinIO 的 bucket 策略中配置允许的来源和方法。这样前端就可以直接从 MinIO 拉取图片或视频。5. 前端 Vue 应用实现与前后端联调5.1 Vue 项目的工程化配置与目录结构前端部分我采用的是 Vue 3 全家桶配合 Vite 构建工具。虽然 Vue 2 还活在很多老项目里但新项目完全没有必要再启用 Vue 2。Vite 在开发热更新速度上比 Webpack 快得多而且配置相对简单对微服务项目来说能够明显提高联调效率。项目目录结构我划分得很明确。src/api放所有后端接口请求src/router存放动态路由src/store是状态管理src/views是页面组件。每个大模块一个目录比如菜品管理、订单管理、库存管理、报表管理。接口请求统一封装 axios 实例设置请求头带上 JWT 令牌响应拦截器统一解析错误码。后端返回结构我也做了统一约定code、message、data三个字段前端根据code判断是否成功这样对接时不用各写各的。Vue 的安装和环境配置有一个高频问题npm install 时不时会报各种依赖版本冲突。我建议在项目开始时就锁定好 Vue 版本使用npm create vuelatest创建项目它会生成相对合理的默认结构。之后再装element-plus、pinia、vue-router、axios、hls.js等依赖。注意Vite 项目里如果要用 path 别名一定要在vite.config.js里手动配置否则页面里写/api会报路径不存在。import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, server: { port: 8080, proxy: { /api: http://localhost:8088 } } })开发环境中我通过 Vite 的代理配置将/api请求转发到网关地址避免开发时反复处理跨域。生产环境则直接构建静态文件扔给 Nginx 托管。Nginx 上用一条location规则将前端路由交给 Vue 的 history 模式处理将/api路径反向代理到后端网关地址。5.2 动态路由与权限控制后台管理系统有一个很烦的问题不同权限的人登录进去看到的菜单不一样。如果全部用静态路由管理员账号登录时能看到“员工管理”“供应商结算”这些菜单普通员工账号就不应该看见。我采用了动态路由方案登录成功后前端携带用户 token 请求用户服务后端返回当前用户的角色和对应菜单树前端根据菜单树不用刷新浏览器即可动态添加路由。要特别说明的是动态路由不能只靠前端做隐藏。真正安全的后端接口必须校验权限即使前端把按钮隐藏了用户还可以手动输入 URL 访问接口。所以我在后端所有接口上加了权限注解Spring Security 会在接口被调用时校验当前用户角色权限。前端隐藏只是提升体验后端拦截才是安全底线。Vue 动态路由的核心代码思路是利用router.addRoute()把后端返回的菜单数据解析成路由对象然后逐个注册。注册完成后再用router.replace()跳转到默认页。菜单树的数据结构一般是{ id, name, path, component, children }前端读取后递归生成路由数组。5.3 播放 M3U8 免安装播放器与前后端联调细节前面提到了厨房监控播放这部分的联调细节比较多。Vue 页面拿到 M3U8 的签名播放地址后需要立刻初始化 hls.js 播放器。由于 hls.js 运行在浏览器中且是异步加载的脚本所以最好在组件挂载完成后再动态 import避免影响首页加载速度。import Hls from hls.js const playM3u8 (url) { const video document.getElementById(monitor-video) if (Hls.isSupported()) { const hls new Hls() hls.loadSource(url) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () { video.play() }) } else if (video.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 m3u8 video.src url } }前端开发时最常用的联调方法是先把后端所有接口在网关层跑通再把 Vue 前端代理到同一个网关然后逐个页面测试核心流程。遇到请求出错优先看浏览器 Network 面板的响应状态码和响应体。400 多是参数问题401 是令牌失效403 是权限不足502 往往是网关转发的下游服务没起来。我调试时习惯在网关层打开日志输出每个请求转发到了哪个服务这样能快速定位是路由配置错误还是服务实例问题。6. 上线前后必经的排查与调优实录6.1 服务发现失败Nacos 列表为空这个问题在我第一次联调时几乎百分百出现。服务启动类明明写了EnableDiscoveryClient启动日志也没报错但 Nacos 控制台的服务列表里就是看不到服务。排查步骤我建议按这个顺序来先检查 Nacos 控制台是否能正常登录确认自己连的 Nacos 地址端口没错。再看服务启动日志里是否打印了注册地址。如果发现服务注册到了内网 IP外部 Nacos 所在机器访问不了就会注册失败。检查spring.cloud.nacos.discovery.ip是否被错误配置在本地开发时不要手动指定 IP。检查服务启动的依赖是否完整有没有漏掉spring-cloud-starter-alibaba-nacos-discovery。我遇到过最深的一个坑是 Nacos 的命名空间配置问题。创建了 namespace 之后服务也必须配置相同的 namespace ID否则注册到默认 public 空间控制台看不到。这种错很隐蔽因为启动日志不报错但这个经验确实值得记下来。6.2 SpringBoot 与 SpringCloud 版本兼容问题SpringCloud 和 SpringBoot 的版本对应关系非常严格用错了就是各种莫名其妙的问题比如 Feign 接口找不到、服务启动后自动注册失败、配置项不生效等。最保险的做法是去 SpringCloud 官方文档查看版本兼容表我用的一组版本可以直接写成表格组件版本SpringBoot2.7.18SpringCloud2021.0.8SpringCloud Alibaba2021.0.5.0Nacos Server2.2.3Seata Server1.7.0MinIO8.5.7JDK1.8 或 17最开始我曾经把 SpringBoot 升到 3.1结果 SpringCloud Alibaba 对应版本尚未完全适配导致 Nacos 配置和注册一直失败。最后我还是回到 2.7.18这套组合在 2026 年看虽然不算最新但非常成熟有大量生产环境验证适合后勤类管理系统这种求稳的场景。6.3 高峰期超时与数据库连接池耗尽系统第一次压测时我用 JMeter 模拟 100 个用户同时提交订单结果订单服务大量抛 SQL 连接超时异常。原因是订单服务默认的数据库连接池最大只有 20而每个订单事务需要同时操作订单表、订单明细表、流水表一个请求就要占 3 个连接100 并发瞬间把连接池打爆。解决方案有两个方向一是扩大连接池我调整成了 50二是在数据库连接池上加了连接等待时间并增加连接检测。除此之外我把订单创建链路里的一些非核心操作挪出了事务例如发送通知消息在事务提交后通过事件监听再做。只保留必要的数据库写操作事务执行时间大幅缩短连接释放速度也就快了。对热点菜品我把菜品列表和库存剩余做了 Redis 缓存直接避免大量请求穿透到数据库。菜品详情接口的 TTL 设置为 5 分钟菜品上下架时主动删除缓存保证数据一致。压测之后最高吞吐量差不多提升了 3 倍数据库负载也稳定了。6.4 分布式事务回滚失效的排查记录Seata AT 模式回滚失效是我排查过最头疼的一个问题。现象是创建采购结算单的时候供应商锁定应付金额成功但财务服务生成应付记录时报错按理说全局事务应把供应商锁定金额回滚掉结果没有回滚。最终排查下来原因有两个。第一个是全局事务的事务 ID 没有通过 OpenFeign 的请求头传递下去。因为 Seata 是靠RootContext中的XID来串联分支事务的Feign 调用必须给定一个请求拦截器把当前事务的 XID 塞进请求头。第二个是财务服务的数据源没有使用 Seata 的代理数据源我需要显式在配置中使用 SeataDataSource 来包裹业务数据源。修复之后我再做验证用了一个整数类型的字段先故意写入 100再让后续分支抛异常刷新数据库发现字段恢复到了操作前的值。说明 AT 模式的 undo_log 确实正常工作。这里提醒大家排查 Seata 问题一定要开启 TC 的调试日志日志里会详细打印每个分支事务的注册和提交回滚情况比自己瞎猜高效得多。7. 这个项目后续还能怎么扩展按照我个人的经验机关食堂后勤系统做完第一版之后最自然的扩展方向是数据分析和设备联动。我们现在已经接入了明厨亮灶监控设备下一步可以把每个档口的销售数据和监控视频联动起来做一个“食品安全追溯”页面。点开某个可疑的售卖记录可以同时回放对应时段的后厨操作视频。这个需求在其他餐饮系统里也比较普遍技术上只需要完善视频切片的索引时间轴。另一个值得做的方向是智能备餐预测。食堂最头疼的问题是菜做多了浪费做少了不够吃。我们可以根据历史每日菜单的销售数据、季节、节假日、机关人员出勤率来预测第二天的备餐量。数据聚合服务现在已经在同步订单数据到数据仓库后续接一个简单的统计模型比如时间序列预测或者回归模型就能实现这个功能。如果需要把系统推广到多个食堂或其他单位可以在现在的服务基础上增加一个“食堂租户”字段把菜品、订单、库存数据按租户隔离。微服务架构天然适合这种扩展只需要在网关层识别租户并在每个服务的核心表中增加租户 ID就能快速复制一整套智慧食堂系统给其他单位使用。8. 写在最后的私人建议和个人体会做这个项目给我最大的感受是微服务不是银弹。如果团队本身还停留在“会用 SpringBoot 写接口”的阶段一上来就拆十几个服务最后大概率会因为运维复杂度和排查链路过长而崩溃。明智的做法是先梳理清楚业务域想清楚哪些模块真的需要独立部署哪些模块聚合在一起反而更省事。像这个食堂系统拆成七个服务对我来说是合理的因为每个服务都对应了不同角色和业务生命周期扩展和维护都清晰。还有一条建议关于代码规范。微服务项目的时间消耗很大一部分在前后端接口联调上所以接口文档直接落地成代码很重要。我给每个服务都配上了 Knife4j 接口文档后端启动后自动生成文档页面前端开发人员直接在页面上调试接口参数大大减少了沟通成本。哪怕用户不多这个投入也非常值得。最后再说一句如果要用一句话概括这套架构那就是业务模块垂直拆分基础设施全部下沉事务尽力最终一致缓存锁保证核心链路。这个思路放在机关智慧食堂是成熟的放在其他管理系统里也同样能打。希望这篇实操记录对你正在设计的系统有所启发。
返回列表