起きる問題:「結果が分からない」から、再送した
オンラインで、決済のリクエストを送ったとします。通信が不安定で、応答が返ってこない。実際には、サーバーは処理を終えていたのかもしれませんし、届いていなかったのかもしれません。
利用者やプログラムは、結果が分からないので、もう一度、同じリクエストを送ります。すると、すでに処理されていた場合、二重に決済されてしまいます。
この問題を防ぐ考え方が、冪等性です(冪等性とは)。そして、決済や登録のように、本来は、繰り返すと結果が変わってしまう操作を、安全に再送できるようにする仕組みが、冪等キーです。
冪等キーの考え方
冪等キーは、「この操作は、これです」という、一意の印です。
| 順番 | 動き |
|---|---|
| ① | クライアントが、操作ごとに、一意のキーを作る |
| ② | キーを付けて、リクエストを送る |
| ③ | サーバーは、そのキーを、初めて受け取ったなら、処理を行い、キーと結果を保存する |
| ④ | 再送で、同じキーを受け取ったら、処理せず、保存した結果を返す |
この仕組みで、何度送っても、処理は1回だけになります。
具体的な流れ(例)
HTTPでは、キーをヘッダーに付けて送る設計が、決済などのAPIで、よく使われます。
POST /payments Idempotency-Key: 3f2a9c1e-...(操作ごとに作った、一意の値)
- 1回目のリクエスト:サーバーが、決済を処理し、キーと結果(成功)を保存する。応答は、通信エラーで届かなかった
- 再送(同じキー):サーバーは、キーが保存されていることを確認し、決済は行わず、保存してあった結果を返す
サーバー側で決めること
| 決めること | 考え方 |
|---|---|
| キーの保存先 | データベースや、キャッシュなど。処理の結果と、一緒に、確実に保存する |
| 同じキーで、同じ内容 | 保存した結果を、そのまま返す |
| 同じキーで、内容が違う | エラーにする(キーの誤用か、不正の可能性があるため) |
| 処理中に、同じキーが来た | 二重に処理しないよう、待たせるか、処理中であることを返す |
| 保存の期限 | 再送が起こり得る期間より、長くする |
| 失敗した場合の扱い | 失敗の結果も保存するか、再試行を許すか、事前に決める |
サンプルコード(考え方の例)
考え方を示す、簡単な例です。実際の運用では、データベースの制約や、同時実行への対応を、きちんと設計してください。
# 保存先の例:キーごとに「リクエストの内容」と「結果」を持つ
saved = {}
def create_payment(idempotency_key, request):
if idempotency_key in saved:
record = saved[idempotency_key]
if record["request"] != request:
# 同じキーで、内容が違う → エラー
raise ValueError("同じキーで、異なる内容のリクエストです")
return record["response"] # 前の結果を、そのまま返す
response = do_payment(request) # 初回のみ、実際の処理を行う
saved[idempotency_key] = {"request": request, "response": response}
return response
実際のシステムでは、「保存」と「処理」が、確実にセットで行われるようにする必要があります。たとえば、キーをデータベースの一意制約で、二重に登録できないようにし、処理と保存を、**1つの取引(トランザクション)**にまとめる設計が、考えられます。
クライアント側の使い方
- 操作ごとに、一意のキーを作る(UUIDのような、重複しにくい値)
- 再送の時は、同じキーを使う
- 新しい操作には、新しいキーを使う
- 再送の方法は、APIのタイムアウトとリトライの決め方と、組み合わせて設計する