做分布式系统的同学,大概率被“分布式事务”这四个字锤过。尤其在订单、库存、支付这条核心链路上,数据不一致就是事故,用户投诉、对账失败、库存超卖,哪一件都够喝一壶的。业界聊得最多的两种方案,就是TCC和Saga。但很多朋友对这俩的理解停留在“TCC强一致,Saga最终一致”,真到选型的时候又拿不准:我这个业务到底适合哪个?TCC的空回滚、悬挂到底怎么处理?Saga在扣库存这种场景怎么防止超卖?这篇文章不打算写教科书式的概念复述,而是结合我自己的实践,把这俩方案的原理、适用场景、落地细节和选型决策串起来讲一遍。如果你正在做订单、库存、支付相关的分布式系统,或者准备改造现有的本地事务方案,这篇文章值得你花十分钟读完。
1. 两种方案的原理解读与适用边界
1.1 TCC的三阶段模型与“资源预留”思想
TCC不是什么高深的新概念,它就是把一个分布式事务拆成三个本地操作:Try、Confirm、Cancel。Try阶段负责预留资源,只占坑、不真正扣减;Confirm阶段在Try全部成功后执行真正的业务提交;Cancel阶段则是在任一Try失败后,把预留的资源释放掉。
用一个生活化的例子来理解:你去西餐厅吃饭,打电话预订位子,这是Try(只是替你占住桌位,还没消费);你到店落座、点菜、开吃,这是Confirm(真正发生交易);如果你突然有事去不了,打电话取消预订,这是Cancel(释放桌位)。整个过程里,“预订”这个动作保证了你到店一定有位置,同时餐厅也不会因为你放鸽子而白白空着桌子。
落实到订单与库存场景里,TCC的三阶段大概长这样:
- Try:创建状态为“待确认”的订单记录,同时预占库存。这里的关键是“预占”而不是扣减,库存表里加一个“锁定库存”字段,可用库存不动,锁定库存累加。
- Confirm:订单状态更新为“已确认”,锁定库存变成真正的扣减,也就是可用库存和锁定库存同时调整。
- Cancel:释放预占的库存,锁定库存减掉,订单标记为“已取消”。
TCC最核心的价值在于“资源预留”带来的强一致性。只要Try全成功,Confirm几乎不会失败,这是它敢说自己“强一致”的底气。但代价也明显:你必须为每个业务操作额外开发Try和Cancel逻辑,编码量翻倍,而且资源预留会拉高锁粒度,在并发激烈的场景下容易成为瓶颈。
1.2 Saga的两种模式与补偿语义
Saga则是另一种思路:把一个分布式事务拆成一串本地事务,让它们顺序执行,每个本地事务都对应一个补偿操作。正常情况下全部执行完,一旦中途某个环节失败,就反向执行已经完成环节的补偿操作,把数据“退回去”。它不预占资源,每一笔本地事务都是真实提交的。
Saga有编排和协同两种落地形态:
- 编排模式:有一个集中式的协调器,它知道整个流程的每一步,按顺序调用各个服务,并根据返回结果决定继续还是触发补偿。这种模式流程清晰、易于追踪,比较适合流程复杂、参与方多的业务,我自己也更推荐团队从编排模式入手。
- 协同模式:没有协调器,各个服务通过事件互相驱动。A服务执行完发事件,B服务收到事件执行自己的逻辑,再发下一个事件。这种模式去中心化,但流程隐式分布在事件流里,出了问题排查复杂度非常高。
Saga的优势是业务侵入少,每个服务只要实现自己的本地事务和补偿逻辑,就能搭出一条事务链路,对已有的业务代码改动比较小。但它的弱点是隔离性差:事务中间态对外可见,如果并发场景下另一个事务也去操作同一份数据,就可能出现覆盖和丢失更新。这也是为什么“Saga扣库存到底行不行”这个问题能吵这么多年。
注意:TCC和Saga解决的是“跨服务的分布式事务一致性”问题,但一致性语义不同。TCC是“尽力而为的强一致”,Saga是“最终一致”。选之前先把业务对一致性的容忍度想清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 订单与库存场景的拆解与事务需求
2.1 拆分业务链路:哪些必须强一致
很多人一遇到分布式事务就想着“全链路强一致”,这是最大的认知误区。真实业务里,订单、库存、支付这条链路上,每个环节对一致性的要求是不一样的。
拿一个典型的电商下单流程来说,用户可以拆成以下几步:
- 创建订单;
- 扣减库存;
- 用户支付;
- 通知仓库发货。
这四步里,订单和库存之间的数据一致性要求最高,因为库存一旦超卖,就是实际损失,补都补不回来。订单和支付之间则需要保证支付结果与订单状态对齐,但支付环节通常走第三方通道,天然就是异步和最终一致的。
所以,真正需要引入分布式事务的,往往是“订单创建 + 库存扣减”这个组合。在这个局部场景里,如果采用TCC,Try阶段预占库存,Confirm阶段真正扣减,任何一步失败都能保证库存不超卖;如果采用Saga,则需要先扣库存再创建订单(或者反过来),失败后通过补偿回滚。但Saga的补偿是“事后回滚”,在扣库存这个动作上存在中间态,就可能出现两个并发请求同时看到可用库存为1,两个都通过了首轮的检查。
我的经验是,先画业务流程图,把整条链路拆成原子操作,再逐个标记每个原子操作的“一致性容忍度”。“容忍度低”的操作集中在一起,用TCC包裹;“容忍度高”的操作通过异步消息或对账机制兜底。不要试图用一个分布式事务管住整条链路,那是把复杂度放大到不可维护的程度。
2.2 幂等是分布式事务的前提
不管选TCC还是 Saga,幂等都是前置条件。分布式环境下网络超时是常态,同一请求可能被重发好几次。如果接收方不幂等,库存多扣一次、订单多建一条,都是灾难。
TCC 的三阶段——Try、Confirm、Cancel——每个阶段都必须支持幂等。我常用的做法是在每个事务分支里维护一张 transaction_record 表,用全局事务ID + 分支ID 做唯一键。每次执行前先查这条记录,如果已经处理过就直接返回成功,不再重复执行。这个做法看起来简单,也确实最能扛压力测试。
Saga 的补偿操作也一样。今天你听到的“消息被重复消费”案例,有相当一部分就出在补偿逻辑没做幂等。比如退款补偿,由于网络原因补偿消息发了两遍,用户账户就被退了两笔钱。所以,在设计之初就把业务ID、请求唯一ID、幂等表这三件套设计好,比事后出问题了再修要省心得多。
3. TCC与Saga选型决策框架
3.1 对比维度与评估标准
选型不是拍脑袋,我习惯用五个维度对两个方案做量化对比:一致性要求、业务侵入程度、并发冲突概率、链路长度、团队维护成本。
| 对比维度 | TCC | Saga |
|---|---|---|
| 一致性语义 | 资源预留,较强的最终一致性 | 真正最终一致性 |
| 业务侵入程度 | 高,每个参与方都要实现Try/Confirm/Cancel | 低,只需实现本地事务和补偿逻辑 |
| 并发冲突处理 | 预留资源,冲突主要在Try阶段暴露 | 无隔离机制,冲突可能出现丢失更新 |
| 链路复杂度 | 链路越长,Confirm/Cancel的协调越复杂 | 编排模式下链路清晰,协同模式下复杂 |
| 典型落地成本 | 高,需要设计幂等和资源释放 | 中,重点在补偿流程设计 |
| 适用场景 | 库存、余额、预算等强一致写场景 | 跨多服务、允许中间态存在、对短时不一致有容忍度的流程 |
这个表不是标准答案,但能帮你把业务需求映射到方案能力上。我见过不少团队在“并发冲突不高的管理后台类系统”里硬上TCC,结果就是花了很多成本写Try和Cancel,收益却很低。这种场景其实用Saga就已经非常舒服了。
3.2 决策步骤:一步一步判断
与其听别人说哪个好,不如自己走一遍判断流程。我建议按下面这个顺序来做决策:
- 列出需要事务保护的具体操作集合,明确是跨数据库还是跨服务。
- 评估每个操作失败后对业务的影响:可接受中间态(比如用户能看到订单但支付还没回执),还是绝对不能出现(比如库存显示有货但实际已经扣掉)。
- 如果存在必须严格一致的写操作,优先TCC;如果所有操作都允许最终一致,选Saga。
- 评估参与方数量。参与方在4个以内,TCC还能撑住;超过4个,TCC的协调复杂度会急剧上升,这时我更倾向Saga的编排模式。
- 评估团队对异步和补偿的接受度。Saga的排错比TCC更难,因为没有全局锁,数据在中间态来回变化,日志追踪要做得更细。
这套流程走完,大多数业务是能明确导向某个方案的。真正难搞的是“部分强一致、部分最终一致”的混合场景,比如订单状态要强一致,但积分赠送可以异步——这时候就要做方案组合了:核心链路用TCC,非核心链路用本地消息表或者事务消息补齐。
3.3 落地TCC和Saga的工程要点
工程落地阶段,有几个要点无论如何都别省。
TCC方面,第一是预留资源要可靠。库存预占操作本身必须落库,不能只存在内存里,否则服务重启就全丢了。第二是Confirm和Cancel的幂等,这个前面说过,不再重复。第三是要有超时机制:某个参与方的Try一直不返回结果,事务协调器应该能通过超时触发Cancel,而不是无限等下去。
Saga方面,最要紧的是补偿事务要可追踪。每一次补偿动作都要记录流水,包括事务ID、步骤ID、补偿状态、补偿时间。否则线上出现数据不一致,你连“补偿到哪一步了”都查不到,那就真的两眼一抹黑了。其次,尽量用编排模式加状态机做流程控制,状态机能把整个Saga流程可视化管理,每一步走到哪、下一步是谁都一目了然,发生异常时也容易做人工干预。
注意:不管选哪种方案,都要预留“人工运维通道”。分布式事务做不到100%自动恢复,总有需要DBA或开发手动修数据的场景。把事务状态表和补偿日志在业务系统里保留充足的历史,就是给未来的自己留一条活路。
4. 常见问题与排查技巧实录
4.1 空回滚、悬挂与幂等冲突
TCC有几个著名的坑,理论上看书很难感受到,真正跑在线上时才会被折腾得怀疑人生。
空回滚:Try方法因为网络超时没执行成功,但Cancel被触发了。这时候Cancel发现没有对应的Try记录,就必须直接返回成功,而不是报错。我见过的错误做法是Cancel里查不到Try记录就抛异常,结果事务一直重试,越搞越乱。正确做法是在Cancel里把“没有Try记录”当成正常情况处理,同时记录下来,方便后面人工核对。
悬挂:和空回滚正好相反。Cancel已经执行完了,但迟到的Try请求才到达。如果这时候执行Try,就会预占一个永远不会被确认、也永远不会被主动释放的资源,这就是悬挂。解决办法是在Try入口先检查事务状态,如果发现事务已经终态(比如已取消),直接拒绝执行,不要落库。
幂等冲突:这个最容易出现在Confirm阶段的并发重试上。好在我坚持在每个分支记录 transaction_record 的唯一索引之后,这类问题基本被挡在门口了。但要特别注意唯一索引的字段选择——必须是全局事务ID + 分支ID 的组合,而不是单一业务ID,因为同一个业务ID可能对应多次事务尝试。
4.2 Saga丢更与恢复策略
Saga在实际项目中,最棘手的问题不是流程跑不通,而是并发场景下的数据丢失更新。
举个例子:一个用户同时下单买一件库存只剩1件的商品,库存服务同时收到两个请求。如果库存服务没有做并发控制,两个请求都读到了“可用库存=1”,都执行了扣减,最终库存变成-1。虽然Saga能补偿,但补偿的前提是能感知到扣减失败——如果你没做乐观锁或版本号校验,那这“失败”根本不会被发现。
我在项目中一般加一道乐观锁:库存表里加一个 version 字段,更新时带上 where version = #{oldVersion},如果影响行数为0,说明数据已经被别人改过,直接抛异常触发Saga的补偿流程。这个设计简单可靠,很多线上超卖问题就是被这行 where 条件挡住的。
另外,Saga的恢复机制要区分“自动恢复”和“人工干预”。网络抖动导致的失败,自动补偿大概率能处理;但如果是代码逻辑本身的bug,补偿也会失败。这时候事务状态机应该把流程标记为“需要人工介入”,同时推送告警。我之前踩过一个坑:补偿失败后没有及时告警,结果到第二天对账才发现数据缺口,排查成本翻了好几倍。所以,告警策略一定要和补偿状态机挂钩,失败重试N次之后必须有人接手。
写到这儿,TCC和Saga的核心差异和选型思路基本说完了。最后再分享一点我自己的体会:分布式事务没有银弹,选TCC还是Saga,取决于业务愿不愿意为“强一致”买单。如果公司有专业的中间件团队,能把TCC的框架打磨得很成熟,那就用TCC;如果团队不大、业务链路又长,我建议认真考虑Saga的编排模式——它更贴近真实的业务流,也更容易在出问题时快速定位。还有一个细节,不管是TCC还是Saga,第一版上线时一定要预留兜底对账任务,每天跑一遍一致性核对,比测试环境里压一百遍都更让人安心。毕竟,分布式事务的复杂度,只有线上流量才会真正教会你。
