SAGA 分布式事务模式

深入 SAGA 分布式事务模式:Choreography 与 Orchestration 两种实现方式,补偿事务的设计原则,消息持久化与重试保障,与 2PC 的对比选型,以及失败恢复与隔离性约束。

跨服务的数据一致性是分布式架构最难啃的骨头。数据库事务能保证单机原子性,却管不了分布在多个服务、多个数据库里的数据。SAGA 把"一个大事务"拆成"一串本地事务 + 补偿动作",用最终一致性换取可用性与吞吐。本文覆盖两种实现、补偿设计、可靠性保障、与 2PC 的对比,以及失败恢复的完整链路。

1. 为什么需要 SAGA

1.1 分布式事务的困境

下单流程横跨订单、库存、账户三个服务、三个数据库。任何一个本地事务都保证自身原子性,但没有一个全局事务能保证三者要么全成、要么全败。

1.2 强一致协议的代价

要让三者原子提交,可选 2PC:准备阶段锁定全部资源、提交阶段统一裁决。代价是同步阻塞、单点协调、长时间持锁——在跨服务、高并发场景下吞吐和可用性都不可接受。

1.3 SAGA 的思路

SAGA 放弃"瞬时全成",接受"每个本地事务都成功,失败则用补偿回滚已完成的动作"——保证最终一致,且全程无全局锁。

一次下单 = 多个本地事务串联:

  创建订单 → 扣减库存 → 扣减余额 → 发送短信

  若"扣减余额"失败:
  倒序执行补偿:
    恢复余额 ← 恢复库存 ← 取消订单
特性数据库事务2PCSAGA
原子性瞬时瞬时(有风险)最终一致
全局锁无(单库)有(阻塞)无
跨服务不支持支持支持
吞吐高低高
一致性窗口无极短明显存在

一句话:SAGA 用**“补偿代替回滚”**换掉全局锁——业务明确接受一段时间的中间状态,换取跨服务的可用性与吞吐。


2. SAGA 的核心概念

2.1 本地事务与全局事务

  • 本地事务:单个服务内、单个数据库内的 ACID 事务;
  • 全局事务(SAGA):一系列有序执行的本地事务,由流程定义连接。

2.2 补偿动作 Compensating Action

补偿是 SAGA 的灵魂:每个正向动作都对应一个语义相反、能抵消其效果的动作。

正向动作补偿动作补偿前提
创建订单取消订单(改状态)订单未发货
扣减库存加回库存库存预占未出库
扣减余额加回余额资金未清分
核销优惠券恢复优惠券券未使用

补偿不是"回滚数据库",而是执行一个新的正向业务操作来抵消效果——这是它与 2PC 回滚的本质区别。

2.3 正向与补偿的边界

  • 已完成的动作必须能补偿;
  • 补偿动作本身要幂等(重复执行结果一致);
  • 补偿失败要有人工介入或重试通道。

一句话:SAGA 的可信度 = 每个正向动作都有可抵消的补偿 + 补偿本身幂等——缺一不可。


3. Choreography 实现事件驱动编排

3.1 原理

没有中央协调器,每个服务在本地事务提交后发布事件,监听相关事件的服务执行下一步并发布后续事件。

订单服务 ──[订单已创建]──→ 库存服务
库存服务 ──[库存已扣减]──→ 账户服务
账户服务 ──[余额已扣减]──→ 通知服务
账户服务 ──[余额扣减失败]──→ 订单服务(触发补偿链)

3.2 实现示例

// 订单服务监听失败事件,启动补偿
@EventListener
public void onBalanceDeductionFailed(BalanceDeductionFailedEvent e) {
    compensationService.cancelOrder(e.getOrderId());   // 取消订单
    compensationService.restoreStock(e.getOrderId());  // 恢复库存
}

3.3 优劣

优点缺点
无单点、实现简单流程分散在多个服务,难追踪
新增环节只加订阅者事件环风险(A→B→A 循环)
天然并发一致性窗口更大
与事件驱动架构契合补偿逻辑难集中审计

一句话:Choreography 适合环节少、事件语义清晰、团队能达成事件契约的流程;环节一多,追踪与排障成本会指数上升。


4. Orchestration 实现中央协调器

4.1 原理

引入协调器(Orchestrator/Saga Execution Coordinator),它显式定义每一步、调用各服务、记录状态,并在失败时决定补偿。

              Saga 协调器
               │  1. 创建订单(调订单服务)
               │  2. 扣减库存(调库存服务)
               │  3. 扣减余额(调账户服务) ← 失败
               │  4. 补偿:恢复库存(调库存服务)
               │  5. 补偿:取消订单(调订单服务)
               ▼
            状态表:每步记录 SUCCESS/FAILED

4.2 协调器的状态机

协调器维护一个 SAGA 状态机,持久化每一步结果:

状态:INIT → STEP_1_OK → STEP_2_OK → COMPLETED
                    ↘ STEP_3_FAIL → COMPENSATING → COMPENSATED

4.3 实现要点

  • 协调器可基于流程引擎(Camunda、Temporal、自建状态机);
  • 每一步调用要幂等,协调器按状态表驱动,不依赖内存;
  • 协调器本身要高可用,状态持久化到数据库。

一句话:Orchestration 把流程、状态、补偿全部集中到一个可审计的协调器——复杂度集中了,但排查与管控也集中了,适合复杂长流程。


5. 两种实现怎么选

5.1 对比

维度ChoreographyOrchestration
控制权分散集中
流程可见性靠事件日志拼状态表一眼可见
补偿编排分散在各服务协调器统一
新增环节加订阅者改协调器
单点风险无协调器是单点(需高可用)
适用简单、快速、事件驱动成熟复杂、长流程、强管控

5.2 落地建议

  • 环节 ≤ 3、事件语义稳定 → Choreography;
  • 环节多、有分支回滚、需审批审计 → Orchestration;
  • 大多数生产系统最终选择 Orchestration(配合流程引擎),因为它把"业务流程"还原成显式代码,而非散落的事件。

5.3 协调器驱动代码示例

// Orchestration:协调器显式驱动,失败走补偿分支
public void runSaga(String orderId) {
    Order o = orderService.createOrder(orderId);      // 步骤1
    try {
        stockService.reserveStock(o);                 // 步骤2
        accountService.deductBalance(o);              // 步骤3
        sagaState.markComplete(orderId);              // 全部成功
    } catch (BalanceDeductFailedException e) {
        // 倒序补偿
        compensationService.restoreStock(o);
        compensationService.cancelOrder(o);
        sagaState.markCompensated(orderId);
    }
}

一句话:选型不是对错之争——Choreography 输在追踪,Orchestration 赢在可控,复杂流程优先集中化。


6. 补偿事务的设计

6.1 补偿设计三原则

  1. 语义补偿:补偿是业务动作,不是数据库回滚;
  2. 幂等:补偿重复执行结果相同;
  3. 可重试:补偿失败要有重试与死信通道。

6.2 设计示例

// 正向:扣减库存(预占)
public void reserveStock(String orderId, String sku, int qty) {
    int updated = jdbc.update(
        "UPDATE stock SET reserved = reserved + ? " +
        "WHERE sku = ? AND available >= ?", qty, sku, qty);
    if (updated == 0) throw new InsufficientStockException();
    publish(new StockReserved(orderId, sku, qty));
}

// 补偿:释放库存(幂等,reserved 不能为负)
public void releaseStock(String orderId, String sku, int qty) {
    jdbc.update(
        "UPDATE stock SET reserved = reserved - ? " +
        "WHERE sku = ? AND reserved >= ?", qty, sku, qty);
}

6.3 常见补偿模式

模式做法场景
状态机回滚订单状态 CANCEL/CLOSED订单类
资源释放库存回补、余额解冻资源类
反交易生成一笔退款流水资金类
幂等替换用幂等键覆盖旧结果通用

一句话:补偿设计的成败在于**“能不能把已发生的效果干净地抵消”**——正向设计时就要想好补偿路径,别等失败时再发明。


7. 可靠性持久化重试与隔离性

7.1 消息持久化与本地消息表

Choreography 的事件、Orchestration 的指令都需持久化,防止进程崩溃丢消息。常用本地消息表 + 事务内写表保证"业务与消息同生共死":

本地事务:写业务数据 + 写 outbox 消息表(同一数据库事务)
后台任务:扫描 outbox → 发布消息 → 标记已发送

7.2 重试与幂等消费

消息消费要"至少一次 + 幂等":消费者用消息唯一 ID 去重,重试指数退避,重试 N 次后进死信队列人工处理。

// 消费端幂等
public void onStockReserved(StockReservedEvent e) {
    if (idempotencyStore.exists(e.getEventId())) return; // 已处理
    process(e);
    idempotencyStore.mark(e.getEventId());
}

7.3 隔离性 Isolation 问题

SAGA 允许中间状态,会带来脏读问题:步骤 2 已提交、步骤 3 还在执行时,外部可能读到"已扣库存但未扣款"的中间态。缓解手段:

手段说明
状态标记订单显式标记 PENDING/IN_PROGRESS
语义锁冻结金额/预占库存,禁止他人动
乐观锁版本号冲突即重试
读取修复读到中间态时延迟或提示"处理中"

7.4 失败恢复流程

崩溃、重启、超时都是常态,SAGA 要靠状态机驱动恢复,而不是靠人工记忆:

协调器崩溃 → 重启后读状态表
  若停在 STEP_2_OK:从 STEP_3 继续正向
  若停在 COMPENSATING:继续执行未完成的补偿
  超时的步骤:按重试策略重试,超过阈值转人工
  • 正向恢复:已成功的步骤不再重复,未完成的继续;
  • 补偿恢复:补偿中断时继续补偿,直到 COMPENSATED;
  • 人工通道:死信与超限步骤进入补偿台,业务人员手工兜底。

一句话:SAGA 的可靠性 = 本地消息表保证不丢 + 幂等保证不重 + 补偿保证可逆,隔离性靠语义锁与状态标记缓解而非消除;恢复则靠持久化状态机,崩溃后从状态表续跑而不是从头再来。


8. 踩坑清单

坑现象对策
补偿不是幂等重复补偿双倍扣减补偿唯一键 + 去重
消息丢失链断裂无人继续本地消息表 + outbox
无持久化状态协调器重启流程丢了状态表落库
正向无补偿设计失败后无法抵消正向设计时同步设计补偿
事件环A→B→A 死循环事件语义收敛 + 环路检测
中间态被外部读到脏读订单状态标记 + 语义锁
补偿顺序错先恢复库存再取消订单严格倒序执行
死信无人管卡住的订单堆积死信队列 + 人工补偿台

9. 总结

维度结论
适用场景跨服务长流程、要求最终一致
推荐实现复杂流程 Orchestration,简单流程 Choreography
核心能力补偿 + 幂等 + 持久化 + 重试
对比 2PCSAGA 无锁高吞吐,2PC 强一致低吞吐
隔离性语义锁缓解,不追求事务级隔离

一句话记住:SAGA 是用"补偿"换"无锁"的分布式事务——把大事务拆成本地事务串,失败倒序补偿,靠幂等与持久化把一致性收敛到最终一致。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「架构」更多文章

  1. 绞杀者模式(Strangler Fig)
  2. 功能开关与发布工程
  3. 边车模式(Sidecar)