
我在一次系统拆分后的压测中被一组对不上账的订单数据折腾到凌晨。“支付成功但库存没扣”“退款了积分却照发”这种问题相信做微服务的兄弟都遇到过。当时我们反复排查接口超时、重试逻辑最后发现问题的根源不在某个服务内部而是跨服务的多个写操作失去了同一个本地事务的保护。也就是从那时候起我开始认真做分布式事务选型最后落到了Seata上。这篇内容就把我选型、实践、踩坑的完整过程写出来算是一份可以直接拿去评估的工程笔记。先说清楚Seata是一款开源分布式事务中间件核心思路是把一次跨服务调用链路上的多个数据库操作纳入同一个全局事务来管理让它们要么一起提交、要么一起回滚。它不绑定某个具体业务框架Spring Cloud、Dubbo、纯Java应用都能接。如果你正在微服务化改造或者已经在为跨服务数据一致性发愁这篇文章基本可以覆盖你要关心的全部问题。1. 先泼冷水你手里这笔账真的一定要上分布式事务吗很多团队把分布式事务当成微服务改造的必选项一拆库就先上Seata。我不反对用Seata但我觉得更稳妥的顺序是先把“这笔账是否真的需要强一致”想清楚。做选型的第一步不是比较AT和TCC哪个好而是判断业务场景需不需要分布式事务。1.1 拆库之后本地事务的保护边界到底发生了什么变化单体时代一次下单操作涉及订单表、库存表、账户流水表都在同一个数据库里一条SQL或者一个Transactional就能搞定。拆成微服务之后订单服务管自己的库库存服务管自己的库账户服务管自己的库。原来那个能回滚多个表的“本地事务负责人”不存在了每个服务只能保证自己库里的那几条数据要么全成、要么全不成。这就是问题背景库存扣减成功了但订单创建失败了两个库之间没人能互相通知回滚。分布式事务要做的就是重新建立一个跨库的“全局事务协调者”替代单体时代那个数据库事务管理器。这里有个很重要的判断标准你遇到的是不是真的跨库跨服务写操作如果只是查询接口串起来或者各服务只有单一写者那根本不需要全局事务。很多人被分布式事务吓得不敢动其实是在给自己造问题。1.2 哪些“看起来像事务”的业务实际上用不到分布式事务我见过不少场景业务方张口就说“这里必须分布式事务不然会脏”。但仔细分析后大部分可以用更轻的手段解决。可异步化的操作比如下单后发短信、发优惠券这类动作用户感知不到瞬时状态。走消息队列异步处理下游失败重试即可不需要全局事务。天然幂等的操作接口重复调用几次结果都一样配合消息去重和状态机能容忍中间短暂不一致。有对账兜底的场景比如支付金额核对T1对账能发现差异并人工介入就不必强实时强一致。单库多表、单一写者的场景这个最简单本地事务依然够用。在这些场景用分布式事务反而会让系统更脆弱全局锁等待、网络抖动导致的长事务、协调者单点压力都会成为新的不稳定因素。1.3 什么时候确实必须考虑Seata这类强一致方案我总结下来真正需要强一致分布式事务的业务通常同时满足几个特征一次用户操作会同步触发多个服务上的多个写操作业务上要求所有写操作必须同生共死不能接受中间状态暴露给用户失败后不能依赖异步重试或人工对账来兜底或者兜底成本高到无法接受。最常见的例子就是扣库存下单扣余额这一条链路。用户点了支付库存扣了、钱也扣了但订单没创建成功这种体验是会直接引发客诉的。另一个例子是跨系统的积分/优惠券发放如果发一半失败用户既损失了钱又拿不到权益也很麻烦。所以选型前先做减法能异步的异步、能幂等的幂等、能对账的对账剩下的“真强一致”场景再交给Seata这样的中间件。这步想不清楚后面无论选什么模式都会很痛苦。2. 认识Seata的三位核心角色TC、TM、RM到底干了什么如果你之前没接触过Seata第一次看官方文档大概率会被TC、TM、RM这些缩写搞混。我换个角度讲把它当成一个“全局事务的导演组”三个角色分工非常明确。2.1 三个角色各自的工作以及XID是怎么串起全链路的TC事务协调者独立部署的Server端负责全局事务的开启、提交、回滚决策以及各个分支事务的注册与状态管理。它是整个分布式事务的“大脑”。我们通常单独部署一台或一个集群来做TC。TM事务管理器嵌入业务代码里负责在方法入口开启全局事务并在业务执行完后告诉TC“这一单整体提交”还是“整体回滚”。它相当于发起方的“项目经理”。RM资源管理器负责管理各个分支事务比如某个微服务里对数据库的一次本地操作。RM向TC注册分支并在二阶段执行业务方要求的提交或回滚指令。它相当于各个服务里的“执行小组”。这三个角色之间靠一个全局事务IDXID串起整条链路。TM开启全局事务时从TC拿到XID通过微服务调用链传递下去每个被调用的服务在RM层拿到同一个XID就能把自己的本地事务登记到同一份全局事务名单里。用过Spring Cloud Sleuth或者SkyWalking的兄弟会感觉很亲切XID和traceId的传播逻辑非常像只是traceId用于日志追踪XID用于事务归属。2.2 一次全局事务从开始到结束完整走一遍是什么样以“下单扣库存”举例整个流程可以拆成五步入口服务标记了全局事务注解TM向TC请求开启全局事务TC生成XID返回并把全局事务状态置为Begin入口服务带着XID继续调用库存服务库存服务里执行本地SQL扣减库存RM把这个分支注册到TC此时库存服务本地事务先提交入口服务再调用账户服务扣款同样RM注册分支、本地事务提交入口服务的业务代码全部执行完TM向TC发送全局提交请求TC检查所有RM分支的状态如果都成功通知各RM做二阶段提交收尾只要有一个分支失败TC就会通知所有RM做二阶段回滚。这个流程能成立的关键在于一阶段只做“本地提交登记分支”真正的一致性和回滚决策放在二阶段由TC统一调度。这也是Seata和各种“事务消息”方案的底层差异所在——它有一个真正意义上的全局协调者而不是靠各个节点之间互相通知。这里我要提醒一点TC虽然是大脑但业务数据不经过它所有的SQL还是直接在各自的数据库上执行。所以TC的性能瓶颈主要在于分支注册和状态决策的QPS而不是数据流量。理解TC只做协调、不做计算对后续做容量评估很有帮助。3. Seata四个模式逐个解构AT、TCC、Saga、XA怎么选Seata最容易被误解的地方就是很多人以为它只是一种事务方案其实它内部集成了四种模式AT、TCC、Saga、XA。它们的实现原理、侵入程度和使用条件差异很大。我一个个拆开讲清楚。3.1 AT模式自动化程度最高但要注意全局锁和脏读AT模式是Seata默认推荐的入门模式也是最“省心”的一种。它对业务代码的侵入非常小你只需要在发起方的方法上加GlobalTransactional在参与方保持原来的Transactional然后建一张undo_log表Seata就会自动接管全局事务的逻辑。AT的核心原理可以概括成“一阶段提交二阶段反向补偿”。一阶段时RM执行本地SQL并提交本地事务同时把数据变更前后的快照记录到undo_log表里二阶段如果TC通知全局提交RM只需删除对应的undo_log如果通知全局回滚RM就根据undo_log里的前镜像生成反向SQL把数据改回去。这样说可能有点抽象我举一个简单例子库存服务把stock100改成stock90一阶段记录前镜像100、后镜像90。全局回滚时RM用前镜像拼一条update stock set stock100 where stock90的执行。看起来逻辑很通顺但实际落地有一堆细节。比如并发情况下另一个请求已经把stock改成了80回滚SQL执行时发现当前值不是90就会触发冲突检测这时TC会认为回滚失败转人工处理。所以AT模式必须配合全局锁在执行业务SQL前RM会尝试获取对应记录的全局锁防止其他全局事务并发修改同一行数据。AT模式优点很明显代码入侵小、接入速度快、回滚逻辑自动生成非常适合大部分 CRUD 型业务。缺点也同样明显全局锁会影响同一行数据的并发写高并发热点行场景下吞吐量会明显下降undo_log表会持续写入和清理设计不好的话容易膨胀复杂SQL特别是多表Join、子查询、存储过程的解析不一定能覆盖只能等后续版本或者换模式。所以我的经验是AT适合业务逻辑以单表单操作为主、热点数据不严重、能接受一定并发损失的场景。如果一张表存在大量同行的并发更新AT的全局锁等待会让你想砸电脑。3.2 TCC模式Try/Confirm/Cancel手写补偿防悬挂是必修课TCC模式是Seata四种模式里最早受到生产环境认可的方案之一核心思想是业务方自己定义三个方法Try资源预留阶段比如扣库存时不直接把库存减掉而是先锁定一部分库存ConfirmTry成功后执行真正的业务逻辑比如把锁定的库存真正扣减CancelTry失败或全局回滚时释放预留的资源。TCC模式最大的优势是把控制权交还给业务。业务方可以在Try阶段做幂等预留在Confirm阶段做安全的提交在Cancel阶段做准确的补偿不受SQL解析能力的限制也能绕过全局锁的性能问题。只要三个方法写得正确TCC能支撑的业务类型比AT多不少。代价也很直接代码量和工作量成倍增加。每个参与TCC的资源都要写三个方法而且必须考虑三个经典问题幂等Try、Confirm、Cancel都可能因为网络重试被调用多次必须保证重复执行不产生副作用悬挂Try方法还没执行完Cancel就被调用了等Try执行完资源已经被释放会导致资源被错误锁定。常规做法是在Try里记录事务状态Cancel执行时判断Try是否完成未完成就不允许继续补执行空回滚全局事务已经回滚但某个分支Try从未执行过Cancel却被调到了此时Cancel要能识别“没有预留的资源”并直接返回成功。TCC模式更适合资金、库存这类的核心资源操作。比如某物流系统的运费试算和账户预冻结就是典型的TCC场景Try阶段把金额冻结Confirm阶段解冻并扣款Cancel阶段直接解冻。这里分享一个踩过的坑我们最初TCC的Try和Confirm之间用了同一个DTP分布式事务处理器里的状态字段结果并发情况下状态被覆盖导致Confirm执行了两次。后来改成基于数据库唯一键做幂等控制同时在Cancel里做空回滚判断才真正稳定下来。写在代码里的不只是业务逻辑而是完整的边界状态机。3.3 Saga模式长流程编排正向与反向的补偿链Saga模式不是Seata发明的最早来自分布式数据库领域的长事务方案。Seata的Saga模式可以理解成一个“状态机补偿执行器”把一条很长的业务流程拆成多个子事务每个子事务定义自己的正向操作和反向补偿操作按顺序执行。执行过程中如果某个正向节点失败状态机会根据已经成功执行过的节点列表按照相反顺序逐个执行反向补偿操作。比如“创建订单 → 扣库存 → 通知仓库 → 生成配送单”四步第三步失败了Saga状态机就依次执行第三步的补偿、第二步的补偿、第一步的补偿把数据恢复到链路起点。Saga模式特别适合两类场景业务流程特别长、参与者特别多的事务比如多级审批、跨系统整合订单、长时间运行的流程型业务**无法用数据库锁来保护资源、只能通过补偿来恢复“业务结果”**的场景比如外部系统接口调用。但Saga的问题也很明显没有隔离性。A用户扣了库存在B用户看来库存已经扣了但如果A后续回滚B可能已经基于“扣过的库存”做了下一笔订单。所以使用Saga时业务设计上必须能接受这种中间状态的暴露或者自行加状态字段来避免误读。用Seata的Saga之前要先在状态设计上花很大功夫。官方提供了状态机语言来描述节点和补偿关系理论上是把“补偿逻辑”变成了可配置的数据。但状态机一复杂调试难度会指数级上升。我的建议是Saga只应用在确实长流程、低并发、高容错容忍的场景别拿它当所有分布式事务的默认解。3.4 XA模式数据库原生支持但同步阻塞是硬伤XA模式是基于数据库的XA协议来做的一阶段直接由数据库资源管理器登记事务二阶段由TC统一调用数据库的提交或回滚原语。因为XA协议是数据库厂商实现的所以Seata接入XA模式后业务代码几乎不用改数据库层面能做到真正的原子提交。听起来很美但XA在分布式系统里有一个致命问题资源锁定时间太长。XA要求所有参与的数据库分支在整个事务期间保持资源锁定因为数据库不知道TC最后会选择提交还是回滚。这会带来两个后果如果某个服务执行较慢或者网络抖动其它服务的数据库资源可能被锁住很久整体可用性和吞吐量直线下降对高并发场景非常不友好热点行的锁等待很容易把数据库拖垮。所以XA模式适合的场景非常窄并发量不高、对一致性要求极强、数据库又是同构体系的系统。如果业务量压到XA模式的锁等待无法接受基本就得放弃XA回到AT或TCC的思路上来。3.5 四个模式一张表看明白适用场景与代价对比模式侵入程度一致性强度性能影响最佳场景主要风险AT低注解建表强有全局锁中高同一行并发锁等待单表单操为主、改造速度快优先复杂SQL解析有限、并发热点性能差TCC高三个方法手写强业务层控制高预留资源灵活可控资金、库存、账户类核心资源代码量大幂等/悬挂/空回滚易出错Saga中状态机补偿较弱无隔离性高补偿执行灵活长流程、低并发、外部系统多状态设计复杂、补偿不完善难排查XA低数据库协议极强数据库原生原子低同步锁等待严重低并发、强一致性要求极高资源锁持有时间太长并发下会拖垮数据库别看表格里AT好像全面占优我自己在工程上的排序是业务能不能接受最终一致、有没有条件手写补偿、流程有多长这三个问题决定了最终模式。很多团队一上来就选AT等遇到热点行锁等待才回头补TCC的方案工程量反而更大。4. 选型的决策逻辑从业务需求倒推技术方案这一部分我想聊真正“选型”的过程而不只是罗列模式原理。因为原理可以看文档但选型的决策逻辑往往才是决定项目能不能长期稳定的关键。4.1 选型前必须理清的五个核心问题我把做选型时反复问自己和团队的问题整理成了五个要点按照优先级排序一致性强度的真实等级业务是否需要所有节点“同时可见”还是允许几百毫秒内的最终一致如果允许最终一致很多场景根本不需要全局事务。业务代码的可改造程度现有服务能不能接受在每个参与方写Try/Confirm/Cancel如果团队资源紧张、业务排期短AT模式可能是唯一现实的选择。热点数据的并发强度同一个账户、同一个商品SKU会不会被高频并发修改如果会AT的全局锁等待会成瓶颈必须考虑TCC或其它方案。流程长度与参与者数量超过3个参与方、流程可能持续数分钟的Saga比AT/TCC更合适。运维与监控的成熟度你能不能接受为TC单独部署服务、监控XID链路、处理分支事务状态异常没有这个准备任何模式落地都会手忙脚乱。想清楚这五个问题其实也就自然排除了大多数不合适的方案。我见过一个项目业务是典型的短事务高并发秒杀团队却拿着Saga状态机在做结果每次补偿触发都要靠日志一点点追踪苦不堪言。这就是没有先做一致性强度和并发强度评估直接套模式的典型例子。4.2 一个实际的选型过程某订单系统的路子之前我们在某订单系统里做支付链路改造业务形态是“创建订单 → 冻结库存 → 处理支付 → 生成出库单”四个服务分布在四个独立库里。当时我先按上面的五问做了评估一致性强度用户支付后必须立刻看到订单有效不能出现“支付成功但订单不存在”属于强一致需要全局事务可改造程度库存和账户两个服务业务代码都比较规整可以接受适度改造热点并发库存表可能存在同SKU高并发但整体业务处于中低并发不算严重流程长度约4个节点、正常都在几百毫秒内完成不算长流程运维成熟度我们当时运维权限有限没法单独维护复杂的Server集群希望尽量自动化。综合来看AT模式在“代码侵入低、接入快、能覆盖短事务”上最匹配。我们最终选了Seata AT模式再加上应用的幂等控制跑了几个月压测和生产都没出过大问题。但有个细节我至今记得因为库存表存在某些特殊商品的集中秒杀AT模式下局部会出现全局锁等待超时。我们后来针对这几个高冲突SKU单独做了库存拆分把热点分散开才彻底缓解。这个案例想说的其实是选型不是“选一个名字”而是选成立条件。同一个系统不同子链路甚至可能用不同模式——比如订单主链路用AT资金账户用TCC完全合法且合理。4.3 对比“外人”本地消息表、MQ事务消息为什么也在候选名单在选型会议上我们同时对比过另外两个常见方案本地消息表、MQ事务消息。它们和Seata的核心差异在于没有全局协调者而是靠消息投递和回调来达成最终一致。本地消息表业务方在本地库里记录一条“待发送消息”先和业务操作一起本地提交再通过定时任务把消息发出去。优点是不依赖额外中间件缺点是消息表耦合业务库、需要自己管理消息状态和重试。MQ事务消息比如基于半消息机制的事务消息先发送半消息执行本地事务再提交或回滚消息下游消费成功后再继续后续操作。优点是可以做到业务和消息的一致性、异步解耦缺点是无法像Seata那样对多个下游的写操作统一回滚。我会这么划分如果链路里下游操作必须同步完成、且失败要回滚所有写操作适合Seata这类全局事务方案如果下游可以接受异步、失败后通过重试和补偿机制自愈MQ事务消息或本地消息表往往更轻量、更可靠。所以不要把Seata看成“分布式事务的唯一解”它只是“强一致同步事务”这个象限里的主流解。最终一致性的象限里MQ方案可能更简单更稳。5. 落地Seata最容易踩的坑部署配置与运行排障选型完成只是第一步落地时真正的麻烦才开始。我把实践中遇到的、以及帮别人排查过的高频问题集中写在这部分涉及部署、配置、代码、数据库几个层面。5.1 TC服务端部署存储模式、注册中心和配置中心怎么搭Seata的TCServer端是一个独立的Java应用官方文档给了很多部署参数但最容易踩的是三个存储模式TC的事务状态要落库支持file、db、redis三种模式。开发环境可以直接file生产环境强烈建议用db因为文件模式在重启或集群部署时容易丢失/重复读事务状态。db模式需要在数据库里建global_table、branch_table、lock_table三张表脚本在打包目录里能找到。很多团队一开始跳过建表结果Server启动正常、一跑事务就报表不存在的错。注册中心与配置中心Seata支持Nacos、Consul、Etcd、Eureka等。常见误区是“Nacos配了就算接入”实际上TC端和Client端的registry.conf、config.conf里要同时配置好地址和命名空间。之前有次排查半天发现TC注册到了Nacos但Client端从Nacos拉不到TC地址原因是两边Nacos的group不同。高可用TC可以多实例部署数据库存储模式下多个实例共享同一套事务表需要保证实例间能协调分支状态。这里就要规划好负载均衡或者域名让Client端能稳定访问任一可用TC。5.2 客户端的三个关键配置别漏了全局事务注解的生效范围Seata客户端接入多数时候只需要两步引入Seata依赖、在配置文件里指向TC地址和注册中心。但代码层面有三个细节非常容易出问题。一是GlobalTransactional和Transactional的嵌套关系全局事务注解要加在最外层发起方方法上参与方服务只保留自己的本地事务注解。如果发起方方法内部又自己开了本地事务注解嵌套层级错了很容易出现“全局事务ID没传到分支”或者“内层事务先提交、出问题后无法回滚”的现象。二是代理方式的限制Seata通过Spring AOP完成拦截如果方法调用发生在同类内部自调用AOP代理不会被触发全局事务就不会生效。这个和Spring事务注解失效的场景几乎一样。我自己就吃过亏在同一个Service里一个方法调另一个带GlobalTransactional的方法结果事务根本没开启排查了很久才发现是自调用问题。三是参数配置里的超时设置client.tm.commitRetryCount、client.tm.rollbackRetryCount这些参数决定了全局提交/回滚失败后的重试次数默认通常够用但业务高峰期最好结合TC日志调整。全局事务超时时间timeout默认是60秒如果你的业务链路包含外部接口调用要主动调大不然正常处理就被判超时回滚。5.3 真实踩坑链路事务失效、回滚失败、全局锁等待超时下面是我整理的三条高频踩坑链路每一条都写了完整的排查思路方便你以后照着走。链路一全局事务没生效异常不触发回滚现象参与方抛了异常但只回滚了自己库里的操作其它库的数据纹丝不动。排查步骤第一步看发起方方法有没有正确加GlobalTransactional以及这个方法的调用方是不是绕过了代理第二步看日志里有没有出现XID相关的打印。如果TM开启事务成功日志会记录到全局事务ID如果没有XID说明代理根本没生效第三步确认客户端配置文件是否正确指向TCTC的注册中心地址和客户端配置是否一致。这条链路90%的问题出在自调用或配置文件指向错误上。链路二回滚失败但业务代码没有任何报错现象全局事务标记为回滚服务也收到了回滚指令但数据库里的数据没变回来。排查步骤第一步查undo_log表有没有相关记录。没有记录说明一阶段就没写入快照可能是AT模式的SQL解析没覆盖到这条SQL第二步看undo_log记录的后镜像和当前库里的值是否一致。如果已经被其它事务改过AT回滚的冲突检测就会失败第三步检查表结构是否缺少唯一约束或索引导致反向SQL在执行时无法准确定位记录。链路三大量全局锁等待超时现象压测时发现接口耗时暴涨日志里频繁出现获取全局锁超时的报错。排查步骤第一步确认是不是同一行记录被多个全局事务并发修改。最简单的验证方式把并发降下来看耗时是否恢复第二步查TC的lock_table看锁记录是否存在堆积、以及被哪个XID持有第三步如果确认热点行冲突严重要么做数据分片、要么把该分支改成TCC模式避免全局锁长时间占用。这三条链路本质上是同一个问题的三个切面全局事务不只是框架组件更是一种需要监控、调优、和业务设计配合的架构决策。不要以为上了Seata就万事大吉仍然需要在数据库设计、接口超时、幂等控制上做好配合。6. 我的落地建议哪些场景优先Seata哪些场景绕开Seata如果这篇文章只能留下一段话我会把落地的建议浓缩成下面几条也是我用完一圈后的个人判断。在选型上短事务、多服务同步写、强一致要求高、团队又希望尽快上线的优先考虑Seata AT模式涉及资金、账户、库存这类核心资源且能接受手写补偿的优先考虑TCC模式流程特别长、外部系统参与、能容忍中间状态的才考虑Saga低并发、强一致且同构数据库的可以看XA但大多数场景我不推荐它。在方案选型上能异步就不要同步能幂等就不要依赖回滚能对账兜底就不要强一致。分布式事务本身是有成本的它不是用来解决所有一致性问题的银弹而是给那些不能用异步和幂等覆盖的场景兜底。最后提醒一句不管选了哪个模式都一定在开发环境搭一个“故障注入测试”。手动让某个分支服务抛异常、杀掉某个参与方服务、甚至直接把数据库连接断开观察全局事务是否按预期回滚。这比任何代码评审都管用能把绝大多数藏在角落的事务失效问题提前暴露出来。我每次接手新的分布式事务项目第一件事就是做这种故障演练做完心里才有底。