1. 为什么单体时代的事务神话到了微服务就失效了先聊个有意思的插曲。最近有个热词特别容易让人误会——TCC。搞分布式事务的人看到TCC第一反应是Try-Confirm-Cancel三阶段补偿模型搞AI训练的人看到TCC想到的是NVIDIA显卡上的Tesla Compute Cluster模式。同一个缩写两个完全不同的世界。更有人拿着tcc跑大模型v100显卡 tcc改为wddm模式来问我怎么调分布式事务我只能哭笑不得地告诉他兄弟你那是显卡驱动模式的问题跟微服务里的TCC八竿子打不着。这事儿也侧面说明分布式事务这个领域名词术语的混淆陷阱实在太多今天这篇就把整条演进链彻底掰开揉碎讲清楚。回到正题。我在早期做电商系统时一个下单接口就能搞定一切扣库存、生成订单、扣余额全部包在一个数据库事务里要么全成功要么全回滚ACID四个字就保证了所有一致性。那个阶段根本没有分布式事务这个槽位因为系统压根没拆开。但业务规模往上走之后订单、库存、支付、积分各自拆成独立服务原本在一个事务里的操作被打散到多个服务、多套数据库甚至多种异构存储里——这个时候单体事务的ACID就彻底失效了。核心矛盾其实很简单分布式环境下网络是不稳定且存在延迟的任何跨节点的操作都无法保证瞬时完成并达成一致。你不能让订单库和库存库在同一个数据库连接里更没办法用BEGIN TRANSACTION包住一次跨服务的HTTP调用。于是分布式事务这个领域就诞生了。它的本质诉求只有一个在无法保证全局原子性的前提下尽可能让多个服务的数据最终达到一致。在展开具体方案之前先记住一个底层框架——CAP定理。在分布式系统中一致性Consistency、可用性Availability、分区容错性Partition tolerance三者不可兼得。分区容错是必须选的所以在一致性和可用性之间做取舍就催生了两条技术路线追求强一致性的2PC、3PC、TCC以及追求最终一致性的Saga、本地消息表、事务消息。这条演进链本质上就是一条从强一致向最终一致妥协的路线图。2. 分布式事务的重型武器2PC如何用两个阶段锁住全局先把最经典的2PCTwo-Phase Commit两阶段提交讲透因为它是后续所有方案的原点。2PC引入了两个角色协调者Coordinator和参与者Participant整个事务被拆成两个阶段准备阶段Prepare和提交阶段Commit。2.1 准备阶段先把所有参与者问一遍协调者向所有参与者发送Prepare指令各参与者收到后执行本地事务把修改写入事务日志并锁定相关资源但不真正提交然后向协调者回复我准备好了或我准备失败。这里有两个关键细节。第一参与者必须把事务日志持久化这是为了保证即使在准备阶段后节点宕机恢复后也能依据日志判断当时的决策。第二资源锁定是真实的——数据库的行锁、表锁都拿住了其他事务对这些数据的读写都会被阻塞。我见过不少刚接触2PC的同学误以为Prepare只是发个消息问一下实际上它是实打实地把资源攥在手里了。2.2 提交阶段全票通过才提交一票否决就回滚当协调者收到所有参与者的Prepare成功回复后进入提交阶段向所有参与者发送Commit指令各参与者完成真正的数据提交并释放锁。但如果任何一个参与者Prepare失败或超时未响应协调者就向所有参与者发送Rollback指令各参与者回滚本地事务并释放锁。这个设计保证了全局原子性要么所有节点提交要么所有节点回滚。从原理上看它直接继承了单体事务全有或全无的语义是分布式环境下最接近ACID的强一致性方案。但2PC有一个非常明显的痛点同步阻塞。在Prepare阶段锁定资源后所有参与者必须等待协调者的最终指令这个等待期间其他事务对这些数据的访问全部被阻塞。如果协调者宕机参与者无法自行决定提交还是回滚只能持续持有锁直到协调者恢复。在高并发场景下这种阻塞会迅速堆成雪崩。一句话总结2PC保证了强一致代价是可用性和性能。2.3 3PC加入超时机制但依然不是银弹3PCThree-Phase Commit三阶段提交在2PC基础上增加了一个CanCommit阶段并在参与者和协调者之间都引入超时机制减少阻塞时间。它把2PC的两阶段拆成了三阶段CanCommit、PreCommit、DoCommit。3PC的改进在于参与者在等待协调者指令时如果超时会默认执行提交而不是像2PC那样一直等。这解决了一部分协调者宕机导致锁死的问题但也带来了新的风险网络分区时部分参与者可能在没有收到协调者最终指令的情况下自行提交导致数据不一致。说实话3PC在实际生产系统中的使用率并不高。它引入的复杂度和收益不成正比而且它的强一致保证也远不如2PC纯粹。业界更常见的做法是直接用2PC的变种——比如XA协议——来支撑强一致场景或者直接跳到业务层面的TCC。3. TCC用业务补偿代替资源锁定的柔性方案从2PC到TCC是分布式事务演进链上最关键的一次思想跃迁从数据库层面的通用锁机制转向业务层面的显式补偿机制。TCC是Try-Confirm-Cancel的缩写要求每个参与者都实现三个业务方法Try阶段对资源进行预检查和预占用Confirm阶段真正执行业务提交Cancel阶段把Try阶段占用的资源释放。3.1 TCC的三个阶段到底在做什么拿最经典的订单库存场景举例。用户下单购买一件商品订单服务和库存服务都需要参与事务。Try阶段订单服务创建一条状态为待确认的订单记录库存服务预扣库存——注意是预扣不是真正扣减库存表中记录的已预占数量加1但可用库存不变。Confirm阶段订单服务把订单状态改为已确认库存服务把预占库存转为实际扣减。Cancel阶段订单服务把待确认的订单作废库存服务把预占数量释放回可用库存。这套模型要求业务方自己保证Try的操作可补偿Cancel能把预占资源释放掉、Confirm的操作是最终态一旦执行不可回滚。相比2PC纯粹依赖数据库锁TCC把资源锁定的粒度从数据库行级锁细化到了业务状态机吞吐量高得多而且不阻塞数据库事务。3.2 TCC的三个魔鬼细节空回滚、悬挂、幂等如果只看上面的流程TCC似乎很简单但生产实践里的坑一个比一个隐蔽。我在项目中至少踩过三次跟下面这三个问题相关的故障。空回滚Try阶段由于网络超时或服务宕机没有执行但Cancel阶段却执行了。Cancel里没有对应的Try记录无法回滚任何资源。解决方法是在Try和Cancel里都做幂等控制——Cancel执行时先查流水记录没有Try记录则直接标记为空回滚并忽略。悬挂Cancel先于Try到达。比如协调者因为超时提前发起了Cancel后面Try的请求才姗姗来迟。此时Cancel已经执行完了Try又跑来预占用资源这份资源就永远挂在账上没人能释放。解决方法是给每个事务生成全局唯一的事务ID在Try中检查Cancel是否已经执行过如果执行过则拒绝本次Try。幂等Confirm和Cancel在网络重传、协调者重试的情况下可能被多次调用。必须在方法实现里保证同一个事务ID只生效一次通常做法是引入事务状态表每次执行前查状态已经处理过的直接返回成功。这三个问题如果不在设计阶段就考虑到上线之后必炸。我见过一个TCC项目就是因为悬挂问题导致库存账目每月对不上最后排查了整整两周才发现是Cancel和Try的时序竞态。3.3 TCC不是万能药什么时候该用它TCC适合那种业务操作天然可拆分为预占和确认的场景比如库存预扣、资金预冻结。但它的落地成本很高——每个参与方都要实现三个方法每个方法都要考虑幂等和补偿业务侵入性强。如果要求分布式事务的业务方有七八个TCC的开发和维护成本会极其惊人。顺带再澄清一个热词梗前面提到的tcc模式正常wddm报错完全是显卡驱动层面的事NVIDIA的TCCTesla Compute Cluster模式只影响显卡的计算和显存使用方式与分布式事务的TCC没有任何关系。如果搞AI的同事跑来问你TCC怎么配事务你可以把两个TCC的区别讲清楚省得两边都糊涂。4. Saga放弃全局锁用正向事务反向补偿走完全程Saga模式最初是数据库领域的概念1987年就被提出后来被微服务架构重新发掘。它的核心思想是把一个长事务拆成一系列有序的本地事务每个本地事务都有对应的补偿事务。正常流程就顺序执行所有正向事务如果某个正向事务失败就逆序执行前面所有事务的补偿事务。4.1 编排式Saga由一个协调者统一指挥编排式OrchestrationSaga用一个独立的协调者服务来管理整个事务流程。协调者向A服务发起创建订单向B服务发起扣库存向C服务发起扣余额每一步根据结果决定下一步是继续正向操作还是转入补偿回滚。这种方式的优点是流程集中可控所有参与方的调用关系都在协调者那里一目了然出问题时也容易排查——直接看协调者的状态机走到哪一步了。缺点是协调者本身成了单点而且协调者与参与方之间的通信一旦失败整个事务状态就需要额外的对账机制来恢复。4.2 协同式Saga让每个服务自己编排后续动作协同式ChoreographySaga没有协调者每个服务完成本地事务后通过消息队列发出一个事件下一个服务监听该事件并执行自己的后续事务。比如订单服务创建订单后发布订单已创建事件库存服务监听后扣减库存再发布库存已扣减事件余额服务监听后扣减余额。协同式的优势是去中心化没有单点瓶颈扩展性好各个服务之间完全解耦。劣势是流程隐式分散在各个服务的事件订阅关系里全局状态极难追踪。一旦某个环节的事件丢失整个链路就会卡住而且排查起来非常痛苦——你根本不知道是哪个服务没发事件还是发的事件没被消费。我个人的建议是小团队、流程简单的场景可以先试协同式流程一旦超过五六个环节就果断转编排式。4.3 Saga和TCC的本质差异补偿vs 预占很多初学者分不清Saga和TCC我提供一个记忆锚点TCC的Cancel回滚的是Try阶段的预占资源Saga的补偿回滚的是前序事务已经完成的正向结果。举个例子Saga里扣库存就是一个真实的扣减操作如果后续订单取消就用一个加库存的补偿事务把库存加回来而TCC里库存扣减拆成了预占和确认扣减两步取消时把预占释放即可。直观感受是TCC更柔性因为预占阶段就发现了资源不足Saga更激进先干活后补偿所以Saga天然存在一个中间状态不一致的窗口期——余额已经扣了但订单还在创建中这个窗口期内的数据对用户是可见的。所以在一致性要求极高的资金类场景里Saga通常不作为首选但在流程长、环节多、每一步都是独立的业务操作的场景比如旅行预订、审批流、跨组织协作里Saga几乎是最优解。5. 最终一致性消息驱动的柔性事务把一致性交给时间如果说Saga还是在事务这个框架里做文章那么最终一致性方案干脆跳出了事务的思维定式不追求任何时刻的强一致只要系统在某个有限的时间窗口后能达到一致状态就行。这类方案的落地形态主要是三种本地消息表、事务消息、最大努力通知。5.1 本地消息表最朴素但最可靠的最终一致方案它的思路非常直白。在业务服务自己的数据库里建一张消息表业务操作和消息写入放在同一个本地事务里。比如订单服务创建订单时在同一事务里插入一条扣减库存的消息记录。事务提交后通过一个定时任务扫描消息表把未发送的消息投递到MQ库存服务消费消息后扣减库存并回调告知结果。这套方案的优点是实现简单、不引入额外组件每个服务只要管好自己的数据库事务就行。缺点是消息表和业务数据强耦合每次业务表结构变更都要同步考虑消息表逻辑而且定时扫描的轮询存在延迟实时性差。我在早期项目里用过多张本地消息表后来觉得维护成本实在太高就逐步迁移到了事务消息。5.2 事务消息把本地消息表下沉到MQ内部RocketMQ的事务消息是这一领域最成熟的产品化实现。它利用半消息机制生产者先把消息发送到MQ此时消息处于半不可见状态消费者无法消费然后生产者执行本地事务根据事务结果向MQ发送Commit或Rollback指令如果生产者一直没发指令MQ服务端会主动回查生产者的事务状态从而保最终确定消息的可见性。事务消息的优势是把消息表和业务事务从应用代码中剥离出来开发者不需要自己维护消息表也不用写定时任务框架层面保证消息和事务的一致性。但代价是你必须信任并依赖MQ本身的可靠性和高可用——毕竟消息没了整个事务链条就断了。我的经验是如果团队已经在用RocketMQ事务消息几乎不用犹豫就直接选如果用的是Kafka需要先确认版本是否支持事务消息Kafka的事务API存在不少使用限制要谨慎评估。值得强调的是无论用本地消息表还是事务消息下游消费方都必须做幂等。消息系统的投递是At Least Once语义意味着消息可能重复投递消费端必须用业务流水号或唯一索引把重复消息拦截掉否则扣两次库存这种事故就会真实发生。5.3 最大努力通知最轻量的诉求最宽松的保证最大努力通知适合那种丢了也没太大关系只要再查一次就能对齐的场景比如支付结果通知、短信发送状态回调。流程是上游不断重试发送通知直到下游确认收到下游收到后处理并返回确认如果长时间不确认上游就放弃。注意最大努力通知并不保证一定通知成功而是尽力而为。正因为如此它的应用范围很受限而且通常要配合下游的主动查询来兜底。比如支付平台回调商户系统失败商户系统可以定时主动向支付平台查询订单状态。这种方案理解成本最低却也最依赖业务侧的宽容度。6. 选型实操2PC、TCC、Saga、最终一致性到底怎么选前文把四种核心方案都过了一遍这一节直接给结论。先把核心对比整理成一张表方案一致性强度资源占用业务侵入性实现复杂度典型场景2PC/3PC强一致高锁资源低对业务透明中数据强一致、低并发、短事务TCC最终一致/准实时一致低预占补偿高三个方法高资金类、库存类高并发场景Saga最终一致窗口期不一致低无锁中正向补偿中长流程、多环节业务流程消息最终一致最终一致极低异步低只发消息低削峰填谷、状态同步、对账兜底6.1 按业务容忍度选型先问自己一个问题业务允许不一致的时间窗口是多长如果答案是一秒都不允许那只能上2PC类方案或者干脆重新审视系统设计看能不能把相关数据合并存储。如果答案是几秒到几分钟优先考虑TCC——它在性能上比2PC好一致性的收敛时间也快。如果答案是可以接受十分钟以上的延迟那Saga或者消息方案就够了成本和复杂度都更低。拿我做过的一个电商案例来说下单主流程用的是TCC库存预占、余额预冻结、优惠券预锁定三个参与方各自实现Try/Confirm/Cancel事务整体在秒级收敛。而订单完成后的一系列衍生动作——发积分、更新用户等级、同步到搜索索引——全部走事务消息最终一致可在几分钟内对齐。一个系统里同时用了TCC和消息最终一致这就是实践里的常态没有哪一套方案能包打天下组合才是常态。6.2 按性能瓶颈选型2PC的数据库锁是并发噩梦。我曾经做过一个压测2PC方案在50并发的时候数据库锁等待就明显增多200并发直接拖垮了事务响应时间。而把同样的操作改造为TCC后因为锁的粒度从行级缩到了业务预占2000并发都稳稳的。所以在写密集型、热点数据库存、余额场景中尽量避免2PC。Saga和消息方案因为是异步的对主链路几乎零影响但它需要额外的存储和消息组件支撑而且峰值时消息积压会导致一致时间窗口拉长——这个必须提前评估别等到双十一才发现消息堆了几十万条。6.3 按团队维护能力选型这一点容易被忽略但我想说它比技术选型更重要。TCC需要参与方每个方法都写对幂等和补偿业务逻辑一旦复杂很容易出现补补偿逻辑的漏洞。Saga编排式需要一个可靠的协调者服务需要团队具备状态机和持久化设计能力。消息方案则要求团队对MQ的运维和监控成熟度足够高。我的建议很务实选择团队最熟悉、最能驾驭的方案而不是理论最优的方案。分布式事务方案没有银弹任何一套方案都要靠持续的维护和对账机制来兜底。上线之后的一致性监控和对账脚本比选型本身更能决定系统的生死。7. 实战复盘从下单到支付一条事务链的完整落地纪要最后用一个我近期打磨过的完整案例来收束全文。这个项目是典型的订单库存支付积分四服务联动一致性要求从强到弱分成三个层级。第一层级是核心交易链路下单预占库存、预扣用户余额采用TCC。具体到实现库存服务的Try是写一条预占流水并把可用库存在缓存中预扣Confirm把预占流水转为正式扣减流水Cancel把预占流水作废并回补可用库存。所有操作都带全局事务ID每次执行先校验幂等记录同时在Cancel里做了空回滚和悬挂防护。第二层级是支付结果同步支付成功回调通知订单服务更新状态。这里用的是事务消息支付服务发一条事务消息订单服务消费后更新订单状态消费逻辑严格幂等——订单状态表加唯一订单号索引重复消息直接丢弃。第三层级是衍生动作积分发放、用户等级更新、搜索索引同步。这些动作允许较大延迟走的是普通MQ消息加上定时对账任务扫底每天凌晨对账脚本比对数据库和MQ的积压数据自动补齐漏发的消息。这个案例跑了一年多最大的收获不是某个具体方案的效果而是对一致性这件事的心态转变从企图在技术层面消灭所有不一致转为接受不一致的存在用工程手段控制它的窗口和影响范围。最终一致性不是不用事务了而是把一个大事务拆成多个普通事务再用消息和补偿把它们串起来。理解这一点回头看单体事务的ACID就会明白分布式事务的演进链本质上是一场从整体原子性到分步可控性的跃迁。最后再分享一个压箱底的经验无论采用哪个方案上线前一定把异常注入测试做足——模拟参与者宕机、网络超时、消息丢失、重复投递这四类故障逐个验证补偿逻辑是否兜得住。我见过太多项目上线时一切正常第一次真实故障就把补偿链路打穿的案例。分布式事务的战场不在正常流程里而全藏在异常分支里。