TT Lab
はじめる
学ぶ 学習パス コース

システム間連携 (EAI)

冪等キーで重複受信を防ぐ

TT Labで続きを見る

目標

冪等キーを設計し、DB制約で重複を防ぎ、同時実行でも安全にし、保存してから応答する方式まで実装して、再送が安全な受信側を作れるようになります。

なぜ重要なのか

非同期連携とリトライがある世界では、重複は例外ではなくデフォルトです。ところが、重複防御をアプリケーションの조회 후 없으면 삽입(韓国語は「照会してなければ挿入」を意味します)で実装すると、同時に2件が入ってきた瞬間に突破されます。照会と挿入の間に隙間があるからです。DB制約は、その隙間をなくし、アプリケーションにバグがあっても突破されません。そして、さらに一歩進めて、最初の応答を保存しておき、重複リクエストにそのまま返すと、送信側がタイムアウト後に安心して再送できるようになります。決済APIがIdempotency-Keyを使う理由が、これです。

ステップ

  1. /root/i/idem.db(sqlite)にinbox_logテーブルを作成してください。カラムはmsg_id、biz_key、status、response、created_atで、msg_idにPRIMARY KEYまたはUNIQUE制約が必要です。
  2. /root/i/key.mdを作成してください。次の3点が本文に含まれている必要があります。
    • このインターフェースの冪等キーを何にするかと、その理由
    • order_noだけで設定してはいけない理由
    • 冪等履歴の保管期間と、その根拠 멱등키、보관、수정(韓国語で、順に冪等キー、保管、修正を意味します)の3つの語がすべて登場する必要があります。
  3. /root/i/apply.shを作成してください。2つの引数(DB파일 JSON파일。プレースホルダーは、DBファイルとJSONファイルです)を受け取り、
    • 新規ならロードして、1行目にappliedを出力し、終了コード0
    • すでにあれば何もせず、1行目にduplicateを出力し、終了コード0 照会してから挿入する方式ではなく、制約を利用した方式である必要があります。
  4. /opt/lab/fixtures/eai/idem/messages/のすべてのJSONを、apply.shでロードしてください。
    • inbox_logの行数が、ユニークなmsg_idの数と同じである必要があります
    • 重複として無視されたmsg_idを、/root/i/dup.txtに昇順で保存します
  5. /root/i/race.shを作成してください。2つの引数(DB파일 JSON파일。プレースホルダーは、DBファイルとJSONファイルです)を受け取り、同じメッセージを同時に10回ロードしようとし、最後の行にrows=<해당 msg_id 의 행 수>(プレースホルダーは、該当のmsg_idの行数です)を出力します。値は1である必要があります。 実行結果を/root/i/race.txtに保存してください。 (sqliteのロック競合に備えて、PRAGMA busy_timeoutを設定してください。)
  6. /root/i/purge.shを作成してください。2つの引数(DB파일 보관일수。プレースホルダーは、DBファイルと保管日数です)を受け取り、created_atが保管日数より古い行だけを削除し、最後の行にdeleted=<건수> remain=<건수>(プレースホルダーは件数です)を出力します。
  7. apply.shを拡張して、保存してから応答する方式を実装してください。
    • 新規処理のとき、応答のJSONをresponseカラムに保存し、appliedの次の行(2行目)に、その応答を出力します
    • 重複なら、duplicateの次の行(2行目)に、保存された最初の応答をそのまま出力します 同じメッセージを2回適用したときに、2回目の出力の2行目が、1回目の応答と同じである必要があります。確認結果を/root/i/replay.txtに保存してください。
  8. /root/i/report.mdを作成してください。重複が発生する4つの経路を、それぞれ1項目として書き、各経路ごとに防御ポイントも一緒に書いてください。 재시도、큐、수동、배치(韓国語で、順にリトライ、キュー、手動、バッチを意味します)の4つの語がすべて登場する必要があります。

参考

冪等履歴テーブル

/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点が本文に含まれている必要があります。

業務キーだけでは不十分な場合が多いです。同じ注文に対する修正電文がありうるし、電文番号が日単位で循環することもあります。

ロードスクリプト

/root/i/apply.shを作成してください。2つの引数(DB파일 JSON파일。プレースホルダーは、DBファイルとJSONファイルです)を受け取り、

すでに処理したキーなら、何もせず、そのことを知らせる必要があります。照会してから挿入する方式は、同時実行で突破されるので、制約を活用する方式を使ってください。

一括ロードと重複の集計

/opt/lab/fixtures/eai/idem/messages/のすべてのJSONを、apply.shでロードしてください。

ロードの結果から、新規と重複を区別して数える必要があります。重複したキーの一覧を残しておくと、あとで原因分析に使います。

同時実行の防御

/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を拡張して、保存してから応答する方式を実装してください。

重複リクエストに最初の応答をそのまま返すと、送信側にとって再送が完全に安全になります。決済APIが使っている方式です。

重複の発生経路の整理

/root/i/report.mdを作成してください。 重複が発生する4つの経路を、それぞれ1項目として書き、各経路ごとに防御ポイントも一緒に書いてください。 재시도、큐、수동、배치(韓国語で、順にリトライ、キュー、手動、バッチを意味します)の4つの語がすべて登場する必要があります。

重複は4つの経路で来ます。各経路で、どの地点が防衛線なのかを一緒に書いてください。