システムデザイン レッスン5

メッセージキューと非同期処理

処理を非同期化しシステム間を疎結合にするメッセージキューの役割と代表的な利用パターンを学ぶ

なぜ非同期処理が必要か

「注文を受け付けたら、確認メール送信・在庫引き当て・請求データ生成をすべて行ってからレスポンスを返す」 という同期的な実装では、どれか1つの処理が遅い・失敗しただけで全体がブロックされ、 ユーザーは長い待ち時間や不必要なエラーを経験します。

時間のかかる処理や、即座の結果を必要としない処理をメッセージキューを介して 非同期に実行することで、ユーザーへの応答を素早く返しつつ、後続処理を安定して進められます。

メッセージキューの基本構造

Producer --> [ Message Queue ] --> Consumer
(注文API)      (メッセージが溜まる場所)   (メール送信ワーカー)

// 擬似コード
function handleOrderRequest(order):
    saveOrder(order)
    queue.enqueue({ type: "OrderCreated", orderId: order.id })
    return response("注文を受け付けました")   // 即座に応答を返す

// 別プロセス/別サーバーで動くConsumer
worker.onMessage((message) => {
    if message.type == "OrderCreated":
        sendConfirmationEmail(message.orderId)
        reserveInventory(message.orderId)
})

ProducerはConsumerの処理完了を待たずにレスポンスを返せるため、ユーザー体験を損なわずに 時間のかかる処理を裏側で進められます。

主要パターン

Pub/Sub(発行/購読)

1つのメッセージを複数のConsumer(購読者)がそれぞれ受け取り、独立して処理する

ワーカーキュー(Work Queue)

1つのメッセージは複数のワーカーのうちどれか1つだけが受け取り処理する。負荷分散の目的で使われる

デッドレターキュー(Dead Letter Queue)

リトライ上限を超えて処理に失敗し続けたメッセージを退避させ、原因調査や手動再処理を可能にする

配信保証(at-least-once等)と冪等性

多くのメッセージキューは「少なくとも1回(at-least-once)」の配信保証を採用しています。 これはメッセージが失われることはほぼないが、ネットワーク障害時の再送などにより重複して届く可能性があることを意味します。

// NG: 冪等でない処理(同じメッセージを2回処理すると二重課金される)
function chargePayment(message):
    charge(message.userId, message.amount)

// OK: 冪等な処理(同じmessageIdの処理は1回しか反映されない)
function chargePaymentIdempotent(message):
    if alreadyProcessed(message.messageId):
        return  // 既に処理済みなら何もしない
    charge(message.userId, message.amount)
    markProcessed(message.messageId)

ポイント

  • メッセージキューはProducerとConsumerを疎結合にし、負荷の急増をバッファとして吸収する
  • Pub/Sub・ワーカーキュー・デッドレターキューなど、目的に応じたパターンを使い分ける
  • at-least-onceのような配信保証では重複が起こりうるため、Consumer側は冪等な処理を設計する

確認クイズ

1 / 3

メッセージキューを導入する主な目的として適切なものはどれか?