こう 聞かれます
- 「決済のような API を、
二重に 送られても 困らないように 作るには、 どうしますか」 - 「通信が
途中で 切れて、 利用者が もう 一度 ボタンを 押しました。 お金が 2 回引かれないように するには」 - 「POST の API を、
再試行しても 安全に するには、 どうしますか」
面接官が 見ている点
- 二重の
送信は、 ボタンの 二度押し、 通信の 切れ、 再試行の 仕組みなど、 ふつうに 起きると 分かっているか - 画面で
ボタンを 押せなく するだけでは 足りず、 サーバーの 側で 止める 必要が あると言えるか - 同じ
鍵が、 処理の 途中に 来た とき、 違う 中身で 来た ときまで 考えているか - 鍵の
保存と本体の 処理を、 同じ トランザクションに そろえる 理由を言えるか
短い答え
- 送る
側が 1 回の 操作ごとに 一意の 鍵 (冪等キー) を 作って リクエストに 付け、 再試行の ときも 同じ鍵を 送ります。 - 受ける
側は 鍵と 結果を 保存し、 同じ 鍵が もう 一度来たら、 処理を やり直さずに 保存した 結果を 返します。 これで 何回送られても、 お金が 動くのは 1 回だけに なります。 - 鍵の
保存は 本体の 処理と 同じ トランザクションで 行い、 鍵に 一意の 制約を 付けて、 同時に 2 つ 来ても 片方しか 通らないようにします。
この
少し 詳しい答え
なぜ 二重に届くのか
送った
HTTP では、
冪等キーの流れ
送るIdempotency-Key という
受ける
JavaScript
const saved = new Map();
let balance = 1000;
function pay(key, amount) {
if (saved.has(key)) {
return saved.get(key);
}
balance -= amount;
const result = "支払い済み 残高 " + balance;
saved.set(key, result);
return result;
}
console.log(pay("order-1-a", 300));
console.log(pay("order-1-a", 300));
console.log(pay("order-2-b", 200));支払い済み 残高 700
支払い済み 残高 700
支払い済み 残高 5002 回目の
抜けやすい 3 つの場面
- 処理の
途中に 同じ鍵が来る 1 回目が まだ 終わっていないのに 2 回目が 来る ことがあります。 鍵の 列に 一意の 制約を 付け、 先に 「処理中」として 鍵を 入れた 側だけが 進み、 後から 来た 側には 「処理中です」と 返して、 少し 待ってから 送り直して もらいます - 同じ鍵で
違う 中身が来る 送る 側の 不具合で、 金額の 違う リクエストに 同じ鍵が 付く ことがあります。 鍵と 一緒に リクエストの 中身の ハッシュも 保存し、 中身が 違えば エラーで 返します。 黙って 前の 結果を 返すと、 利用者は 別の 支払いを したつもりになります - 鍵の
保存と本体の 処理がずれる 支払いの 記録だけ書けて 鍵が 書けないと、 再試行で また 支払いが 走ります。 鍵と 結果の 保存は、 本体の 書き込みと 同じ トランザクションで行います
外の
トレードオフ
鍵を
注文番号のように、
話す例は
よく ある間違い
- 画面で
ボタンを 押せなく するだけで済ませる 通信の 切れや、 再試行の 仕組みからの 二重の 送信は 止められません。 サーバーの 側で止めます - 鍵の
有無を 調べてから 書く、を 別々に行う 調べた 直後に 同じ鍵が もう 1 つ 来ると、 両方が 「無い」と 判断して 両方とも 進みます。 一意の 制約で DB に 止めて もらいます - 鍵だけ
見て、 中身を 照らさない 同じ鍵で 違う 金額が 来た とき、 前の 結果を そのまま 返してしまいます
深掘りの問い
- 「鍵は
送る 側と サーバーの、 どちらが 作りますか」 送る 側が 作ります。 サーバーが 作ると、 最初の 応答が 失われた ときに 送る 側は 鍵を 知らず、 同じ 操作だと 伝えられないから、と答えます - 「1 回目が
失敗していたら、 同じ鍵の 再試行は どうしますか」 入力の 誤りのような 決まった 失敗は、 その 結果を 保存して 同じ 失敗を 返します。 途中で 落ちて 結果が 無い ときは、 やり直して よいように 鍵を 処理中から 外す、 と答えます - 「キューから
同じ メッセージが 2 回届くときは」受け取る 側も 同じで、 メッセージの id を 処理済みとして 保存し、 2 回目は 何もしない、と答えます
