1 二重送信の概要

1.1 定義概念整理

1.1.1 意図しない再送との違い

二重送信は、利用者やシステムの意図に反して、同一内容の送信・処理要求が短時間に複数回発生する状態を指す。意図しない再送には、利用者が再度送信ボタンを押すケースだけでなく、クライアントや中継機構が「失敗した」と誤認して同じ要求を繰り返すケースも含まれる。対して、意図しない再送が必ずしも「同一内容」ではない場合があるのに対し、二重送信は実装上の同一性(同じ対象、同じパラメータ、同じ更新意図など)が成立しやすい点に特徴がある。

1.1.2 同時送信・短時間重複の扱い

二重送信は完全に同時に限らず、ミリ秒から数分程度の短い間隔で重複が起こる。サーバ側では同じリクエストが別のワーカーやスレッドで同時に処理されることがあり、順序が入れ替わると結果の差分が拡大する。したがって「同時」よりも「短時間に複数回の受付が成立すること」を中心概念として扱うと、検知と設計実務に即しやすい。

1.2 発生シーン

1.2.1 Webフォームの送信

Webフォームでは、画面遷移の遅延や通信の揺らぎにより、利用者が完了前に再操作することで重複が生じる。特に、送信ボタンが即座に無効化されない実装、ブラウザの戻る・再読み込み、フォームの自動再送機能の影響などが典型である。また、サーバからの応答遅れタイムアウト扱いになると、フロントエンド側が同じ入力内容を再度送ろうとすることもある。

1.2.2 API呼び出しと連携処理

API連携では、呼び出し側がリトライを行う設計、ゲートウェイやプロキシによるタイムアウト、バッチ処理とオンライン処理の二系統化などにより、同一要求が複数回到達し得る。入力が同じでも副作用が異なる実装(例:処理開始時刻を内部に記録して結果を変えるなど)だと、重複排除が難しくなる。連携先が「少なくとも1回(at-least-once)」の配信を行う場合、重複前提の設計が特に重要になる。

1.2.3 メッセージ送信・通知

通知系では、メール、SMS、プッシュ通知などが対象になりやすい。配信基盤の再試行や、受信者側の一時的な失敗(キュー滞留、期限切れ、認証エラー)によって同じ通知が繰り返される。通知は金銭損失に直結しない場合もあるが、ユーザ体験を損ねたり、問い合わせ増につながったりするため、抑止や抑制の設計が欠かせない。

1.3 影響とリスク

1.3.1 二重登録・二重更新

同一の作成要求が複数回実行されると、重複レコードが増える。更新要求でも、順序違いによって最終状態が変わることがある。例えば、古い値で上書きされる形で整合性が崩れると、後工程での参照が誤った前提に基づく。結果として、データの修復作業や監査対応の負担が増大する。

1.3.2 二重課金・二重発注

決済や注文では、二重送信が直接的に金銭や在庫へ影響し得る。処理が複数回走ると、支払の重複・注文数量の重複・返品相殺の増加といった事態になり得る。さらに、返金処理は再度の認証や外部連携を伴うため、二重発生が連鎖して被害範囲が拡大する。

1.3.3 データ整合性の破壊

二重実行は、参照整合性や因果関係(どの入力がどの結果を生んだか)を崩す。例えば、在庫引当と注文確定が同一意図で複数回行われると、引当の合計が実在庫を超える可能性がある。加えて、イベント駆動の連携では順序逆転が起きると、状態遷移矛盾して復旧コストが上がる。

2 発生要因の分類

2.1 クライアント側要因

2.1.1 操作の重複(連打・戻る)

利用者の連打は最も分かりやすい要因である。送信直後に画面が更新されない、完了通知が遅い、通信が詰まって応答が見えない場合に発生しやすい。加えて、ブラウザの戻る操作や再読み込みでフォームが再送されることがある。こうした行動は、意図しないまま複数の同型リクエストを発生させる起点になる。

2.1.1.1 タイムラグと画面遷移

クライアントは「送信した」という認識を持っていても、ネットワーク遅延で応答が返らないことがある。その間に画面遷移が完了せず、利用者が同じ操作を重ねると二重発生につながる。特に、ローディング表示が不十分であったり、非同期処理の完了タイミングがUI更新に反映されなかったりすると、誤操作を誘発する。

2.1.2 ネットワーク再試行

モバイル回線や不安定な環境では、クライアントが通信失敗を検知して再試行する。タイムアウトの判定が遅すぎる、あるいはサーバ側が処理を完了しているのに応答が失われた場合、再試行は重複を生む。HTTPの仕様上、冪等性が定義されていない操作を繰り返しても結果が一意に定まらないため、再試行ポリシーは慎重な設計が必要になる。

2.2 サーバ側要因

2.2.1 再処理の設計不備

サーバが同一要求を受けたとき、処理開始から完了までを一意に紐づける仕組みを持たないと、重複がそのまま二重実行に至る。例えば、内部ワーカーがキューから取り出した後に状態記録がない場合、失敗・再取り出しで同じ副作用が再発する。加えて、外部API連携の再試行が重複排除と連動していないと、二次被害が増える。

2.2.2 受付状態管理の不足

受付段階と実行段階を区別せず、同じ入力が複数回到達しても統制できないことがある。受付済みかどうかを識別するキー(リクエストIDや注文番号など)と、そのキーの保持期間、ロック戦略が不足すると、競合時の整合性が破れる。結果として、同じ対象に対する更新が並列で走り、最後に処理されたものが勝つという状態が固定される。

2.3 インフラ・連携要因

2.3.1 監視・リトライ基盤の誤設定

監視が「失敗」を誤検知し過剰なリトライを発生させると、二重送信が増える。例えば、遅延をタイムアウトとみなして再投入する設定、リトライ間隔が短すぎる設定、同一ジョブを識別する仕組みがない設定が組み合わさると、重複が連鎖する。さらに、障害復旧時にリカバリの再実行が集中すると、正常系のトラフィックまで巻き込まれることがある。

2.3.2 メッセージングの配信保証

メッセージング基盤では「少なくとも1回」配信が採用されることが多い。これは配信漏れを防ぐ一方で、重複到達が起きる前提になる。配信側が重複削減を保証しない、受信側が処理結果を記録しない、あるいは順序制御が欠如している場合、二重処理が起きる。よって、受信側は重複を安全に吸収できる設計(冪等性、デデュープ、状態遷移の制約)を持つことが求められる。

3 検知・抑止の考え方

3.1 冪等性(イデンポタンス)

3.1.1 「同じ入力なら同じ結果」設計

冪等性は、同一入力が繰り返し与えられても、最終結果が一意に保たれる性質として扱われる。処理対象を特定するキーと、副作用(作成、更新、課金、在庫引当など)をそのキーに結び付けることで、重複が起きても結果が増殖しないようにする。実務では「返答が同じ」ことより「状態が矛盾しない」ことを目標に設計することが多い。

3.1.2 更新と作成の扱い

更新系は重複で上書きが変動しやすいため、どの条件で更新を許すかを明確にする必要がある。作成系は、既存がある場合に新規を作らない制約(ユニーク制約、存在チェック)を用いると効果的である。両者を統一的に扱うには、「受付時の識別子」「対象の現在状態」「実行済み判定」を組み合わせ、同型入力の多重到達でも挙動が収束するようにする。

3.2 重複排除(デデュープ)

3.2.1 受付番号・リクエストID

重複排除には、同一意図を識別するキーが必要になる。典型例は受付番号やリクエストIDで、クライアントが送信し、サーバが受け取って処理済みかどうかを判定する。重要なのはキー生成の一意性だけでなく、生成範囲(どこまで同一意図をカバーするか)と、保持期間(再送が起き得る時間窓)を適切に設定する点である。これにより、同じ処理が繰り返されても副作用が再実行されにくくなる。

3.2.2 タイムウィンドウによる制限

保持し続けるとストレージや照合コストが増えるため、一定期間で重複排除の対象を区切る設計が一般的である。この期間は通信遅延やリトライの挙動、外部連携の再試行頻度を踏まえて決める。短すぎると重複がすり抜ける一方、長すぎると照合負荷が増えるため、運用実績に基づく調整が必要になる。

3.3 受付状態と整合性

3.3.1 二段階処理(受付→実行)

二段階処理では、まず「受付」を記録し、その後に「実行」を行う。受付の記録により、同一キーの重複到達が検知できるため、実行側の再入を制御しやすい。受付から実行までの中断や障害も想定し、受付状態がどのように進行するか(完了、失敗、再実行可能)を明確にすることで、復旧時の挙動が安定する。

3.3.2 トランザクション設計の要点

整合性確保には、状態の更新と副作用の整合をどう保証するかが課題になる。単一データベース内で収められる場合はトランザクションにより一貫性を確保しやすい。分散環境では、ロックや楽観的競合制御、補償処理などの組み合わせが必要になる。設計の要点は「競合が起きたときにどのルールで勝敗を決めるか」と「失敗時に状態がどのように残るか」を事前に決めることである。

3.4 フィードバック設計

3.4.1 送信中表示と連打抑止

UIは二重送信を誘発する要因のひとつであるため、送信中の表示と操作抑止が有効になる。送信ボタンの無効化、入力欄のロック、進捗の明示により、利用者が完了前に再操作する確率を下げられる。非同期処理の場合は、完了判定に合わせて状態を戻すタイミングを誤らないことが重要になる。

3.4.2 成功・失敗の明確化

成功と失敗の区別が曖昧だと、利用者は不安から再送しがちになる。サーバ応答が遅延した場合の挙動も含め、どの状態にあるかを伝える設計が望ましい。例えば、タイムアウト時に「処理が実行されたか不明」と明示し、再送する代わりに問い合わせ番号や進捗確認手段へ誘導することで、重複発生を抑えられる。

4 実装・運用の実践

4.1 クライアント実装の対策

4.1.1 ボタンの無効化と送信抑止

送信中は操作を止めるのが基本となる。二重送信対策として、ボタン無効化、スピナー表示、入力値の固定を組み合わせると効果が高い。ページ遷移が伴う場合も、再読み込みや戻る操作をした際にフォームが再送される可能性を考慮し、適切なガードを置く。

4.1.2 送信完了の判定

完了判定は応答ステータスだけでなく、結果の受領条件に基づいて行う。例えば、注文処理なら「注文IDが返ってきた」「状態が確定した」など、実務に即した条件を用いる。完了判定が緩いと、結果が確定する前にUIを戻してしまい、連打が再発するため注意が必要である。

4.2 サーバ実装の対策

4.2.1 重複排除キーの採用

サーバは、重複排除キーを受け取って処理済み判定を行う。キーはクライアントから供給する場合もあれば、サーバが生成して応答に含める場合もある。いずれにせよ、同一意図の粒度が合うように設計し、永続化するデータの項目(キー、状態、作成物の参照)を最小限かつ十分にするのが実装の要点となる。

4.2.2 再試行時の整合性確認

外部連携のタイムアウト時には再試行が起きるため、「再実行してよいか」を判断する確認手順を組み込む。確認は、処理済み状態の照合、既存レコードの存在チェック、結果参照の取得などで構成できる。重要なのは、再試行が起きた場合でも状態が矛盾しないよう、更新順序と例外時の扱いを統一することである。

4.3 テストと検証

4.3.1 並行リクエストの再現テスト

二重到達は並行性に由来するため、単体テストだけでは見落としが起こりやすい。並列に同じキーを送るテスト、応答遅延を意図的に発生させるテスト、順序を反転させるシナリオを用意すると挙動が確認できる。期待する最終状態(重複が生じない、あるいは既存結果が参照される)を明確にして評価する。

4.3.2 障害注入による挙動確認

障害注入では、処理中断、ネットワーク切断、タイムアウト応答、ワーカー再起動などを意図的に発生させる。これにより、受付状態と実行状態が不整合にならないか、復旧時に正しい収束が行われるかを検証できる。とくに「受付は記録されたが実行が完了していない」などの中間状態を中心に確認すると有効である。

4.4 運用・監視

4.4.1 二重処理の検知指標

運用では、重複排除キーの命中率、同一対象に対する短時間の複数受付、ユニーク制約違反、異常な在庫引当差分、差戻し・返金の増加などを指標化する。検知はイベント単位だけでなく、処理全体の整合を示す集計にも広げると早期発見につながる。閾値は誤検知と見逃しのバランスを考慮して調整する。

4.4.2 ログ設計と追跡性

ログには、受付キー、対象識別子、状態遷移、外部連携の相関情報を含めると追跡が容易になる。特に、同一キーで複数の処理経路が生じた場合に、因果のつながりが追える構造にしておくと原因調査が速い。ログ粒度は必要最小限から始め、実害や問い合わせが出た局面で拡充する運用が現実的である。