Skip to content

Saga = 跨服务事务的补偿模式 ​

解决什么问题:分布式场景下不能用 2PC(两阶段提交)做跨服务事务—— 2PC 慢、脆弱、单点故障;Saga = "长事务拆成多个本地事务 + 失败时补偿"。 用"业务补偿"代替"技术锁" = 用业务能接受的方式达到最终一致。

它是什么 ​

Saga 是分布式系统处理跨服务事务的标准模式——

核心理念:

  • 每个子事务有对应的补偿操作(如"扣库存" + "释放库存","扣款" + "退款")
  • 失败时反向补偿,不是回滚(没有原子性锁可回滚)
  • 接受最终一致(补偿完成后,业务状态 = 一致)

两种实现方式:

Choreographed Saga(事件驱动,无中心)
  Step 1: OrderService.create() → 发 OrderCreated
  Step 2: InventoryService.consume(OrderCreated) → 发 InventoryReserved
  Step 3: PaymentService.charge(OrderCreated) → 发 PaymentCompleted
  Step 4: ShippingService.ship(OrderCreated) → 发 OrderShipped
  
  任一步失败 → 该服务发失败事件 → 上游服务监听 → 执行补偿
  
Orchestrated Saga(中央协调器)
  OrderOrchestrator.execute():
    step1 = inventory.reserve()
    if step1.failed: cancel order, return
    step2 = payment.charge()
    if step2.failed: 
      inventory.release()  # 补偿 step1
      cancel order, return
    step3 = shipping.ship()
    ...

IDDD Ch 13(Vernon): "Sagas manage long-running business transactions that span multiple Bounded Contexts. Each step has a compensating action to undo the step if a later step fails."

EDM Ch 10(Bellemare): "Sagas prefer business-level consistency over technical atomicity. They acknowledge that distributed transactions across services are impractical and provide a practical alternative."

V2 视角(V2.7 B3 段 5 分解 + B6 段 4 三难): Saga = 承认分布式系统的 CAP 现实——2PC 想要"强一致 + 跨服务",但代价是性能 + 可用性都崩。 Saga 选最终一致 + 业务补偿,放弃"原子性" = 接受代价换工程现实。 这是 B6 权衡的典型案例——分布式事务的"三难"无法绕过,只能选牺牲维度。

关键属性 ​

属性值/含义
无 2PC不依赖数据库的分布式事务
可补偿每个子事务都有对应的反向操作
最终一致全部成功 OR 全部补偿后 = 业务一致
隔离性弱中间状态对外可见(订单已建但库存还没扣)
失败可恢复补偿可重试(幂等)
长事务友好跨小时/天的事务也能处理(如订单 → 发货 → 签收 → 复购)
反模式用 Saga 实现"看似必须强一致"的业务 / 补偿不可幂等
判错标志补偿失败导致业务不一致 / 中间状态对外暴露太久

怎么用 ​

场景 1:Orchestrated Saga(中央协调) ​

python
class PlaceOrderSaga:
    def __init__(self, order_id):
        self.order_id = order_id
        self.steps = []
    
    def execute(self):
        try:
            # Step 1: 创建订单
            order = self.order_service.create(self.order_id)
            self.steps.append(("create_order", order))
            
            # Step 2: 扣库存
            self.inventory_service.reserve(self.order_id)
            self.steps.append(("reserve_inventory", None))
            
            # Step 3: 收款
            self.payment_service.charge(self.order_id)
            self.steps.append(("charge_payment", None))
            
            # Step 4: 发货
            self.shipping_service.ship(self.order_id)
            self.steps.append(("ship_order", None))
            
            return order
            
        except InsufficientStock:
            self._compensate()
            raise OrderRejected("库存不足")
        
        except PaymentFailed:
            self._compensate()
            raise OrderRejected("支付失败")
        
        except ShippingFailed:
            self._compensate()
            raise OrderRejected("发货失败")
    
    def _compensate(self):
        # 反向补偿(注意顺序:从后往前)
        for step in reversed(self.steps):
            action, data = step
            if action == "create_order":
                self.order_service.cancel(self.order_id)
            elif action == "reserve_inventory":
                self.inventory_service.release(self.order_id)
            elif action == "charge_payment":
                self.payment_service.refund(self.order_id)
            # ship_order 不需要补偿(发货失败时还没发货)

场景 2:Choreographed Saga(事件驱动) ​

python
# OrderService
class OrderService:
    def place_order(self, cmd):
        order = Order.create(cmd)
        order_repo.save(order)
        event_bus.publish(OrderCreated(order.id, order.items))
    
    # 监听其他服务的失败事件,做补偿
    def on_InventoryReservationFailed(self, event):
        if event.order_id == self.order_id:
            self.order_service.cancel(self.order_id)
    
    def on_PaymentFailed(self, event):
        if event.order_id == self.order_id:
            self.order_service.cancel(self.order_id)
            event_bus.publish(OrderCancelled(self.order_id))

# InventoryService
class InventoryService:
    def on_OrderCreated(self, event):
        try:
            self.inventory.reserve(event.items)
            event_bus.publish(InventoryReserved(event.order_id))
        except InsufficientStock:
            event_bus.publish(InventoryReservationFailed(
                event.order_id, reason="库存不足"
            ))
    
    # 监听支付失败做补偿
    def on_PaymentFailed(self, event):
        if event.order_id == self.order_id:
            self.inventory.release(event.items)

# PaymentService
class PaymentService:
    def on_InventoryReserved(self, event):
        try:
            self.payment.charge(event.order_id, event.amount)
            event_bus.publish(PaymentCompleted(event.order_id))
        except PaymentDeclined:
            event_bus.publish(PaymentFailed(event.order_id))
    
    def on_OrderCancelled(self, event):
        if self.has_charged(event.order_id):
            self.payment.refund(event.order_id)

场景 3:Saga 的 4 个核心设计要点 ​

要点 1:补偿必须幂等

python
# ✅ 补偿幂等
def release_inventory(order_id):
    # 已释放过的不报错
    if self.inventory.is_released(order_id):
        return
    self.inventory.release(order_id)

# ❌ 补偿不幂等(重试会双倍释放)
def release_inventory(order_id):
    self.inventory.release(order_id)

要点 2:补偿顺序与执行顺序相反

执行顺序: A → B → C
补偿顺序: ~C → ~B → ~A

要点 3:中间状态对外可见

订单状态机:
  Created → Paid → Shipped → Received
              ↓
          Cancelled (失败时)

中间状态"已创建但未支付"对外可见 ——
这是 Saga 的本质,不是 bug

要点 4:超时与重试

python
class SagaOrchestrator:
    TIMEOUT = timedelta(minutes=30)
    
    def execute(self):
        future = self.executor.submit(self._run_steps)
        try:
            return future.result(timeout=self.TIMEOUT)
        except TimeoutError:
            self._compensate()
            raise SagaTimeout()

场景 4:Saga 与 2PC 的对比 ​

|| 维度 | Saga | 2PC(两阶段提交)| |---|---|---| | 一致性 | 最终一致 | 强一致 | | 可用性 | 高(每步独立提交)| 低(协调者挂了全锁) | | 性能 | 高(无锁)| 低(全程持锁)| | 隔离性 | 弱(中间状态可见)| 强(持锁期间隔离)| | 故障恢复 | 自动补偿 | 协调者恢复 | | 跨语言 | 友好 | 依赖数据库支持 | | 适用场景 | 跨服务长事务 | 单数据库短事务 |

V2 B6 权衡视角:Saga 放弃"强一致 + 隔离性"换"高可用 + 高性能" = 选 2 个角,放弃 1 个角(CAP 的分布式版本)。

场景 5:长 Saga(跨小时 / 跨天) ​

python
# 电商订单的全生命周期 Saga
class OrderLifecycleSaga:
    def execute(self, order_id):
        # Step 1: 下单(毫秒)
        self.place_order(order_id)
        
        # Step 2: 支付(分钟)
        wait_for_event(PaymentCompleted, order_id, timeout=15min)
        
        # Step 3: 发货(小时)
        wait_for_event(OrderShipped, order_id, timeout=24h)
        
        # Step 4: 签收(天)
        wait_for_event(OrderReceived, order_id, timeout=14d)
        
        # Step 5: 复购邀请(天)
        schedule_after(7d, self.send_repurchase_invitation, order_id)

长 Saga 的关键 = 用事件持久化 + 状态机 + 超时机制,而不是把所有状态放在内存里。

常见误区 ​

  1. "Saga = 分布式事务"——错。Saga 是没有原子性的事务,明确放弃强一致换工程现实
  2. "所有跨服务调用都要 Saga"——错。能避免就避免——能异步就异步,能单 BC 就单 BC,Saga 是最后手段
  3. "补偿 = 回滚"——错。补偿是业务补偿(可能不完美,如"已发货后无法回滚"),不是数据库 rollback
  4. "中间状态应该对外隐藏"——错。Saga 明确接受中间状态可见——业务要设计成"中间状态也能合理"
  5. "Choreographed Saga = 简单"——错。Choreographed Saga 难追踪、难测试,简单的标准是 Orchestrated
  6. "Saga 不需要幂等"——大错。补偿可能重试,幂等是 Saga 的硬要求
  7. "Saga 能保证一致性"——半对。Saga 保证最终业务一致,不保证任意时刻都一致

关键洞察 ​

Saga = 承认分布式系统的现实,用业务补偿代替技术锁。

2PC 想"既要跨服务原子性" = 现实不允许。 Saga 选"放弃强一致,换工程可用" = 业务上能接受。 这不是工程妥协,是工程智慧。

V2 B6 权衡的典型案例—— Saga 的代价 = 中间状态可见 + 补偿可能失败 + 业务设计变复杂。 Saga 的收益 = 高可用 + 高性能 + 跨服务事务可行。

Saga 设计的 3 个核心问题:

  1. 每个子事务真的有补偿吗?(不是所有业务都有补偿)
  2. 补偿幂等吗?(重试不能双倍)
  3. 中间状态对外业务合理吗?(用户看到"已下单未付款"是正常的)

判断 Saga 用得对不对的标志:

  • 补偿可幂等
  • 失败可恢复(不需人工介入)
  • 中间状态对外业务可接受
  • Saga 状态可查询 / 可恢复(长 Saga 关键)

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。