ひとことで言うと#
同じリクエストを2回以上送っても、1回実行したのと同じ結果になるように設計するパターン。ネットワーク障害やタイムアウトによるリトライで、決済の二重課金やデータの重複作成を防ぐ。
押さえておきたい用語#
- 冪等性(Idempotency)
- 同じ操作を何度実行しても結果が変わらない性質。数学のf(f(x)) = f(x)から由来する。
- 冪等性キー(Idempotency Key)
- クライアントが生成する一意の識別子(UUID等)。サーバーはこのキーで同一リクエストの重複を検知する。
- リトライセーフ
- リトライしても副作用が重複しないこと。冪等なAPIはリトライセーフである。
- 楽観的ロック
- レコードのバージョン番号で更新競合を検知する仕組み。同時リクエストによる二重処理防止に使われる。
- UPSERT
- INSERT(新規作成)とUPDATE(更新)を1つのSQL文で安全に処理する操作。同じデータが来ても安全に処理できる。
冪等性パターンの全体像#
こんな悩みに効く#
- リトライで決済が二重に処理されてしまった
- ネットワークタイムアウト後の再送で同じデータが2件作成される
- クライアント側で「送信ボタンを2回押した」ことによる重複処理が発生する
基本の使い方#
すべてのAPIに冪等性が必要なわけではない。リスクの高い操作を優先的に対応する。
- 冪等性が必須: 決済処理、注文確定、送金、リソース作成
- 元々冪等: GET(読み取り)、DELETE(存在しなければ何もしない)、PUT(同じ値で上書き)
- 注意が必要: POST(作成系)。同じPOSTを2回送ると2件作成されるのがデフォルト動作
ポイント: HTTPメソッドの仕様上、GETとPUTとDELETEは冪等であるべき。POSTは設計次第。
リクエストごとに**一意の識別子(冪等性キー)**をクライアントに発行させる。
- クライアントがUUIDを生成し、リクエストヘッダーに含める(例:
Idempotency-Key: 550e8400-...) - サーバーはこのキーをDBやRedisに記録する
- 同じキーのリクエストが再度来たら、前回の結果をそのまま返す
ポイント: 冪等性キーの生存期間(TTL)を設定し、永遠にキーを保持しないようにする(例: 24時間)。
冪等性キーに基づくサーバー側の処理フローを実装する。
- リクエスト受信 → 冪等性キーを確認
- キーが存在しない → 通常処理を実行 → 結果とキーを保存
- キーが存在し処理完了済み → 保存済みの結果を返す(再処理しない)
- キーが存在し処理中 → 409 Conflictを返す(同時リクエストを防止)
ポイント: ステップ4の「処理中」状態を管理することで、並行リクエストによる二重処理も防げる。
冪等性キーだけに頼らず、データベース側でも重複を防止する仕組みを入れる。
- ユニーク制約: 注文番号やトランザクションIDにUNIQUE制約を設定
- 楽観的ロック: バージョン番号で更新の競合を検知
- UPSERT: INSERT OR UPDATE で同じデータが来ても安全に処理
ポイント: 多層防御の考え方。冪等性キー + DB制約 の二重ガードが最も安全。
具体例#
状況: ECサイトの決済API。ネットワーク不安定時にクライアントがリトライし、同じ注文に対して2回課金が発生。月間15件のクレーム。
実装:
- クライアントが注文確定時にUUIDを生成し、
Idempotency-Keyヘッダーに含める - サーバーはRedisにキーを記録(TTL: 24時間)
- 決済処理の結果(成功/失敗/レスポンスボディ)もキーに紐づけて保存
- 同じキーの2回目のリクエストには、保存済みのレスポンスをそのまま返す
結果: リトライによる二重課金がゼロに。月間の課金トラブル問い合わせが15件から0件に激減。
状況: モバイルアプリからのユーザー登録。電波が不安定な環境で「登録ボタンを押したが応答がない」ためユーザーが再タップし、同じメールアドレスで2アカウントが作成される事象が月30件発生。
対策:
- メールアドレスにUNIQUE制約を追加(DB多層防御)
- 登録APIにIdempotency-Key対応を追加
- アプリ側で登録ボタン押下時にUUIDを生成、リトライ時は同じUUIDを送信
結果: 重複アカウント作成がゼロに。UNIQUE制約違反時は既存アカウントの情報を返却するフォールバック処理も追加し、ユーザー体験も向上。
状況: 毎日深夜に実行する給与計算バッチ。サーバー障害でバッチが途中で止まり、再実行したところ一部の社員に給与が二重振込される事故が発生。影響額は合計480万円。
対策:
- 各給与計算レコードに
処理ID(年月+社員ID)を付与 - 振込テーブルに
処理IDのUNIQUE制約を設定 - バッチ処理をUPSERT(INSERT ON CONFLICT DO NOTHING)に変更
- 再実行時は未処理のレコードのみが処理される
結果: バッチの途中停止からの再実行が完全に安全に。運用チームが深夜の障害対応で「再実行して大丈夫か」を悩む必要がなくなった。
やりがちな失敗パターン#
- 冪等性キーのTTLを設定しない — キーが永遠に残るとストレージが肥大化する。24〜72時間程度のTTLを設定する
- レスポンスを保存せずにキーだけチェックする — キーの存在だけ確認して「処理済み」と返すと、クライアントが前回の結果を得られない。レスポンス全体を保存して返すことが重要
- GETリクエストに冪等性キーを要求する — GETは元々冪等なので、冪等性キーは不要。状態を変更する操作(POST/PATCH)にのみ適用する
- 並行リクエストのハンドリングを忘れる — 同じキーの2リクエストがほぼ同時に到達した場合、両方とも「キーなし」と判定して二重処理される。アトミックなロック取得(Redis SETNXやDBのSELECT FOR UPDATE)で防ぐ
まとめ#
冪等性は分散システムの信頼性を支える基盤。ネットワークは必ず失敗する前提で、リトライしても安全なAPIを設計することが重要。冪等性キーの導入とDB制約の二重防御で、決済の二重処理やデータの重複作成を確実に防ごう。