LESSON 40分

ストーリー

佐藤CTO
2PCとSaga、どちらを使うべきか。答えはほぼ決まっているが、“なぜ”を理解することが重要だ

詳細比較

項目2PCSaga
整合性強整合性(即座)結果整合性(遅延あり)
分離性SERIALIZABLE分離なし(対策必要)
可用性低(全参加者の応答必要)高(非同期処理可能)
パフォーマンス低(同期ブロッキング)高(非同期並列)
複雑さプロトコルは単純補償ロジックが複雑
障害回復Coordinator依存各サービスが自律回復

Saga の分離性問題と対策

Saga は複数のローカルトランザクションで構成されるため、ACID の分離性がありません:

Lost Update(更新喪失)

Saga A: 注文の合計金額を計算中
Saga B: 同じ注文にクーポンを適用
→ Saga A の計算結果が Saga B の変更を上書き

対策: セマンティックロック

// 注文レコードにステータスフラグを使って「処理中」を示す
class Order {
  status: 'PENDING' | 'PROCESSING' | 'CONFIRMED' | 'CANCELLED';

  startProcessing(): void {
    if (this.status !== 'PENDING') {
      throw new Error('Cannot process: order is ' + this.status);
    }
    this.status = 'PROCESSING'; // セマンティックロック
  }
}

Dirty Read(未コミット読み取り)

Saga A: Step1完了(注文作成)→ Step2実行中(在庫引当)
Saga B: 注文一覧を読み取り → Saga Aの未完了注文が見える

対策: フラグで未確定データをフィルタ

// 確定済みデータのみ返す
async getConfirmedOrders(): Promise<Order[]> {
  return this.orderRepo.find({
    where: { status: 'CONFIRMED' }, // PROCESSING は除外
  });
}

冪等性の実装

分散環境ではメッセージの重複配信は避けられません:

class IdempotentMessageHandler {
  constructor(private processedRepo: ProcessedMessageRepository) {}

  async handle(message: Message): Promise<void> {
    // 1. 既に処理済みか確認
    const exists = await this.processedRepo.exists(message.id);
    if (exists) {
      console.log(`Message ${message.id} already processed, skipping`);
      return;
    }

    // 2. ビジネスロジック実行 + 処理済み記録をアトミックに
    await this.db.transaction(async (tx) => {
      await this.processBusinessLogic(tx, message);
      await this.processedRepo.save(tx, {
        messageId: message.id,
        processedAt: new Date(),
      });
    });
  }
}

冪等キーの設計

パターン適用場面
イベントIDevt-abc-123全てのイベント処理
ビジネスキーorder-789-payment1注文1決済を保証
リクエストIDreq-xyz-456API呼び出しの重複排除

Saga の状態管理

// Saga状態をDBに永続化
interface SagaState {
  sagaId: string;
  type: 'ORDER_SAGA';
  status: 'STARTED' | 'RESERVING' | 'CHARGING' | 'COMPLETED' | 'COMPENSATING' | 'FAILED';
  context: Record<string, unknown>;  // ビジネスデータ
  currentStep: number;
  completedSteps: string[];
  createdAt: Date;
  updatedAt: Date;
}

// Saga状態の永続化により、サービス再起動後も継続可能
class SagaStateRepository {
  async save(state: SagaState): Promise<void> {
    await this.db.upsert('saga_states', state, { conflictKey: 'sagaId' });
  }

  async findPending(): Promise<SagaState[]> {
    // サービス起動時に未完了のSagaを復旧
    return this.db.find('saga_states', {
      where: { status: Not(In(['COMPLETED', 'FAILED'])) },
    });
  }
}

まとめ

ポイント内容
2PC vs Sagaマイクロサービスでは Saga が推奨
分離性対策セマンティックロックで更新喪失を防止
冪等性メッセージID + 処理済み記録で重複排除
状態管理Saga 状態を DB に永続化して障害回復

チェックリスト

  • 2PC と Saga の長短を比較できる
  • Saga の分離性問題と対策を説明できる
  • 冪等性の実装パターンを理解した
  • Saga 状態の永続化の重要性を理解した

次のステップへ

次は補償トランザクションの設計パターンを詳しく学びます。


推定読了時間: 40分