分布式理论03:分布式事务

拆解跨服务提交为什么难,以及 2PC、TCC、Saga、本地消息表这些方案分别适合什么业务后果和补偿边界。

字数 1772 阅读时长 ≈ 6 分钟 2026-5-31 2026-7-12
分布式理论03:分布式事务

分布式事务难在哪里

单体应用里,一个下单流程可能都在同一个数据库事务里:保存订单、扣库存、使用优惠券,要么一起提交,要么一起回滚。

拆成服务以后,订单服务写订单库,库存服务写库存库,优惠券服务写优惠券库。每个服务都有自己的事务,调用方没有一个天然的总开关,可以让所有数据库同时提交或同时回滚。

于是问题变成:如果订单成功了,库存失败了,系统怎么解释这个中间状态?是立刻回滚订单,还是保留订单等待补偿?用户看到什么?后台怎么修?

分布式事务不是先选框架,而是先判断业务后果。

先按业务后果分层

我通常会先把链路分成三类。分类之后,方案才有意义。

链路类型例子特点处理方式
强约束链路支付扣款、账户余额、核心库存数据错了直接影响资产或履约强状态校验、对账、幂等、人工兜底
可补偿链路优惠券占用、积分发放、会员等级失败后可以修回来,但要有明确状态和告警消息重试、补偿任务、状态修复
旁路链路通知、搜索索引、统计报表不应该阻塞主流程消息异步、最终一致

否则把所有链路都做成强一致,系统会很重;把所有链路都异步化,出问题时又没人能解释状态。

四类常见方案对比

方案核心思想优点缺点适用场景
2PC先准备、再提交,协调者统一决策一致性语义清楚协调成本高、阻塞风险大低并发、强一致要求极高的内部系统
TCCTry 预留、Confirm 确认、Cancel 释放一致性强、业务可控业务侵入强、每个阶段要幂等库存预占、额度冻结等资源可明确冻结的场景
Saga拆成多个本地事务,每步有补偿长流程友好、无阻塞补偿不一定等于反向操作订单、物流、履约等长流程
本地消息表/事务消息业务数据和事件在同一个本地事务里可靠、解耦、落地简单有延迟、需要消费端幂等主库写成功后可靠通知下游

没有哪个方案是万能的。真实项目里,同一条链路可能混用多种方案:核心扣减用 TCC,下游通知用本地消息表,长流程编排用 Saga。

一个下单场景怎么选

假设用户下单需要锁库存、使用优惠券、创建支付单。

库存是强约束,可以用预占模型:库存服务先冻结库存,订单支付成功后确认扣减,超时未支付再释放。这更接近 TCC 的思想。

优惠券可以做状态机:未使用、锁定、已使用、已释放。订单创建时锁定,支付成功后确认使用,订单关闭后释放。这里同样要求接口幂等,不能因为重试重复锁定。

支付单通常不适合简单补偿,因为涉及外部支付渠道。更稳的是用支付状态机和回调对账:本地状态只按合法方向推进,定时任务和渠道对账修复异常状态。

通知、积分、搜索索引这些旁路动作,则不要阻塞下单主流程。可以通过本地消息表或 MQ 异步推进。

边界情况比 happy path 更重要

分布式事务最容易漏的是边界情况。

接口超时但下游成功了怎么办?Confirm 重试了两次怎么办?Cancel 比 Try 先到怎么办?消息重复消费怎么办?补偿任务执行一半失败怎么办?

这些问题决定方案是否能上线。比如 TCC 至少要考虑:

Try:预留资源,允许重复请求返回同一结果
Confirm:只确认已经 Try 成功的记录,重复确认不产生副作用
Cancel:Try 不存在时也要安全返回,避免空回滚失败
状态:不能从 Confirmed 回退到 Cancelled

真正可靠的分布式事务,靠的是状态机、幂等、补偿、对账和告警一起工作。

状态表比流程图更重要

分布式事务设计不能只画”订单调用库存再调用优惠券”。真正能落地的是状态表。

比如库存预占可以有这样的状态:

TRYING     已预占,等待订单确认
CONFIRMED  已确认扣减
CANCELLED  已释放
EXPIRED    超时自动释放

对应的更新必须是条件更新:

update stock_reservation
set status = 'CONFIRMED', updated_at = now()
where reservation_id = ?
  and status = 'TRYING';

这条 SQL 的关键不是语法,而是它拒绝非法状态流转。重复 Confirm 时,第一次更新成功,后续更新影响行数为 0,业务可以查询当前状态后返回成功。Cancel 也是同理,不能把已经 Confirmed 的记录释放掉。

TCC 里最容易踩的三个坑也都和状态有关:

  • 空回滚:Cancel 先于 Try 到达,必须安全返回并记录状态
  • 悬挂:Cancel 已经执行,迟到的 Try 不能再预占资源
  • 幂等:Try / Confirm / Cancel 都可能重复调用

如果没有状态表,这些坑只能靠代码里的 if-else 硬扛,迟早会漏。

对账不是补充功能,而是事务的一部分

很多分布式事务方案在设计文档里写到”失败后补偿”,但没有说明补偿从哪里发现问题。

真实项目里至少要有三类对账:

对账类型例子目的
本地状态对账订单成功但库存预占还在 TRYING发现内部状态不一致
跨服务对账支付成功但订单仍是待支付发现服务间状态不同步
外部渠道对账本地支付状态和渠道账单不一致发现与外部系统的差异

对账任务不能只打印日志。它要能产生待处理记录、重试次数、最后失败原因和人工处理入口。否则补偿失败后,问题只是从在线链路转移到了无人认领的后台任务里。

常见误区

误区一:引入一个分布式事务框架就结束了

框架可以帮你协调流程,但业务状态、幂等键、补偿语义、异常处理仍然要自己设计。

误区二:补偿就是反向 SQL

很多业务动作不可逆,只能通过作废、冲正、追加记录来修复。

误区三:只保证写成功,不设计查询态

用户看到”处理中”多久?后台怎么查卡在哪一步?没有这些,异常会变成客服和运维的黑洞。

项目落地时先问

做分布式事务设计时,可以先问:

  1. 哪一步失败会产生不可接受后果?
  2. 哪些资源可以冻结、确认和释放?
  3. 哪些动作只能补偿,不能回滚?
  4. 每个接口的幂等键是什么?
  5. 中间状态用户能不能看到,能看多久?
  6. 对账和人工处理入口在哪里?

回答完这些,再谈 2PC、TCC、Saga、本地消息表,方案就不会飘。