EXERCISE 60分

ストーリー

佐藤CTO
分散トランザクションの設計は、失敗ケースをどれだけ想定できるかで品質が決まる

ミッション概要

ミッションテーマ目安時間
Mission 12PCフローの設計と限界分析15分
Mission 2Saga + 補償の実装15分
Mission 3Outboxパターンの実装15分
Mission 4障害シナリオのテスト設計15分

前提シナリオ

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


Mission 1: 2PCフローの設計と限界分析(15分)

要件

上記フローを2PCで実装した場合のシーケンスを描き、発生しうる問題を3つ挙げてください。

解答例
sequenceDiagram
    participant C as Coordinator
    participant O as Order DB
    participant I as Inventory DB
    participant P as Payment DB
    participant S as Shipping DB

    C->>O: PREPARE
    O-->>C: OK
    C->>I: PREPARE
    I-->>C: OK
    C->>P: PREPARE
    P-->>C: OK
    C->>S: PREPARE
    S--xC: TIMEOUT(問題発生)

    Note over C,S: 全DBがPREPARE状態でロック保持中<br/>Coordinatorがロールバック指示を出すまで全DBがブロック

問題点:

  1. Prepare後のCoordinator障害: 全参加者がロック保持したまま待機。手動復旧が必要
  2. ネットワーク分断: 一部参加者にCommit到達、他にRollback到達の可能性(ヒューリスティック判断が必要)
  3. レイテンシ: 4つのDBの同期的ロック取得で応答時間が数秒に。ピーク時にスループット低下

結論: マイクロサービス(特にDB分離後)では2PCは非推奨。Sagaを採用すべき。


Mission 2: Saga + 補償の実装(15分)

要件

注文フローのOrchestration Sagaを設計し、各ステップの補償アクションを定義してください。

解答例
const orderSaga: SagaDefinition = {
  name: 'OrderSaga',
  steps: [
    {
      name: 'createOrder',
      type: 'COMPENSATABLE',
      execute: { service: 'order', action: 'create', data: '${orderData}' },
      compensate: { service: 'order', action: 'cancel', data: '${orderId}' },
    },
    {
      name: 'reserveStock',
      type: 'COMPENSATABLE',
      execute: { service: 'inventory', action: 'reserve', data: '${items}' },
      compensate: { service: 'inventory', action: 'release', data: '${reservationId}' },
    },
    {
      name: 'chargePayment',
      type: 'PIVOT', // 補償不可(返金は別フロー)
      execute: { service: 'payment', action: 'charge', data: '${paymentData}' },
      compensate: null, // Pivot以降は補償不要
    },
    {
      name: 'arrangeShipment',
      type: 'RETRIABLE', // 必ず成功するまでリトライ
      execute: { service: 'shipping', action: 'arrange', data: '${shippingData}' },
      compensate: null,
      retryPolicy: { maxRetries: 10, backoffMs: 1000 },
    },
  ],
};

Mission 3: Outboxパターンの実装(15分)

要件

注文サービスのOutboxテーブル設計とCDCによる転送フローを実装してください。

解答例
// Outbox テーブルスキーマ(Drizzle ORM)
const outboxTable = pgTable('outbox', {
  id: uuid('id').primaryKey().defaultRandom(),
  aggregateType: varchar('aggregate_type', { length: 255 }).notNull(),
  aggregateId: varchar('aggregate_id', { length: 255 }).notNull(),
  eventType: varchar('event_type', { length: 255 }).notNull(),
  payload: jsonb('payload').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
  publishedAt: timestamp('published_at'),
});

// アトミックな書き込み
class OrderService {
  async confirmOrder(orderId: string): Promise<void> {
    await this.db.transaction(async (tx) => {
      // ビジネスロジック
      await tx.update(orders)
        .set({ status: 'CONFIRMED' })
        .where(eq(orders.id, orderId));

      // Outboxに同時記録
      await tx.insert(outboxTable).values({
        aggregateType: 'Order',
        aggregateId: orderId,
        eventType: 'order.confirmed',
        payload: { orderId, confirmedAt: new Date().toISOString() },
      });
    });
  }
}

Mission 4: 障害シナリオのテスト設計(15分)

要件

以下の障害シナリオのテストケースを設計してください。

  1. 在庫引当成功後、決済が失敗
  2. Outbox relay がダウン中にイベントが蓄積
  3. Consumer が同じイベントを2回受信
解答例
describe('OrderSaga 障害テスト', () => {
  test('決済失敗時に在庫引当が補償される', async () => {
    // Arrange
    paymentService.charge.mockRejectedValue(new Error('Card declined'));

    // Act
    await expect(saga.execute(orderContext)).rejects.toThrow('Card declined');

    // Assert: 補償が逆順に実行された
    expect(inventoryService.release).toHaveBeenCalledWith(reservationId);
    expect(orderService.cancel).toHaveBeenCalledWith(orderId);
    expect(sagaState.status).toBe('COMPENSATED');
  });

  test('Outbox relay停止中もイベントが失われない', async () => {
    // Arrange: Relayを停止
    await outboxRelay.stop();

    // Act: 注文を3件作成
    await orderService.confirmOrder('ord-1');
    await orderService.confirmOrder('ord-2');
    await orderService.confirmOrder('ord-3');

    // Assert: Outboxに3件蓄積
    const pending = await db.select().from(outbox).where(isNull(outbox.publishedAt));
    expect(pending).toHaveLength(3);

    // Relay再開後に全件発行
    await outboxRelay.start();
    await waitFor(() => expect(kafka.published).toHaveLength(3));
  });

  test('同一イベントの重複処理が冪等に動作する', async () => {
    const event = { id: 'evt-001', type: 'order.confirmed', data: { orderId: 'ord-1' } };

    // Act: 同じイベントを2回処理
    await inboxHandler.handle(event);
    await inboxHandler.handle(event); // 2回目

    // Assert: ビジネスロジックは1回のみ実行
    expect(processOrder).toHaveBeenCalledTimes(1);
  });
});

まとめ

ポイント内容
2PC の限界ブロッキングとスケーラビリティの問題
Saga 設計ステップ分類と補償の定義
OutboxDB + イベントの原子的保証
テスト障害シナリオを体系的にテスト

チェックリスト

  • 2PC の問題点を具体的に示せた
  • Saga の各ステップと補償を設計できた
  • Outbox パターンを実装できた
  • 障害テストケースを設計できた

次のステップへ

次はチェックポイントクイズで分散トランザクションの理解度を確認します。


推定読了時間: 60分