分布式事务方案对比:2PC / TCC / SAGA / 本地消息 / 最大努力通知
一句话速记
五种方案从强到弱:2PC(强一致,数据库级别,阻塞协调)→ TCC(业务级别 Try-Confirm-Cancel,高侵入性但高性能)→ SAGA(长事务编排,正向 + 补偿,最终一致)→ 本地消息表(最终一致,最常用,依赖 MQ)→ 最大努力通知(最弱,适合不要求强一致的通知场景如支付通知)。互联网业务首选本地消息表或 TCC,避免 2PC(性能差、可用性低)。
通俗解释
类比:餐厅多人 AA 结账
2PC:服务员问所有人"能付吗?"(Phase 1),全部确认后统一执行(Phase 2)
→ 中途有人跑路(协调者宕机)→ 所有人卡在那等
TCC:每人先冻结钱(Try),全部成功后再真正扣(Confirm),失败则解冻(Cancel)
→ 复杂但快,冻结期不阻塞其他操作
SAGA:依次执行:A 付款 → B 付款 → C 付款,失败则反向取消
→ A 可能先成功后被撤销,临时暴露不一致状态
本地消息表:把"发消息"变成本地 DB 写操作(同一事务),再由 MQ 异步投递
→ 最终一致,实现最简单
最大努力通知:发通知,失败重试几次,超过就放弃(对方自己查)
→ 适合支付回调等"对方容忍延迟"的场景
关键细节
1)2PC(Two-Phase Commit)
角色:协调者(Transaction Coordinator)+ 参与者(各微服务/DB)
Phase 1(Prepare):
协调者 → 所有参与者:能执行吗?
参与者:执行但不提交,redo/undo log 写好,回复 Yes/No
Phase 2(Commit/Rollback):
全部 Yes → 协调者发 Commit → 参与者提交
有 No → 协调者发 Rollback → 参与者回滚
问题:
1. 阻塞:参与者在 Prepare 阶段持锁等待协调者 Commit(阻塞其他事务)
2. 协调者单点故障:Phase 2 协调者挂了,参与者永久阻塞(2PC 的死结)
3. 网络分区:协调者发出 Commit 后宕机,部分参与者提交,部分未提交 → 不一致
适用:传统数据库(XA 事务),MySQL 支持 XA 协议
不适用:互联网高并发服务(性能差,可用性低)
2)TCC(Try-Confirm-Cancel)
三个阶段(全是业务代码实现):
Try:
预留资源(冻结余额、锁库存)
检查业务约束
不执行实际业务
Confirm:
Try 全部成功 → 执行实际业务(扣款、减库存)
幂等:可重试
Cancel:
Try 失败 → 释放预留资源(解冻余额、解锁库存)
幂等:可重试
框架:Seata(阿里开源),ByteTCC,hmily
TCC 的难点:
// 每个服务都要实现三个接口
@TccTransaction
public interface PaymentService {
@TccTry
boolean tryDeduct(String userId, BigDecimal amount); // 冻结
@TccConfirm
boolean confirmDeduct(String userId, BigDecimal amount); // 真扣
@TccCancel
boolean cancelDeduct(String userId, BigDecimal amount); // 解冻
}
// 难点 1:空回滚(Try 没执行,Cancel 先到)
// Try 由于网络超时未到达,协调者超时发 Cancel → 需要判断"是否 Try 过"
// 难点 2:悬挂(Cancel 先于 Try 完成)
// 先执行 Cancel → 再执行 Try → Try 成功但 Cancel 已执行 → 资源永久锁定
// 难点 3:幂等
// Confirm/Cancel 可能重试多次,必须幂等3)SAGA
将长事务分解为一系列本地事务(T1, T2, ... Tn)
每个本地事务有对应的补偿事务(C1, C2, ... Cn)
正向:T1 → T2 → T3 → ... Tn
失败:... Cn → Cn-1 → ... C1(反向补偿)
两种实现:
编排式(Orchestration):中央协调器(Saga Orchestrator)驱动流程
协同式(Choreography):各服务监听事件,自主响应(无中心协调者)
优点:
无需锁(各步骤独立执行),高可用,适合长流程(如电商订单 → 支付 → 发货)
缺点:
临时不一致(T1 完成但 T2 失败,C1 还在执行时外部能看到 T1 的效果)
补偿逻辑复杂(退款、库存恢复等)
典型场景:酒店 + 机票 + 用车的行程预订(各系统独立,可以退订)
4)本地消息表(最常用)
-- 与业务数据在同一 DB,同一事务
BEGIN;
INSERT INTO orders (id, status) VALUES (123, 'CREATED');
INSERT INTO outbox_messages (id, topic, payload, status)
VALUES (UUID(), 'order.created', '{"orderId": 123}', 'PENDING');
COMMIT;
-- 保证"订单写入"和"消息记录"原子性
-- 后台定时任务(Outbox Worker):
SELECT * FROM outbox_messages WHERE status='PENDING' LIMIT 100;
-- 发送到 MQ(Kafka/RocketMQ)
-- 发送成功 → UPDATE status='SENT'
-- 失败重试(MQ 消费端幂等处理重复消息)改进版:Transactional Outbox + CDC(Canal):
Canal 监听 outbox_messages 表的 Binlog INSERT
→ 自动发送到 MQ
→ 应用层不需要定时任务(Canal 实时驱动)
→ 更低延迟,更可靠
5)最大努力通知
场景:支付宝支付成功 → 通知商户系统
商户系统不需要实时知道(有查询接口兜底)
支付宝尽力通知(重试 N 次:1s, 5s, 30s, 5min, 30min, 1h...)
超过次数就不再通知(商户自己定时查询对账)
特点:
- 不保证送达(最大努力)
- 异步,消息可能延迟
- 适合:容忍延迟 + 有查询接口兜底的场景
接收端必须幂等(可能收到多次通知)
6)五种方案对比矩阵
方案 一致性 侵入性 性能 适用场景
───────────────────────────────────────────────────────────────────
2PC 强一致 低 差 传统数据库、内部系统
TCC 最终一致 极高 好 金融核心链路(对一致性要求高)
SAGA 最终一致 高 好 长流程、跨多系统、可补偿
本地消息表 最终一致 低 好 互联网常规业务(推荐)
最大努力通知 弱一致 极低 极好 外部通知(支付回调、短信)
延伸追问
- Q:互联网公司为什么不用 2PC? → 2PC 的协调者是单点(SPOF),协调者宕机导致所有参与者阻塞;锁定期间的性能极差(持锁等待);网络分区下无法保证一致性。互联网优先可用性(AP),用最终一致 + 补偿替代强一致。
- Q:TCC 的”空回滚”和”悬挂”怎么解决?
→ 引入防悬挂/空回滚表(
tx_control表):Try 时插入记录;Cancel 时如果无 Try 记录则是空回滚(直接返回成功,标记已 cancel);Try 收到时如果已有 cancel 记录则是悬挂(拒绝执行,直接返回失败)。 - Q:本地消息表 vs 事务消息(RocketMQ)怎么选? → 本地消息表:简单,不依赖特定 MQ,任何 MQ 都可以用,但需要额外的 outbox 表和定时任务;RocketMQ 事务消息:框架支持,不需要额外表,但只能用 RocketMQ,回查机制需要实现。两种等价,看基础设施偏好。
我的记法
- 2PC = 数据库级强一致,阻塞,互联网不用
- TCC = 冻结-确认-取消,金融核心用,侵入性高
- SAGA = 长流程拆本地事务 + 补偿,适合跨系统
- 本地消息表 = 业务 + outbox 同一事务,最常用
- 最大努力通知 = 支付回调这类弱一致场景
- 一句话:「强一致 2PC 不用,金融 TCC,普通业务本地消息表」
状态
- 已背速记(5 种方案)
- 能画出本地消息表的数据流
- 能解释 TCC 的三个难点