重複受信の防御を実装する
目標
二重請求を自分で作ってみてから冪等キーで防ぎ、同時リクエストと本文の不一致まで処理して、実際の決済APIの水準の防御を完成させます。
なぜ重要なのか
ネットワークをまたぐ配信で、純粋な意味のちょうど1回は、不可能に近いです。送った側は、確認応答が来なかったとき、「届かなかった」と「届いたが応答だけが消えた」を区別できないからです。そのため、実務の正解は決まっています。少なくとも1回で送り、受け取る側で重複を消します。このラボで最も微妙なのは、ステップ5です。キーを参照して、なければ保存するという素朴な実装は、2つのリクエストが同時に届くと、両方が通ってしまいます。RedisのSET ... NXの1回で、参照と先取りをアトミックに合わせる必要があります。この1行の違いが、プロモーションのときに二重請求の報告が入るかどうかを分けます。
ステップ
/root/idem/pay.pyを127.0.0.1:8130で起動します。起動時にRedisのキーbalanceを10000に初期化し、POST /payは{"amount":100}を受け取って残高を減らし、{"charged":100,"balance":<잔액>}を返します(プレースホルダーは残高です)。- 冪等性なしで同じリクエストを2回送った結果を
/root/idem/double.outにbalance_before=10000 balance_after=9800の形式で書きます。 pay.pyがIdempotency-Keyヘッダーを要求するように変えます。ヘッダーがなければ400を返します。- 同じキーで2回送ると、最初の応答がそのまま繰り返され、残高は1回だけ減ります。
/root/idem/dedup.outにbalance_after=9900 calls=2を書きます。 - 同じキーで同時に2件を送っても、1回だけ処理されます。
/root/idem/concurrent.outにprocessed=1 balance_delta=100を書きます。 - 同じキーに別の金額を送ると、422を返します。
/root/idem/mismatch.outにstatus=422を書きます。 - 冪等キーの残りの寿命が0より大きく、86400以下である必要があります。
/root/idem/ttl.txtにttl=<초>を書きます(プレースホルダーは秒数です)。
参考
- アトミックな先取り:
SET idem:<key> IN_PROGRESS NX EX 86400。このコマンドが0を返したら、すでに誰かが先取りしています。 - 同時リクエストの作り方:
curl ... & curl ... & wait - 確定的な失敗(残高不足)は応答をキャッシュし、一時的な失敗(タイムアウト)はキーを解放して、リトライが本当にもう一度試されるようにします。
- よくある間違いは、参照してから保存する2段階の実装です。その間が競合区間です。
決済サービスを起動する
/root/idem/pay.pyを127.0.0.1:8130で起動してください。起動時にRedisのキーbalanceを10000に初期化し、POST /payは{"amount":100}を受け取って残高を減らし、{"charged":100,"balance":<잔액>}を返します(プレースホルダーは残高です)。
残高をRedisに置けば、再起動しても状態が保たれます。最初の残高を決まった値に初期化してください。
二重請求を再現する
冪等性なしで同じリクエストを2回送った結果を/root/idem/double.outにbalance_before=10000 balance_after=9800の形式で書いてください。
同じ決済リクエストを2回送ると、残高が2回減ります。これが防ぐべき状態です。
冪等キーのヘッダーを受け取る
pay.pyがIdempotency-Keyヘッダーを要求するように変えてください。ヘッダーがなければ400を返します。
ヘッダーがなければ400で拒否するほうが安全です。クライアントにキーを作らせることが契約です。
同じキーは1回だけ処理する
同じキーで2回送ると、最初の応答がそのまま繰り返され、残高は1回だけ減るようにしてください。/root/idem/dedup.outにbalance_after=9900 calls=2を書きます。
キーに対して保存しておいた最初の応答を、そのまま返します。2回目のリクエストで、残高が変わってはいけません。
同時リクエストでも1回だけ処理する
同じキーで同時に2件を送っても、1回だけ処理されるようにしてください。/root/idem/concurrent.outにprocessed=1 balance_delta=100を書きます。
参照してから保存すると、その間にほかのリクエストが割り込みます。1回のアトミックなコマンドで先取りする必要があります。
キーは同じなのに本文が違う場合は拒否する
同じキーに別の金額を送ると、422を返すようにしてください。/root/idem/mismatch.outにstatus=422を書きます。
最初のリクエストの本文のハッシュも一緒に保存しておけば、比較できます。ステータスコードは422です。
キーの期限切れを設定する
冪等キーの残りの寿命が0より大きく、86400以下になるようにしてください。/root/idem/ttl.txtにttl=<초>を書きます(プレースホルダーは秒数です)。
キーを永遠に保管すると、メモリが際限なく増えます。24時間が慣行です。残りの寿命を取得して確認してください。