LESSON 40分

ストーリー

佐藤CTO
分散システムでは”全部成功か全部失敗”を保証するのが難しい
佐藤CTO
Sagaパターンは、補償トランザクションで”ビジネスレベルの一貫性”を実現する方法だ

なぜSagaが必要か

モノリスでは1つのDBトランザクションで整合性を保証できました:

// モノリス: 単一トランザクション
await db.transaction(async (tx) => {
  await tx.orders.create(order);
  await tx.inventory.reserve(items);
  await tx.payments.charge(payment);
  // 全て成功 or 全てロールバック
});

マイクロサービスでは各サービスが独自DBを持つため、分散トランザクションが必要です。


Choreography Saga vs Orchestration Saga

Choreography(振付型)

各サービスが自律的にイベントを発行・購読:

sequenceDiagram
    participant Order as 注文Service
    participant Stock as 在庫Service
    participant Pay as 決済Service
    participant Ship as 配送Service

    Order->>Stock: OrderCreated
    Stock->>Pay: StockReserved
    Pay->>Ship: PaymentCharged
    Ship-->>Order: ShipmentCreated

Orchestration(指揮者型)

中央のオーケストレーターが全体を制御:

graph TD
    Orch["Saga Orchestrator"] -->|"① 注文作成"| OrderSvc["注文 Svc"]
    Orch -->|"② 在庫引当"| StockSvc["在庫 Svc"]
    Orch -->|"③ 決済請求"| PaySvc["決済 Svc"]
    Orch -->|"④ 配送手配"| ShipSvc["配送 Svc"]

    style Orch fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#5b21b6
    style OrderSvc fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style StockSvc fill:#d1fae5,stroke:#059669,color:#065f46
    style PaySvc fill:#fef3c7,stroke:#d97706,color:#92400e
    style ShipSvc fill:#fee2e2,stroke:#dc2626,color:#991b1b

比較

項目ChoreographyOrchestration
結合度低(イベントのみ)中(オーケストレーター)
可視性低(追跡困難)高(一元管理)
複雑さサービス増で急増オーケストレーターに集中
適用場面3-4サービス以下5サービス以上
単一障害点なしオーケストレーター

補償トランザクション

Sagaの各ステップには「補償(undo)」アクションを定義します:

正常フロー:
  注文作成 → 在庫引当 → 決済処理 → 配送手配

補償フロー(決済失敗時):
  配送手配 ← 決済処理(失敗) ← 在庫引当解除 ← 注文キャンセル
// Sagaステップの定義
interface SagaStep<T> {
  name: string;
  execute: (context: T) => Promise<void>;
  compensate: (context: T) => Promise<void>;
}

const orderSagaSteps: SagaStep<OrderContext>[] = [
  {
    name: 'createOrder',
    execute: async (ctx) => {
      ctx.orderId = await orderService.create(ctx.orderData);
    },
    compensate: async (ctx) => {
      await orderService.cancel(ctx.orderId);
    },
  },
  {
    name: 'reserveStock',
    execute: async (ctx) => {
      await inventoryService.reserve(ctx.orderId, ctx.items);
    },
    compensate: async (ctx) => {
      await inventoryService.release(ctx.orderId);
    },
  },
  {
    name: 'chargePayment',
    execute: async (ctx) => {
      ctx.paymentId = await paymentService.charge(ctx.payment);
    },
    compensate: async (ctx) => {
      await paymentService.refund(ctx.paymentId);
    },
  },
];

Saga Orchestrator の実装

class SagaOrchestrator<T> {
  private completedSteps: SagaStep<T>[] = [];

  async execute(steps: SagaStep<T>[], context: T): Promise<void> {
    for (const step of steps) {
      try {
        await step.execute(context);
        this.completedSteps.push(step);
      } catch (error) {
        console.error(`Saga failed at step: ${step.name}`, error);
        await this.compensate(context);
        throw new SagaFailedError(step.name, error);
      }
    }
  }

  private async compensate(context: T): Promise<void> {
    // 完了したステップを逆順に補償
    for (const step of this.completedSteps.reverse()) {
      try {
        await step.compensate(context);
      } catch (error) {
        // 補償失敗はアラート + 手動対応キューへ
        console.error(`Compensation failed: ${step.name}`, error);
        await this.alertManualIntervention(step, error);
      }
    }
  }
}

// 使用例
const orchestrator = new SagaOrchestrator<OrderContext>();
await orchestrator.execute(orderSagaSteps, {
  orderData: { customerId: 'cust-1', items: [...] },
  payment: { method: 'credit_card', amount: 15000 },
});

Saga設計のポイント

原則説明
冪等性各ステップは何度実行しても同じ結果
ピボットトランザクション補償不可能なステップは最後に実行
リトライ可能一時的な失敗はリトライで対応
タイムアウト各ステップにタイムアウトを設定
状態永続化Saga の進行状態をDBに保存

まとめ

ポイント内容
Choreographyイベント駆動の自律的な協調
Orchestration中央制御で可視性が高い
補償トランザクション失敗時にビジネスレベルでundo
冪等性分散環境での信頼性の基盤

チェックリスト

  • Choreography と Orchestration の違いを説明できる
  • 補償トランザクションを設計できる
  • Saga Orchestrator の基本構造を理解した
  • 冪等性の重要性を理解した

次のステップへ

次は演習でイベント駆動アーキテクチャを設計します。


推定読了時間: 40分