仕組みから学ぶ Web
応用読了目安 14#冪等性#リトライ#決済#分散システム

冪等性 — 同じリクエストが2回届く前提で設計する

ネットワークがある限り、再送は異常ではなく正常系。2回実行されても壊れない形をどう作るか。

この記事の進み方

What — 何が冪等で、何がそうでないか

冪等(idempotent) とは、同じ操作を何回実行しても、結果の状態が同じであることです。

Compare操作の性質

何回やっても、終わったときの状態が同じ。安全にリトライできる。

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回作ることは、意味のある操作でもあるからです。

そこで、クライアントが「同じ要求である」ことを示すキーを送る仕組みを使います。

Figure冪等キーを使ったリトライ
クライアントAPI サーバーDBPOST /payments + Idempotency-Key: abc123キー abc123 で INSERT を試みる登録成功(初回)決済処理を実行し、結果を保存200 + 決済結果リトライ(同じキー abc123)キー abc123 で INSERT → 重複エラー保存しておいた結果を返す
1/8
POST /payments + Idempotency-Key: abc123クライアントがリクエストごとに一意のキー(UUID など)を生成して送る。リトライのときは同じキーを使うのが要点。

キーが同じなら「前と同じ要求」だと分かる。サーバーは前回の結果を返すだけで、処理は繰り返さない。

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回送っても結果が変わらないのはどれですか?

まず選ぶ(解答例は a〜d の記号で説明します)

Why — なぜリトライは避けられないのか

分散システムには「2つの将軍問題」と呼ばれる原理的な限界があります。ネットワークで隔てられた2者が、確実に合意したことを確認する方法は存在しません

  • A が B にメッセージを送る
  • B は受け取ったが、確認の返事が届いたか分からない
  • A は返事が来ないが、B が受け取ったのか、そもそも届かなかったのか分からない
  • 確認の確認を送っても、同じ問題が再帰する

したがって、送信側は「届いたか分からない」状態を必ず経験します。そのときに取れる選択は2つ。

  1. 送らない(At most once) — 重複はしないが、失われる可能性がある
  2. 再送する(At least once) — 失われないが、重複する可能性がある

「ちょうど1回(Exactly once)」は、ネットワーク層では実現できません。 実現できるように見えるものは、すべて「At least once + 受信側の重複排除」で作られています。

重複が発生する経路

冪等性が必要になるのは、明示的なリトライだけではありません。

経路発生の仕方
クライアントの自動リトライタイムアウト後の再送
ユーザーの操作ボタン連打、ブラウザの戻る、リロード
ロードバランサタイムアウト時に別のサーバーへ転送
メッセージキューack が届かず再配信(SQS などは at-least-once)
Webhook送信側が成功を確認できず再送(Stripe なども再送する)
バッチの再実行障害復旧後にジョブを再実行

このうち、フロントエンドで防げるのはボタン連打だけです。 他はすべてサーバー側で対処するしかありません。

確認 — ここまで読めたか

タイムアウトしたリクエストについて、クライアントが確実に言えることはどれですか?

まず選ぶ(解答例は a〜d の記号で説明します)

演習 — まず自分で判断する

解説を読む前に、まず自分で判断してみてください。ここで一度詰まっておくと、 次の節の判断軸が「なるほど」ではなく「そう来たか」に変わります。

演習 — 設計判断を問う

この一括処理を、安全にリトライできる形にするにはどうしますか?

与えられた条件
  • 夜間バッチ: 前日分の注文を集計し、加盟店ごとに精算データを作って振込 API を呼ぶ
  • 処理内容: ①対象注文を抽出 ②加盟店ごとに集計 ③ settlements テーブルに INSERT ④銀行の振込 API を呼ぶ ⑤メール通知
  • 加盟店は約 3000 社。処理には 40 分かかる
  • 先週、④の途中でバッチが異常終了した。再実行したところ、一部の加盟店に二重振込が発生した
  • 銀行の振込 API は冪等キーに対応している(リクエスト ID を受け付ける)
  • 運用チームは「失敗したら再実行する」ことを前提にしている
この軸で考える
  • · ①〜⑤のうち、再実行しても安全なのはどれか
  • · 外部 API(銀行)とローカル DB で、対処の仕方はどう違うか
  • · バッチ全体を冪等にするのと、ステップごとに冪等にするのは何が違うか
まず選ぶ(解答例は a〜d の記号で説明します)

演習 — 説明できるか

「リトライで二重決済が起きた」という事故を、原因と対策としてチームに説明してください

与えられた条件
  • クライアントはタイムアウト後に自動リトライしていた
  • 最初のリクエストは成功していた
  • リトライを止める以外の対策を示したい

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。

この知識を使う実践ケース