メール送信や帳票生成をリクエスト処理の中で同期的に実行していて、外部サービスの遅延に引きずられてAPI全体がタイムアウトする——非同期化を検討するきっかけは、たいていこうした障害です。しかし場当たり的にキューを導入すると、今度は「ジョブが消えた」「二重に実行された」という別の問題に悩まされます。
この記事では、非同期処理を設計するときに最初から決めておくべき4つのポイント——切り出しの判断基準、リトライ設計、冪等性、デッドレターキュー——を整理します。
同期処理から切り出す判断基準
何でも非同期にすればよいわけではありません。非同期化はエラー通知やデバッグを難しくするコストを伴うため、次の条件に当てはまるものから切り出します。
- レスポンスに結果が不要: メール送信、通知、ログ集計など「受け付けた」ことだけ返せばよい処理
- 外部サービスに依存する: 相手の遅延・障害を自システムのレスポンスから切り離したい処理
- 処理時間が長い・変動する: 帳票生成、画像変換、大量データの一括更新
- 失敗してもリトライで回復できる: 後述する冪等性を担保できる処理
逆に、ユーザーがその場で結果を必要とする処理(決済の成否確認など)を安易に非同期化すると、ポーリングや通知の仕組みが別途必要になり、かえって複雑化します。
リトライは「間隔」と「上限」をセットで設計する
非同期ジョブの失敗は前提です。問題は失敗したときの振る舞いを決めていないことで、固定間隔で無限リトライする実装は、障害中の外部サービスへ負荷をかけ続けて復旧を妨げます。
- 指数バックオフ: 1分→2分→4分…と間隔を広げる。ジッター(ランダムなゆらぎ)を加えて再試行の同時集中を避ける
- リトライ上限: 5回前後を目安に設定し、超えたらデッドレターキューへ
- リトライすべきでないエラーの区別: バリデーションエラーなど再実行しても成功しないものは即座に失敗扱いにする
「一時的エラー(ネットワーク断、429/503)はリトライ、恒久的エラー(400系の入力不正)はリトライしない」という分類をワーカー実装の規約として決めておくのが実務のポイントです。
冪等性の担保——「少なくとも1回」を前提に書く
多くのキュー(Amazon SQSなど)の配信保証は「at least once(少なくとも1回)」です。つまり同じジョブが2回配信されることは正常系として扱う必要があります。二重実行でメールが2通届く、ポイントが二重加算される、といった事故はここを見落とすと起きます。
基本パターンは、ジョブに一意なIDを持たせ、処理済みIDを記録してから実行することです。
// 擬似コード: 処理済み記録のユニーク制約で二重実行を防ぐ
void handle(Job job) {
try {
insertProcessedRecord(job.getId()); // UNIQUE制約あり
} catch (DuplicateKeyException e) {
return; // すでに処理済み → 何もせず正常終了
}
doWork(job);
}
UPDATE系の処理なら「絶対値をセットする」(加算ではなく結果の値を書く)形に寄せるだけでも、二重実行の影響を抑えられます。
デッドレターキューは「作って終わり」にしない
リトライ上限を超えたジョブの行き先がデッドレターキュー(DLQ)です。DLQがないと失敗ジョブは黙って消え、「処理されたはずのデータがない」という調査困難な問い合わせになって返ってきます。
ただしDLQは作るだけでは機能しません。運用として次の3点をセットで用意します。
- DLQにメッセージが入ったらアラートを飛ばす(滞留数の監視)
- 原因調査に必要な情報(元のペイロード、失敗理由、試行回数)が残るようにする
- 修正後にDLQから元のキューへ再投入する手順をあらかじめ整備しておく
まとめ
非同期処理の設計は、キュー製品の選定よりも「失敗をどう扱うか」の決め事が本体です。結果を待たなくてよい処理から切り出し、指数バックオフと上限つきのリトライを設定し、二重配信を前提に冪等な実装にして、行き場を失ったジョブはDLQで監視する。この4点を最初に設計書へ明文化しておけば、非同期化にありがちな「消えるジョブ」「二重実行」の障害を大きく減らせます。
