
看到“谷粒商城”这四个字很多人都不会陌生。这个项目在Java后端学习圈子里几乎是微服务架构入门必刷的一个实战项目。而整个谷粒商城里面含金量最高、面试被问得最多的模块就是下单这条链路。原因很简单下单不是简单地往数据库里插一条订单记录而是牵一发动全身——要锁库存、扣积分、核销优惠券、生成支付单。如果在单体应用里这些操作可以用一个数据库事务搞定但放到微服务架构下订单库、库存库、会员库被拆成了独立的数据源本地事务彻底失效这时候怎么保证数据不出乱子就成了整个项目最核心的技术难点。这篇博客就围绕谷粒商城的分布式事务与下单展开把我自己在做这个项目时踩过的坑、捋清楚的原理以及最终落地的一套方案全部整理出来。不管你是刚开始学微服务的小白还是准备面试冲刺的选手这篇文章都能帮你在“订单与库存分布式事务”这个点上建立起一套完整且能落地的心智模型。1. 下单场景下的分布式事务为什么一个小小点击会引发数据一致性危机1.1 一次下单操作背后到底调用了多少个服务谷粒商城是标准的Spring Cloud Alibaba微服务电商项目拆分的服务非常多。以用户点击“提交订单”这个动作为例后端真正参与的流程远比表面上复杂得多。一个典型的谷粒商城下单流程是这样的用户在购物车界面勾选商品点击结算前端会带着选中的商品列表、收货地址、支付方式等信息请求订单服务的创建订单接口。订单服务收到请求后第一件事是查询商品服务确认商品是否还在售、价格是否变动接着调用库存服务锁定商品的库存同时调用会员服务扣减用户的积分或余额还要调用优惠券服务将用户使用的优惠券标记为已核销。等这些步骤都执行完了订单服务才在自己的订单库里生成订单记录和订单项最后再发一条消息给支付服务让用户可以继续完成支付。看到没有一次下单操作至少要跨订单、库存、会员、优惠券、商品等好几个微服务。而且这些服务的数据存储是完全独立的订单数据落在订单库库存数据落在库存库会员积分又在会员库。这就引出了分布式事务最本质的矛盾每个服务都有自己独立的本地事务但业务上又要求这些分散在不同库里的数据变更要么全部成功要么全部失败。1.2 为什么本地事务管不住跨库的账很多刚接触微服务的同学会有一个疑惑我在订单服务的方法上加了 Transactional 注解为什么库存服务扣库存失败后订单还能创建成功原因很简单Transactional 的底层是Spring管理的数据源事务而每个微服务的数据源是独立的。订单服务的 Transactional 只能保证订单库里的那几条SQL在一个事务里执行它管不了库存服务那边的事务提交或回滚。换句话说当订单服务调用库存服务的Feign接口时库存服务自己开启了一个JDBC事务这个事务和订单服务的事务是两回事。哪怕库存服务扣减失败抛了异常这个异常传递给订单服务之后订单服务回滚的也只有自己数据库里的事务库存服务那边如果已经提交了数据就变不回来了。我在最初做谷粒商城的时候就实实在在踩过这个坑。测试的时候先创建订单再锁库存后来改了逻辑先锁库存再创建订单想着这样能安全一点结果发现锁库存成功之后、订单创建报错库存依然被扣掉了。用户那边显示下单失败但仓库里少了一件商品后台库存数据就和实际对不上了。那一刻我才真正理解分布式事务不是一个技术名词它是一个随时可能把线上数据搞崩的现实问题。1.3 分布式事务要解决的一致性问题到底有哪些分布式环境下数据不一致主要出现在几个环节。第一种是服务之间调用链路过长中间某个服务挂了导致后续服务没有被调用前面的操作却已经提交。第二种是服务之间调用成功但返回结果因为网络超时没有送达调用方以为失败实际上数据已经变了。第三种是多个服务各自提交了本地事务但其中某一个服务在最后阶段回滚失败造成整体数据不完整。这三种问题在谷粒商城下单场景里都能找到对应的位置。锁库存接口等待超时但库存服务实际扣减成功了订单创建成功但锁库存失败需要把订单数据撤销优惠券核销了但订单最终没支付优惠券状态错乱。这些情况如果不处理用户端看起来可能只是一个模糊的“下单失败”但后台的数据逻辑已经完全乱了套。所以下单场景必须引入分布式事务来保证跨服务的数据最终一致性甚至在某些节点要做到强一致性。2. 分布式事务主流方案横向对比为什么谷粒商城最终选择了Seata2.1 2PC和TCC方案的特点与局限分布式事务不是一个新问题行业里早有各种解决方案。先说说最经典的2PC两阶段提交协议也就是XA事务。2PC的核心思想是引入一个事务协调者让所有参与者在第一阶段先做预处理并锁住资源第二阶段再由协调者统一决定提交还是回滚。这个方案的优点是强一致性有保障但缺点非常致命第一阶段要锁定资源整个事务执行期间数据库的吞吐量会大幅下降一旦某个参与者挂了协调者要一直等它整个链路都被阻塞住。互联网电商这种高并发场景根本承受不住这种性能损耗。TCCTry-Confirm-Cancel方案则是另一种思路它把每个分布式操作拆分成三个阶段Try阶段检查并预留资源Confirm阶段真正执行业务Cancel阶段回滚释放资源。TCC的优点是不用锁数据库资源性能比2PC好很多但缺点也突出对业务代码侵入性强每个操作都要手写Try、Confirm、Cancel三段逻辑而且很多业务场景根本不容易拆成这三个阶段。比如锁库存操作Try阶段锁定库存Confirm阶段扣减库存Cancel阶段回滚库存听上去简单但遇到库存表结构复杂、还要记录操作流水的时候代码量直接翻倍。2.2 本地消息表和MQ事务消息适合什么场景再来看另外一类方案本地消息表和MQ事务消息。这两类方案的核心思路都是牺牲实时一致性换取最终一致性很适合异步化的业务场景。本地消息表的大致做法是在业务操作所在的数据库里建一张本地消息表把要发给其他服务的消息和业务操作放在同一个本地事务里保证业务操作和消息记录要么一起成功要么一起失败。然后再用一个定时任务轮询消息表把状态为待发送的消息发给MQ或者直接调用下游接口。这个方案的好处是实现简单、不依赖额外组件坏处是要自己维护消息表的状态轮询而且消息的实时性会差一些。MQ事务消息则把消息发送拆成了两个阶段。以RocketMQ为例发送方先把半消息发送到MQ然后执行本地事务如果本地事务执行成功再提交半消息让消费者可以消费。如果本地事务执行失败就回滚半消息消费者不会收到消息。这个方案是目前比较推荐的异步解耦方案但问题在于它解决的问题只是“消息可靠投递”下游消费之后如果处理失败了还得额外配合重试和补偿机制。2.3 Seata AT模式凭什么成为最合适的落地方案对比了一圈最后回到谷粒商城这个场景来看。下单链路里订单服务和库存服务之间的数据一致性是要相对实时、相对强一致的。用户下单瞬间库存不能多扣也不能少扣订单和库存的绑定关系必须准确。如果这中间走异步消息最终一致性理论上可行但会带来一个问题用户下单成功后如果库存回滚最终失败了订单已经展示给用户了处理起来非常麻烦。Seata AT模式最核心的优势在于它在不侵入业务代码的前提下实现了基本接近于强一致的分布式事务效果。开发者只需要在业务方法上加上一个 GlobalTransactional 注解Seata框架就会自动接管事务提交和回滚的协调工作典型的低侵入、易落地。谷粒商城这种以学习重业务逻辑为主的项目选Seata AT模式既能快速跑通整个下单流程又能帮助理解分布式事务的底层运行机制。为了让大家更直观地做选型我把几种方案放在一个表格里对比方案一致性级别业务侵入性性能影响典型应用场景2PC/XA强一致高数据源层大资源锁定传统金融、跨库强一致的内部系统TCC最终一致/准实时一致高业务拆三个阶段小高并发且业务能清晰拆分场景本地消息表最终一致中需建表维护状态小异步通知、任务补偿类场景MQ事务消息最终一致中配合消息重试小异步解耦、削峰填谷场景Seata AT准强一致极低注解式中有全局锁开销中小规模微服务业务强一致场景3. Seata AT模式底层原理精讲三个角色、两阶段设计、undo_log回滚3.1 全局事务的三个角色TC、TM、RM分别干什么要真正理解Seata AT模式得先从它的全局事务模型说起。Seata框架定义了三个核心角色事务协调者TCTransaction Coordinator、事务管理器TMTransaction Manager和资源管理器RMResource Manager。TC是独立运行的Seata Server负责全局事务的注册、分支事务的状态记录、全局锁的协调以及最终下发全局提交或全局回滚的指令。TM在代码层面就是发起全局事务的那个入口方法它负责向TC申请开启一个全局事务拿到一个全局事务ID也就是XID然后在所有分支事务执行完毕之后再向TC发起全局提交或全局回滚的请求。RM则是每个参与分布式事务的微服务里的资源管理器它负责把本地事务注册成全局事务的一个分支事务并配合TC执行分支的提交和回滚。三个角色协作起来就是这样一个流程TM开启全局事务拿到XIDXID通过微服务调用链传递到下游下游的RM执行本地SQL时把本地事务注册成分支事务并上报给TC。所有分支都执行完TM发起提交或回滚指令TC统一协调。3.2 一阶段提交和二阶段提交到底做了什么Seata AT模式的设计思想可以概括成“一阶段提交本地事务二阶段决定全局结果”。在一阶段RM拦截到业务SQL之后并不会直接让这条SQL交给数据库执行了事而是会做额外的三件事。第一件事是解析SQL语句生成这条SQL对应的前后镜像数据也就是查询出SQL执行前那一行的原始数据beforeImage然后执行SQL再查询出执行后那一行的数据afterImage。第二件事是把业务SQL放在本地事务里执行同时把这组前后镜像数据写入一张undo_log表业务数据和undo_log在同一个本地事务里一起提交。第三件事是向TC注册分支事务并上报本分支事务涉及的数据行的全局锁信息。到了二阶段TC会根据所有分支事务的执行情况决定是全局提交还是全局回滚。如果所有分支都成功了TC通知各个RM做全局提交。RM收到提交指令后只需要异步删除之前写入的undo_log记录即可因为业务数据在一阶段已经实际提交到数据库里了。这个过程很快不需要再做其他数据操作。如果任何一个分支失败TC就会通知所有RM做全局回滚。RM收到回滚指令后会读取undo_log中的前后镜像数据然后根据这些数据生成反向SQL。举个例子如果一阶段执行的是一条update语句把库存从100改成了90那么回滚时Seata会生成一条update语句把库存从90改回100。这个反向SQL执行完之后再删除undo_log记录分支事务的回滚就算完成了。3.3 全局锁和undo_log是怎么配合防脏写的这里有一个非常关键的设计细节Seata在二阶段回滚的时候并不是无条件执行反向SQL的它要先做“脏写校验”。具体来说RM会取出undo_log里的afterImage与当前数据库里这一行的实际数据做比较。如果两者一致说明在一阶段提交之后这行数据没有被其他事务修改过可以安全地执行反向SQL回滚。如果不一致说明这行数据已经被其他事务改动过了如果强制回滚就会把别人新写入的数据也一并覆盖掉这时候Seata会抛出异常转而走人工介入补偿的流程。全局锁就是用来减少这种脏写冲突概率的。Seata的全局锁由TC统一管理当一个全局事务的分支RM要更新某行数据时会先向TC申请这行数据的全局锁。如果这行数据的全局锁已经被其他全局事务占用了当前事务就需要等待。这样设计的好处是避免两个分布式事务同时对同一行数据做修改导致回滚时无法判断数据归属。我实际做项目时发现理解undo_log为主、全局锁为辅这个关系很重要。全局锁降低的是多个全局事务之间的冲突概率但并不能完全避免脏写尤其是在并发比较高的电商场景下。所以后期如果要优化下单性能全局锁往往会成为瓶颈这一点放到后面第五节详细聊。4. 谷粒商城下单模块的分布式事务落地实操4.1 Seata Server环境的完整搭建步骤说了这么多原理终归要落到代码上。我做的谷粒商城项目里Seata用的版本是1.4.0配合Spring Cloud Alibaba 2.2.x版本使用这两个版本序列的兼容性比较成熟资料也多踩坑了容易搜到解决方案。第一步是启动Seata Server。从Seata官方GitHub把对应版本的server压缩包下载下来解压之后重点修改两个配置文件。registry.conf里要把注册中心类型配置成nacos并填上Nacos服务器的地址。file.conf里要修改store模式我是直接用的默认的file模式做的单机学习如果做生产配置建议改成db模式把事务会话信息存到数据库里避免Seata Server重启之后事务状态丢失。第二步是初始化数据库。每个参与分布式事务的业务数据库里都需要新建一张undo_log表。这张表的SQL脚本Seata官方文档里有提供核心字段包括branch_id、xid、context、rollback_info、log_status、log_created、log_modified。其中rollback_info字段存放的就是前后镜像数据的JSON序列化结果。这一步经常有人漏掉因为业务代码能正常启动Seata也没有报错等到真正发生回滚的时候才发现undo_log表不存在导致回滚直接失败。我在第一次集成的时候就是只给订单库建了表库存库忘了建测回滚测到怀疑人生。第三步是启动NacosSeata Server注册进去之后在Nacos的服务列表里能看到seata-server这个服务说明注册成功。4.2 订单服务和库存服务引入Seata的方式Seata Server启动好之后接下来就要改造业务服务了。核心工作可以分成三块引入依赖、配置数据源代理、添加全局事务注解。先看依赖。订单服务和库存服务的pom.xml文件里都需要引入spring-cloud-alibaba-seata这个starter依赖同时引入seata-spring-boot-starter。这里要注意版本对齐Spring Cloud Alibaba的版本管理里其实已经包含了seata的依赖管理直接用不带版本号的引入方式更稳妥避免手动指定版本导致冲突。然后是数据源配置这是集成Seata时最容易踩的坑。Seata AT模式之所以能在业务SQL执行前后自动生成镜像数据核心原理是在数据源层面做了一层代理它会拦截所有的JDBC连接在真正的SQL执行前后插入额外的查询逻辑。因此我们需要把原来的DataSource包装成Seata的DataSourceProxy。具体的做法是在启动类上排除DataSourceAutoConfiguration不让Spring Boot自动创建数据源然后自己写一个配置类手动创建一个DataSourceProxy类型的Bean。我在做的时候用的Druid连接池所以是在DruidDataSource创建完之后用new DataSourceProxy(druidDataSource)包装一层。如果这一步没有做对Seata的全局事务拦截器就感知不到数据源即使方法上加了GlobalTransactional也只会把本地事务提交掉根本不会注册分支事务。对于使用MyBatis-Plus的项目还有一个小的注意事项MyBatis-Plus的分页插件等配置要确保拿到的是Seata包装后的数据源否则分页查询和Seata的SQL拦截可能会冲突。我在实际项目中遇到过MyBatis-Plus的分页失效问题排查了半天最终发现是因为数据源代理顺序不对导致的。4.3 在核心下单方法上开启全局事务环境都准备好了最后一步就是在业务代码上添加全局事务注解。在我做的谷粒商城下单模块里OrderServiceImpl里的submitOrder方法是整个下单链路的入口。这个方法内部逻辑大致是这样的Override GlobalTransactional(rollbackFor Exception.class) public SubmitOrderResponseVo submitOrder(OrderSubmitVo orderSubmitVo) { // 1. 校验用户的收货地址、支付方式、商品信息 // 2. 调用库存服务锁定库存 // 3. 调用会员服务扣减积分 // 4. 调用优惠券服务核销优惠券 // 5. 在订单库创建订单记录和订单项 // 6. 返回下单结果 }GlobalTransactional 注解是这个方法能参与分布式事务的关键它相当于告诉Seata的TM这个方法是一个全局事务的入口请给我生成一个XID并让通过Feign调用的下游服务都把各自的分支事务归属到这个XID下面。需要注意的是Feign调用时要保证XID能在服务之间传递。这一点Spring Cloud Alibaba已经帮我们封装好了引入依赖后它会自动配置Seata的RequestInterceptor在Feign请求的Header里自动携带XID。所以一般来说只需要专注业务逻辑即可。但如果自己手动拼接HTTP请求去调其他服务就必须手动把XID放到Header里传递否则下游服务那边的RM就不知道当前请求属于哪个全局事务分支事务就不会注册。4.4 事务边界的设计哪些操作该放进全局事务哪些不该放事务边界的设计直接决定了这个分布式事务方案的成败。很多同学在做项目时容易犯一个错误就是把所有业务操作一股脑都塞进 GlobalTransactional 方法里结果导致全局事务的执行时间非常长数据库连接被长时间占用全局锁竞争加剧系统并发能力直线下降。在谷粒商城中我最终划定的全局事务范围是这样的锁定库存、扣减积分、核销优惠券、创建订单记录这四步操作是必须放进同一个全局事务里的。因为它们的共同特点是对数据一致性的要求非常高必须在同一时刻全部成功或者全部失败。但是像发送短信通知、记录操作日志、清理购物车记录这类操作就没有必要放进全局事务里了。这些操作对数据一致性的要求没那么严格延迟几秒甚至几分钟都能接受完全可以通过MQ异步消费来实现。比如清理购物车完全可以等订单创建成功之后发一条消息到RabbitMQ消费者再异步把用户勾选的购物车条目删除。如果购物车清理失败最多就是用户下次看到购物车里还有这个商品不影响订单和库存的正确性。还有一步非常关键就是下单成功之后跳转支付页面的动作绝对不能放在全局事务里。因为支付环节本质上是和第三方支付系统交互这个调用的响应时间完全不可控如果放进全局事务里会让Seata全局锁持有时间无限拉长严重时会把整个下单接口拖垮。正确的做法是全局事务只负责生成本地订单并把订单状态置为待支付事务提交之后再让前端带着订单号去请求支付服务发起支付。4.5 集成后的完整时序流程走读把整个集成做完之后我习惯用一条业务链路去验证代码是否真正生效。模拟一次正常的下单请求整套流程走下来是这样的用户点击提交订单请求打到订单服务的submitOrder方法GlobalTransactional 生效TM向TC发起全局事务注册拿到一个XID。订单服务接着调用库存服务的Feign接口Feign拦截器自动把XID放进请求Header库存服务收到请求后RM发现Header里带着XID就先把库存扣减这个本地事务注册到一个分支事务然后向TC申请库存行数据的全局锁拿到锁之后在本地事务里执行UPDATE库存SQL和写入undo_log一起提交。订单服务继续调用会员服务扣积分会员服务一样的套路注册分支事务执行本地更新提交。三四个服务依次走完最后订单服务在自己库里插入订单记录插入完成后TM向TC发起全局提交请求。TC收到之后对比所有分支事务的状态都是成功于是通知各个RM执行二阶段提交RM们收到指令后各自删除自己的undo_log记录。整个分布式事务到此干净利落地结束。如果中间任何一个环节抛了异常比如库存服务扣库存时报库存不足订单服务的全局事务拦截器就会感知到异常TM向TC发起全局回滚请求。TC通知各RM回滚已被调用的服务会根据undo_log里的镜像数据生成反向SQL把库存、积分、优惠券状态都恢复到一阶段之前的样子订单库那边如果已经插入了订单数据也会被删除掉。用户那边看到的就是一个统一的下单失败提示后台数据却纹丝不乱。5. 谷粒商城下单链路常见问题与排查实录5.1 全局事务注解明明加了事务却完全没有生效这是很多人第一次在谷粒商城集成Seata时最容易遇到的坑。代码里确实加了 GlobalTransactional 注解方法执行也正常Feign调用也成功但去看Seata Server的日志压根就没有任何全局事务创建的记录。我排查这个问题时的经验是第一步先确认引入的依赖版本是否对应。Spring Cloud Alibaba 2.2.x的版本管理里seata-spring-boot-starter的版本是固定的如果你手贱在pom里额外指定了一个其他版本的seata很容易出现starter的自动配置类找不到的情况。第二步检查启动类上是否把自己定义的数据源排除掉了如果没排除Seata的DataSourceProxy可能不会被正确初始化全局事务拦截器和数据源代理之间就会对不上。第三步检查 GlobalTransactional 是否加在了通过外部调用进入的方法上在同一个类的内部方法调用Spring AOP代理默认不生效这个是最容易忽略的。比如有的同学会把下单逻辑抽到一个内部方法上在controller里直接调this.submitOrder()注解是不起作用的必须从外部代理对象调用才行。5.2 库存扣减成功但订单创建失败后库存没有回滚这个问题比上一个要隐蔽一些。从现象上看整个调用链确实报了异常下单失败但库存还是被扣了。最直接的原因是这个异常发生在库存服务的本地事务已经提交之后而且是超时或者异常丢失导致的。排查的时候先看Seata Server的控制台确认全局事务是否正常创建并且进入了回滚流程。如果控制台显示回滚成功但库存数据没变那就要去库存库检查undo_log表里有没有对应的回滚记录。如果undo_log表里没有数据说明库存服务的本地事务在一阶段就提交掉了但分支事务根本没注册成功。这种情况大概率是XID传递失败库存服务流量入口那个Feign调用的Header里没有XID。可以在库存服务的锁库存方法里打日志输出RootContext.getXID()如果是null那就说明请求进来时XID就已经丢了去检查服务间的Feign是否有自定义拦截器覆盖了Seata默认的拦截器。还有一种情况是回滚的SQL执行了但反向SQL没有正确匹配到数据。比如库存表里除了库存数量之外还有一个version字段或者modified_time字段反向SQL是根据beforeImage和afterImage的差异动态生成的如果beforeImage和afterImage的比对逻辑因为字段类型转换问题出了问题就可能更新不到正确的行。这个需要看回滚日志一般来说会根据报错信息定位到具体字段。5.3 高并发下出现大量的全局锁等待超时当我做完基础的分布式事务集成开始做并发压力测试的时候一个非常头疼的问题暴露出来一旦同时有多个用户抢购同一个商品就会出现大量的全局锁等待超时异常。后台日志里最常见的报错是GlobalLockWaitTimeoutException。原因在于Seata AT模式的全局锁是粒度比较粗的行锁而且锁的持有时间和整个全局事务的生命周期绑定。如果两个用户同时下单都锁同一个商品的库存第二个用户的全局事务就必须等第一个用户全局提交后才能拿到锁。如果全局事务里业务逻辑执行时间又长比如调用了响应很慢的第三方服务那等待超时的概率就会进一步放大。对谷粒商城这类电商项目我的实际优化思路是这样的首先严格控制全局事务内的远程调用确保全局事务里只有必要的数据操作。其次对热点库存的商品可以把库存预扣操作从Seata全局事务里剥离出来改用Redis预扣库存的方案。具体来说下单前先从Redis里扣减库存扣减成功才继续后面的订单和数据库库存的同步操作库存数据库只做异步持久化。最后如果确实需要依赖Seata可以通过调大全局锁超时时间来缓解但这不是治本之策。5.4 回滚失败Branch transaction rollback failed, undo_log中镜像数据不一致这个问题我处理过很多次基本上可以定位为脏写校验没过。前面讲过Seata回滚前会对比当前数据和afterImage如果发现当前数据被别的线程改过了就会抛出这个异常。我遇到过的一个典型场景是订单服务在下单过程中调用库存服务锁库存锁库存成功之后订单服务又发了一条MQ消息消费者异步去更新库存表的一个冗余字段。结果这个异步更新操作和Seata的回滚操作撞到了一起把afterImage对应的行数据改掉了导致Seata回滚时发现镜像不一致。解决这个问题的思路有两个方向。一个方向是从业务上规避比如所有对库存表的写操作只要可能和下单链路产生竞争都要加入全局事务的考虑范围不要把同一行的更新操作散落在不同的事务模型里。另一个方向是确认这个隔离级别的取舍如果某些字段确实可以被异步修改那就不要放在undo_log捕获的更新语句里或者改用其他方式做数据补偿。实战中我最后选择了把库存异步更新改成同步放在全局事务内执行彻底避免了这类脏写冲突。5.5 常见问题速查表现象直接原因排查步骤解决方案GlobalTransactional不生效版本不匹配/代理失效/同类内部调用查Seata Server日志、检查pom依赖、确认Spring代理方式对齐依赖版本、排除自建数据源类、从外部代理调用库存不回滚分支事务未注册/XID未传递打印RootContext.getXID()检查Feign自定义拦截器、确认XID传递的Header全局锁等待超时全局事务持有锁时间过长看超时日志定位全局事务ID缩短事务边界、Redis预扣库存、必要时调大超时时间分支回滚失败脏写校验未通过对比当前数据与afterImage业务上规避同行的多事务模型并发写找不到undo_log表初始化遗漏确认每个业务库都有该表补建undo_log表并核对字段6. 下单场景还能怎么优化写在最后的几句实操心得谷粒商城完整做下来我觉得最有价值的不仅仅是跑通了Seata这套分布式事务方案而是让我形成了一个认知分布式事务不能只盯着一套方案不同的业务阶段应该有不同的处理策略。在我最终维护的下单版本里短链路上强一致的数据操作走Seata长链路、耗时长、不需要实时一致的操作全部走RabbitMQ加定时任务补偿。订单三十分钟未支付自动关单这个场景Seata就帮不上什么忙了它就是标准的延迟消息加最终一致性下单成功发一条延迟消息出去三十分钟后消息被消费消费者去查订单状态发现还是待支付就调用释放库存接口把库存补回去。如果释放库存失败消息队列的重试机制会自动再投递几次加上定时任务兜底保证库存最终会释放。这里面的关键心得就是四个字缩小边界。能让数据在一个服务里保持一致就不要拆出去做分布式事务能用普通消息队列解决的就不要引入全局锁必须用分布式事务的也尽量让每个分支事务短小精悍。很多同学做谷粒商城的时候喜欢把所有功能都拆成微服务结果本来一个本地事务能搞定的事硬生生变成了分布式事务最后被各种一致性问题折磨得焦头烂额。这个项目的含金量不在于你用了多少新技术而在于你通过它理解了什么场景该用、什么场景不该用。最后再分享一个小细节做Seata集成测试的时候不要只在正常流程里验证一定要手动模拟各种异常比如让库存服务抛异常、让Feign调用超时、让下游服务宕机然后观察Seata的回滚日志亲眼看一次数据恢复的过程。我对Seata的理解就是在一次一次制造故障又看着数据自动恢复的过程中慢慢建立起来的。这套流程走通了面试时再被问到分布式事务你就不会再怕了。