はじめに
ネットワーク障害、タイムアウト、ユーザーの誤操作——現代のソフトウェアはあらゆる場所で「失敗」と向き合っている。失敗した操作はリトライされる。そのリトライが安全かどうかを保証するのが、冪等性 (Idempotency) だ。
「同じ操作を何度繰り返しても、システムの状態が同じになる性質」。これが冪等性の核心であり、分散システムにおける安全設計の基盤となる概念だ。
冪等性とは
定義
同一の操作を 1 回実行しても N 回実行しても、システムの状態が同じになる性質のこと。
数学では f(f(x)) = f(x) と表現される。同じ関数を 2 回適用しても結果が変わらないという性質だ。絶対値関数 |x| がわかりやすい例で、||x|| = |x| が成り立つ。
冪等 vs 非冪等
| 操作 | 冪等? | 理由 |
|---|---|---|
GET /users/1 | ○ | 読み取りのみで状態変化なし |
PUT /users/1 (同一ボディ) | ○ | 上書きなので結果は常に同じ |
POST /orders (新規作成) | × | 呼ぶたびに注文が増える |
DELETE /items/1 | △ | 1 回目は削除、2 回目は 404 (状態は一致する) |
HTTP メソッドと冪等性 (RFC 7231)
RFC 7231 では、GET, HEAD, PUT, DELETE を冪等なメソッドとして定義している。POST は冪等ではない。これがなぜ重要かというと、プロキシやクライアントが「このメソッドは安全にリトライできる」と判断する根拠になるからだ。
どういったアプリケーションで必要か
ECサイトの決済
ユーザーが「購入」ボタンを二重タップしたとき、2 回課金されてはならない。フロントエンドだけでガードしても、ネットワーク遅延によって同じリクエストが 2 回サーバーへ届くケースは排除できない。
送金サービス (銀行/フィンテック)
タイムアウト後にクライアントがリトライしても、二重送金を防ぐ必要がある。「送金されたかどうか不明」な状態でリトライするのは避けられないため、サーバー側での冪等性保証が必須だ。
マイクロサービス間の非同期通信
Amazon SQS や Google Cloud Pub/Sub は "at-least-once delivery" が基本仕様だ。同一メッセージが複数回配信されることを前提に、コンシューマー側を設計しなければならない。
外部 Webhook の受信
Stripe, GitHub, Slack など外部サービスからの Webhook は、配送保証のために同じイベントを複数回 push してくることがある。受信側での重複排除が必要だ。
共通点: 通信が失敗してリトライされる可能性がある、あるいはユーザーが誤って同一操作を繰り返す可能性がある場面では、冪等性の設計が必須となる。
設計: 決済 API の冪等キー設計
最もシンプルかつ実績のある実装手法が 冪等キー (Idempotency Key) パターン だ。
パターンの概要
- クライアントがリクエストごとにユニークな
Idempotency-Keyヘッダを生成して送付する (例: UUID v4) - サーバーはそのキーをキャッシュ/DB に保存し、同一キーの 2 回目以降のリクエストには保存済みのレスポンスをそのまま返す
フロー
クライアント サーバー
│ │
│─── POST /payments ────────>│
│ Idempotency-Key: abc123 │
│ │─── DB に abc123 存在チェック
│ │ 存在しない → status: in_progress で INSERT (UNIQUE 制約)
│ │ → 決済処理実行
│ │ → status: completed + レスポンス保存 に UPDATE
│<── 200 OK (決済完了) ──────│
│ │
│ (タイムアウト・ネット障害) │
│ │
│─── POST /payments ────────>│ ← 同一キーで再送
│ Idempotency-Key: abc123 │
│ │─── DB に abc123 存在チェック
│ │ completed → 保存済みレスポンスを返す
│<── 200 OK (決済完了) ──────│ ← 二重課金なし
実装のポイント
よくある注意点
注意点 1: 処理中 (In-Flight) のレースコンディション
「1 回目が DB に保存する前に 2 回目が届く」パターンが最も厄介だ。
単純な「存在チェック → 処理 → 保存」では、チェックと保存の間に割り込まれると 2 重実行が発生する。
対策: キーを受け付けた瞬間に status: in_progress で INSERT し、処理完了後に completed へ UPDATE する。
これにより「先着 1 件だけが処理に進める」構造をアトミックに実現できる。
注意点 2: 同一キーで異なるペイロードが送られてくるケース
クライアントのバグにより「同じ Idempotency-Key で金額だけ違うリクエスト」が来ることがある。
ナイーブな実装では 1 回目のリクエスト結果をそのまま返すため、金額の不一致が気づかれない。
対策: キー保存時にリクエストボディのハッシュ値 (SHA-256 など) も一緒に保存し、再リクエスト時にハッシュを比較する。
設計時の検討ポイント
1. キー生成はクライアント責任にする
サーバーがキーを生成してしまうと、初回リクエスト自体の重複を防げない。
フロントエンド側のライフサイクル設計が重要だ。フォーム画面がマウントされたタイミングで UUID を生成し、決済完了まで画面の State に保持し続ける。画面をリロード・閉じると新しい UUID になり、自然に「別リクエスト」として扱われる。
「ボタンを押したときに UUID を生成」すると二重クリックで 2 つの UUID が生まれてしまうため、マウント時生成が正しい設計だ。
2. レスポンスを丸ごとキャッシュする
処理結果だけでなく HTTP ステータスコードも保存し、リトライ時に完全一致のレスポンスを返す。ステータスコードが変わると、クライアントの挙動が変わってしまう。
3. 部分成功状態 (Partial Failure) を作らない
DB への書き込みとキャッシュ保存をトランザクションで束ねる。途中で失敗するとキーが汚染され、以降のリトライが壊れたレスポンスを返し続ける。
4. 副作用を持つ処理に注意する
メール送信・プッシュ通知は処理の一部として発火させると重複しやすい。「送信済み」フラグを DB に持ち、送信前に必ずチェックする。
5. 冪等性とリトライ戦略はセットで設計する
Exponential Backoff + Jitter をリトライ側に実装することで、サーバー側の負荷集中を防ぐ。冪等性だけ担保してリトライが集中すると、障害時にサーバーがダウンする。
6. 自動テストで担保する
同一キーで 2 回リクエストを送り、副作用 (課金・DB レコード) が 1 回分しか発生しないことを自動テストで確認する。「たぶん大丈夫」では不十分で、テストで明示的に保証することが重要だ。
まとめ
冪等性は「失敗を前提とした設計」の中心的な概念だ。
次のアクションとして、自分のプロジェクトの POST エンドポイントを洗い出し、冪等性が担保されているかを確認してみてほしい。
参考
- 2026.05.30 · 1 MINはじめての投稿