后端 · 分布式事务
分布式事务终极指南:从 2PC 到 Saga 到 TCC
单体里一个 @Transactional 就能搞定的事,到了微服务就变成"订单付了、库存没扣、钱退了"。
分布式事务的本质是:在无法共享一个数据库、无法共享一把锁的世界里,保证一系列操作要么都成、要么都回退。
本文把主流方案一次性讲透,并给出选型地图。
1先对齐:CAP 与"最终一致"
分布式系统里,一致性、可用性、分区容错三者最多取二。网络分区无法避免,所以实际是CP 或 AP的取舍。
- 强一致:写入即全局可见,代价是可用性(2PC 类);
- 最终一致:短暂不一致,但最终收敛,代价是业务需容忍中间态(Saga 类)。
💡 多数业务其实只需要最终一致:用户下单后库存"几秒内"扣成功,完全可接受。强行追求强一致,往往换来系统脆弱。
22PC:两阶段提交(强一致)
引入一个协调者(Coordinator),分两阶段:
阶段一 准备:协调者问各参与者「能提交吗?」 参与者锁定资源,写 undo/redo 日志,答 Yes/No 阶段二 提交:若全 Yes → 发 Commit;任一 No → 发 Rollback 参与者执行并最终释放锁
- 优点:强一致,语义简单;
- 缺点:同步阻塞(资源全程锁定)、协调者单点、存在"协调者宕机后参与者悬挂"的脑裂风险。
⚠️ 2PC 在跨服务、长事务场景几乎不可用:锁定期间并发被拖垮。它更适合数据库内部或短事务。
3TCC:Try-Confirm-Cancel(业务层补偿)
把"预留—确认"的控制权交回业务,三阶段:
- Try:预留资源(如冻结库存、预扣额度),不真正提交;
- Confirm:所有 Try 成功,真正确认(幂等);
- Cancel:任一失败,释放预留(幂等)。
// 订单支付 TCC 示意 try { orderService.freeze(id); inventoryService.freeze(sku); } confirm { orderService.confirm(id); inventoryService.deduct(sku); } // 幂等 cancel { orderService.unfreeze(id); inventoryService.unfreeze(sku); } // 幂等
- 优点:无长锁、性能好、可控;
- 缺点:业务侵入大,每个接口要写三套逻辑,且 Confirm/Cancel 必须幂等。
4Saga:长事务的救星(最终一致)
把一个大事务拆成一串本地小事务,每步都有对应的补偿操作。失败则按相反顺序回滚:
正向: 下单 → 扣库存 → 扣余额 → 发通知 失败: 发通知✗ → 退余额 → 补库存 → 撤单
两种协调方式:
- 编排式(Choreography):服务间用事件互相驱动,去中心化,但链路长时难追踪;
- 编制式(Orchestration):一个编排器统一指挥各步,清晰可控(如用状态机)。
💡 Saga 不锁定资源,适合跨多个服务、长流程(下单、旅行预订、工单流)。代价:中途是"不一致"的,业务要能展示中间态。
5还有两把轻武器
- 本地消息表 + 消息队列:本地事务与"发消息"写同一库,再由 MQ 投递,消费者幂等处理。实现简单、最常用;
- 最大努力通知:多次重试 + 对账,适用于"允许最终一致、不要求实时"的场景(如支付结果通知)。
6怎么选:一张决策表
| 方案 | 一致性 | 侵入 | 适用 |
|---|---|---|---|
| 2PC | 强一致 | 低(中间件) | 短事务 / 库内 |
| TCC | 强一致 | 高(三套逻辑) | 高并发、需强一致的核心链路 |
| Saga | 最终一致 | 中(补偿) | 长流程、跨服务 |
| 消息表/MQ | 最终一致 | 低 | 一般异步解耦 |
7落地避坑清单
- 幂等第一:所有 Confirm/Cancel/消费端必须幂等,靠唯一键或去重表;
- 空补偿与防悬挂:网络乱序时可能先收到 Cancel 再收到 Try,要能识别并拒绝;
- 对账兜底:任何方案都要有定时对账,修复"漏补偿";
- 能避则避:优先考虑"把关联数据放进同一个库"用本地事务,比分布式事务简单一个量级。
8小结
分布式事务没有"最好",只有"最合适"。记住这条心法:优先避免分布式事务,其次最终一致(Saga/消息),最后才上强一致(TCC/2PC)。 越往后,复杂度和代价越高。把一致性级别和需求对齐,而不是和"完美"对齐。
© 2026 JokerChou's Blog · 用 ❤️ 与 ☕ 制作 · 返回博客首页