mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2104 字
6 分钟
Saga 模式(Saga Pattern)
2026-06-13

一、为什么需要 Saga 模式#

电商下单,表面上一个操作,背后要跨四五个服务:创建订单 → 扣减库存 → 扣款支付 → 通知仓储发货。每个服务有自己的数据库,没有一个数据库能同时锁住订单表、库存表和支付账户。如果扣款失败,库存已经扣了,怎么办?

传统做法是两阶段提交(2PC):协调者让所有参与者先”准备”,全部同意后才”提交”。问题在于——准备阶段要锁资源,扣库存的服务锁住了那行记录,支付服务也在锁账户,所有人都等着协调者说”提交”还是”回滚”。协调者本身也是单点:它挂了,所有参与者就一直锁着。在微服务架构里,服务之间通过网络调用,网络分区是常态,2PC 的阻塞特性会让可用性急剧下降。

Saga 的思路完全不同:不做分布式锁,把长事务拆成一串本地事务。每个本地事务提交后就释放资源,如果后续步骤失败,通过执行补偿操作来”撤销”已完成的前置步骤。扣了库存但支付失败?那就把库存加回去。每个正向操作都有一个对应的补偿操作,按逆序执行。资源不锁,可用性不受影响——代价是放弃了隔离性,中间状态对其他事务可见。

二、现实类比#

办婚礼:订酒店 → 请婚庆 → 订花。如果婚庆临时来不了,你要取消订花,可能还要退酒店。每一步都有一个对应的”撤销”操作,出了问题按反方向一步步回退。没有谁能同时锁住酒店、婚庆和花店的档期——酒店等你确认的时候,花店早就把那天的花安排给别人了。

三、核心思想#

Saga 把一个分布式事务拆成一组有序的本地事务 T1, T2, …, Tn,每个 Ti 都有一个对应的补偿操作 Ci。如果 Tk 失败,按逆序执行 Ck-1, Ck-2, …, C1 来回滚已经完成的步骤。补偿操作本身必须是幂等的——因为网络超时可能导致重试,同一个补偿可能执行多次。

3.1 两种协调方式#

编排(Choreography):没有中心协调者,每个服务完成自己的事务后发布事件,下一个服务监听到事件后执行。事件驱动的去中心化方式。

协调(Orchestration):有一个中心协调者(Orchestrator),它告诉每个服务该做什么。服务只负责执行和返回结果,协调逻辑集中在协调者中。

sequenceDiagram participant O as 订单服务 participant I as 库存服务 participant P as 支付服务 Note over O,P: 编排模式(Choreography)——事件驱动 O->>O: T1: 创建订单 O-->>I: OrderCreated 事件 I->>I: T2: 扣减库存 I-->>P: InventoryDeducted 事件 P->>P: T3: 扣款支付(失败) P-->>I: PaymentFailed 事件 I->>I: C2: 恢复库存 I-->>O: InventoryRestored 事件 O->>O: C1: 取消订单
sequenceDiagram participant OR as 协调者 participant O as 订单服务 participant I as 库存服务 participant P as 支付服务 Note over OR,P: 协调模式(Orchestration)——中心控制 OR->>O: 执行 T1 O-->>OR: T1 成功 OR->>I: 执行 T2 I-->>OR: T2 成功 OR->>P: 执行 T3 P-->>OR: T3 失败 OR->>I: 执行 C2(补偿) I-->>OR: C2 完成 OR->>O: 执行 C1(补偿) O-->>OR: C1 完成

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 触发补偿链。好处是服务之间松耦合,新增步骤只需监听已有事件并发布新事件;坏处是流程隐含在事件链中,没有一个地方能看到完整的事务流程,步骤多了之后调试和追踪都很困难。

六、生产验证#

项目源码位置用途
Temporalgithub.com/temporalio/temporal分布式工作流引擎,原生支持 Saga 补偿模式
Apache Seatagithub.com/apache/incubator-seata阿里开源的分布式事务框架,Saga 模式是核心模式之一
Netflix Cadencegithub.com/Netflix/cadenceTemporal 的前身,Netflix 内部大规模使用
  • Temporalworkflow.goWithChildWorkflow 和补偿机制直接支持 Saga。在 workflow 定义中,每个 activity 可以注册一个补偿函数,workflow 失败时自动按逆序执行。Temporal 的优势在于自动重试、状态持久化和可视化追踪,补偿失败不会丢状态。
  • Apache Seatastatelang 模块实现了 JSON 驱动的 Saga 状态机,可以定义服务调用图和补偿关系,运行时自动编排和回滚。适合不想在代码中硬编码补偿逻辑的场景。
  • Netflix Cadence — Temporal 的前身,Netflix 用它管理视频编码、A/B 测试等长流程。Cadence 的工作流模型天然适配 Saga:每个步骤都是一个 activity,失败后框架自动触发补偿 activity。

七、小结#

何时使用:

  • 跨服务的业务流程——订单履约、旅行预订等天然涉及多服务的场景
  • 长时间运行的事务——2PC 锁资源太久,中间步骤需要秒级甚至分钟级
  • 可接受最终一致性——业务上允许”库存扣了但还没发货”的中间态
  • 补偿操作语义清晰——“取消订单""退款""恢复库存”这类操作业务上有明确定义

何时不用:

  • 需要强隔离性——资金划拨类场景,中间态不可见,用 TCC
  • 步骤只有两三个且在同一数据库——直接用本地事务或 2PC,不需要引入 Saga 的复杂性
  • 补偿操作无法定义——有些操作不可逆(发邮件、发短信),补偿无法”取消”已发出的消息
  • 服务数量过多——编排模式的事件链难以追踪,协调模式变成一个上帝协调者,两种方式都会失控

八、参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Saga 模式(Saga Pattern)
https://blog.souloss.com/posts/programming/system-patterns/system-patterns-saga/
作者
Tsukimi
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时