- 技術 💻
冪等性とは?二重課金を防ぐ冪等キーとリトライ設計
冪等性(べきとうせい・idempotency)とは、「同じ操作を1回実行しても、複数回実行しても、結果が同じになる」性質のことです。たとえば「ユーザーのステータスを"有効"にする」操作は、何度繰り返しても結果は"有効"のまま——これが冪等な操作です。一方「残高から1,000円引く」操作は、繰り返すたびに残高が減るので冪等ではありません。
なぜこの性質が重要なのか。ネットワーク越しの通信では「1回だけ届ける」ことを保証できないからです。この記事では、その仕組みから、二重課金が起きる原因、冪等キーの実装、リトライ設計との組み合わせまで、決済・金融システムを手がける株式会社RJCの実務の視点で解説します。
なぜ冪等性が必要なのか:「ちょうど1回」は保証できない
分散システムには、根本的な制約があります。メッセージを「ちょうど1回(exactly once)」届けることは、原理的に保証できません。
送信して応答が返ってこなかったとき、送り手には2つの可能性が区別できません。
- 相手に届いていない(再送すべき)
- 相手に届いたが、応答だけが失われた(再送すると重複する)
再送すれば重複の可能性があり、再送しなければ欠落の可能性がある。だから現実の設計は、「最低1回は届ける(at-least-once)」+「受け手が重複を無害化する」という組み合わせになります。この「重複を無害化する」役割を担うのが、冪等性です。
二重課金はこうして起きる
決済APIで典型的な事故の流れです。
- クライアントが「1,000円の決済」をリクエスト
- サーバーは決済を実行し、DBに記録した
- しかし応答がネットワークの途中で失われた
- クライアントは「失敗した」と判断し、同じリクエストを再送
- サーバーは新しいリクエストとして受け取り、もう一度1,000円を決済
ここに悪意も不具合もありません。タイムアウト+リトライという、ごく正常な動作の組み合わせで二重課金が発生します。リトライを実装するなら、冪等性はセットで必須です。片方だけだと、リトライが二重課金の製造機になります。
HTTPメソッドと冪等性
HTTPの仕様では、メソッドごとに冪等性が定義されています。
| メソッド | 冪等 | 理由 |
|---|---|---|
| GET | ○ | 取得は何度しても状態が変わらない |
| PUT | ○ | 「この内容で置き換える」は何度しても同じ結果 |
| DELETE | ○ | 「削除済み」は何度削除しても削除済み(2回目が404でも状態は同じ) |
| POST | × | 「新規作成」「加算」は繰り返すと結果が変わる |
| PATCH | 内容次第 | 「値をXにする」は冪等、「値に1足す」は非冪等 |
問題になるのは、決済・注文・登録といったPOSTで表現される操作です。仕様上は非冪等なので、アプリケーション側で冪等性を実装する必要があります。
冪等キー(Idempotency Key)の実装パターン
もっとも広く使われている手法が、冪等キーです。主要な決済サービスのAPIが要求する Idempotency-Key ヘッダは、この仕組みです。
基本の流れ
- クライアントがリクエストごとに一意のキー(UUIDなど)を生成し、ヘッダやパラメータに付けて送る
- サーバーは処理の前に、そのキーが処理済みかを確認する
- 未処理なら実行し、キーと結果を保存して応答を返す
- 処理済みなら実行せず、保存しておいた前回の結果をそのまま返す
再送されたリクエストは同じキーを持つので、2回目以降は「前回の答えを返すだけ」になります。これで、クライアントが何度リトライしても、決済は1回しか走りません。
実装で押さえるべき5つのポイント
1. キーはクライアントが生成する サーバーが生成すると、応答が失われたときにクライアントがキーを知り得ません。「リトライ時に同じキーを送れる」ことが本質なので、生成はクライアント側です。
2. 「確認」と「保存」を原子的に行う 「キーを確認→未処理→実行→保存」の間に同じキーの2本目が来ると、両方が「未処理」と判断して二重実行します。DBのユニーク制約でキーの挿入を先に行い、成功した方だけが処理を進める設計にします。
3. 処理中の状態も記録する 「実行中」のキーに2本目が来たら、失敗ではなく「処理中」として待たせるか、同じ結果を後で返します。実行中を「未処理」と扱うと、ここでも二重実行が起きます。
4. 同じキーで内容が違うリクエストは拒否する 同じキーで金額だけ変えたリクエストが来た場合、前回の結果を返すのではなくエラーにします。キーと内容のハッシュを一緒に保存しておくと検出できます。
5. キーには有効期限を設ける 永久に保存すると肥大化します。リトライが起こり得る期間(数時間〜数日)を超えたら削除する運用が一般的です。
冪等キーを使わない冪等化の方法
冪等キーが唯一の手段ではありません。操作の設計自体で冪等にできる場合もあります。
- 「加算」を「置換」に変える — 「残高を1,000円減らす」ではなく「残高を9,000円にする」と表現する。ただし競合更新には別途対策が必要
- 業務上の一意キーを使う — 「注文番号ごとに1回しか決済できない」というユニーク制約をDBに置く。注文番号自体が冪等キーの役割を果たす
- 状態遷移で表現する — 「未払い→支払済み」の遷移として実装し、すでに支払済みなら何もしない
リトライ設計との組み合わせ
冪等性を確保したうえで、リトライ側にも定石があります。
- 指数バックオフ — 再送の間隔を1秒→2秒→4秒と伸ばす。障害中のサーバーへの再突撃を防ぐ
- ジッター — 待ち時間にランダムなずれを加える。多数のクライアントが同時に再送する「リトライストーム」を防ぐ
- リトライ回数の上限 — 無限に再送しない。上限に達したら人間が判断できる形で記録する
- リトライすべきエラーの選別 — タイムアウトやネットワークエラーは再送、認証エラーや不正な入力は再送しない
「エラー時は3回リトライ」とだけ書かれた設計書を見たら、冪等性・バックオフ・ジッター・対象エラーの4点を確認するのが、レビューの定石です。
冪等性が求められる場面
- 決済・課金 — 二重課金は信頼を直撃する
- 注文・予約 — 二重注文、二重予約
- メール・通知送信 — 同じ通知が複数回届く
- メッセージキューの消費 — キューは基本的にat-least-once配送なので、消費側の冪等性が前提
- バッチ処理の再実行 — 途中で落ちたバッチを頭から再実行しても、結果が壊れない設計
よくある質問
Q. GETは冪等なので、リトライは何も考えなくていいですか? A. 取得自体は冪等ですが、「GETのたびに閲覧数を加算する」ような副作用を持たせていると非冪等になります。仕様上の冪等性と、実装上の冪等性は別に確認が必要です。
Q. 冪等キーは、どこに保存すればいいですか? A. 処理本体と同じトランザクションで保存できるDBが最も安全です。別のストア(キャッシュ等)に置く場合は、保存と処理の間の整合性をどう保つかを設計してください。
Q. 冪等性と排他制御(ロック)は何が違いますか? A. 排他制御は「同時に走らせない」仕組み、冪等性は「何度走っても同じ結果にする」性質です。冪等キーの実装では、確認と保存を原子的に行うために排他制御(ユニーク制約など)を使います。両者は組み合わせて使うものです。
Q. 小規模なシステムでも必要ですか? A. 外部APIを呼ぶ、またはキューを使う時点で必要です。規模ではなく「ネットワークを越えるか」で判断してください。
まとめ
冪等性は、「メッセージは重複して届く」という分散システムの現実を受け入れたうえで、正しさを守るための設計です。
- ネットワーク越しに「ちょうど1回」は保証できない
- だから「最低1回届ける+受け手が重複を無害化する」
- 無害化の定番が冪等キーで、確認と保存の原子性が実装の急所
- リトライは冪等性・バックオフ・ジッター・対象選別とセットで設計する
株式会社RJCが携わる決済や金融のシステムでは、冪等性は「あると良い設計」ではなく「前提」です。外部連携を書く全員に、今日から効く知識としてお役に立てば幸いです。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。