分布式事务实战:跨服务一致性,四种方案怎么选
拆成微服务之后,"下单成功但库存没扣"成了经典事故。这篇讲清 2PC、TCC、Saga、消息表四种方案的机制与代价、怎么选,以及绕不开的三个工程细节:幂等、状态机、对账。
一、先立地基:三个概念
单体时代,一致性靠数据库本地事务:BEGIN、COMMIT、ROLLBACK,ACID 四平八稳。拆成微服务之后,一次下单要动订单库、库存库、账户库——分属三个服务、三个数据库,本地事务管不了跨库操作。
- CAP 的直觉版:网络分区不可避免,于是「强一致」与「高可用」只能二选一——所有分布式事务方案,都是在这两极之间找位置
- BASE 与最终一致:绝大多数业务接受「短暂不一致、最终一致」——这是 Saga 与消息表的立足点
- 补偿不是回滚:分布式下没有数据库级回滚,只有反向操作(把扣掉的库存加回去);反向操作可能失败、可能重复——这引出了后面所有工程细节
分布式事务的本质,是用「补偿 + 幂等 + 对账」去逼近一致,而不是真的拥有回滚。
二、四种方案:机制与代价
| 方案 | 一致性 | 侵入性 | 性能 | 典型场景 |
|---|---|---|---|---|
| 2PC / XA | 强一致 | 低(框架层) | 差(同步阻塞) | 强一致、低频、小规模 |
| TCC | 强一致(业务层) | 高(三接口) | 好 | 资金、库存等核心链路 |
| Saga | 最终一致 | 中 | 好 | 长流程、可补偿业务 |
| 本地消息表 / 事务消息 | 最终一致 | 低 | 好 | 事件驱动、已用 MQ |
2PC:准备与提交两步走
协调者先让所有参与方「准备」(锁住资源、写好日志),全部就绪后再统一「提交」;任何一方准备失败就统一回滚。优点是业务无侵入;代价是同步阻塞——事务期间资源被锁住,参与者一慢全体等;协调者本身还有单点风险。它适合「强一致 + 低频 + 参与方少」,不适合高并发主链路。
TCC:把事务拆成三段业务代码
Try(预留资源)→ Confirm(确认扣减)→ Cancel(释放预留)。以扣库存为例:Try 阶段把 10 件「冻结」而不是扣减,Confirm 时真正扣掉,Cancel 时解冻。性能好、一致性强,代价是侵入性高——每个参与方都要实现三个接口,且要处理三个经典坑:空回滚(Cancel 先于 Try 到达,必须识别并直接成功)、幂等(Confirm 与 Cancel 可能被重复调用,用事务 ID 去重)、防悬挂(空回滚后迟到的 Try 不能再生效)。
Try : 冻结资源(frozen = 10)
Confirm : 扣减(stock -= frozen)并清除冻结
Cancel : 解冻(frozen = 0),若未曾 Try 则直接成功
Saga:长流程的补偿链
把一个大事务拆成一串本地事务:T1、T2、T3 依次执行;一旦 T3 失败,就按逆序执行补偿 C2、C1。它没有「预留」概念,靠补偿兜底,因此适合跨多个服务的长流程(旅行预订:机票、酒店、租车)。两种实现风格:编排式(中心协调者决定下一步,好理解、好排障)与协同式(各服务监听事件自行反应,耦合低但链路难追踪)。新手建议从编排式起步。
本地消息表与事务消息
业务写入与「待发消息」放进同一个本地事务,再由异步投递器可靠送到 MQ,下游各自消费。它的关键是:不追求同步一致,而是保证「业务成功则消息必然发出」,配合下游幂等与 T+1 对账,最终收敛。
三、选型:能不用就不用
选型的第一个问题不是「选哪个」,而是「这个链路真的需要分布式事务吗」——很多所谓的跨服务一致性问题,可以通过重新设计边界(把强一致的数据放回一个库)直接消除。真的需要时:资金、库存等核心链路要求强一致且并发可控 → TCC;长流程、每步都有天然反向操作 → Saga;事件驱动、已经用了 MQ → 本地消息表 / 事务消息;存量系统、参与方都支持 XA 且频率极低 → 2PC 兜底;非核心链路(通知、积分)→ 干脆别上,接受最终一致 + 对账。
一条经验:越靠近钱的链路越用强一致方案,越靠近「体验」的链路越用最终一致。
四、绕不开的三个工程细节
- 幂等:所有 Confirm / Cancel / 补偿接口必须幂等——没有幂等,重试就会变成重复扣款,这是头号事故源
- 状态机与去重表:记录每个事务走到哪一步,用事务 ID 做唯一键;空回滚、防悬挂、重复调用全靠它兜住
- 对账:T+1 比对上下游数据、发现差异并修复——没有对账的最终一致,等于最终不一致
五、五个常见误区
- 用 2PC 扛大流量:同步阻塞与锁等待会把系统拖垮——它天生不适合高并发
- 只测成功路径:TCC 与 Saga 的价值恰恰在失败与补偿路径,必须专门写测试
- 把补偿当回滚:补偿是有副作用的业务操作,可能部分失败或重复执行
- 忽略对账:把一致性完全托付给中间件,出事时没有兜底手段
- 过度设计:给积分、通知这类链路套上 TCC,用复杂度换来了不需要的「安全感」
六、落地路线:四步走
- 分类:盘点所有跨服务写操作,标注需要强一致还是最终一致——多数可以在重设计中降级
- 起步:非核心链路统一走消息表 / 事件驱动;长流程用编排式 Saga
- 补基础件:幂等组件、事务状态机、对账任务——三件套是所有方案的公共底座
- 评估 TCC:只对资金与库存等真正敏感的链路引入,并把空回滚、防悬挂、幂等全部纳入测试
速查卡
| 场景 | 方案 |
|---|---|
| 强一致、低频、参与方少 | 2PC / XA |
| 资金、库存,要求强一致 | TCC(三接口 + 幂等 + 防悬挂) |
| 长流程、每步可反向操作 | Saga(编排式起步) |
| 事件驱动、已用 MQ | 本地消息表 / 事务消息 |
| 通知、积分等非核心 | 最终一致 + 对账 |
| 公共底座 | 幂等去重表 + 事务状态机 + T+1 对账 |
写在最后
分布式事务没有银弹,只有权衡:越强的一致,越贵的代价。正确的姿势是先问「这个链路能不能不要分布式事务」,再在剩下的场景里按「钱 vs 体验」分配方案,最后老老实实补上幂等、状态机、对账这三件公共底座。
评论(0)