一、为什么需要 Saga 模式
电商下单,表面上一个操作,背后要跨四五个服务:创建订单 → 扣减库存 → 扣款支付 → 通知仓储发货。每个服务有自己的数据库,没有一个数据库能同时锁住订单表、库存表和支付账户。如果扣款失败,库存已经扣了,怎么办?
传统做法是两阶段提交(2PC):协调者让所有参与者先”准备”,全部同意后才”提交”。问题在于——准备阶段要锁资源,扣库存的服务锁住了那行记录,支付服务也在锁账户,所有人都等着协调者说”提交”还是”回滚”。协调者本身也是单点:它挂了,所有参与者就一直锁着。在微服务架构里,服务之间通过网络调用,网络分区是常态,2PC 的阻塞特性会让可用性急剧下降。
Saga 的思路完全不同:不做分布式锁,把长事务拆成一串本地事务。每个本地事务提交后就释放资源,如果后续步骤失败,通过执行补偿操作来”撤销”已完成的前置步骤。扣了库存但支付失败?那就把库存加回去。每个正向操作都有一个对应的补偿操作,按逆序执行。资源不锁,可用性不受影响——代价是放弃了隔离性,中间状态对其他事务可见。
二、现实类比
办婚礼:订酒店 → 请婚庆 → 订花。如果婚庆临时来不了,你要取消订花,可能还要退酒店。每一步都有一个对应的”撤销”操作,出了问题按反方向一步步回退。没有谁能同时锁住酒店、婚庆和花店的档期——酒店等你确认的时候,花店早就把那天的花安排给别人了。
三、核心思想
Saga 把一个分布式事务拆成一组有序的本地事务 T1, T2, …, Tn,每个 Ti 都有一个对应的补偿操作 Ci。如果 Tk 失败,按逆序执行 Ck-1, Ck-2, …, C1 来回滚已经完成的步骤。补偿操作本身必须是幂等的——因为网络超时可能导致重试,同一个补偿可能执行多次。
3.1 两种协调方式
编排(Choreography):没有中心协调者,每个服务完成自己的事务后发布事件,下一个服务监听到事件后执行。事件驱动的去中心化方式。
协调(Orchestration):有一个中心协调者(Orchestrator),它告诉每个服务该做什么。服务只负责执行和返回结果,协调逻辑集中在协调者中。
3.2 正向操作与补偿操作
| 正向操作 | 补偿操作 | 说明 |
|---|---|---|
| 创建订单 | 取消订单 | 标记为已取消,非物理删除 |
| 扣减库存 | 恢复库存 | 数量加回可用库存 |
| 扣款支付 | 退款 | 原路退回 |
| 通知发货 | 取消发货 | 通知仓储拦截 |
3.3 关键属性
| 属性 | 值 |
|---|---|
| 一致性模型 | 最终一致性 |
| 隔离性 | 无(中间状态可见) |
| 补偿要求 | 幂等、可重入 |
| 回滚方式 | 语义补偿,非物理回滚 |
| 资源锁定 | 无(每步提交即释放) |
四、变体与对比
| 模式 | 一致性保证 | 性能影响 | 适用场景 |
|---|---|---|---|
| Saga(编排) | 最终一致 | 低——无锁、无协调者 | 步骤少、服务少,事件流清晰 |
| Saga(协调) | 最终一致 | 中——协调者是单点,但无资源锁 | 步骤多、流程复杂,需要集中管控 |
| 2PC | 强一致 | 高——阻塞、锁资源 | 短事务、同构数据库、可用性要求低 |
| TCC | 最终一致 | 中——Try 阶段预留资源 | 需要隔离性、资金类场景 |
TCC(Try-Confirm-Cancel)和 Saga 的核心区别在于隔离性:TCC 的 Try 阶段预留资源,其他事务看不到中间状态;saga 的每一步直接提交,中间状态对外可见。资金转账这种场景,你不想让用户看到”钱扣了但还没到账”的中间态,TCC 更合适。订单履约这种场景,“库存扣了但还没发货”完全可以接受,saga 更轻量。
五、多语言实现
5.1 Go 实现
package saga
import ( "context" "fmt" "log")
// Step 定义 saga 的一个步骤type Step struct { Name string // 步骤名称 Execute func(ctx context.Context) error // 正向操作 Compensate func(ctx context.Context) error // 补偿操作}
// Saga 协调者type Saga struct { name string steps []Step}
func NewSaga(name string, steps []Step) *Saga { return &Saga{name: name, steps: steps}}
// Execute 执行 saga:顺序执行所有步骤,失败则逆序补偿func (s *Saga) Execute(ctx context.Context) error { // 记录已完成的步骤,用于补偿 completed := make([]int, 0, len(s.steps))
for i, step := range s.steps { log.Printf("[Saga:%s] 执行步骤 %d: %s", s.name, i+1, step.Name) if err := step.Execute(ctx); err != nil { log.Printf("[Saga:%s] 步骤 %d 失败: %v,开始补偿", s.name, i+1, err) s.compensate(ctx, completed) return fmt.Errorf("步骤 %q 失败: %w,已执行补偿", step.Name, err) } completed = append(completed, i) }
log.Printf("[Saga:%s] 所有步骤完成", s.name) return nil}
// compensate 逆序执行已完成步骤的补偿操作func (s *Saga) compensate(ctx context.Context, completed []int) { // 逆序遍历 for i := len(completed) - 1; i >= 0; i-- { idx := completed[i] step := s.steps[idx] log.Printf("[Saga:%s] 补偿步骤 %d: %s", s.name, idx+1, step.Name)
// 补偿操作必须幂等,重试直到成功或上下文取消 if err := step.Compensate(ctx); err != nil { log.Printf("[Saga:%s] 补偿步骤 %d 失败: %v,需要人工介入", s.name, idx+1, err) } }}package main
import ( "context" "fmt" "log" "saga")
func main() { orderSaga := saga.NewSaga("创建订单", []saga.Step{ { Name: "创建订单", Execute: func(ctx context.Context) error { log.Println("创建订单:写入订单表") return nil }, Compensate: func(ctx context.Context) error { log.Println("取消订单:标记订单为已取消") return nil }, }, { Name: "扣减库存", Execute: func(ctx context.Context) error { log.Println("扣减库存:库存 -1") return nil }, Compensate: func(ctx context.Context) error { log.Println("恢复库存:库存 +1") return nil }, }, { Name: "扣款支付", Execute: func(ctx context.Context) error { log.Println("扣款支付:余额不足,扣款失败") return fmt.Errorf("余额不足") }, Compensate: func(ctx context.Context) error { log.Println("退款支付:无需退款,支付未成功") return nil }, }, })
if err := orderSaga.Execute(context.Background()); err != nil { log.Printf("Saga 执行失败: %v", err) }}Go 实现的关键点:completed 切片记录已成功执行的步骤索引,失败时逆序遍历这个切片执行补偿。补偿操作如果失败,只记录日志,不中断其他补偿——因为一个补偿失败不应该阻止后续补偿的执行,否则可能留下更多未回滚的中间状态。生产中补偿失败需要告警和人工介入。
5.2 TypeScript 实现
// 编排模式(Choreography):事件驱动的 Saga
type EventHandler = (data: unknown) => Promise<void>;
// 简单的事件总线class EventBus { private handlers = new Map<string, EventHandler[]>();
// 注册事件处理器 on(event: string, handler: EventHandler): void { const list = this.handlers.get(event) ?? []; list.push(handler); this.handlers.set(event, list); }
// 发布事件,触发所有注册的处理器 async emit(event: string, data: unknown): Promise<void> { const handlers = this.handlers.get(event) ?? []; for (const handler of handlers) { await handler(data); } }}
// 订单服务class OrderService { constructor(private bus: EventBus) { // 监听库存恢复事件,执行补偿 this.bus.on("InventoryRestored", async (data: unknown) => { const { orderId } = data as { orderId: string }; console.log(`取消订单 ${orderId}:标记为已取消`); }); }
async createOrder(orderId: string): Promise<void> { console.log(`创建订单 ${orderId}:写入订单表`); // 发布订单创建事件,触发下游 await this.bus.emit("OrderCreated", { orderId }); }}
// 库存服务class InventoryService { constructor(private bus: EventBus) { // 监听订单创建事件 this.bus.on("OrderCreated", async (data: unknown) => { const { orderId } = data as { orderId: string }; console.log(`扣减库存:订单 ${orderId} 库存 -1`); await this.bus.emit("InventoryDeducted", { orderId }); });
// 监听支付失败事件,执行补偿 this.bus.on("PaymentFailed", async (data: unknown) => { const { orderId } = data as { orderId: string }; console.log(`恢复库存:订单 ${orderId} 库存 +1`); await this.bus.emit("InventoryRestored", { orderId }); }); }}
// 支付服务class PaymentService { constructor(private bus: EventBus) { // 监听库存扣减事件 this.bus.on("InventoryDeducted", async (data: unknown) => { const { orderId } = data as { orderId: string }; console.log(`扣款支付:订单 ${orderId} 余额不足,扣款失败`); // 支付失败,发布失败事件触发补偿链 await this.bus.emit("PaymentFailed", { orderId }); }); }}
// 使用示例async function main(): Promise<void> { const bus = new EventBus(); const orderService = new OrderService(bus); new InventoryService(bus); // 注册事件处理器 new PaymentService(bus);
// 从订单服务开始,事件链自动驱动 await orderService.createOrder("ORD-001");}TypeScript 实现展示了编排模式:没有中心协调者,每个服务只关心自己监听的事件和需要发布的事件。OrderCreated 触发库存扣减,InventoryDeducted 触发支付,PaymentFailed 触发补偿链。好处是服务之间松耦合,新增步骤只需监听已有事件并发布新事件;坏处是流程隐含在事件链中,没有一个地方能看到完整的事务流程,步骤多了之后调试和追踪都很困难。
六、生产验证
| 项目 | 源码位置 | 用途 |
|---|---|---|
| Temporal | github.com/temporalio/temporal | 分布式工作流引擎,原生支持 Saga 补偿模式 |
| Apache Seata | github.com/apache/incubator-seata | 阿里开源的分布式事务框架,Saga 模式是核心模式之一 |
| Netflix Cadence | github.com/Netflix/cadence | Temporal 的前身,Netflix 内部大规模使用 |
- Temporal — workflow.go 中
WithChildWorkflow和补偿机制直接支持 Saga。在 workflow 定义中,每个 activity 可以注册一个补偿函数,workflow 失败时自动按逆序执行。Temporal 的优势在于自动重试、状态持久化和可视化追踪,补偿失败不会丢状态。 - Apache Seata — statelang 模块实现了 JSON 驱动的 Saga 状态机,可以定义服务调用图和补偿关系,运行时自动编排和回滚。适合不想在代码中硬编码补偿逻辑的场景。
- Netflix Cadence — Temporal 的前身,Netflix 用它管理视频编码、A/B 测试等长流程。Cadence 的工作流模型天然适配 Saga:每个步骤都是一个 activity,失败后框架自动触发补偿 activity。
七、小结
何时使用:
- 跨服务的业务流程——订单履约、旅行预订等天然涉及多服务的场景
- 长时间运行的事务——2PC 锁资源太久,中间步骤需要秒级甚至分钟级
- 可接受最终一致性——业务上允许”库存扣了但还没发货”的中间态
- 补偿操作语义清晰——“取消订单""退款""恢复库存”这类操作业务上有明确定义
何时不用:
- 需要强隔离性——资金划拨类场景,中间态不可见,用 TCC
- 步骤只有两三个且在同一数据库——直接用本地事务或 2PC,不需要引入 Saga 的复杂性
- 补偿操作无法定义——有些操作不可逆(发邮件、发短信),补偿无法”取消”已发出的消息
- 服务数量过多——编排模式的事件链难以追踪,协调模式变成一个上帝协调者,两种方式都会失控
八、参考资料
- Sagas - Hector Garcia-Molina & Kenneth Salem - 1987 年原论文,提出 Saga 作为长事务的解决方案
- Temporal Documentation - Saga Pattern - Temporal 对 Saga 模式的原生支持
- Apache Seata Saga Mode - Seata 的 Saga 状态机实现
- Microsoft Azure - Saga Pattern - 云架构中心的 Saga 模式参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






