Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

第 4 章 大事务处理方法论:Saga、补偿与最终一致性

从电商创单出发,理解为什么关键业务动作会天然演化为大事务问题,以及如何在同步 RPC、Saga、TCC、Outbox 和补偿机制之间做出可恢复、可审计、可收敛的设计选择。

很多工程师第一次听到“大事务”时,会自然联想到数据库事务、分布式事务,甚至 2PC / XA。这些概念当然重要,但如果把它们直接套到互联网业务里,往往会把问题理解偏。

互联网系统里的大事务,真正要解决的不是“如何让多个数据库像一个数据库那样一起提交”,而是:

当一个关键业务动作必须跨库存、营销、订单、支付、账务等多个系统共同完成时,系统应该如何在失败必然发生的现实里,仍然把结果收敛到可解释、可恢复、可补偿的状态。

这也是为什么本章会从电商创单切入。创单不是唯一的大事务场景,但它最能把大事务设计里的关键矛盾一次性暴露出来:多系统、副作用、补偿、超时、幂等、状态收敛。

这里先提前建立一个边界:

  • 大事务方法论,关注的是一个关键节点的最终一致性。
  • 长生命周期业务流程方法论,关注的是一个业务对象在多个阶段中的推进与治理。

例如,订单从创单到支付、履约、售后,是一个长生命周期业务流程;而“创单”这个关键节点内部的锁库存、占优惠、落订单,则是一个典型大事务。下一章会专门展开长生命周期业务流程,本章只聚焦“大事务”本身。

4.1 大事务到底在解决什么问题

先把三个经常混在一起的概念拆开:

概念解决的问题典型手段
数据库事务单库内的原子性和一致性本地事务、锁、隔离级别
分布式事务多资源之间的提交一致性2PCXATCC
业务大事务跨系统业务目标如何推进和收敛Saga、状态机、Workflow、补偿

数据库事务关心的是“同一个数据库里的几条 SQL 要不要一起成功或失败”。
分布式事务关心的是“多个资源管理器之间能不能形成统一提交语义”。
业务大事务关心的则是“一个业务流程跨多个系统推进时,如何保证结果最终正确且过程可治理”。

这三者并不是谁替代谁的关系,而是处理层次不同的问题。

在互联网系统里,很多流程天生就不适合直接上 2PC / XA

  • 支付网关、短信平台、物流系统、供应商接口都不可能加入你的本地事务。
  • 用户支付不是毫秒级动作,可能会等待几十秒、几分钟,甚至跨天。
  • 高并发下长时间持锁会迅速击穿吞吐和可用性。
  • 很多步骤天然只能做“确认 / 取消 / 补偿”,而不是“全局原子提交 / 回滚”。

所以大事务设计的重点很少是“全局一次提交”,更常见的是:

  1. 找到权威状态源。
  2. 把业务流程拆成多个可提交、可恢复、可补偿的阶段。
  3. 为每个阶段定义状态、超时、补偿和审计规则。
  4. 确保流程在局部失败后仍然能够最终收敛。

这也意味着,大事务系统的设计对象不是“接口调用顺序”,而是“状态推进机制”。

4.2 为什么创单节点天然是大事务

电商创单看起来只是用户点了一次“提交订单”,但“创单”这个动作本身通常至少涉及下面这些子动作:

  1. 校验商品是否仍然可售。
  2. 校验价格、优惠、税费、运费是否仍然有效。
  3. 预占库存或其他资源。
  4. 创建订单主记录和订单行。
  5. 生成支付单或支付上下文。
  6. 为后续支付准备支付上下文。

这条链路的真正难点,不在于步骤多,而在于每一步都可能拥有独立的本地事务和独立的失败语义:

  • 库存预占成功,但订单写库失败。
  • 订单创建成功,但支付发起失败。
  • 营销额度占用成功,但库存预占失败。
  • 支付上下文创建成功,但订单落库失败。
  • 订单超时取消后,库存释放和优惠释放只完成了一部分。

所以创单节点天然带有四个大事务特征:

  • 跨系统:库存、价格、订单、支付、履约通常不是同一个服务。
  • 多副作用:库存、营销、订单、支付上下文都可能留下真实副作用。
  • 可失败:任何一步都可能独立失败,而且失败之后未必能简单回滚。
  • 需收敛:失败之后必须知道哪些动作要撤销、哪些状态要保留。

用一句话概括:创单节点不是“一个接口调多个下游”,而是“一次业务动作里包含多个必须一起收敛的副作用”。

订单生命周期当然还会继续进入支付、履约、售后,但那是下一章要讨论的长流程问题。本章只把“创单这一刻怎么做对”拆开讲透。

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 直接改库,而是通过受控动作收敛状态。

所以大事务系统设计时,必须优先回答:

  1. 哪些副作用属于这个大事务边界?
  2. 每个副作用的成功、失败和补偿语义是什么?
  3. 超时如何转移?
  4. 失败后由谁补偿?
  5. 什么状态允许人工接管?

这也是为什么很多成熟系统最后都会自然走向“显式状态机 + Saga / TCC / 补偿协调器”的设计。

4.6 状态机、Saga、TCC 与 Outbox 怎么选

这里不要从“喜欢哪个框架”开始,而要从复杂度来源开始。

4.6.1 数据库状态机足够的情况

如果流程满足下面这些特点,数据库状态机通常就够:

  • 步骤有限。
  • 主控服务明确。
  • 失败补偿比较简单。
  • 编排逻辑仍然能被一个服务清晰维护。

典型例子:简单创单、简单退款、简单额度占用。

4.6.2 应用层 Saga 更合适的情况

当业务动作已经明显跨系统、需要显式补偿和统一审计时,可以选择编排式 Saga。

典型例子:交易主链路创单、售后退款、商品发布确认。

4.6.3 TCC 值得引入的情况

如果参与方都能暴露明确的预留、确认、取消语义,那么引入 TCC 会更合理:

  • 资源本身可以被预占。
  • ConfirmCancel 具有明确业务语义。
  • 失败代价高,不能依赖事后修账来兜底。
  • 参与方改造能力足够强。

典型例子:库存冻结确认、钱包余额预扣、额度占用。

4.6.4 Outbox 适合解决什么问题

很多团队会把 Outbox Pattern 误认为一种完整大事务方案。更准确地说,它解决的是:

  • 本地事务提交之后,事件如何可靠发出。
  • 避免“数据库写成功了,但消息没发出去”。
  • 为后续 Saga 或补偿链路提供稳定起点。

所以 Outbox 通常不是独立的大事务终局方案,而是大事务体系里的可靠事件基座。

4.6.5 一个务实的升级路径

建议遵循下面的演进顺序:

  1. 先用同步短流程解决最简单问题。
  2. 补偿和状态边界开始复杂时,升级到状态机 / Saga。
  3. 关键资源需要明确预留确认时,再考虑 TCC
  4. 需要稳定事件桥接时,把 Outbox 作为基础设施补上。

这条路径通常比一开始就追求“全局最强方案”更符合互联网系统的实际演进。

4.7 从创单扩展到其他关键业务动作

创单并不是例外,它只是最典型的大事务样本。用同样的视角看,很多关键节点都属于同一类问题。

4.7.1 用户注册 / 认证流程

注册往往不只是“写一条用户记录”,还会涉及账号创建、风控校验、实名状态更新、欢迎权益发放。只要这些动作跨多个系统共同完成,它就已经具备大事务特征。

4.7.2 退款 / 售后流程

退款这个关键动作内部,同样可能涉及退款单创建、支付网关退款、权益回补、账务回冲等多个副作用,而且强依赖补偿和审计。

4.7.3 审核 / 发布流程

商品发布里的 Publish 动作,虽然不是资金交易,但同样具备更新主数据、推送索引、清缓存、发事件等多个副作用,往往很适合编排式 Saga。

4.7.4 一个统一视角

这些流程表面业务不同,但底层共性高度一致:

  • 都不是单库事务问题,而是跨系统副作用收敛问题。
  • 都需要显式状态。
  • 都需要超时、补偿和审计。
  • 都需要在简单方案和严格方案之间做选型。

4.8 方法论沉淀:如何在真实系统中做出选择

到这里可以把整章压缩成一组可复用判断。

4.8.1 五个判断句

  1. 只要一个关键业务动作跨多个系统和多个本地事务共同完成,它就可能是大事务问题。
  2. 只要失败后不能简单整体回滚,就必须设计补偿和状态收敛。
  3. 只要副作用真实存在,就必须先定义幂等和补偿,再谈重试。
  4. 真正决定系统能否生产可用的,不是接口链路,而是状态收敛模型。
  5. 选型顺序永远应该是:先解决一致性问题,再决定是否值得引入更重的协调机制。

4.8.2 一张务实选型表

场景特征推荐方案
步骤少、同步闭环、失败整体返回即可同步 RPC + 本地事务
跨系统、多步骤、需要补偿应用层状态机 / 编排式 Saga
多系统自治、事件天然存在、主控方不强协同式 Saga
资源可预占、确认取消边界清晰TCC

4.8.3 面试与评审中的一句话表达

如果要把这一章压缩成一句可直接复用的话,可以这样讲:

互联网系统里的大事务,本质上不是把多个数据库事务绑成一个全局事务,而是把一个关键业务动作建模成可补偿、可幂等、可审计、可收敛的状态机;同步 RPC、Saga、TCC 和 Outbox 只是不同复杂度下的实现手段。