冪等キーで重複受信を防ぐ
目標
冪等キーを設計し、DB制約で重複を防ぎ、同時実行でも安全にし、保存してから応答する方式まで実装して、再送が安全な受信側を作れるようになります。
なぜ重要なのか
非同期連携とリトライがある世界では、重複は例外ではなくデフォルトです。ところが、重複防御をアプリケーションの조회 후 없으면 삽입(韓国語は「照会してなければ挿入」を意味します)で実装すると、同時に2件が入ってきた瞬間に突破されます。照会と挿入の間に隙間があるからです。DB制約は、その隙間をなくし、アプリケーションにバグがあっても突破されません。そして、さらに一歩進めて、最初の応答を保存しておき、重複リクエストにそのまま返すと、送信側がタイムアウト後に安心して再送できるようになります。決済APIがIdempotency-Keyを使う理由が、これです。
ステップ
/root/i/idem.db(sqlite)にinbox_logテーブルを作成してください。カラムはmsg_id、biz_key、status、response、created_atで、msg_idにPRIMARY KEYまたはUNIQUE制約が必要です。/root/i/key.mdを作成してください。次の3点が本文に含まれている必要があります。- このインターフェースの冪等キーを何にするかと、その理由
order_noだけで設定してはいけない理由- 冪等履歴の保管期間と、その根拠
멱등키、보관、수정(韓国語で、順に冪等キー、保管、修正を意味します)の3つの語がすべて登場する必要があります。
/root/i/apply.shを作成してください。2つの引数(DB파일 JSON파일。プレースホルダーは、DBファイルとJSONファイルです)を受け取り、- 新規ならロードして、1行目に
appliedを出力し、終了コード0 - すでにあれば何もせず、1行目に
duplicateを出力し、終了コード0 照会してから挿入する方式ではなく、制約を利用した方式である必要があります。
- 新規ならロードして、1行目に
/opt/lab/fixtures/eai/idem/messages/のすべてのJSONを、apply.shでロードしてください。inbox_logの行数が、ユニークなmsg_idの数と同じである必要があります- 重複として無視された
msg_idを、/root/i/dup.txtに昇順で保存します
/root/i/race.shを作成してください。2つの引数(DB파일 JSON파일。プレースホルダーは、DBファイルとJSONファイルです)を受け取り、同じメッセージを同時に10回ロードしようとし、最後の行にrows=<해당 msg_id 의 행 수>(プレースホルダーは、該当のmsg_idの行数です)を出力します。値は1である必要があります。 実行結果を/root/i/race.txtに保存してください。 (sqliteのロック競合に備えて、PRAGMA busy_timeoutを設定してください。)/root/i/purge.shを作成してください。2つの引数(DB파일 보관일수。プレースホルダーは、DBファイルと保管日数です)を受け取り、created_atが保管日数より古い行だけを削除し、最後の行にdeleted=<건수> remain=<건수>(プレースホルダーは件数です)を出力します。apply.shを拡張して、保存してから応答する方式を実装してください。- 新規処理のとき、応答のJSONを
responseカラムに保存し、appliedの次の行(2行目)に、その応答を出力します - 重複なら、
duplicateの次の行(2行目)に、保存された最初の応答をそのまま出力します 同じメッセージを2回適用したときに、2回目の出力の2行目が、1回目の応答と同じである必要があります。確認結果を/root/i/replay.txtに保存してください。
- 新規処理のとき、応答のJSONを
/root/i/report.mdを作成してください。重複が発生する4つの経路を、それぞれ1項目として書き、各経路ごとに防御ポイントも一緒に書いてください。재시도、큐、수동、배치(韓国語で、順にリトライ、キュー、手動、バッチを意味します)の4つの語がすべて登場する必要があります。
参考
- 制約を利用した挿入:
INSERT OR IGNORE INTO ...のあとに、changes()で反映されたかを判定します - 同時実行:
for i in $(seq 10); do ... & done; wait - ロック待ち:
PRAGMA busy_timeout=5000; - 日付の比較:
created_at < datetime('now', '-30 days') - よくあるミス1:
SELECTで確認してからINSERTする方式です。同時実行で突破されます。 - よくあるミス2: 整理バッチを「古いものからN件」で作ることです。流入が急増すると、最近のものを消します。
- よくあるミス3:
busy_timeoutなしで並列実行して、database is lockedで失敗することです。
冪等履歴テーブル
/root/i/idem.db(sqlite)にinbox_logテーブルを作成してください。
カラムはmsg_id、biz_key、status、response、created_atで、msg_idにPRIMARY KEYまたはUNIQUE制約が必要です。
重複防御の最後の防衛線は、アプリケーションではなくDB制約です。どのカラムに制約をかけるべきかを、先に決めてください。
冪等キーの設計文書
/root/i/key.mdを作成してください。次の3点が本文に含まれている必要があります。
- このインターフェースの冪等キーを何にするかと、その理由
order_noだけで設定してはいけない理由- 冪等履歴の保管期間と、その根拠
멱등키、보관、수정(韓国語で、順に冪等キー、保管、修正を意味します)の3つの語がすべて登場する必要があります。
業務キーだけでは不十分な場合が多いです。同じ注文に対する修正電文がありうるし、電文番号が日単位で循環することもあります。
ロードスクリプト
/root/i/apply.shを作成してください。2つの引数(DB파일 JSON파일。プレースホルダーは、DBファイルとJSONファイルです)を受け取り、
- 新規ならロードして、1行目に
appliedを出力し、終了コード0 - すでにあれば何もせず、1行目に
duplicateを出力し、終了コード0 照会してから挿入する方式ではなく、制約を利用した方式である必要があります。
すでに処理したキーなら、何もせず、そのことを知らせる必要があります。照会してから挿入する方式は、同時実行で突破されるので、制約を活用する方式を使ってください。
一括ロードと重複の集計
/opt/lab/fixtures/eai/idem/messages/のすべてのJSONを、apply.shでロードしてください。
inbox_logの行数が、ユニークなmsg_idの数と同じである必要があります- 重複として無視された
msg_idを、/root/i/dup.txtに昇順で保存します
ロードの結果から、新規と重複を区別して数える必要があります。重複したキーの一覧を残しておくと、あとで原因分析に使います。
同時実行の防御
/root/i/race.shを作成してください。2つの引数(DB파일 JSON파일。プレースホルダーは、DBファイルとJSONファイルです)を受け取り、同じメッセージを同時に10回ロードしようとし、最後の行にrows=<해당 msg_id 의 행 수>(プレースホルダーは、該当のmsg_idの行数です)を出力します。値は1である必要があります。
実行結果を/root/i/race.txtに保存してください。
(sqliteのロック競合に備えて、PRAGMA busy_timeoutを設定してください。)
同じメッセージを並列で何度入れても、1件でなければなりません。sqliteはロック競合が多いので、待機時間を設定する必要があります。
保管期間の整理
/root/i/purge.shを作成してください。2つの引数(DB파일 보관일수。プレースホルダーは、DBファイルと保管日数です)を受け取り、created_atが保管日数より古い行だけを削除し、最後の行にdeleted=<건수> remain=<건수>(プレースホルダーは件数です)を出力します。
整理した瞬間に、その区間の重複防御が失われます。期間の条件だけで削除する必要があり、件数を基準に削除すると、最近のものを消すことがあります。
保存してから応答する方式
apply.shを拡張して、保存してから応答する方式を実装してください。
- 新規処理のとき、応答のJSONを
responseカラムに保存し、appliedの次の行(2行目)に、その応答を出力します - 重複なら、
duplicateの次の行(2行目)に、保存された最初の応答をそのまま出力します 同じメッセージを2回適用したときに、2回目の出力の2行目が、1回目の応答と同じである必要があります。確認結果を/root/i/replay.txtに保存してください。
重複リクエストに最初の応答をそのまま返すと、送信側にとって再送が完全に安全になります。決済APIが使っている方式です。
重複の発生経路の整理
/root/i/report.mdを作成してください。
重複が発生する4つの経路を、それぞれ1項目として書き、各経路ごとに防御ポイントも一緒に書いてください。
재시도、큐、수동、배치(韓国語で、順にリトライ、キュー、手動、バッチを意味します)の4つの語がすべて登場する必要があります。
重複は4つの経路で来ます。各経路で、どの地点が防衛線なのかを一緒に書いてください。