分布式事务怎么选型?2PC、TCC、SAGA、本地消息表一次讲清
分布式事务没有"最好"的方案,只有"最匹配业务"的方案。
选型的本质不是选技术,而是选一致性、性能、复杂度、团队承载力之间的取舍。
一、先问一句:你真的需要分布式事务吗?
很多"分布式事务问题",其实在架构阶段就能消灭:
- 把数据放回到同一个库 → 本地事务搞定
- 把强依赖改成事件驱动 → 异步最终一致
- 把"必须一起成功"改成"先成功,再补偿"
- 把服务边界重新划,让聚合根待在一个服务里
**能不用分布式事务,就不用。**
一旦跨服务、跨库、跨团队,复杂度是指数级上涨。
二、四种方案一句话定性
| 方案 | 一句话 | 一致性 | 像什么 |
|---|---|---|---|
| 2PC / XA | 数据库层强一致,大家先举手,再一起提交 | 强一致 | 开会全体表决 |
| TCC | 业务层两阶段:先冻住,再确认/释放 | 业务层准强一致 | 预定座位 |
| SAGA | 长流程拆成多步,失败了反向补偿 | 最终一致 | 工单流转 |
| 本地消息表 | 业务+消息同事务,后台投递,下游幂等消费 | 最终一致 | Outbox 模式 |
三、2PC / XA:强一致,但代价很重
原理
- 协调者发
prepare - 各参与者锁资源、预执行、投票
- 全 OK →
commit,否则rollback
优点
- 数据库原生支持(MySQL XA、PostgreSQL 2PC)
- 应用代码侵入低
- 一致性最强
缺点
- Prepare 阶段持锁,并发一高就堵
- 协调者单点,挂了参与者干等
- 跨服务、跨语言、跨中间件时很难搞
- 高并发互联网业务基本扛不住
适合
- 单体多数据源
- 传统金融核心、账务系统
- 低并发、短事务、强一致要求极高
不适合
- 高 QPS 下单链路
- 长流程业务
- 跨团队微服务
结论:2PC 是保底方案,不是默认方案。
四、TCC:资金类场景的"手工强一致"
三个动作
- Try:预留资源(冻结金额、预扣库存)
- Confirm:真正提交(扣钱、扣库存)
- Cancel:释放预留(解冻、回补)
优点
- 不靠数据库锁,性能比 2PC 好
- 资源可预留,适合资金、库存、额度
- 一致性比 SAGA 更可控
缺点
- 业务侵入极高:每个接口要写三套逻辑
- 要处理三大坑:
- 空回滚:Cancel 来了,但 Try 没成功
- 悬挂:Cancel 先到,Try 后到
- 幂等:Confirm/Cancel 被重复调用
- 开发成本是本地消息表的 3 倍以上
适合
- 支付扣款
- 账户转账
- 库存预占
- 优惠券/积分冻结
不适合
- 小团队
- 流程经常变
- 业务动作不可撤销
结论:TCC 不是"高级微服务标配",而是资金链路的重武器。
五、SAGA:长事务和复杂流程的解药
原理
把大事务拆成多个本地事务:
创建订单 → 扣库存 → 扣积分 → 通知物流
某一步失败,就按反方向补偿:
取消订单 → 回补库存 → 回退积分 → 取消物流
两种写法
- 编排式 Orchestration:有个中心协调器
- 协同式 Choreography:大家靠事件互相触发
优点
- 无长锁,适合长流程
- 比 TCC 开发成本低
- 订单、履约、出行、预订类业务非常贴
缺点
- 没有隔离性 :中间状态对外可见
- A 扣了钱,B 还没加钱,别的请求能看到"钱少了"
- 补偿不一定能成功
- 短信发过了、积分花掉了、票已经出票了
- 要设计状态机、补偿链、人工兜底
适合
- 电商下单履约
- 机票/酒店/打车预订
- 开户、审批、物流调度
不适合
- 要求"别让别人看到中间状态"
- 步骤不可逆
- 团队没做过状态机
结论:SAGA 解决"流程长",不解决"强一致"。
六、本地消息表:最土,但最稳
原理
BEGIN;
UPDATE orders SET status = 'PAID' WHERE order_id = ?;
INSERT INTO outbox_messages(message_id, topic, payload, status)
VALUES (?, 'order.paid', ?, 'PENDING');
COMMIT;
然后:
- 定时任务扫
PENDING - 发到 MQ
- 成功改
SENT - 下游消费必须幂等
这就是经典的 Transactional Outbox。
优点
- 只依赖数据库,不依赖高级 MQ 特性
- 业务侵入低
- 实现简单、可观测、可运维
- 中小团队也能维护住
缺点
- 有延迟(扫表间隔 + MQ 延迟)
- 只保证最终一致
- 消息表会变大,要归档
- 下游必须做幂等
适合
- 订单创建后发通知
- 积分发放
- 日志/审计/埋点
- 跨服务异步解耦
不适合
- 必须同步返回最终结果
- 资金扣减这种不能等一等的场景
结论:80% 的互联网业务,本地消息表 + 幂等 + 对账 就够了。
七、横向对比表
| 维度 | 2PC | TCC | SAGA | 本地消息表 |
|---|---|---|---|---|
| 一致性 | 强一致 | 业务层准强一致 | 最终一致 | 最终一致 |
| 性能 | 差 | 中高 | 高 | 高 |
| 延迟 | 低 | 低~中 | 中 | 秒级 |
| 业务侵入 | 低 | 很高 | 中 | 低 |
| 开发成本 | 低~中 | 很高 | 中高 | 低 |
| 隔离性 | 有 | 有 | 无 | 无 |
| 运维复杂度 | 中 | 高 | 中 | 低 |
| 适合并发 | 低 | 中高 | 中高 | 中 |
| 典型场景 | 金融核心、短事务 | 支付、库存预占 | 订单履约、长流程 | 通知、积分、异步解耦 |
八、选型决策树(直接照着问)
1. 能不能收回到单库 / 单服务?
能 → 本地事务
别上分布式事务。
2. 能不能接受"暂时不一致,后面补齐"?
不能 → 去 3
能 → 去 4
3. 是否资金/库存/额度,且资源可预留?
是 → TCC
否,但必须强一致、并发低 → 2PC / XA
4. 是长流程、多步骤、可补偿吗?
是 → SAGA
否,只是"做完一件事,通知另一个服务也做点事" → 本地消息表 / 事务消息
九、工程落地里的"通用铁律"
不管选哪个,下面几条都逃不掉:
1. 幂等是第一优先级
用:
- 业务唯一键(order_id、tx_id)
- 消费记录表
- 唯一索引
- 乐观锁 version
消息队列基本都是"至少一次投递",重复是常态。
2. 补偿要可重试
补偿接口必须:
- 幂等
- 可重试
- 有日志
- 有告警
- 有死信 / 人工介入通道
3. 最终一致必须有对账
别信"消息不会丢"。
要做:
- 订单状态 ↔ 库存状态
- 账户流水 ↔ 订单状态
- 消息表 ↔ MQ 投递状态
系统自己恢复不了时,人能介入。
4. 别把所有链路都搞成分布式事务
真实系统里通常是混用:
下单创建订单 → 本地事务
扣库存 → TCC / SAGA
支付 → TCC
发积分 → 本地消息表
发短信 / 推消息 → 本地消息表 / 最大努力通知
订单履约长流程 → SAGA
强一致只用在"错了会资损"的地方。
其他地方,最终一致就是工程正义。