🔗

分散トランザクション - 整合性を保つ設計パターン

15分 で読める | 2025.12.25

分散トランザクションの課題

マイクロサービスでは、1つのビジネス操作が複数のサービスにまたがることがあります。各サービスは独自のデータベースを持つため、従来のACIDトランザクションが使えません。

flowchart LR
    Order["1. 注文サービス<br/>注文を作成"]
    Inventory["2. 在庫サービス<br/>在庫を減らす"]
    Payment["3. 決済サービス<br/>支払いを処理"]
    Shipping["4. 配送サービス<br/>配送を手配"]

    Order --> Inventory --> Payment --> Shipping

    Question["❓ どこかで失敗したら?<br/>全体をロールバック?"]

2相コミット(2PC)

すべての参加者が準備完了してから、一斉にコミットするプロトコルです。

sequenceDiagram
    participant C as コーディネーター
    participant A as 参加者A
    participant B as 参加者B

    C->>A: 1. Prepare
    C->>B: 1. Prepare
    A-->>C: 2. Ready
    B-->>C: 2. Ready
    C->>A: 3. Commit
    C->>B: 3. Commit
    A-->>C: 4. Done
    B-->>C: 4. Done

2PCの問題点

問題説明
ブロッキング参加者はコーディネーターの応答を待つ間ロック
単一障害点コーディネーターがダウンすると全体が停止
低パフォーマンス同期的な通信が必要
分散環境での限界ネットワーク分断時の対応が困難

現代のマイクロサービスでは2PCは推奨されません

Sagaパターン

長時間のトランザクションを、一連のローカルトランザクションに分割します。失敗時は補償トランザクションでロールバックします。

flowchart LR
    subgraph Normal["正常フロー"]
        T1["T1<br/>注文作成"] --> T2["T2<br/>在庫確保"] --> T3["T3<br/>決済"] --> T4["T4<br/>配送手配"]
    end
flowchart LR
    subgraph Failure["失敗時(T3で失敗)"]
        T1b["T1"] --> T2b["T2"] --> T3b["T3<br/>❌ 失敗"] --> C2["C2<br/>在庫戻し"] --> C1["C1<br/>注文キャンセル"]
    end

オーケストレーション方式

中央のオーケストレーターがSagaを制御します。

class OrderSaga {
  async execute(orderData) {
    const sagaId = uuid();
    let currentStep = 0;

    try {
      // Step 1: 注文作成
      const order = await orderService.create(orderData);
      currentStep = 1;

      // Step 2: 在庫確保
      await inventoryService.reserve(order.items);
      currentStep = 2;

      // Step 3: 決済
      await paymentService.process(order.total);
      currentStep = 3;

      // Step 4: 配送手配
      await shippingService.schedule(order);
      currentStep = 4;

      return { success: true, orderId: order.id };

    } catch (error) {
      // 補償トランザクションを実行
      await this.compensate(currentStep, order);
      throw error;
    }
  }

  async compensate(step, order) {
    if (step >= 3) await paymentService.refund(order.id);
    if (step >= 2) await inventoryService.release(order.items);
    if (step >= 1) await orderService.cancel(order.id);
  }
}

コレオグラフィ方式

各サービスがイベントを発行・購読して協調します。

flowchart TB
    subgraph Success["正常フロー"]
        OS1["注文サービス"] --> E1["OrderCreated"]
        E1 --> IS1["在庫サービス"] --> E2["InventoryReserved"]
        E2 --> PS1["決済サービス"] --> E3["PaymentProcessed"]
        E3 --> SS1["配送サービス"] --> E4["ShippingScheduled"]
    end
flowchart TB
    subgraph Failure["失敗時"]
        PS2["決済サービス"] --> EF1["PaymentFailed"]
        EF1 --> IS2["在庫サービス"] --> EF2["InventoryReleased<br/>(補償)"]
        EF2 --> OS2["注文サービス"] --> EF3["OrderCancelled<br/>(補償)"]
    end

比較

観点オーケストレーションコレオグラフィ
可視性フローが明確フローが分散
結合度オーケストレーターに依存サービス間が疎結合
複雑さ単純なロジックイベント設計が複雑
デバッグ容易困難

補償トランザクション

失敗時にロールバックする代わりに、逆の操作を実行します。

// 元の操作
async function reserveInventory(items) {
  for (const item of items) {
    await db.inventory.update({
      where: { productId: item.productId },
      data: { quantity: { decrement: item.quantity } }
    });
  }
}

// 補償トランザクション
async function releaseInventory(items) {
  for (const item of items) {
    await db.inventory.update({
      where: { productId: item.productId },
      data: { quantity: { increment: item.quantity } }
    });
  }
}

補償の考慮点

すべての操作が補償可能とは限らない:

操作補償方法
メール送信「取り消しメール」を送信
物理的な作業開始手動介入が必要
外部APIコール外部システムの補償APIが必要

Outboxパターン

データベース更新とイベント発行を確実に行います。

// 1. トランザクション内で両方を実行
async function createOrder(orderData) {
  await db.transaction(async (tx) => {
    // ビジネスデータを保存
    const order = await tx.orders.create(orderData);

    // outboxテーブルにイベントを保存
    await tx.outbox.create({
      eventType: 'OrderCreated',
      payload: JSON.stringify(order),
      status: 'pending'
    });
  });
}

// 2. 別プロセスでoutboxをポーリング
async function publishOutboxEvents() {
  const events = await db.outbox.findMany({
    where: { status: 'pending' }
  });

  for (const event of events) {
    await messageQueue.publish(event.eventType, event.payload);
    await db.outbox.update({
      where: { id: event.id },
      data: { status: 'published' }
    });
  }
}

結果整合性

即座の整合性ではなく、最終的に整合性が取れることを許容します。

整合性モデル説明
強い整合性書き込み直後に全員が同じデータを見る
結果整合性書き込み後、しばらくすると全員が同じデータを見る(一時的な不整合を許容)

結果整合性の対処

// UI側での対応例
async function placeOrder(orderData) {
  const result = await api.createOrder(orderData);

  // 即座に確定ではなく「処理中」として表示
  return {
    orderId: result.orderId,
    status: 'processing',
    message: 'ご注文を受け付けました。確定後にメールでお知らせします。'
  };
}

まとめ

分散トランザクションは、マイクロサービスアーキテクチャにおける難しい課題です。2PCは現代の分散システムには適さず、Sagaパターンと補償トランザクションが推奨されます。結果整合性を受け入れ、ビジネス要件に合った整合性モデルを選択することが重要です。

← 一覧に戻る