
准备仿照本地一家市级博物馆的预约加文创商城流程来做项目时我最初的想法特别简单SpringBoot 写 CRUDVue 写页面能跑通预约下单就行。但真正动手之后才发现一旦把“预约放票”“商品库存”“支付回调”这些环节都考虑进去单体应用会越来越臃肿改一个功能要重启一整套服务接口之间也缠在一起。所以这一版我干脆用微服务架构重写以 SpringBoot SpringCloud 作为后端底座Vue 做前端把整个业务按领域拆成独立的服务。用这个项目练手最大的价值在于它不是一个纯展示型的 CRUD 系统预约业务天然有集中放票的流量压力商城业务有订单、库存、支付、退款还涉及用户、展品、文创商品等多个业务域拆成微服务几乎能碰到实际开发中所有常见问题——分布式锁、分布式事务、缓存一致性、服务熔断、接口幂等。这篇文章记录的就是我从单体改造成微服务、最终跑通整套预约商城系统的完整过程以及一步步踩坑后的排错思路。如果你是正面临 Java 毕设选题、或者想从单体过渡到微服务的开发者这篇内容应该能帮你省下不少时间。1. 项目整体定位与架构设计思路1.1 为什么选博物馆预约商城这个场景很多同学做毕设或者练手项目喜欢选“图书借阅系统”“新闻发布系统”这类业务。不是不行但这类系统几乎只有用户的增删改查很难体现微服务的优势反而容易做成“为了微服务而微服务”。博物馆预约商城这个场景的妙处在于它天然就是多业务域协作的形态用户要预约就得有用户认证服务用户要看展品就得有展品与藏品服务预约要限流就得有预约时段库存管理用户还可能顺便买文创产品这就引出商品、订单、支付、库存这一整条商城链路支付完成后还要发短信、发邮件通知这又是一个独立的通知服务。每个业务之间的边界相对清晰数据模型也不交叉拆分起来不会像“订单和支付到底归谁管”那样纠结。更关键的是这个场景里有真实的并发压力。热门博物馆节假日放票往往是几万人在同一秒抢这比一个普通的管理后台更能激发你做分布式设计的动机。你会主动去想库存放在哪里扣同一个用户重复预约怎么办支付回调延迟怎么处理这些问题都来自真实需求不是凭空造出来的。1.2 服务拆分边界怎么定服务拆分这件事我的原则是不要按页面拆要按业务能力拆。也就是说不能因为“预约管理页面”就建一个预约管理服务而是要把“预约”这个业务域完整地放进一个服务里包括预约的创建、取消、查询、库存校验。按页面拆会导致一个服务什么都沾一点服务之间循环依赖严重。我这个项目最终拆成了六个核心服务和两个基础组件服务名职责边界依赖的主要中间件gateway-server统一入口、路由转发、跨域处理、简易鉴权Nacos、Spring Cloud Gatewayuser-server注册登录、用户信息、会员等级、积分MySQL、Redis、JWTexhibit-server展品/藏品信息、展览排期、展品多媒体MySQL、Redis、Elasticsearch可选reserve-server预约时段管理、门票库存、预约单MySQL、Redis、RabbitMQ可选order-server文创商品、购物车、订单、库存扣减MySQL、Redis、Seatapay-server支付单创建、支付回调、退款MySQL、Redis、Seata每个服务独立数据库虽然在开发环境我并没有真的分库部署但表归属严格按服务划分service 之间不直接跨库查询。这是微服务最基本的一条纪律如果需要查另一个服务的数据调接口而不是连它的库。刚开始可能觉得麻烦但到后面你会发现正是这条纪律保证了各服务可以独立开发和独立部署。1.3 一次预约请求的完整调用链用一个具体的场景说明整体架构用户在小程序或者网页端选择“3 月 15 日上午场”的预约票点击提交。浏览器请求先到 gateway-server网关根据路径前缀把请求路由到 reserve-server。reserve-server 收到请求后先校验预约时段是否开放、当日余票是否充足然后调用 user-server 的接口确认用户身份和实名信息因为博物馆预约通常要实名校验通过后扣减 Redis 中的余票计数再向数据库写入预约单最后通过消息队列通知 notify-server 发送预约成功短信。整个过程里服务之间的调用通过 OpenFeign 完成服务实例的地址全部从 Nacos 注册中心动态获取网关本身不感知后端服务部署在哪台机器上。这就实现了最基础的“分布式”后端服务的多个实例可以水平扩展前端只需要面对一个统一的网关入口。2. 技术选型版本组合是最大的坑2.1 SpringBoot 与 SpringCloud 版本对照这个项目里我踩的第一个大坑就是版本兼容问题。微服务项目里SpringBoot、Spring Cloud、Spring Cloud Alibaba 三者有严格的版本对应关系随意组合轻则启动报错重则某些组件静默失效查起来极其痛苦。我最终锁定的版本组合如下组件版本说明JDK1.8或 11不要轻易上 17部分 Alibaba 组件适配有坑Spring Boot2.7.x2.x 生态最成熟资料最多Spring Cloud2021.0.x对应 Spring Boot 2.7 版本线Spring Cloud Alibaba2021.0.x配套版本包含 Nacos、Sentinel、SeataNacos2.2.x注册中心和配置中心注意与客户端版本一致Vue2.7 ElementUI前后端分离Vue 3 Element Plus 也可以为什么不选 SpringBoot 3.x因为 3.x 强制 JDK17Spring Cloud 版本也需要跳到 2022.0.x 以上很多第三方 starter 还没完全跟上。如果你只是做项目而不是生产环境验证新特性没必要拿自己练手的项目去试生态兼容性。SprinBoot 版本太高遇到的问题往往比解决的问题多这一点后面第 6 章会详细说。2.2 微服务核心组件选型注册中心和配置中心用 Nacos这是目前最主流的选择。Nacos 相比 Eureka 的优势在于它同时把“注册中心”和“配置中心”两件事都干了我们不再需要额外部署 Spring Cloud Config。对于开发环境Nacos 还是一个独立进程启动后浏览器访问 8848 端口就能看到控制台非常直观。网关我选了 Spring Cloud Gateway而不是 Zuul。Gateway 基于 WebFlux 响应式模型性能好而且天然集成 Spring Cloud 的熔断和限流配置。虽然大多数人用 Gateway 只做路由转发但它在项目里还可以统一做跨域配置和账号状态校验避免每个后端服务都处理一次 CORS。远程调用用 OpenFeign它内部集成了 Ribbon 负载均衡配合 Nacos 注册中心调用方只需要声明一个接口就能像调本地方法一样调用远程服务。对于初学者OpenFeign 可能是上手门槛最低的 RPC 工具。限流和熔断我接入了 Sentinel。预约放票那一刻流量会瞬间打上来如果不对接口做限流一个瞬间的峰值就可能把 MySQL 连接池打满。Sentinel 的 QPS 限流按服务维度配置控制台上能实时看到通过 QPS 和被拦截的请求数对理解“高并发保护”非常直观。分布式事务组件选用 Seata这个是第 5 章的重点这里先提一句不要一开始就把 Seata 引进来等单体架构跑通、服务拆分稳定之后再加否则你分不清问题是出在业务代码还是出在事务拦截器上。2.3 Vue 前端环境与依赖配置Vue 部分我用了 Vue 2.7 配合 ElementUI如果你更想用新语法直接上 Vue 3.4 Vite Element Plus Pinia差别主要在于组合式 API 和状态管理库。注意 Vite 和 Vue CLI 的启动方式不一样别对着 Vue CLI 的文档跑 Vite 项目。前端环境配置有几个高频坑npm 安装依赖极慢先设置国内镜像源node-sass 和 sass-loader 版本与 Node 版本强绑定Node 18 以上建议直接用 sassdart-sass。如果是做视频流相关的展厅直播功能Vue 播放 m3u8 视频流推荐使用 video.js 配合 videojs-contrib-hls不需要额外安装原生播放器插件。如果你需要在页面上直接展示展品介绍 PDF最简单的方案是用 iframe 直接指向 PDF 文件地址但如果涉及带签名的私有文件就需要用 pdf.js 来解析和渲染具体做法在第 4 章说。3. 数据库设计与核心业务实现3.1 预约业务的表设计与限流思路预约业务是整项目的核心。博物馆预约有一个典型特征同一时段可预约数量有限而且用户通常要实名登记。所以预约表的设计要考虑的唯一性约束不是主键而是“用户 参观日期”的组合约束防止同一用户重复预约同一天。预约单表我用近似这样的结构CREATE TABLE reserve_order ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, exhibit_id BIGINT NOT NULL, visit_date DATE NOT NULL, time_slot TINYINT NOT NULL COMMENT 1上午场 2下午场 3夜场, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已预约 2已取消 3已参观, ticket_count INT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL, UNIQUE KEY uk_user_visit (user_id, visit_date) );注意这里的主键 id 不是自增而是由后端生成的分布式 ID。因为服务拆分之后每个库都有自己的自增序列跨服务合并数据时会冲突所以主键统一用 MyBatis-Plus 的 ASSIGN_ID 策略底层是雪花算法。关于分布式 ID 的细节在第 3.3 节展开。限流思路分两层数据库层通过唯一索引保证同一用户不重复预约内存层用 Redis 保存每个时段的可预约余量。用户提交预约时先走 Redis 预扣扣减成功再创建订单。如果订单创建失败再把额度回补。这套“预扣-确认-回滚”的流程和商城库存扣减本质是一样的在第 3.2 节里一起讲。3.2 商城库存扣减如何避免超卖商城模块的库存扣减是另一个容易出问题的点。我一开始用数据库的乐观锁update stock set count count - 1 where id ? and count 0这个写法在单体应用里完全没问题。但微服务场景下用户下单要走 order-server库存服务如果独立拆分两次数据库操作之间会有网络调用间隔并发场景下就会出现超额扣减。项目里最终方案是 Redis 预扣 数据库落单。具体逻辑是用户提交订单时先用 Lua 脚本原子地扣减 Redis 中的库存计数扣减成功后才调用数据库创建订单支付超时或者用户取消再异步回补 Redis 库存。-- 扣减库存 Lua 脚本 if redis.call(get, KEYS[1]) tonumber(ARGV[1]) then return redis.call(decrby, KEYS[1], ARGV[1]) end return -1这种方案的好处是 Redis 单线程执行 Lua 脚本扣减操作天然原子不依赖分布式锁。库存的最终一致性由“订单状态”驱动订单支付成功后数据库库存才真正扣减订单超时关闭Redis 库存回补。短期不一致是可以接受的因为用户能看到的“有票”只是可下单的凭据真正占住名额的是订单不是 Redis 中的数字。3.3 分布式ID与MyBatis-Plus集成前面说过主键不用自增。分布式场景下多个服务同时写自己的库如果都用自增 ID未来数据汇聚、联调排查、做分库分表都会很难受。所以主键统一用雪花算法生成的趋势递增长整型。MyBatis-Plus 集成非常简单在实体主键上加注解即可TableId(value id, type IdType.ASSIGN_ID) private Long id;这会使用 MyBatis-Plus 内置的雪花算法生成 ID。唯一要注意的是雪花算法依赖机器时间如果服务器时钟发生回拨可能出现 ID 重复。开发环境不受影响生产环境需要确保 NTP 时间同步。项目里你可以把时钟回拨做一次简易兜底生成 ID 前记录上一次的时间戳如果当前时间小于上次时间就 sleep 几毫秒等待时钟修正后再生成。3.4 接口幂等与前端防重预约系统和支付系统对幂等要求很高。用户快速双击“提交预约”按钮网络超时后重试同一笔支付回调这些场景如果后端不做幂等就会生成重复订单或者重复扣款。后端幂等方案我用了 Redis 幂等令牌用户进入预约页面时后端生成一个唯一 token 存在 Redis 并返回前端提交预约时必须携带这个 token后端处理请求时先尝试删除 Redis 中的 token删除成功说明这是第一次请求可以继续执行删除失败说明请求重复直接返回“请勿重复提交”。Redis 单线程删除的原子性保证了同一 token 只会被处理一次。支付回调的幂等更简单支付服务通过 out_trade_no商户订单号去查支付单状态如果已经处于“支付成功”状态直接返回成功响应不重复触发后续流程。不要小看这一点银行和支付平台的回调在弱网环境下的重试频率非常高没有幂等保护你会看到通知短信被发好几遍的“奇观”。4. 前端Vue的关键实现4.1 axios封装与登录状态管理前端如果每个页面都自己写 axios 请求到后面必然是一团乱麻。项目里我单独封装了一个 request.js创建 axios 实例设置 baseURL 指向网关地址然后在请求拦截器里统一从 localStorage 取 token 加在请求头里在响应拦截器里统一处理 401 跳转登录、500 弹出错误提示。// request.js 核心片段 const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { router.push(/login) } return Promise.reject(error) } )开发环境下 baseURL 配成/api利用 Vue 开发服务器的 proxy 把请求转发到后端网关这样就不需要后端处理跨域。生产环境用 Nginx 把/api反向代理到网关前后端完全隔离。注意不要在前端直接写后端服务的 IP 和端口否则每次部署位置变化都要重新打包前端。4.2 路由映射与权限控制博物馆预约系统里有普通用户和管理员两种角色。普通用户看展览预约和商城管理员看后台管理页面。如果所有页面都一起打包进前端路由任何懂前端的人都能从代码里看到后台接口地址所以路由必须做权限控制。我采用的是动态路由方案用户登录后后端返回该用户可访问的菜单列表前端拿到菜单后通过 router.addRoute 动态添加路由。未登录用户只保留登录页和公开页面的路由。路由守卫里同时判断 token 是否存在router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { next() } })按钮级别的权限用自定义指令实现比如删除商品按钮加一条v-permissionorder:delete没有该权限的用户在当前组件里就看不到这个按钮。这种前端控制只是体验层面的优化真正的接口权限还是要靠后端校验前端权限无论如何都不能作为安全边界。4.3 展品详情中的PDF与视频流处理展品详情页会遇到两类多媒体展示需求一是展品的简介 PDF 文件二是展厅慢直播的视频流。PDF 展示最简单可靠的办法是把文件放到对象存储或者服务器静态目录然后前端直接用 iframe 指向文件 URL浏览器原生支持 PDF 预览。但如果文件是私有资源、带访问签名就不能直接暴露 URL这时用 pdf.js 加载二进制流渲染。项目里我把两种方式都做了封装优先用 iframe 直链私有文件走 pdf.js。视频流这里我需要提醒一句Vue 播放 m3u8 视频流如果你用原生 video 标签去指向 m3u8 地址在 Chrome 上是播放不了的因为浏览器原生不支持 HLS 流。需要引入 hls.js或者用 video.js 配合 videojs-contrib-hls。我选择的是 hls.js安装后用十几行代码就能跑通import Hls from hls.js const video document.getElementById(liveVideo) if (Hls.isSupported()) { const hls new Hls() hls.loadSource(https://your-cdn/live/stream.m3u8) hls.attachMedia(video) }4.4 前端打包并集成到SpringBoot项目开发完成后如果只想把前端塞进后端一起发布可以执行npm run build得到 dist 目录然后把静态文件复制到某个服务的src/main/resources/static下。这样确实省事但要注意两个问题。第一个是路由模式Vue 如果用 history 模式刷新一个非根路径时会请求后端接口而后端没有对应接口就返回 404。需要在后端加一个 fallback 控制器把所有非 API 请求转发到 index.html。开发时这个坑特别隐蔽因为前端开发服务器自己会处理 history fallback你不会察觉部署后才暴露。第二个问题是资源路径Vue 打包出来的静态资源默认使用绝对路径/assets/xxx.js如果后端服务的 context-path 不是根路径资源就会加载失败。要么把 publicPath 配成相对路径./要么让网关层统一转发静态资源。如果你有条件我建议不要用 Java 后端托管前端静态文件而是独立部署一个 Nginx。前后端分离的项目Nginx 处理静态资源效率更高同时可以统一做 gzip、缓存控制、反向代理。把后端托管作为快速演示方案就好。5. 分布式场景下的核心问题5.1 分布式事务订单、支付与库存的一致性单体应用里一个事务可以同时操作订单表、库存表、支付流水表要么全部成功要么全部回滚。但微服务拆开后订单数据在 order-server库存数据在 reserve-server支付数据在 pay-server三个库之间没有本地事务可言。在 Seata 的 AT 模式里发起方方法上加上GlobalTransactional注解Seata 会拦截所有参与分支事务的数据源操作自动记录数据快照和 undo_log。如果后续某个分支事务失败Seata 会根据 undo_log 逆向回滚之前已提交的数据。使用起来非常方便几乎是“零侵入”。GlobalTransactional(name create-order-tx, timeoutMills 30000) public void createOrderAndDeductStock(OrderDTO dto) { // 这里会调用 order-service 写订单 // 调用 reserve-service 扣库存 // 调用 pay-service 创建支付单 // 任何一个子调用异常前面已经提交的本地事务都会被自动回滚 }但要清醒地认识到AT 模式适合并发量不高的业务场景它通过锁和日志换一致性性能开销不小。对于预约这种高并发抢票场景更合理的设计是先扣 Redis 库存、异步建单、支付成功后再异步扣减数据库库存把一致性从“强一致”降级为“最终一致”。项目里我把 Seata 用在订单创建和支付回调的低并发环节把 Redis 预扣放在高并发入口这个组合是我实测下来比较稳妥的分层方案。5.2 分布式锁Redis锁的正确写法微服务每个服务都可以多实例部署比如 order-server 起了两个实例同一个用户同时提交两笔订单时两个实例各自执行代码如果有共享资源需要互斥就必须引入分布式锁。分布式锁最经典的实现是 Redis 的SET key value NX EX seconds但如果你自己封装要注意两个细节一是 value 要唯一释放锁时只能释放自己加的锁二是释放锁要用 Lua 脚本原子完成“检查-删除”避免先 get 再 del 之间的并发窗口。项目里我用的 Redisson它封装的RLock天然支持看门狗自动续期业务执行时间长于锁有效期时锁不会因为超时被提前释放。但 Redisson 默认锁等待时间是 30 秒如果业务执行时间确实长可以在加锁时显式指定 leaseTime避免锁无限续期导致其他请求长时间阻塞。RLock lock redissonClient.getLock(reserve:user: userId); boolean locked lock.tryLock(3, 10, TimeUnit.SECONDS); if (locked) { try { doReserve(); } finally { lock.unlock(); } }这里最核心的心得是锁粒度要小。不要对整个“创建预约”方法加锁要只对真正需要互斥的资源加锁比如“某个用户同一时段”加锁或“某个展览的库存”加锁。锁范围越大系统吞吐越差分布式锁也就从保护机制变成了性能瓶颈。5.3 缓存一致性展品信息缓存更新展品信息、展览排期这类读多写少的数据一定需要加缓存。我在 exhibit-server 里用 Redis 缓存展品详情key 类似exhibit:detail:{id}value 是 JSON。问题在于运营人员修改展品介绍后缓存如何更新。最简单的策略是“更新数据库后删除缓存”而不是“更新数据库后更新缓存”。因为更新缓存涉及并发写很容易出现旧缓存覆盖新缓存的问题删除缓存则让下一次读取时回源数据库并重建缓存实现简单也不容易出现数据不一致。但删除缓存也有一个经典的并发问题一个线程更新数据库后删除缓存另一个线程在删除前读到了旧缓存并准备回写刚好覆盖了新写的数据。所以项目里我用了延迟双删策略更新数据库后立即删一次缓存然后等 500 毫秒再删一次。第二次删除把并发线程回写的旧值清掉最终下一次读取会拿到最新数据。这个延迟时间要大于一次业务查询的耗时实测 500ms 在大多数场景是够用的。5.4 服务容错与降级微服务链路很长任何一个节点出问题都可能拖垮整个调用链。预约高峰时段如果展品服务因为慢 SQL 响应变慢预约服务调用它时 Feign 默认的 1 秒超时就会大量报错进而占用预约服务的线程池。所以每个 Feign 调用都要配置超时和降级。超时时间不要设太短也不要太长内部接口 3 秒左右比较合适降级逻辑返回一个稳定的默认值或友好提示而不是把异常直接抛给用户。Sentinel 的熔断规则可以按接口的异常比例或慢调用比例触发熔断后请求直接走降级方法让后端服务有时间恢复。我在预约场景里对两个非核心功能做了降级展品 3D 模型加载和展厅直播流。这两个功能流量大但对主流程预约下单没有直接影响放票高峰期把它们的 Sentinel 阈值调低可以保证预约接口的稳定。这个取舍在单体架构里很难做但微服务架构天然支持按服务维度独立控制这也是微服务架构对比单体的一个真实优势。6. 常见问题与排查技巧实录6.1 启动类问题速查表我项目排错过程中遇到的启动问题基本都能归到下面几类整理成速查表供直接对照现象常见原因处理方式Nacos 控制台访问不了8848 端口被占用或未放行netstat -ano查端口占用改 Nacos 配置或释放端口服务启动后注册不上 Nacos客户端版本与 Nacos 服务端版本不一致统一使用 Spring Cloud Alibaba 2021.0.x 对应的 nacos-client 版本启动报 NoSuchMethodErrorSpringBoot 与 Cloud 版本不匹配按第 2.1 节版本表对齐 pom 依赖Feign 调用一直超时服务在 Nacos 中注册的 IP 是内网不可达地址检查 Nacos 所在 host 配置微服务注册使用真实 IPspring.cloud.nacos.discovery.ip可手动指定数据库连接池被占满高峰期请求量过大存在慢 SQL给核心接口加 Sentinel 限流加索引排查慢查询日志Redis 内存不断增长缓存 key 没有设置过期时间或过期策略有误检查缓存写入逻辑统一设置 TTL必要时用 SCAN 扫描大 key6.2 服务间调用与IDEA配置技巧用 IDEA 同时启动多个服务时会有不少小问题。多模块项目里多个 SpringBoot 默认端口都是 8080如果不改端口后启动的服务会因为端口占用直接退出。我建议每个服务在application.yml里显式设置不同端口server: port: 8801 # user-serverIDEA 里 Run Configuration 很强大但你不需要给每个服务都新建一份启动配置直接在服务模块的 Application 类右键 Run 就行关键是确认每个服务的spring.profiles.active正确加载了对应环境的配置。如果你要把启动参数传给 SpringBoot比如临时改端口在 IDEA 的 VM options 里加-Dserver.port8802如果希望端口随机分配比如本地起多个实例调试负载均衡可以设置server.port0SpringBoot 会自动找一个空闲端口然后在 Nacos 控制台观察服务实例注册情况。这是验证负载均衡最简单的方法我在本地起了两个 user-server 实例通过网关连续请求几次能看到请求被分发到不同端口实例上。6.3 前端跨域与联调问题前后端联调阶段跨域是必踩的坑。开发环境我推荐用 Vue 的 devServer proxy 解决因为它完全不需要后端配合还不会导致生产环境遗留额外的跨域代码// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8800, // 网关地址 changeOrigin: true, pathRewrite: { ^/api: } } } } }生产环境如果你用 Nginx做一层反向代理同样不需要后端开 CORS。只有在“前端静态文件在 CDN、后端接口在另一个域名”这种场景下才需要后端网关统一配置 CORS。注意网关层面配置 CORS 时一旦透传头重复比如你又在某个微服务里加了 CORS 配置浏览器会报“Multiple CORS header”错误这个排查起来很容易懵。6.4 三条避坑建议最后分享三条项目过程中沉淀下来的实操经验也是我如果再做一个微服务项目会立刻执行的三件事。第一先跑通骨架再加业务。不要一上来就写预约下单的逻辑。先把六个基础模块建好、Nacos 启动、网关连上、一个最简单的 user 查询接口调通把整条链路走通后再往里面填业务。微服务项目的大部分复杂度都集中在“通信链路”上业务逻辑反而是相对简单的部分。骨架通了后面就是往流水线上加零件。第二依赖版本要锁死。整个项目的 pom 里不要出现一个依赖多个版本的情况特别是 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者一定要用官方版本矩阵里对应的组合。最好在建项目初期就把 parent 的依赖管理理顺后期不要轻易升级。这个项目的成功一半功劳要给稳定的版本组合。第三日志要先规划好。每个服务都要配置独立的日志文件并且日志中要带上 traceId这样一次请求经过多个服务时你可以根据同一个 traceId 把所有日志串起来看。没有 traceId微服务里排查一个问题要在好几个服务之间来回翻日志效率低到怀疑人生。哪怕你不用专门的链路追踪框架也要在网关过滤器中生成一个 UUID放入请求头透传给各个服务在日志 pattern 中打印出来。做完这个项目我的个人体会是微服务本身不难难的是在拆分边界和一致性方案上做权衡。预约商城这个题材刚好能逼你面对这些权衡——你是选择强一致还是最终一致是把库存放在 Redis 还是 MySQL是同步调服务还是异步走消息队列每个决定背后都有真实的业务代价。把这些想清楚比敲一万行业务代码更有成长。也建议你做完后再把 Sentinwel 限流规则细化、把 Seata 换成 TCC 模式、或者用 Docker 把整套服务编排起来这些方向的扩展会让这个项目从“毕业设计”变成真正能说自己“懂微服务”的验证作品。