Skip to content
Go back

分布式事务——从2PC到Seata AT的选型全景

分布式事务:2PC、TCC、SAGA、Seata AT 选型全景

一句话结论(30s)

分布式事务没有银弹——2PC 强一致但协调者单点、TCC 无锁但业务侵入、SAGA 适合长事务但补偿要兜底、Seata AT 无侵入但依赖 undo_log——因为强一致性代价极高,所以绝大多数场景应该优先考虑”最终一致性 + 对账补偿”,只在真正要求”任何时刻数据绝对一致”时才上强一致方案。

核心原理(2min)

底层深入(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 本身也带来额外存储和性能开销。


Share this post on:

Previous Post
大规模登录系统——从Session到JWT到OAuth2
Next Post
不停机数据迁移——PostgreSQL到Redis ZSet的双写灰度三阶段