LESSON 40分

ストーリー

「注文サービスは成功したのに、在庫サービスが失敗して在庫が減ってない。でも注文は確定状態になってる…」

マイクロサービスに移行した後、こうした整合性の問題が増えた。高橋アーキテクトが説明する。

「単一DBならBEGIN-COMMITで済む。しかし複数サービス、複数DBにまたがるトランザクションは根本的に難しい。2PCやSagaパターンで対処するんだ」


なぜ分散トランザクションが必要か

モノリス → マイクロサービス

モノリス:
[注文処理]
  BEGIN;
    INSERT INTO orders ...
    UPDATE inventory SET stock = stock - 1 ...
    INSERT INTO payments ...
  COMMIT;
  → 1つのDBトランザクションで完結

マイクロサービス:
[注文サービス] → 注文DB
[在庫サービス] → 在庫DB
[決済サービス] → 決済DB
  → 3つのDBにまたがる。1つのCOMMITでは済まない

2PC(Two-Phase Commit)

コーディネーターが全参加者の合意を取って一斉にコミットする。

Phase 1: Prepare(準備)

Coordinator → Service A: "コミットできるか?"
Coordinator → Service B: "コミットできるか?"
Coordinator → Service C: "コミットできるか?"

Service A → Coordinator: "Yes, 準備完了"
Service B → Coordinator: "Yes, 準備完了"
Service C → Coordinator: "Yes, 準備完了"

Phase 2: Commit(確定)

全員 Yes の場合:
Coordinator → Service A: "コミットせよ"
Coordinator → Service B: "コミットせよ"
Coordinator → Service C: "コミットせよ"

1つでも No の場合:
Coordinator → All: "ロールバックせよ"

2PCの問題点

問題説明
ブロッキングPrepare後にコーディネーターがダウンすると全参加者がブロック
パフォーマンス全サービスのレスポンスを待つため遅い
可用性の低下1つのサービスがダウンすると全体が止まる
複雑さリカバリロジックが複雑

Sagaパターン

分散トランザクションを一連のローカルトランザクションに分解し、失敗時は補償トランザクション(undo操作)で巻き戻す。

Choreography(振り付け)方式

各サービスがイベントを発行し、次のサービスがリアクションする。

1. 注文サービス: 注文作成 → "OrderCreated" イベント発行
2. 在庫サービス: 在庫確保 → "InventoryReserved" イベント発行
3. 決済サービス: 決済処理 → "PaymentProcessed" イベント発行
4. 注文サービス: 注文確定

失敗時(決済失敗):
3. 決済サービス: 失敗 → "PaymentFailed" イベント発行
2. 在庫サービス: 在庫戻し(補償トランザクション)
1. 注文サービス: 注文キャンセル(補償トランザクション)
// Choreography方式の実装イメージ
// 注文サービス
class OrderService {
  async createOrder(orderData: OrderInput): Promise<Order> {
    // ローカルトランザクション
    const order = await this.orderRepo.create({
      ...orderData,
      status: 'pending',
    });

    // イベント発行
    await this.eventBus.publish('OrderCreated', {
      orderId: order.id,
      items: order.items,
      userId: order.userId,
    });

    return order;
  }

  // 補償トランザクション
  async handlePaymentFailed(event: PaymentFailedEvent): Promise<void> {
    await this.orderRepo.updateStatus(event.orderId, 'cancelled');
  }
}

// 在庫サービス
class InventoryService {
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    try {
      await this.reserveStock(event.items);
      await this.eventBus.publish('InventoryReserved', {
        orderId: event.orderId,
      });
    } catch (error) {
      await this.eventBus.publish('InventoryReservationFailed', {
        orderId: event.orderId,
        reason: error.message,
      });
    }
  }

  // 補償トランザクション
  async handlePaymentFailed(event: PaymentFailedEvent): Promise<void> {
    await this.releaseStock(event.orderId);
  }
}

Orchestration(指揮)方式

中央のオーケストレーターがすべてのステップを制御する。

// Orchestration方式の実装
class OrderSagaOrchestrator {
  async execute(orderData: OrderInput): Promise<OrderResult> {
    const sagaId = generateId();
    const steps: SagaStep[] = [];

    try {
      // Step 1: 注文作成
      const order = await this.orderService.createOrder(orderData);
      steps.push({ service: 'order', action: 'create', data: order });

      // Step 2: 在庫確保
      await this.inventoryService.reserveStock(order.items);
      steps.push({ service: 'inventory', action: 'reserve', data: order.items });

      // Step 3: 決済処理
      await this.paymentService.processPayment(order);
      steps.push({ service: 'payment', action: 'charge', data: order });

      // Step 4: 注文確定
      await this.orderService.confirmOrder(order.id);

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

    } catch (error) {
      // 補償トランザクション(逆順で実行)
      await this.compensate(steps);
      return { success: false, error: error.message };
    }
  }

  private async compensate(steps: SagaStep[]): Promise<void> {
    // 逆順で補償処理
    for (const step of steps.reverse()) {
      try {
        switch (`${step.service}:${step.action}`) {
          case 'payment:charge':
            await this.paymentService.refund(step.data);
            break;
          case 'inventory:reserve':
            await this.inventoryService.releaseStock(step.data);
            break;
          case 'order:create':
            await this.orderService.cancelOrder(step.data.id);
            break;
        }
      } catch (compensationError) {
        // 補償トランザクション自体の失敗 → 要手動対応
        await this.alertService.notifyCompensationFailure(step, compensationError);
      }
    }
  }
}

Choreography vs Orchestration

特性ChoreographyOrchestration
結合度低い(イベント駆動)やや高い(中央制御)
可視性低い(フロー追跡が困難)高い(全体が1箇所で見える)
複雑さサービス数増で複雑化ロジック集中で管理しやすい
障害対応各サービスで個別対応オーケストレーターで一元管理
適用規模2-3ステップの単純なSaga4ステップ以上の複雑なSaga

Transactional Outbox パターン

ローカルトランザクションとイベント発行の原子性を保証する。

-- 同一トランザクション内でデータ更新とイベント格納
BEGIN;
  INSERT INTO orders (id, user_id, status) VALUES (1, 100, 'pending');

  INSERT INTO outbox_events (
    aggregate_type, aggregate_id, event_type, payload
  ) VALUES (
    'Order', 1, 'OrderCreated',
    '{"orderId": 1, "userId": 100}'
  );
COMMIT;

-- 別プロセスがoutbox_eventsをポーリングしてイベントを発行
-- 発行後にイベントを削除(またはマーク)

まとめ

パターン特徴適用場面
2PC強い整合性、ブロッキング同一組織内の少数のDB
Saga (Choreography)イベント駆動、疎結合2-3ステップの単純なフロー
Saga (Orchestration)中央制御、可視性高い複雑な複数ステップ
Transactional Outboxイベント発行の信頼性保証Saga と組み合わせ

理解度チェックリスト

  • 2PCの2フェーズの流れを説明できる
  • 2PCの問題点を理解している
  • Sagaパターンの補償トランザクションを設計できる
  • ChoreographyとOrchestrationの使い分けを判断できる

次のステップ

次のレッスンではイベントソーシングとCQRSを学ぶ。状態ではなくイベント(出来事)を記録する設計パターンで、監査証跡と柔軟なデータ活用を実現する。


推定読了時間: 40分