← 中文资源

简体中文版本

Saga 模式

Saga 把跨服务业务拆成一系列本地事务,后续步骤失败时用补偿动作而非分布式事务保持一致。

阅读英文原文

本地事务,全局结果

订单流程可能包括预留库存、扣款、创建发货单。每个服务有自己的数据库,无法与其他服务参与同一个 ACID 事务。Saga 把流程显式化:预留库存、扣款、创建发货单;如果发货失败,就退款并释放库存。每一步本地提交,Saga 负责顺序和补偿逻辑。

有两种协调方式。编排式由一个中心执行器调用各服务并决定下一步;协同式由每个服务响应事件并发布下一个事件,没有组件拥有完整流程。复杂流程用编排式更容易推理和观测;协同式降低耦合,但服务增多后全局流程更难看清。

type Step = {
  name: string;
  run: () => Promise<void>;
  compensate: () => Promise<void>;
};

export async function runSaga(steps: Step[]): Promise<void> {
  const completed: Step[] = [];
  try {
    for (const step of steps) {
      await step.run();
      completed.push(step);
    }
  } catch (error) {
    for (const step of completed.reverse()) {
      await step.compensate();
    }
    throw error;
  }
}

这个最小执行器不足以生产使用。调用下一步前要持久化流程状态和已完成步骤,使进程崩溃后可以恢复或补偿。每个 run 和 compensate 都应使用由 saga id 和步骤名派生的幂等键。增加超时和可运维的最终失败状态。

设计中转状态

Saga 会暴露中间状态:订单可能在支付成功前存在。要显式建模 pending_payment、reserved、cancelled,并决定哪些对用户可见。语义锁把记录标记为正在被 Saga 修改,阻止其他操作把它当作最终状态。这不是隔离,但能防止最危险的并发更新。

补偿不总是原操作的逆操作。退款是带手续费和延迟的新交易;释放预留可能无法恢复原库存位置,因为其他订单已经消耗。应把补偿设计成业务操作,在部分失败下测试,并使其可观测。

何时使用

当工作流跨服务、运行时间长,并且可以接受带显式中转状态的最终一致性时,使用 Saga。优点是不用持有跨网络调用的全局锁,每个服务保留自己的事务边界;缺点是必须显式设计并运维补偿、幂等和中间状态。与分布式事务相比,Saga 放弃隔离,换取可用性和独立演进。不要用它来回避单个服务内的事务边界设计;本地 ACID 事务更简单、更强。如果流程要求严格隔离或立即全局一致,Saga 就是错误的抽象,需要改变服务边界或业务流程。