冪等性 — 同じリクエストが2回届く前提で設計する
ネットワークがある限り、再送は異常ではなく正常系。2回実行されても壊れない形をどう作るか。
先に読んでおくとよい記事
この記事の進み方
What — 何が冪等で、何がそうでないか
冪等(idempotent) とは、同じ操作を何回実行しても、結果の状態が同じであることです。
何回やっても、終わったときの状態が同じ。安全にリトライできる。
PUT /users/123
{ "name": "山田", "email": "y@example.com" }
→ 1回目も5回目も、同じ状態になる
DELETE /users/123
→ 1回目: 削除される
→ 2回目: すでに無い(404 だが、状態は同じ)
UPDATE orders SET status = 'paid' WHERE id = 1
→ 何回やっても paid- リトライ
- 安全
- HTTP メソッド
- GET / PUT / DELETE
- 設計の要点
- 絶対値を指定する
- –「〜にする」という書き方は冪等になりやすい。
- –2回目が 404 や 409 を返しても、状態が変わっていなければ冪等。レスポンスの一致は要件ではない。
「レスポンスが毎回同じ」ではなく「サーバーの状態が同じになる」が定義。2回目が 404 を返しても、状態が同じなら冪等。
冪等キーによる重複排除
作成系(POST)は本質的に冪等にできません。同じ注文を2回作ることは、意味のある操作でもあるからです。
そこで、クライアントが「同じ要求である」ことを示すキーを送る仕組みを使います。
キーが同じなら「前と同じ要求」だと分かる。サーバーは前回の結果を返すだけで、処理は繰り返さない。
CREATE TABLE idempotency_records (
key VARCHAR(64) PRIMARY KEY, -- ← UNIQUE 制約が排他制御になる
request_hash VARCHAR(64), -- 同じキーで違う内容が来たら弾く
status ENUM('processing', 'completed'),
response_body JSON,
created_at DATETIME,
expires_at DATETIME
);
UNIQUE 制約そのものが排他制御として機能します。 アプリ側でロックを取る必要がなく、複数サーバー構成でも正しく動きます。
同じリクエストを2回送っても結果が変わらないのはどれですか?
Why — なぜリトライは避けられないのか
分散システムには「2つの将軍問題」と呼ばれる原理的な限界があります。ネットワークで隔てられた2者が、確実に合意したことを確認する方法は存在しません。
- A が B にメッセージを送る
- B は受け取ったが、確認の返事が届いたか分からない
- A は返事が来ないが、B が受け取ったのか、そもそも届かなかったのか分からない
- 確認の確認を送っても、同じ問題が再帰する
したがって、送信側は「届いたか分からない」状態を必ず経験します。そのときに取れる選択は2つ。
- 送らない(At most once) — 重複はしないが、失われる可能性がある
- 再送する(At least once) — 失われないが、重複する可能性がある
「ちょうど1回(Exactly once)」は、ネットワーク層では実現できません。 実現できるように見えるものは、すべて「At least once + 受信側の重複排除」で作られています。
重複が発生する経路
冪等性が必要になるのは、明示的なリトライだけではありません。
| 経路 | 発生の仕方 |
|---|---|
| クライアントの自動リトライ | タイムアウト後の再送 |
| ユーザーの操作 | ボタン連打、ブラウザの戻る、リロード |
| ロードバランサ | タイムアウト時に別のサーバーへ転送 |
| メッセージキュー | ack が届かず再配信(SQS などは at-least-once) |
| Webhook | 送信側が成功を確認できず再送(Stripe なども再送する) |
| バッチの再実行 | 障害復旧後にジョブを再実行 |
このうち、フロントエンドで防げるのはボタン連打だけです。 他はすべてサーバー側で対処するしかありません。
タイムアウトしたリクエストについて、クライアントが確実に言えることはどれですか?
演習 — まず自分で判断する
解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。
この一括処理を、安全にリトライできる形にするにはどうしますか?
- 夜間バッチ: 前日分の注文を集計し、加盟店ごとに精算データを作って振込 API を呼ぶ
- 処理内容: ①対象注文を抽出 ②加盟店ごとに集計 ③ settlements テーブルに INSERT ④銀行の振込 API を呼ぶ ⑤メール通知
- 加盟店は約 3000 社。処理には 40 分かかる
- 先週、④の途中でバッチが異常終了した。再実行したところ、一部の加盟店に二重振込が発生した
- 銀行の振込 API は冪等キーに対応している(リクエスト ID を受け付ける)
- 運用チームは「失敗したら再実行する」ことを前提にしている
- · ①〜⑤のうち、再実行しても安全なのはどれか
- · 外部 API(銀行)とローカル DB で、対処の仕方はどう違うか
- · バッチ全体を冪等にするのと、ステップごとに冪等にするのは何が違うか
「リトライで二重決済が起きた」という事故を、原因と対策としてチームに説明してください
- クライアントはタイムアウト後に自動リトライしていた
- 最初のリクエストは成功していた
- リトライを止める以外の対策を示したい
読み終わりましたか?
読了にすると、これを前提とする記事がロードマップで開放されます。