订单库存分布式事务实战:TCC方案、幂等控制与对账兜底
直接起因是有次线上促销订单库和库存库分开部署下单链路在扣减库存那一步超时了重试又把库存扣成了负数。那天晚上我和运维盯着监控到凌晨两点一边拿脚本手工修数据一边被运营催着恢复活动。从那以后我就意识到分布式事务这个问题不解决业务做得再花哨也迟早被数据一致性反噬。这篇文章就是把我这些年处理订单与库存这类分布式事务场景的经验完整记录下来从方案选型到TCC落地再到故障排查希望能帮正在这个泥潭里挣扎的同学少走些弯路。1. 订单与库存场景为什么天然逃不开分布式事务1.1 微服务拆分把本地事务拆没了最早做电商系统的时候订单、库存、账户是在同一个数据库里的一个下单请求对应一个本地数据库事务扣库存和生成订单要么一起成功要么一起失败数据库的ACID帮我们把所有脏数据问题都挡在了门外。后来业务大了必须拆库拆服务。库存服务单独部署订单服务单独部署账户服务又单独部署。这时候一个下单动作要跨三个服务、三个数据库才能完成原本那个完整的本地事务被物理切成了三段。你订单建好了库存扣减失败了怎么办库存扣减成功了但订单没建成功怎么办这些跨服务的数据不一致问题就是分布式事务要解决的。很多人上来就背CAP定理、BASE理论但真正在业务里摸爬滚打之后你会发现理论只是给了你一个方向落地的时候全是细节。订单和库存这个场景最典型的特征就是实时性要求高。用户点完下单按钮你不可能让他等10秒再去对账他等不了所以你不能直接拿最终一致性方案来处理整条链路得分层设计。1.2 一个下单请求背后有多少个一致性风险点我把一次下单的完整调用链拆开给你看创建订单主记录状态为待支付调用库存服务扣减库存锁定库存数量调用账户服务锁定用户账户余额或信用额度如果用了优惠券还要调用营销服务把券状态置为已使用最后向消息队列发送一条订单已创建的消息触发后续的支付结果监听、超时关单等流程这五步里任何一步失败都可能导致数据不一致。最经典的是第三步扣库存成功了但第二步创建订单反而失败了——注意这里顺序还可能颠倒——这时候库存被扣了订单却不存在用户那边看到的还是下单失败请重试但库存在系统里已经少了。到了大促高峰期网络抖动一多这种不一致就会被放大到肉眼可见的程度。1.3 顺序问题先发消息还是先提交事务还有一个非常隐蔽的坑就是数据库操作和消息发送的顺序问题。很多初学分布式的人会这样写先UPDATE订单状态再发MQ消息觉得这两步没什么问题。但如果没有把消息发送包在事务里一旦消息发送成功但事务回滚了下游消费者拿到的就是一条幽灵消息——它对应的业务数据根本不存在。反过来如果先发消息再提交数据库事务那消息先到了消费者查订单发现还没创建又变成了一次无效调用。这个经典的「先有鸡还是先有蛋」问题在分布式事务方案里都有一个明确的解法后面我展开说。2. 常规分布式事务方案对比为什么最终选了TCC2.1 强一致方案为什么在互联网场景里不好用先说说那些听起来很简单的强一致方案比如两阶段提交2PC。2PC的核心思路是引入一个协调者先让所有参与者做Prepare都准备好了再让协调者发Commit任何一个人没准备好就整体回滚。这个方案理论上很完美但落地到我们这种高并发订单场景就有几个硬伤协调者是单点它一挂整个事务链路直接卡死资源锁定时间太长Try阶段就把库存锁住等Commit这个过程中其他用户根本动不了这批库存参与者阻塞网络抖动导致协调者和某个参与者失联的时候所有资源全部挂起3PC虽然有改进但仍没有解决本质问题——强一致协议太依赖网络质量而互联网环境最不可靠的就是网络。所以你会发现真正核心的交易链路上很少看到裸用2PC的方案。2.2 TCC和本地消息表、事务消息的分工逻辑业界主流的落地方案其实就三大类TCC、本地消息表或者升级成事务消息、最大努力通知。TCC的核心思想是把一个业务操作拆成三个动作Try阶段只是预留资源Confirm阶段真正执行Cancel阶段释放预留的资源。它比2PC好在Try阶段做完之后不需要把资源一直锁着Confirm和Cancel可以异步执行而且它的资源锁定是业务层面的粒度可以控制得非常细。本地消息表核心是消息和业务操作同库同事务写业务数据的同时把消息也写到一张表里一起提交然后靠后台任务轮询把消息发给MQ。这方案能保证不丢消息但存在一个明显的问题是本地消息表会随着业务量增长变得越来越重而且无法解决下游消费失败后业务侧的补偿问题。事务消息的典型代表是RocketMQ事务消息本质上是把本地消息表搬到了MQ服务器端用半消息机制来保证事务一致性。它比自建消息表更省心但同样有局限只解决了上游发消息和业务操作的一致性下游消费方要做的是自己幂等。2.3 我在订单与库存场景里的取舍标准给你看看我当时的决策依据做了个对比方案一致性强度性能损耗开发成本适合场景2PC/3PC强一致高中金融核心账务、数据库同构场景TCC最终一致业务补偿中高高频交易链路、资源预留敏感场景本地消息表最终一致低中异步解耦、下游可重试场景事务消息最终一致低低上游业务与消息发送一致性场景最大努力通知最终一致尽力而为低低对账要求不高的辅助场景订单扣库存这个链路我最终选了TCC为主、消息对账为辅的混合方案。理由就三条第一库存扣减天然适合Try/Confirm/Cancel的模型——Try阶段预占库存、Confirm阶段正式扣减、Cancel阶段释放库存逻辑非常清晰不会出现那种为了套TCC而硬造的别扭设计。第二订单主流程是同步链路用户需要立即看到结果用TCC可以做到一旦扣库存失败订单即刻返回失败不用等待异步对账。而本地消息表方案在订单创建成功但下游处理失败的情况下用户看到的可能是下单成功但实际上库存没锁住这是不能接受的。第三参与方数量可控。订单、库存、账户、优惠券参与方不多TCC的协调成本可以接受。如果一条链路要串联十几个服务我会直接放弃TCC改成SAGA或者纯消息驱动。3. TCC落地过程中最常踩的三个坑空回滚、幂等、悬挂3.1 空回滚你Cancel了一个根本没执行过的TryTCC有个非常经典的坑叫空回滚。场景是这样的TCC框架在执行Try阶段时因为网络超时框架认为Try失败了于是触发了Cancel逻辑。但事实上Try请求可能根本没有到达库存服务或者到达了但被负载均衡转给了另一台机器库存服务压根没执行过资源预留。这时候Cancel跑过来企图释放库存就是空回滚。如果Cancel不判断Try是否执行过就直接释放就会出现库存变成负数的问题——你释放了一笔根本不存在的预占。我见过有人写Cancel逻辑直接是库存数量1完全没想过Try可能没执行结果线上库存数据彻底乱了。解决办法是设计一张事务控制表每笔分布式事务生成一个全局事务ID所有分支操作都要在控制表里留下操作痕迹。Cancel执行之前必须查控制表确认对应的Try操作确实执行过如果没有直接忽略本次Cancel。我见过很多TCC框架比如比较流行的几个开源框架内部其实已经做了类似处理但如果你是自己手写TCC这个控制表绝对不能省。举个例子假设你的控制表叫tcc_transaction_control核心字段就是全局事务ID、分支事务ID、Try状态、Confirm状态、Cancel状态。Try执行前插入一行状态标记为已TryCancel执行时先查这行如果查不到就返回成功什么都不做。这套逻辑听起来简单但漏掉的人比想象中多。3.2 幂等问题重试导致的重复执行是常态第二个坑是幂等。分布式系统里超时之后重试是常态所以Try、Confirm、Cancel三个方法都必须做到幂等。你想想库存服务接到同一个订单的两次Try请求第一次预占了5个库存第二次又预占了5个库存那用户什么都没多买库存却少了10个这不就是bug吗。幂等设计的通用套路是在每个分支方法里先用全局事务ID去查操作记录如果已经执行过就直接返回上一次的结果不要重复操作。这里推荐直接用数据库的唯一约束来兜底比如在库存扣减流水表里对订单号加唯一索引重复请求触发到唯一键冲突直接catch住当成功处理这样即便并发请求同时打过来数据库也能替你把关。我踩过最典型的坑是代码里手工查了操作记录发现已经执行过就直接return以为这就够了。但两个请求并发过来都查到没执行过然后都去扣库存绕过了那层判断。所以必须把判断和操作放进同一个数据库事务或者依靠唯一键冲突。我在库存明细表里加了一个order_id warehouse_id sku_id联合唯一键这一下就根治了。3.3 悬挂问题Cancel先到了Try还没来第三个坑是悬挂比空回滚更隐蔽。空回滚是Try压根没执行而悬挂是Try延迟到达——Cancel都已经执行完了Try请求才慢悠悠地过来。这时候如果你在Try里不做任何判断直接预占库存那这笔预占就永远悬挂在那儿了后续Confirm不会来Cancel也不会再来库存就白白锁死了一部分。为什么会出现这种情况因为TCC框架的Try和Cancel一般是不在同一线程里执行的。Try可能因为网络问题在MQ或者网关里排队而Cancel因为是补偿逻辑走了另一条链路反而先到。我实际遇到的情况是某个时段数据库连接池满了Try请求被阻塞在连接池等待队列里而Cancel请求因为走了另一个轻量级连接池先执行完了等Try挤进去的时候Cancel早跑完了。解决悬挂的手段还是要回到那张控制表Try执行之前同样要查Cancel的记录如果发现Cancel已经执行过了Try直接丢弃不再做任何业务操作。简单来说Try和Cancel在业务方法入口处都要互相检查对方的状态谁先执行谁就拥有最终话语权。为了说清楚这三件事的关系我整理了一张内部培训用的逻辑表异常类型现象根因防护手段空回滚Cancel执行时Try未执行网络超时/事务框架误判Cancel前置检查Try状态重复执行Try/Confirm/Cancel被多次执行超时重试/消息重复投递操作表记录唯一键约束悬挂Try到达时Cancel已执行网络延迟/执行顺序颠倒Try前置检查Cancel状态这三个坑只要有一个没处理干净TCC方案上线后大概率会在某个大促节点给你惹出大事。我在生产环境全部解决掉之后才敢把流量逐渐放进来前两周每天人工盯對账单盯着看有没有异常。3.4 控制表设计的一个完整示例既然这张控制表这么关键我把字段设计放出来完整的建表语句CREATE TABLE tcc_transaction_control ( id bigint(20) NOT NULL AUTO_INCREMENT, global_tx_id varchar(64) NOT NULL COMMENT 全局事务ID, branch_tx_id varchar(64) NOT NULL COMMENT 分支事务ID, biz_type varchar(32) NOT NULL COMMENT 业务类型如ORDER/STOCK/ACCOUNT, try_status tinyint(4) NOT NULL DEFAULT 0 COMMENT Try状态0未执行1已执行, confirm_status tinyint(4) NOT NULL DEFAULT 0 COMMENT Confirm状态0未执行1已执行, cancel_status tinyint(4) NOT NULL DEFAULT 0 COMMENT Cancel状态0未执行1已执行, try_time datetime DEFAULT NULL COMMENT Try执行时间, confirm_time datetime DEFAULT NULL COMMENT Confirm执行时间, cancel_time datetime DEFAULT NULL COMMENT Cancel执行时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_global_branch (global_tx_id, branch_tx_id), KEY idx_biz_type (biz_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENTTCC事务控制表;每个参与方的方法里第一步都是往这张表里插入一条状态为Try已执行的记录第二步才是真正的业务操作。Confirm和Cancel执行前都需要在同一个数据库事务里先查控制表再执行业务操作。只有这样才能结合数据库的锁机制彻底避免并发判断失效的问题。4. 对账兜底任何分布式事务方案都甩不掉的安全网4.1 为什么TCC保证不了百分之百一致你可能觉得TCC解决掉空回滚、幂等、悬挂这三个问题是不是就万事大吉了不是的我给你说个真实场景。某个分支服务的数据库磁盘满了Confirm执行到一半数据库事务回滚了但TCC框架记录的状态已经是Confirm已执行。后面TCC重试因为状态是已执行它不会再去重试Confirm。但是实际业务数据并没扣成功——这就是框架状态和真实业务状态之间的缺口。还有网络抖动导致Confirm成功返回了但确认响应在传输途中丢了TCC框架认为Confirm没执行于是走Cancel结果把已经确认的资源又释放了一遍。这种情况虽然时间窗口很短但造成的后果是订单已经创建成功了库存却被释放了用户下单成功了可库存实际上没有锁住。只要一个系统里没有超能力这种极端情况就一定会出现。所以我说TCC解决的是95%的常规问题剩下的5%必须靠对账兜底。4.2 对账系统的核心是流水表和状态机做对账不要想得太复杂核心就是两个东西一条完整的业务流水和一套明确的状态机。我先说流水。每一笔订单从创建开始每一个服务的关键动作都要落一条流水流水里带上订单号、全局事务ID、操作类型、操作时间、操作前后的数据快照。拿库存服务举例库存扣减流水表至少应该有这些字段全局事务ID订单号SKU编码变更前库存数量变更后库存数量变更类型预留/扣减/释放/回补关联的TCC控制记录ID操作时间为什么要记录变更前和变更后的数据快照因为对账发现问题之后你要能判断这笔库存到底是怎么变成现在这个数字的。没有快照你只能看到结果推断不出过程排查起来会非常痛苦。状态机就更直观了。一笔订单的主状态应该是待支付、已支付、已取消库存状态应该是无、已预留、已扣减、已释放账户状态应该是无、已锁定、已扣款、已退回。对账的核心逻辑就说一句话把同一笔全局事务ID对应的所有参与方状态拉出来比对看它们是否符合状态机的流转规则。比如订单是已支付库存却是已释放那必有异常订单是已取消库存却是已扣减也必有异常订单已支付账户却是未扣款同样有问题。把这些规则配置成对账SQL每天定时跑一遍异常单据自动进人工处理队列。4.3 对账任务应该怎么设计和跑我按经验给你一个可执行的参考方案。第一层对账是准实时对账。每5分钟拉一次过去15分钟内创建的事务的全部分支状态比对不一致的直接进异常队列触发业务告警。为什么拉15分钟前而不是最近5分钟因为要给分布式系统留出状态扩散的时间差避免大量误报。这个频率对准交易场景够用了不会给数据库造成太大压力。第二层对账是每日全量对账。凌晨两点业务低峰期把前一天所有的订单、库存流水、账户流水做全量状态比对。这个比的是数据完整性防止有些异常一直没被发现、一直挂在系统里。全量对账要跑多久取决于数据量我在千万级订单量的系统上跑大概半小时左右。第三层对账是人工抽查。每天从异常队列里随机抽10%的单据DBA或业务研发手工核对。别觉得这是在浪费时间有些异常是自动对账规则发现不了的——比如两边状态都符合状态机但业务逻辑其实是错的。有个真实的教训有次促销活动配置买一送一技术侧在订单服务和库存服务各写了一遍赠送逻辑两边都对对账也通过。但后来运营改了活动规则只改了商品中心没同步到库存服务导致库存赠送逻辑和订单赠送逻辑不一致。自动对账看不出问题因为两边状态机都是正常的最后靠人工抽查才发现了一批订单只赠了库存没赠订单金额。从那以后我规定凡是改动过营销规则当天必须人工复核一批关联订单。4.4 对账发现异常之后怎么处理对账发现不一致之后处理策略按严重程度分三类。自动补偿级别比如订单已取消但库存还是预留状态对账程序直接触发一次Cancel或释放库存操作。这种操作的前提是Cancel逻辑幂等重复执行不会出错。半自动级别比如订单已支付但库存没有扣减大概率是前面说的框架状态和业务状态不一致问题。系统先把单据标记为异常发告警然后自动重放一次Confirm操作。重放之前会做一次预检确认订单确实已支付、库存确认没扣过再执行。人工介入级别比如库存流水丢失、全局事务ID查不到对应记录、数据快照对不上这些必须人工处理。人工处理的前提是流水足够完整DBA能根据快照手工补正数据。我见过不少团队对账发现了问题但流水不全根本没法还原现场只能拿最终一致的数据拍脑袋修这种最痛苦。对账异常处理一定要有独立的权限体系不能用一个普通账号去执行补偿SQL。分布式事务里的补偿操作本质上是在改钱改货操作审计必须严格出过问题就麻烦了。5. 真实踩坑复盘一次库存超扣事故的完整排查链路5.1 事故现象去年大促期间我负责的系统突然接到一堆用户投诉说下单之后收到的库存扣减短信比实际购买数量多。紧接着运营反馈后台显示的当日成交件数和库存系统扣减数差距越来越大。库存系统的管理员说没有人工干预过数据却对不上。那是一个周五下午三点大促还在进行中每分钟都有几千个订单在走TCC链路。我当时的第一个反应不是改代码而是把TCC控制表、库存流水表、订单信息表三个来源拉在一起按全局事务ID逐单核对。5.2 排查链路第一步定位差异单据范围我先把最近一小时库存扣减流水和订单创建流水做了关联用LEFT JOIN找出那些库存有扣减记录但订单状态不是有效状态的单据。结果发现异常单据足足有几百条而且有一个共同特征这些单据的全局事务ID出现在库存流水表里超过两次。正常情况下一笔全局事务ID在库存流水表里应该是两条记录Try预留一条Confirm扣减一条或者Try预留一条Cancel释放一条。但异常单据有的出现三条、四条最多的一条出现了七条记录。数据一拉出来问题范围就清楚了。5.3 排查链路第二步定位到框架重试和业务幂等的冲突我用全局事务ID把库存服务端日志和TCC框架日志拼起来看到了这样的时间线18:32:10.100Try请求到达库存服务预占库存成功写入控制表18:32:10.350TCC框架发出Confirm请求库存服务收到并扣减库存也成功18:32:10.380Confirm的响应返回超时实际上库存服务处理成功了是响应包在网络上丢了18:32:10.390TCC框架重试Confirm18:32:10.391库存服务再次收到Confirm请求因为库存服务端在Confirm方法里没有做幂等校验第二次Confirm又扣减了一次库存第十八分钟三次、四次重试陆续到来每次都在扣减库存这个问题的根因清晰了Confirm重试导致的重复扣减根源是业务方法没有做幂等。我的幂等控制表虽然设计了但只在Try和Cancel里做了检查Confirm方法里查到记录就直接走了没有检查这条记录是不是已经被Confirm执行过。框架层的状态判断和业务层的实际执行存在两个不同的时间点中间差了那几百毫秒重试就钻了这个空子。5.4 排查链路第三步修复方案和上线验证修复分两步。第一步是止血。我手动跑数据修复脚本把重复扣减的库存加回去然后修改Confirm方法的逻辑执行扣减之前必须先查控制表确认状态为Try已执行且Confirm未执行才允许扣减。同一时间用数据库事务保证检查状态和更新业务数据两者原子执行。第二步是防复发。在库存扣减流水表上增加唯一约束以全局事务ID, 操作类型作为唯一键。这样即使业务层判断漏了数据库层也会把重复扣减挡住抛唯一键冲突后捕获异常当作已执行成功处理。上线验证跑了两周最直观的数据是异常对账单据从每天几十条降到了零。而且我还发现这个唯一键约束不仅防了TCC重试的重复扣减还顺便挡住了另一类问题——上下游系统自己发起的重试调用简直是一箭双雕。5.5 第二条事故线消息重复消费导致重复入账同一个月里我们还遇到一次账户余额重复入账的事故。某支付回调消息因为消费方处理超时MQ重投了两次消费方没有做幂等直接给用户账户加了两笔钱。这次事故和库存超扣本质上是一类问题下游系统重复处理了同一笔业务语义上的同一个操作。TCC框架本身不管你下游的MQ消费者是否幂等你能做的就是在每一个入口都做好幂等设计。支付回调消费的幂等方案是支付结果表加唯一约束以支付流水号交易类型作为唯一键。消费时先尝试插入插入成功了才处理业务插入冲突就直接返回成功。这套方案花了一个小时就上线了再也没出现过重复入账。库房的人一开始还不太理解为什么多了个表我和他们解释就像你去餐厅吃饭服务员上菜之前先看订单上有没有盖过章盖过章的菜不上第二次。这两条事故线给我的最大教训是分布式事务框架管的是跨系统的一致性幂等设计管的是单系统内的重复防护两者缺一不可。你哪怕把TCC玩出花来下游一个MQ消费重复就能把数据搞脏。6. 事务消息本地消息表在订单场景里的具体配合6.1 哪些环节适合用事务消息替代TCC前面说了TCC是主链路的核心但并不代表所有环节都适合TCC。TCC有个特点——每个参与方都要实现Try、Confirm、Cancel三个方法开发成本高而且对于那种不需要立即回滚的场景用它就是杀鸡用牛刀。订单链路里哪些环节适合用消息驱动的最终一致性我按照我的经验列一下订单创建成功后的异步通知比如发送订单创建短信、推送App站内信这些动作晚几秒到达完全没关系用事务消息最合适库存扣减成功后触发下游的采购补货提醒这个不需要实时晚几分钟也没问题订单支付成功之后触发发货单生成这个可以接受短暂延迟但消息不能丢事务消息能保证这一点这些环节的共同特征就是业务动作的主流程已经完成了后续动作只是锦上添花失败了下游可以通过重试补上不补也不会造成资损。6.2 本地消息表在订单确认环节的一个实用案例我举一个用本地消息表解决的经典场景订单创建后需要给用户发放积分但积分服务单独部署且经常抖动。如果每次下单都同步调用积分服务积分服务一抖动订单就创建失败了这不可接受。所以我把发积分改成异步订单创建事务里除了插入订单表同时插入一条待发放积分消息到本地消息表两者同库同事务提交。后台任务每秒扫描本地消息表把消息投递到MQ。投递成功之后消息表里那条记录标记为已投递但如果后续积分服务消费失败或者确认失败消息还是会被重新扫描重新投递。为了避免重复投递导致积分重复发放积分服务消费时必须以订单号积分类型做幂等查不到积分发放记录才真正发放。这套方案没有引入任何跨服务事务框架成本极低却解决了订单创建和发积分不一致的问题。本地消息表方案适用于你不想改造成TCC、但又需要跨系统数据最终一致的场景。不过随着数据量增长本地消息表会越积越大必须定期归档否则查询性能会拖慢主流程。6.3 事务消息和TCC可以组合使用我在一个复杂的业务链路里经常混用两种方案。举个例子下单主流程里订单、库存、账户用TCC保证实时一致性下单成功后触发积分发放、短信发送、物流预分配这些走事务消息最终一致。这么设计的好处是主链路需要强一致投入高成本非核心链路允许最终一致只花很小的成本。很多团队把分布式事务方案当成一刀切来选要么全部不用要么全部消息化要么强制所有接口都实现TCC其实都是没想清楚业务的核心诉求是什么。我见过最头疼的项目是把所有服务都套上TCC框架结果大量不必要的代码开发效率极低排查问题还得掀开TCC框架的日志一层层找得不偿失。正确姿势是核心交易链路上的核心服务必须做分布式事务非核心的、异步友好的、可以容忍延迟的环节用事务消息驱动就足够了。7. 关于分布式事务的几个反直觉认知异常永远比你想的多7.1 本地事务保证的ACID分布式事务一个都别想完整最后聊几个认知层面的东西。很多人对分布式事务的期待是像本地事务一样好用这个期待本身就错了。分布式事务能保证的一定是最终一致而不是瞬时一致。无论是TCC、SAGA、事务消息本质上都是通过先做一部分业务动作再根据后续状态决定是确认还是补偿来实现最终一致的。这个思路有一个很关键的副产品你必须在业务的每个阶段都保留可补偿的能力。比如库存扣减Try阶段是预留不是真正的扣减就是为了给Cancel留余地订单状态更新每一步都记录上一步的完整状态就是为了能回滚。这种处处留后路的设计比追求一步到位的强一致方案更符合分布式系统的本质。7.2 异常场景不能靠永远不会发生来规避做分布式系统久了你会发现一个规律任何异常场景只要你没在代码里显式处理它就一定会在你最忙的时候发生。数据库挂了、磁盘满了、网络分区了、消息丢了、CPU飙升了、磁盘IO爆了、服务被防火墙误杀了、程序员的同事上线时手滑删了表——这些情况我都遇到过。所以我在设计分布式事务方案时永远有一条原则考虑异常的时候假设任何一步都有可能失败、任何一次调用都有可能超时、任何一条消息都有可能重复。所有方法都做幂等所有状态机都有异常分支所有对账规则都考虑状态互相组合的穷举。7.3 架构师的真正价值在于设计可诊断性分布式事务排查起来比普通bug难得多因为你手头的信息是分散在几十台机器上的日志、数据库里的流水、MQ里的消息轨迹。我有一次排查一个旷日持久的数据不一致问题最后定位到根源是某个服务的时钟偏差导致时间戳判断错误整整花了两周。从那以后我在所有分布式事务相关代码里强制要求打全链路日志全局事务ID、分支ID、每个阶段的入参出参、每一步状态变更。日志是后端排查的唯一地图没地图你就是在黑屋里乱摸。另外所有流水表必须保留历史版本不能原地更新覆盖否则哪天对账对不上了你连当时的原始数据都找不回来。很多团队把分布式事务的难点理解为选型困难其实真正难的是方案落地之后的可观测能力和可运维能力。你方案选得再对出了问题查不出来、修不了一样是事故。7.4 最后一个实用建议根据我自己的踩坑经验最后给准备上分布式事务的同学几个关键建议第一先梳理业务链路的每一条分支把所有可能的失败场景列成清单再开始选型。不要一上来就选TCC如果你的业务链路全是异步友好型事务消息也许就够了。第二TCC方案务必先解决空回滚、幂等、悬挂这三个问题再进行全量发布。这三个问题在方案设计阶段就要明确落进代码里不要指望上线之后靠对账去发现和补救事故的成本会远远高于开发的成本。第三把对账系统当成分布式事务方案的一部分来建设而不是事后补充。对账规则要和业务需求一起评审上线第一天就开始跑积累基线数据。等出了事故再补对账你连正常的数据长什么样都不知道怎么判断什么是异常第四不要迷信任何框架。框架保证了流程引擎的可靠性但业务方法的幂等性、控制表的设计、对账规则的完备性全都得你自己负责。框架只是工具一致性永远需要人脑来兜底。分布式事务这条路没有一劳永逸的银弹。我做了这么多年踩了无数坑最深的体会就是方案的复杂度不是靠选择哪个框架来解决的而是靠对业务链路的深刻理解、对异常场景的充分敬畏、以及对数据可诊断性的持续投入。希望这篇基于真实订单与库存场景的实践记录能帮你少走一段我走过的弯路。