LESSON 40分

ストーリー

フードデリバリーシステムが動き始めたある日、障害レポートが上がりました。

あなた
お客さんの口座からお金は引き落とされたのに、注文がキャンセル状態になっています…
高橋アーキテクト
これが分散トランザクションの最も怖い問題だ。決済サービスは成功、でも注文サービスは失敗。2つのデータベースにまたがる操作の一貫性が崩れたんだ

モノリスでのトランザクション

モノリスでは、1つのデータベースでACIDトランザクションが使えます。

// モノリスでは簡単
async function createOrder(data: OrderData): Promise<Order> {
  return await db.transaction(async (tx) => {
    // 1. 注文を作成
    const order = await tx.insert("orders", data);

    // 2. 在庫を減らす
    await tx.update("inventory",
      { quantity: sql`quantity - ${data.quantity}` },
      { productId: data.productId }
    );

    // 3. 決済を記録
    await tx.insert("payments", {
      orderId: order.id,
      amount: data.totalAmount,
      status: "COMPLETED",
    });

    return order;
    // すべて成功 or すべて失敗(ACID保証)
  });
}

マイクロサービスでの問題

マイクロサービスでは、各サービスが独自のデータベースを持つため、単一のACIDトランザクションは使えません。

Order Service     Payment Service    Inventory Service
   [Order DB]       [Payment DB]       [Inventory DB]

  ┌─ 注文作成 ─────── 決済処理 ─────── 在庫確保 ─┐
  │                                              │
  │  ← この3つの操作をアトミックにしたい          │
  │     でも3つの異なるDBにまたがっている         │
  └──────────────────────────────────────────────┘

2フェーズコミット(2PC)の限界

従来の分散トランザクション解決策である2PCは、マイクロサービスには不向きです。

2PC(Two-Phase Commit):

Phase 1: Prepare(準備)
  Coordinator → Order DB:     "コミットできる?" → "OK"
  Coordinator → Payment DB:   "コミットできる?" → "OK"
  Coordinator → Inventory DB: "コミットできる?" → "OK"

Phase 2: Commit(確定)
  Coordinator → Order DB:     "コミットしろ"
  Coordinator → Payment DB:   "コミットしろ"
  Coordinator → Inventory DB: "コミットしろ"

2PCの問題点

問題説明
同期的ブロッキングPrepare〜Commit間、全参加者がロックを保持
単一障害点Coordinatorがダウンすると全体が停止
パフォーマンスロック保持時間が長く、スループット低下
可用性1つの参加者がダウンすると全体がロールバック
サービス自律性各サービスが同じトランザクションプロトコルをサポート必要
// 2PCの問題を示す例
// Coordinatorがフェーズ2で障害→一部コミット、一部未コミット
// → データ不整合が発生する可能性

// さらに、マイクロサービスでは:
// - HTTP/RESTで2PCプロトコルを実装するのは非現実的
// - NoSQL DBは2PCをサポートしないものが多い
// - サービスの独立デプロイが阻害される

なぜSagaパターンが必要か

2PCの代替として、Sagaパターンが登場しました。

2PC: 全部成功するまでロックして待つ(悲観的)
Saga: 各ステップを順に実行し、失敗したら補償する(楽観的)

Saga の基本的な流れ:
  T1 → T2 → T3 → 完了!

  T1 → T2 → T3(失敗) → C3 → C2 → C1 → ロールバック完了
  (C = 補償トランザクション)
// Sagaの概念
interface SagaStep {
  // 正常系の処理
  execute(): Promise<void>;

  // 補償処理(undo)
  compensate(): Promise<void>;
}

// 注文Sagaの例
const orderSaga: SagaStep[] = [
  {
    execute: () => orderService.createOrder(data),       // T1: 注文作成
    compensate: () => orderService.cancelOrder(orderId),  // C1: 注文キャンセル
  },
  {
    execute: () => paymentService.charge(payment),        // T2: 決済
    compensate: () => paymentService.refund(paymentId),   // C2: 返金
  },
  {
    execute: () => inventoryService.reserve(items),       // T3: 在庫確保
    compensate: () => inventoryService.release(items),    // C3: 在庫解放
  },
];

Sagaの特性

特性2PCSaga
一貫性強い一貫性(ACID)結果整合性
ロック長時間ロックロックなし
可用性低い高い
複雑さプロトコルが複雑補償ロジックが複雑
スケーラビリティ低い高い

ACD特性(ACIDからIsolationを除外)

Sagaは以下の特性を持ちます。

A(Atomicity): 全ステップ成功 or 全補償実行
C(Consistency): 結果整合性(途中状態が見える)
D(Durability): 各ステップの結果は永続化される
※ I(Isolation)はない → 途中状態が他から見える

中間状態の可視性問題

// Sagaの途中状態が見える問題
// T1: 注文作成(PENDING)
// T2: 決済処理中......

// この時点で別のリクエストが注文を参照すると
// → status = "PENDING" が見える

// もしT2が失敗してC1が実行されると
// → status = "CANCELLED" に変わる

// つまり、一時的に不整合な中間状態が外部に見える
// → これが「結果整合性」の意味

// 対策: セマンティックロック
interface Order {
  id: string;
  status: "PENDING" | "CONFIRMED" | "CANCELLED";  // 中間状態を明示
  sagaStatus: "IN_PROGRESS" | "COMPLETED" | "COMPENSATING";
}

まとめ

ポイント内容
モノリスとの違いACIDトランザクションが使えない
2PCの限界ブロッキング、単一障害点、低パフォーマンス
Sagaの基本各ステップを順に実行、失敗時は補償
トレードオフ強い一貫性を諦め、結果整合性を受け入れる

チェックリスト

  • マイクロサービスでACIDトランザクションが使えない理由を説明できる
  • 2PCの問題点を3つ以上挙げられる
  • Sagaパターンの基本的な流れを説明できる
  • 結果整合性の意味を理解した

次のステップへ

次はSagaパターンの2つのタイプ「Choreography」と「Orchestration」を学びます。それぞれの特徴と使い分けを理解しましょう。


推定読了時間: 40分