第 4 章 大事务处理方法论:Saga、补偿与最终一致性
从电商创单出发,理解为什么关键业务动作会天然演化为大事务问题,以及如何在同步 RPC、Saga、TCC、Outbox 和补偿机制之间做出可恢复、可审计、可收敛的设计选择。
很多工程师第一次听到“大事务”时,会自然联想到数据库事务、分布式事务,甚至 2PC / XA。这些概念当然重要,但如果把它们直接套到互联网业务里,往往会把问题理解偏。
互联网系统里的大事务,真正要解决的不是“如何让多个数据库像一个数据库那样一起提交”,而是:
当一个关键业务动作必须跨库存、营销、订单、支付、账务等多个系统共同完成时,系统应该如何在失败必然发生的现实里,仍然把结果收敛到可解释、可恢复、可补偿的状态。
这也是为什么本章会从电商创单切入。创单不是唯一的大事务场景,但它最能把大事务设计里的关键矛盾一次性暴露出来:多系统、副作用、补偿、超时、幂等、状态收敛。
这里先提前建立一个边界:
- 大事务方法论,关注的是一个关键节点的最终一致性。
- 长生命周期业务流程方法论,关注的是一个业务对象在多个阶段中的推进与治理。
例如,订单从创单到支付、履约、售后,是一个长生命周期业务流程;而“创单”这个关键节点内部的锁库存、占优惠、落订单,则是一个典型大事务。下一章会专门展开长生命周期业务流程,本章只聚焦“大事务”本身。
4.1 大事务到底在解决什么问题
先把三个经常混在一起的概念拆开:
| 概念 | 解决的问题 | 典型手段 |
|---|---|---|
| 数据库事务 | 单库内的原子性和一致性 | 本地事务、锁、隔离级别 |
| 分布式事务 | 多资源之间的提交一致性 | 2PC、XA、TCC |
| 业务大事务 | 跨系统业务目标如何推进和收敛 | Saga、状态机、Workflow、补偿 |
数据库事务关心的是“同一个数据库里的几条 SQL 要不要一起成功或失败”。
分布式事务关心的是“多个资源管理器之间能不能形成统一提交语义”。
业务大事务关心的则是“一个业务流程跨多个系统推进时,如何保证结果最终正确且过程可治理”。
这三者并不是谁替代谁的关系,而是处理层次不同的问题。
在互联网系统里,很多流程天生就不适合直接上 2PC / XA:
- 支付网关、短信平台、物流系统、供应商接口都不可能加入你的本地事务。
- 用户支付不是毫秒级动作,可能会等待几十秒、几分钟,甚至跨天。
- 高并发下长时间持锁会迅速击穿吞吐和可用性。
- 很多步骤天然只能做“确认 / 取消 / 补偿”,而不是“全局原子提交 / 回滚”。
所以大事务设计的重点很少是“全局一次提交”,更常见的是:
- 找到权威状态源。
- 把业务流程拆成多个可提交、可恢复、可补偿的阶段。
- 为每个阶段定义状态、超时、补偿和审计规则。
- 确保流程在局部失败后仍然能够最终收敛。
这也意味着,大事务系统的设计对象不是“接口调用顺序”,而是“状态推进机制”。
4.2 为什么创单节点天然是大事务
电商创单看起来只是用户点了一次“提交订单”,但“创单”这个动作本身通常至少涉及下面这些子动作:
- 校验商品是否仍然可售。
- 校验价格、优惠、税费、运费是否仍然有效。
- 预占库存或其他资源。
- 创建订单主记录和订单行。
- 生成支付单或支付上下文。
- 为后续支付准备支付上下文。
这条链路的真正难点,不在于步骤多,而在于每一步都可能拥有独立的本地事务和独立的失败语义:
- 库存预占成功,但订单写库失败。
- 订单创建成功,但支付发起失败。
- 营销额度占用成功,但库存预占失败。
- 支付上下文创建成功,但订单落库失败。
- 订单超时取消后,库存释放和优惠释放只完成了一部分。
所以创单节点天然带有四个大事务特征:
- 跨系统:库存、价格、订单、支付、履约通常不是同一个服务。
- 多副作用:库存、营销、订单、支付上下文都可能留下真实副作用。
- 可失败:任何一步都可能独立失败,而且失败之后未必能简单回滚。
- 需收敛:失败之后必须知道哪些动作要撤销、哪些状态要保留。
用一句话概括:创单节点不是“一个接口调多个下游”,而是“一次业务动作里包含多个必须一起收敛的副作用”。
订单生命周期当然还会继续进入支付、履约、售后,但那是下一章要讨论的长流程问题。本章只把“创单这一刻怎么做对”拆开讲透。
4.3 如何判断一个业务动作是不是大事务
不是所有跨系统调用都值得上 Saga、TCC 或补偿编排。真正需要按大事务来设计的业务动作,通常至少满足下面几个判断维度:
| 判断维度 | 典型问题 |
|---|---|
| 跨系统副作用 | 是否会同时修改库存、营销、订单、账务等多个系统 |
| 失败代价 | 某一步部分成功后,是否会出现超卖、重复扣减、资损 |
| 补偿需求 | 任一步失败后是否需要显式回收已有副作用 |
| 幂等要求 | 重试时是否可能重复扣库存、重复占券、重复落单 |
| 状态收敛 | 是否必须把结果收敛成“成功 / 失败 / 待补偿 / 待人工处理” |
| 可审计性 | 事后是否必须解释“哪些动作已执行、哪些动作已回滚” |
可以先记一个很实用的经验判断:
- 普通同步调用:步骤少、依赖少、失败后整体返回即可,没有明显跨系统副作用。
- 大事务设计:需要显式补偿、幂等和状态收敛,但问题仍集中在一个关键业务动作内部。
- 长流程编排:存在长等待、审批、人工介入和多阶段推进,已经超出单个业务动作的边界。
对创单来说,如果只是“查库存 -> 算价 -> 落单”三步确认,短流程可能够用;但只要开始有库存预占、优惠占用、订单创建、失败补偿这些跨系统副作用,它就已经进入大事务问题域了。
4.4 四种主流方案的能力边界
围绕创单这类关键节点,最常见的四种实现路线是:
4.4.1 同步 RPC + 本地事务
做法是由一个入口服务串行调用库存、营销、订单等下游,所有动作尽量在一次同步请求里完成,本地状态通过单库事务保护。
优点:
- 实现简单,链路直观。
- 对短流程最友好。
- 不需要额外引入 Saga Coordinator。
缺点:
- 一旦步骤变多,超时、重试、局部失败会迅速复杂化。
- 中间状态不清晰,补偿难以标准化。
- 对“部分成功后如何回收副作用”支持很弱。
它适合副作用较少的短闭环动作,不适合复杂创单节点。
4.4.2 协同式 Saga
做法是各系统通过事件彼此驱动:订单服务发事件,库存系统消费后再发事件,营销系统再消费,最终把整个业务动作收敛完成。
优点:
- 松耦合,系统自治性强。
- 对异步链路友好。
- 能顺着领域边界自然扩展。
缺点:
- 全局副作用不容易一眼看清。
- 补偿逻辑分散在多个系统里。
- 审计和排障成本偏高。
它适合多系统自治较强的场景,但对交易主链路来说,可见性往往不够。
4.4.3 编排式 Saga
做法是引入一个协调者,由它显式推进业务动作:先预占库存,再占用优惠,再创建订单,失败时按策略触发补偿。
优点:
- 流程可见性强。
- 补偿逻辑集中,便于治理。
- 很适合创单、退款、发布这类有明确主控方的关键节点。
缺点:
- 协调者本身会成为复杂系统。
- 需要认真设计状态持久化、重试、幂等和高可用。
它是很多互联网主交易链路里最常见的大事务折中方案。
4.4.4 TCC / 预留确认取消模型
当副作用系统本身支持显式的 Try / Confirm / Cancel 语义时,可以进一步使用 TCC。
优点:
- 业务语义清晰,预留和确认边界明确。
- 对库存、额度、余额这类可预占资源尤其自然。
- 成功路径和取消路径都更可控。
缺点:
- 要求参与方都暴露
Try / Confirm / Cancel接口。 - 对已有遗留系统改造成本高。
- 不适合所有副作用场景。
它更适合高价值资源的严谨控制,而不是所有普通业务动作。
4.4.5 一张对比表
| 方案 | 实现复杂度 | 状态可见性 | 补偿能力 | 审计能力 | 适用场景 |
|---|---|---|---|---|---|
| 同步 RPC + 本地事务 | 低 | 低 | 低 | 低 | 副作用少、同步闭环 |
| 协同式 Saga | 中 | 中偏低 | 中 | 中偏低 | 多系统异步协作 |
| 编排式 Saga | 中偏高 | 高 | 高 | 高 | 创单、退款、发布等主控节点 |
| TCC | 高 | 高 | 高 | 高 | 库存、余额、额度等可预留资源 |
4.5 大事务系统的核心不是“调接口”,而是“状态收敛”
很多失败的大事务系统,都犯了同一个错误:把大事务理解成“把多个下游接口按顺序调完”。这会导致系统看似有链路,实则没有治理能力。
真正可生产的大事务系统,设计对象应该是“副作用之后如何收敛”,而不是“调用顺序”。
以创单为例,比“库存接口调没调成功”更重要的问题是:
- 当前创单动作处在“待执行”“部分成功”“待补偿”“已完成”还是“已失败”?
- 当前库存预占是“未预占”“已预占”“待释放”还是“已确认”?
- 当前营销额度是“未占用”“已占用”“待回补”还是“已回补”?
一旦把系统设计成状态收敛问题,很多治理动作才有落点:
- 超时:不是简单重试接口,而是把状态推进到“待补偿 / 待人工确认”。
- 补偿:不是“反向再调一遍接口”,而是把已经产生的副作用有控制地回收。
- 幂等:不是只看请求 ID,而是同一状态迁移不能重复制造副作用。
- 人工接管:不是 DBA 直接改库,而是通过受控动作收敛状态。
所以大事务系统设计时,必须优先回答:
- 哪些副作用属于这个大事务边界?
- 每个副作用的成功、失败和补偿语义是什么?
- 超时如何转移?
- 失败后由谁补偿?
- 什么状态允许人工接管?
这也是为什么很多成熟系统最后都会自然走向“显式状态机 + Saga / TCC / 补偿协调器”的设计。
4.6 状态机、Saga、TCC 与 Outbox 怎么选
这里不要从“喜欢哪个框架”开始,而要从复杂度来源开始。
4.6.1 数据库状态机足够的情况
如果流程满足下面这些特点,数据库状态机通常就够:
- 步骤有限。
- 主控服务明确。
- 失败补偿比较简单。
- 编排逻辑仍然能被一个服务清晰维护。
典型例子:简单创单、简单退款、简单额度占用。
4.6.2 应用层 Saga 更合适的情况
当业务动作已经明显跨系统、需要显式补偿和统一审计时,可以选择编排式 Saga。
典型例子:交易主链路创单、售后退款、商品发布确认。
4.6.3 TCC 值得引入的情况
如果参与方都能暴露明确的预留、确认、取消语义,那么引入 TCC 会更合理:
- 资源本身可以被预占。
Confirm和Cancel具有明确业务语义。- 失败代价高,不能依赖事后修账来兜底。
- 参与方改造能力足够强。
典型例子:库存冻结确认、钱包余额预扣、额度占用。
4.6.4 Outbox 适合解决什么问题
很多团队会把 Outbox Pattern 误认为一种完整大事务方案。更准确地说,它解决的是:
- 本地事务提交之后,事件如何可靠发出。
- 避免“数据库写成功了,但消息没发出去”。
- 为后续 Saga 或补偿链路提供稳定起点。
所以 Outbox 通常不是独立的大事务终局方案,而是大事务体系里的可靠事件基座。
4.6.5 一个务实的升级路径
建议遵循下面的演进顺序:
- 先用同步短流程解决最简单问题。
- 补偿和状态边界开始复杂时,升级到状态机 / Saga。
- 关键资源需要明确预留确认时,再考虑
TCC。 - 需要稳定事件桥接时,把
Outbox作为基础设施补上。
这条路径通常比一开始就追求“全局最强方案”更符合互联网系统的实际演进。
4.7 从创单扩展到其他关键业务动作
创单并不是例外,它只是最典型的大事务样本。用同样的视角看,很多关键节点都属于同一类问题。
4.7.1 用户注册 / 认证流程
注册往往不只是“写一条用户记录”,还会涉及账号创建、风控校验、实名状态更新、欢迎权益发放。只要这些动作跨多个系统共同完成,它就已经具备大事务特征。
4.7.2 退款 / 售后流程
退款这个关键动作内部,同样可能涉及退款单创建、支付网关退款、权益回补、账务回冲等多个副作用,而且强依赖补偿和审计。
4.7.3 审核 / 发布流程
商品发布里的 Publish 动作,虽然不是资金交易,但同样具备更新主数据、推送索引、清缓存、发事件等多个副作用,往往很适合编排式 Saga。
4.7.4 一个统一视角
这些流程表面业务不同,但底层共性高度一致:
- 都不是单库事务问题,而是跨系统副作用收敛问题。
- 都需要显式状态。
- 都需要超时、补偿和审计。
- 都需要在简单方案和严格方案之间做选型。
4.8 方法论沉淀:如何在真实系统中做出选择
到这里可以把整章压缩成一组可复用判断。
4.8.1 五个判断句
- 只要一个关键业务动作跨多个系统和多个本地事务共同完成,它就可能是大事务问题。
- 只要失败后不能简单整体回滚,就必须设计补偿和状态收敛。
- 只要副作用真实存在,就必须先定义幂等和补偿,再谈重试。
- 真正决定系统能否生产可用的,不是接口链路,而是状态收敛模型。
- 选型顺序永远应该是:先解决一致性问题,再决定是否值得引入更重的协调机制。
4.8.2 一张务实选型表
| 场景特征 | 推荐方案 |
|---|---|
| 步骤少、同步闭环、失败整体返回即可 | 同步 RPC + 本地事务 |
| 跨系统、多步骤、需要补偿 | 应用层状态机 / 编排式 Saga |
| 多系统自治、事件天然存在、主控方不强 | 协同式 Saga |
| 资源可预占、确认取消边界清晰 | TCC |
4.8.3 面试与评审中的一句话表达
如果要把这一章压缩成一句可直接复用的话,可以这样讲:
互联网系统里的大事务,本质上不是把多个数据库事务绑成一个全局事务,而是把一个关键业务动作建模成可补偿、可幂等、可审计、可收敛的状态机;同步 RPC、Saga、TCC 和 Outbox 只是不同复杂度下的实现手段。