分布式事务:2PC、TCC、SAGA、Seata AT 选型全景
一句话结论(30s)
分布式事务没有银弹——2PC 强一致但协调者单点、TCC 无锁但业务侵入、SAGA 适合长事务但补偿要兜底、Seata AT 无侵入但依赖 undo_log——因为强一致性代价极高,所以绝大多数场景应该优先考虑”最终一致性 + 对账补偿”,只在真正要求”任何时刻数据绝对一致”时才上强一致方案。
核心原理(2min)
- 2PC:协调者先 Prepare(参与者锁资源但不提交),全部 READY 再 Commit,任一 NO 则 Rollback;优点是强一致,缺点是协调者单点 + 同步阻塞。
- TCC:业务层实现 Try(冻结)/Confirm(确认)/Cancel(取消),无锁但侵入性强。
- SAGA:正向执行 + 失败逆序补偿,适合长事务,补偿必须重试兜底。
- Seata AT:拦截 SQL 自动记 undo_log,
@GlobalTransactional无侵入,代价是 undo_log 解析有限制。 - 选型口诀:强一致 + 跨服务资金 → TCC;强一致 + 其他 → Seata AT;允许最终一致 → 本地消息表 + RocketMQ 事务消息。
底层深入(5-10min)
2PC:强一致性 + 协调者瓶颈
Phase 1 (Prepare/投票):
Coordinator → 所有参与者: "准备提交"
各参与者: 执行事务但不提交,锁住资源,回复 READY
Phase 2 (Commit):
Coordinator: 全部 READY → 发 COMMIT → 各参与者提交
任意 NO → 发 ROLLBACK → 各参与者回滚
优点:强一致性。致命缺点:协调者单点故障导致事务挂起(参与者全部锁住等协调者恢复);同步阻塞(参与者等协调者决策时资源锁死)。
想一想:2PC 为什么会有阻塞问题? 因为参与者在 Prepare 阶段就锁住了资源,而锁的释放必须等协调者发来 COMMIT/ROLLBACK 指令。一旦协调者在两个阶段之间宕机,所有参与者就只能一边持锁一边干等——资源被无限期锁死,这就是”同步阻塞”的本质。也正因为锁的释放权集中在一个协调者手里,协调者才成了整个系统的单点瓶颈。
TCC:业务层强一致性
Try: 冻结资源(预留库存、预扣金额)
Confirm: 确认执行(实际扣减)
Cancel: 取消回滚(释放冻结资源)
每个 T、C、C 都是业务层实现——完全无锁(Try 只是预留,没有真正扣减)。代价:业务侵入性极强(每个接口都要实现三个方法)。
想一想:TCC 为什么能做到无锁? 因为 Try 阶段只”冻结”不”提交”——比如库存从”可售”划到”冻结”,并没有真正扣减,所以不依赖数据库行锁的长期持有,锁的粒度从”事务持续期间”缩短到”单条 SQL”。代价就是业务要自己实现 Try/Confirm/Cancel 三个方法,把分布式事务的复杂度从框架转移到了业务代码里。
SAGA:长事务编排
正向操作: 创建订单 → 扣库存 → 扣金额
补偿回滚: 取消订单 → 恢复库存 → 恢复金额
不需中间状态冻结,直接正向执行,失败时逆序调用补偿操作。适合长事务(如电商下单链路)。缺点:补偿操作必须保证成功(需要重试机制兜底),且业务代码侵入。
想一想:TCC 和 SAGA 分别适用什么场景? 关键看”要不要提前锁定资源”。TCC 靠 Try 预留,适合资源竞争激烈、需要强一致兜底的短链路(比如资金扣减,宁可预留也不许超卖);SAGA 不预留,直接执行 + 失败补偿,适合链路长、步骤多、中途可能很久才失败的场景(比如下单 → 扣库存 → 发货,中间有异步延迟),因为长时间冻结资源代价太高。一句话:TCC 用”预留”换强一致,SAGA 用”补偿”换不阻塞。
Seata AT:无侵入的两阶段提交
AT 模式通过拦截 SQL 自动记录 undo_log(解析 SQL 生成反向 SQL),事务提交时异步执行补偿回滚或清理 undo_log。应用代码只需加 @GlobalTransactional 注解。
Phase 1: 各服务执行本地事务 + 记录 undo_log(自动!)
Phase 2: 事务管理器收尾——全部成功: 异步删除 undo_log
任意失败: 异步回放 undo_log 反向 SQL
Seata AT 是无侵入的——不需要业务层实现 TCC 的三个方法,不需要写补偿逻辑。代价:undo_log 的解析和回放有限制(对 UPDATE/DELETE 自动理解,复杂 SQL 需人工确认)。
想一想:Seata AT 为什么能做到无侵入? 因为它把 TCC 里”业务要自己写的补偿逻辑”下沉到了框架层——通过拦截 SQL、解析出反向 SQL 写进 undo_log,回滚时回放 undo_log 即可。业务代码只加一个 @GlobalTransactional 注解,补偿由框架自动生成。代价是”自动化”依赖对 SQL 的解析能力,遇到复杂 SQL、批量更新或存储过程,框架生成的 undo_log 可能不准确,需要人工兜底。
生产决策树
需要强一致性?
→ 是 → 涉及跨服务资金流转? → 是 → TCC
→ 否 → Seata AT
→ 否 → 允许最终一致? → 是 → 本地消息表 + RocketMQ事务消息
→ 否 → 重新评估是否真的是分布式事务问题
大多数”分布式事务”其实可以用最终一致性 + 对账补偿解决——强一致性只在你真的需要”任何时刻数据绝对一致”时才值得。
想一想:为什么强一致性代价这么高? 因为”任何时刻绝对一致”要求所有参与者在同一瞬间就同一状态达成共识,这必然引入协调、锁定和等待——要么像 2PC 那样用协调者 + 锁,要么像 TCC 那样侵入业务。而绝大多数业务其实能容忍”短暂不一致、最终收敛”,这时用消息 + 对账补偿,把一致性压力从”实时”推迟到”异步”,成本会低一个数量级。
章末提问
1. 2PC 的协调者单点故障具体会引发什么问题?怎么缓解?
结论先行:协调者宕机会让所有已 Prepare 的参与者无限期持锁等待,造成”事务挂起 + 资源锁死”。
因为参与者在 Prepare 阶段就锁定了资源,释放锁依赖协调者的 COMMIT/ROLLBACK 决策;协调者一挂,决策就没人发了,参与者只能干等,锁住的资源无法被其他事务使用,严重时直接拖垮整个系统。缓解手段包括:协调者高可用(主备/选举)、参与者等待超时自动 Rollback,但超时回滚又可能引发”协调者与参与者状态不一致”的新问题,这正是 2PC 难以根治的根源。
2. TCC 和 SAGA 的本质区别是什么?各自适用什么场景?
结论先行:区别在”要不要提前预留资源”——TCC 用 Try 预留换强一致,SAGA 用失败补偿换不阻塞。
因为 TCC 的 Try 阶段会冻结资源(预留库存/预扣金额),事务执行期间资源被锁定,天然适合资源竞争激烈、绝不能超卖的资金场景;SAGA 直接正向执行、失败再逆序补偿,过程中不长期持有锁,适合链路长、有异步等待、中途可能很久才失败的长事务(如订单履约),代价是补偿逻辑必须幂等且重试兜底。
3. Seata AT 为什么对业务无侵入?它的代价是什么?
结论先行:无侵入是因为补偿逻辑由框架通过”拦截 SQL + 自动生成 undo_log”下沉实现,业务只加一个注解;代价是自动化依赖 SQL 解析能力,复杂场景要人工兜底。
因为 TCC/SAGA 的补偿逻辑是业务自己写的,Seata AT 则拦截业务 SQL,自动生成反向 SQL 存入 undo_log,回滚时回放即可,所以业务无感。但”自动生成反向 SQL”只对标准 UPDATE/DELETE 可靠,遇到复杂 SQL、批量操作、存储过程时 undo_log 可能不准,且 undo_log 本身也带来额外存储和性能开销。