分布式事务解决方案详解:XA、AT、TCC、Saga、本地消息表与事务消息
下单要同时写订单库、库存库、支付库,任一环节失败如何整体收场?单机事务管不了跨进程。本文从 2PC/XA 的阻塞与单点讲起,拆解 TCC 的补偿本质与空回滚/悬挂两大经典坑,讲透本地消息表与 RocketMQ 事务消息的最终一致路径,梳理 Saga 与 Seata 的定位,最后给出方案对比与选型思路。
它解决什么问题
微服务按业务边界拆库之后,一个”下单”动作横跨订单、库存、支付多个服务,每个服务都有自己的数据库。单机事务的 ACID 由单个数据库的 redo log、undo log、锁与 MVCC 保证,一旦操作跨进程跨库,数据库各自提交、互不知晓,”要么都成功要么都失败”这个前提就不存在了。分库分表场景同理:同一业务的表散落在不同物理库,本地事务同样失效。
有人会说:那把 2PC 搬过来不就行了?问题是 2PC 在整个提交过程中长期持有数据库锁,吞吐直线下降,协调者还是单点。分布式事务工程上的真实目标,是在一致性、可用性、性能、开发成本四者之间取一个业务能接受的点——这也是为什么 BASE 与最终一致性才是互联网场景的主流答案。
核心原理
从 ACID 到 BASE
ACID 里 A、I、D 是数据库能提供的手段,C(一致性)其实是应用层属性——数据库保证原子与隔离,应用借此达到业务意义上的”总额不变”。跨服务之后,应用层的一致性必须靠协议或补偿显式构建。于是方案分两个阵营:刚性事务(2PC/3PC/XA)追求强一致,柔性事务(TCC/Saga/消息方案)接受中间态、追求最终一致。
2PC 与 XA
两阶段提交把全局事务拆成准备与提交两阶段:准备阶段协调者(TM)询问所有参与者(RM)”能否提交”,各 RM 执行本地事务、写 redo/undo log 但不提交,回复 Yes/No;提交阶段若全员 Yes 则 TM 广播 Commit,否则广播 Rollback。XA 是 X/Open 定义的接口规范,标准化了 TM 与 RM 之间的交互,MySQL InnoDB、Oracle、PostgreSQL 都支持,业务代码几乎无感。
代价同样清晰:
- 同步阻塞:准备阶段结束后到提交完成,参与者一直持有锁和资源,其他事务排队。
- 协调者单点:TM 在准备完成后宕机,参与者卡在”已就绪”状态进退不得,只能等 TM 恢复。
- 提交阶段不一致:网络分区下部分 RM 收不到 Commit/Rollback 指令,出现部分提交。
3PC 把准备阶段拆成 CanCommit 与 PreCommit 并引入超时机制,缓解了 TM 宕机后的永久阻塞,但”参与者超时默认提交”本身又会制造新的不一致(TM 决定 Abort,部分 RM 却因超时自行提交),工程上很少真正落地。
TCC:业务层补偿
TCC 把两阶段思想从数据库层抬到业务层:Try 完成业务检查并预留资源(冻结库存、冻结余额),Confirm 真正消费预留资源,Cancel 释放预留资源。它不再长期持有数据库锁,隔离性靠”冻结”这一业务语义实现,性能远好于 XA,代价是每个参与者要写三套接口。
TCC 落地有三个绕不过去的坑:
- 幂等:Confirm/Cancel 可能因超时被协调者重复调用,必须用事务状态表保证多次执行结果一致。
- 空回滚:Try 请求根本没到达参与者(网络丢失),协调者却发起了 Cancel。Cancel 必须识别”这个事务从未 Try 过”,直接返回成功,不能误执行回滚逻辑。
- 悬挂:Cancel 比 Try 先到(Try 网络拥堵迟到),若迟到的 Try 继续预留资源,这份资源将永远无人 Confirm/Cancel。解法是在 Try 中先检查该事务是否已回滚,已回滚则拒绝执行。
Seata 的 TCC Fence 机制就是用一张本地事务状态表统一处理这三件事。
本地消息表与事务消息
最终一致阵营里最常用的两条路径:
- 本地消息表(Outbox):业务写入和”待发送消息”记录在同一张库的同一个本地事务里提交,之后由独立任务轮询消息表投递到 MQ,投递成功再改状态。MQ 短暂不可用不影响业务提交,代价是要维护消息表、投递线程、重试与对账。
- RocketMQ 事务消息:先发”半消息”(对消费者不可见),半消息成功再执行本地事务,最后按本地事务结果 Commit 或 Rollback 半消息;若结果上报失败,Broker 定期回查生产者”本地事务到底成了没”,生产者查库应答。消费端失败则自动重试,超过上限进死信队列人工介入。
两者差别在对 MQ 可用性的依赖位置:事务消息要求发半消息时 Broker 可用;本地消息表把依赖推迟到异步投递环节,对 MQ 抖动更宽容。无论哪条路径,消费端幂等都是硬要求。
Saga 与 Seata 的定位
Saga 面向长事务:把一个长流程拆成 T1…Tn 个本地短事务,每个 Ti 配一个补偿动作 Ci。任一 Ti 失败,反向恢复就按 Ci…C1 倒序补偿已完成的步骤,正向恢复则原地重试 Ti 直到成功。Saga 没有 Try 预留资源,每步本地事务直接提交,性能最好,但隔离性最弱——中间态对其他事务可见,脏读要靠业务语义消化(比如订单先处于”待确认”状态)。补偿动作必须幂等,且需要 Saga log 记录执行进度,框架崩溃恢复后才知道补到哪一步。
Seata 是一站式框架而非某一种模式:TC 维护全局事务状态,TM 定义边界,RM 管理分支。其 AT 模式在一阶段代理数据源解析 SQL、生成 before/after image 写入 undo_log 并向 TC 注册分支,二阶段提交则异步删 undo_log,回滚则按 undo_log 生成反向 SQL 自动补偿——业务低侵入,但受限于关系型数据库、SQL 语法支持范围与全局锁。Seata 同时支持 XA、TCC、Saga 三种模式,按场景混用。
工程实践
- 第一原则是减少分布式事务:能合并到一个服务一个库的,就别拆分;能用单库事务解决的,是上上策。
- AT 模式注意 undo_log 清理与全局锁竞争,上线前压测热点行场景。
- TCC 的 Confirm/Cancel 幂等、空回滚、悬挂防护缺一不可,建议直接用框架的 Fence 机制。
- 消息方案必须配消费幂等 + 死信告警 + 对账任务,别假设”重试几次总能成功”。
- Saga 要画状态机并明确哪些中间态对用户可见,补偿链路要有监控和人工介入入口。
对比与权衡
| 方案 | 一致性 | 业务侵入 | 性能/阻塞 | 依赖 | 适用场景 |
|---|---|---|---|---|---|
| 2PC/XA | 强一致倾向 | 低 | 差(长锁、阻塞) | 数据库 XA 支持 | 金融账务短事务、低并发 |
| Seata AT | 最终一致 | 低 | 较好(全局锁热点需注意) | 关系库 + undo_log | 常规 CRUD 跨库 |
| TCC | 最终一致(业务强约束) | 高(三套接口) | 好(资源预留替代长锁) | 事务状态表 | 资金冻结、库存预留 |
| Saga | 最终一致(隔离性弱) | 高(正向+补偿) | 好 | Saga log / 状态机 | 长流程编排、审批履约 |
| 本地消息表 | 最终一致 | 中 | 好 | 消息表 + 投递任务 | 通知类、可异步场景 |
| RocketMQ 事务消息 | 最终一致 | 中 | 好 | RocketMQ Broker | 业务写入 + 消息原子发布 |
| 最大努力通知 | 最终一致(最弱) | 低 | 好 | 重试 + 对账 | 支付回调、积分通知 |
面试视角
2PC 有什么问题? 三个关键词:同步阻塞——准备后提交前参与者一直持锁;协调者单点——TM 在准备完成后宕机,参与者永远阻塞;提交阶段网络分区导致部分参与者收不到指令而数据不一致。XA 是 2PC 在数据库层的标准化落地。
TCC 的空回滚和悬挂是什么? 空回滚:Try 未到达参与者,Cancel 却来了,Cancel 必须识别并直接成功返回,不能误回滚。悬挂:Cancel 先于迟到的 Try 到达,Try 若继续预留资源将无人释放,需在 Try 中检查事务是否已回滚。两者都靠本地事务状态表(Fence)解决,同时 Confirm/Cancel 必须幂等。
RocketMQ 事务消息怎么保证一致性? 先发对消费者不可见的半消息,成功后执行本地事务,再按结果 Commit/Rollback 半消息;结果上报失败时 Broker 定时回查生产者的本地事务状态。消费失败自动重试,超限进死信队列。它保证的是”本地事务结果与消息可见性”的最终一致,不是 XA 语义的强一致。
AT 和 TCC 怎么选? AT 业务侵入低,框架解析 SQL 自动生成 undo_log 做补偿,适合常规关系库 CRUD;TCC 要在业务层实现 Try/Confirm/Cancel,成本高但能表达资源预留语义、不受 SQL 范围限制,适合资金、库存这类需要冻结资源的短链路。
Saga 的缺点是什么? 没有 Try 阶段,每步本地事务直接提交,隔离性弱、中间态可见,可能产生脏读;补偿逻辑全由业务实现且必须幂等;长链路下需要 Saga log 记录进度以便崩溃恢复。适合长流程而非强约束场景。
小结
- 分布式事务的第一性问题是跨进程后单机 ACID 失效,真实目标是在一致性、可用性、性能、成本间取点。
- 2PC/XA 强一致但同步阻塞、协调者单点;3PC 用超时换阻塞却引入新不一致,工程上少有人用。
- TCC 把两阶段抬到业务层,用资源预留替代长锁,但要处理好幂等、空回滚、悬挂三件事。
- 本地消息表与 RocketMQ 事务消息是最终一致的两大工程主力,差异在对 MQ 可用性的依赖位置;消费幂等与对账不可省。
- Saga 服务长流程,Seata 提供 AT/TCC/Saga/XA 全家桶;选型的起点永远是”能不能先消灭这个分布式事务”。

