LESSON 40分

ストーリー

高橋アーキテクト
注文確定のAPIが遅い。決済処理、在庫更新、メール送信、レシート生成…全部同期的にやっているからだ

高橋アーキテクトが指摘した。

高橋アーキテクト
全部終わるまでユーザーを待たせる必要はない。注文を受け付けたらすぐにレスポンスを返し、残りの処理はバックグラウンドで行う。これが非同期処理だ

同期 vs 非同期

同期処理(Before)

// 全ての処理を順番に実行 → 合計2000ms以上
async function placeOrder(orderData: OrderData): Promise<Order> {
  const order = await db.createOrder(orderData);        // 200ms
  await paymentService.charge(order.totalAmount);        // 500ms
  await inventoryService.reserve(order.items);           // 300ms
  await emailService.sendConfirmation(order);            // 400ms
  await receiptService.generate(order);                  // 300ms
  await analyticsService.trackPurchase(order);           // 200ms
  return order; // ユーザーは2000ms以上待つ
}

非同期処理(After)

// 必須処理だけ同期、残りはキューに投入 → 700ms
async function placeOrder(orderData: OrderData): Promise<Order> {
  // 同期的に必要な処理のみ
  const order = await db.createOrder(orderData);         // 200ms
  await paymentService.charge(order.totalAmount);        // 500ms

  // 残りはメッセージキューに投入(非同期)
  await messageQueue.publish('order.created', {
    orderId: order.id,
    items: order.items,
    userEmail: orderData.email,
  });

  return order; // ユーザーは700msで応答を受け取る
}

// バックグラウンドワーカーで処理
class OrderWorker {
  async handleOrderCreated(event: OrderCreatedEvent): Promise<void> {
    await inventoryService.reserve(event.items);
    await emailService.sendConfirmation(event.orderId);
    await receiptService.generate(event.orderId);
    await analyticsService.trackPurchase(event.orderId);
  }
}

メッセージキューの基本

// メッセージキューの抽象化
interface MessageQueue {
  // メッセージを送信
  publish(topic: string, message: any): Promise<void>;

  // メッセージを受信(ワーカー側)
  subscribe(topic: string, handler: (message: any) => Promise<void>): void;
}

// 代表的なメッセージキュー
const messageQueues = {
  redis: {
    name: 'Redis (Bull/BullMQ)',
    useCase: 'シンプルなジョブキュー、小〜中規模',
    features: ['優先度付きキュー', '遅延実行', 'リトライ'],
  },
  rabbitmq: {
    name: 'RabbitMQ',
    useCase: '複雑なルーティング、メッセージパターン',
    features: ['柔軟なルーティング', 'メッセージの永続化', 'クラスタリング'],
  },
  sqs: {
    name: 'Amazon SQS',
    useCase: 'AWSでの大規模処理',
    features: ['フルマネージド', '無限スケーリング', 'DLQ'],
  },
  kafka: {
    name: 'Apache Kafka',
    useCase: '大量のイベントストリーミング',
    features: ['超高スループット', 'イベントログ保持', 'コンシューマーグループ'],
  },
};

リトライとデッドレターキュー

// リトライ戦略の実装
class RetryableWorker {
  async processWithRetry(
    message: QueueMessage,
    handler: (msg: any) => Promise<void>,
    maxRetries: number = 3
  ): Promise<void> {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        await handler(message.body);
        await message.ack(); // 処理成功
        return;
      } catch (error) {
        console.error(`Attempt ${attempt}/${maxRetries} failed:`, error);

        if (attempt < maxRetries) {
          // 指数バックオフで待機
          const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
          await sleep(delay);
        } else {
          // 最大リトライ回数超過 → デッドレターキューへ
          await this.moveToDeadLetterQueue(message, error);
        }
      }
    }
  }

  private async moveToDeadLetterQueue(
    message: QueueMessage,
    error: Error
  ): Promise<void> {
    await this.dlq.publish({
      originalMessage: message.body,
      error: error.message,
      failedAt: new Date().toISOString(),
      attempts: message.retryCount,
    });
    await message.ack(); // 元のキューからは削除
  }
}

指数バックオフとジッター

リトライ待機時間(バックオフのみ)待機時間(ジッター付き)
1回目2秒1-3秒
2回目4秒2-6秒
3回目8秒4-12秒
4回目16秒8-24秒

べき等性(Idempotency)

非同期処理では同じメッセージが複数回処理される可能性があるため、べき等性が重要です。

// べき等性の確保
class IdempotentOrderProcessor {
  async processOrder(event: OrderCreatedEvent): Promise<void> {
    const idempotencyKey = `order-process:${event.orderId}`;

    // 既に処理済みかチェック
    const alreadyProcessed = await this.redis.get(idempotencyKey);
    if (alreadyProcessed) {
      console.log(`Order ${event.orderId} already processed, skipping`);
      return;
    }

    // 処理を実行
    await this.inventoryService.reserve(event.items);
    await this.emailService.sendConfirmation(event.orderId);

    // 処理完了を記録(24時間保持)
    await this.redis.setex(idempotencyKey, 86400, 'processed');
  }
}

非同期化の判断基準

同期で行うべき非同期にできる
決済処理(結果が即座に必要)メール/通知送信
認証・認可レポート生成
データの整合性が必須な操作分析データの記録
ユーザーに即座にフィードバックが必要な操作画像のリサイズ/変換

まとめ

ポイント内容
非同期化不要な待ち時間をなくしてレスポンスを高速化
メッセージキュープロデューサーとコンシューマーを分離
リトライ指数バックオフ + ジッターで安全にリトライ
DLQ処理不能メッセージの退避先
べき等性同じ処理を複数回実行しても結果が変わらない設計

チェックリスト

  • 同期処理と非同期処理の使い分けを判断できる
  • メッセージキューの役割と代表的な製品を理解した
  • リトライとデッドレターキューの仕組みを把握した
  • べき等性の重要性と実装方法を理解した

次のステップへ

次は演習です。スケーラブルなアーキテクチャを設計し、ここまで学んだスケーリング技法を統合しましょう。


推定読了時間: 40分