必ず取り入れたい4つの制御──「サブミットボタンの制御」「PRGパターン」「排他制御」「テーブル設計」
三谷氏は、2重リクエストの脅威からシステムを守るための具体的なアプローチとして、基本から応用まで9つの実践的な手法を提示した。これらは単発の技術ではなく、システムの特性や要求されるセキュリティレベルに応じて組み合わせるべきものだ。
第1のステップは、ユーザーの誤操作を未然に防ぎ、不要なリクエストの発生頻度を極限まで下げる基礎的なUI/UXの制御だ。最も基本的な対策として、フォーム送信時にサブミットボタンを非活性化し、処理完了まで二度押しできないようにする手法が挙げられた。
最も基本的な対策である「サブミットボタンの制御」
これに加えて、Web開発における定石であるPRG(Post/Redirect/Get)パターンの導入が推奨された。 これは、データの登録や更新を伴うPOSTリクエストを受け付けた後、サーバー側から直接完了画面のHTMLを返すのではなく、完了画面のURLへのリダイレクト(GETリクエスト)を指示するレスポンスを返す手法だ。これにより、ユーザーが完了画面でブラウザのリロードを行っても、安全なGETリクエストが再送されるだけとなり、意図しないデータの二重登録を防ぐことができる。
ブラウザのリロードによる2重リクエストを防ぐ「PRGパターンによる制御」
しかし、これらのフロントエンド側の対策をすり抜けて到達するバックエンドへのリクエストに対しては、データベース層での防御が必要だ。三谷氏は「ユニーク制約」と「状態遷移のルール化」という2つのアプローチを解説した。ユニーク制約は、注文データとキャンセルデータを別テーブルに分割し、外部キーに一意性を持たせることで、重複したリクエストが来てもデータベースレベルで挿入エラーを誘発させ、整合性を保つ手法だ。
一方の状態遷移のルール化は、単一のテーブル内に「注文状態」「キャンセル状態」といったステータスカラムを設け、アプリケーション側で「キャンセル状態からは遷移させない」といったバリデーションを実装する手法だ。
ここで三谷氏が強調したのは、状態遷移を利用する際に発生しうる「ロストアップデート」という致命的な課題と、その解決策としての「排他制御」の重要性である。複数の処理が同時に同じデータを読み込み、それぞれが更新をかけようとした際、後から保存したデータによって前の更新が上書きされ消失してしまう現象を防ぐ必要がある。
状態遷移のルール化はロストアップデートに注意
そのために、リレーショナルデータベース(RDB)の悲観的ロック(SELECT文にFOR UPDATEを付与して該当行をロックする手法)や、MySQLのGET_LOCK関数を用いた特定の文字列に対するアドバイザリーロックが有効であると解説された。なぜこれらの技術が最適かといえば、アプリケーションのコードベースを複雑にしすぎることなく、データ永続化の最終防衛ラインであるデータベース自身に一貫性の担保を委ねることができるからである。
さらに、外部サービスとの連携という複雑な要件に対しては、より高度な技術が導入されている。その一つが「APIキャッシュ」である。Amazon API Gatewayなどの基盤を活用し、一度成功したリクエストのキーとレスポンスを保存しておく。同じキーでリクエストが来た場合は、再処理を行わずに保存しておいた成功レスポンスを返却する仕組みだ。
APIキャッシュのフローを図解
もう一つ、決済ドメインで特に威力を発揮するのが「Idempotency-Key(冪等性キー)ヘッダ」の活用である。 冪等性(べきとうせい)とは、ある操作を1回行っても複数回行っても、結果が全く同じになるという概念を指す。この仕組みでは、クライアント側がリクエストごとに一意なキーを生成し、HTTPヘッダーに付与して送信する。サーバー側はこのキーをデータベースなどで管理し、同一キーの再送に対しては冪等な振る舞い(例えば、すでに処理済みの結果を返す)を保証する。
Idempotency-Keyヘッダのフローを図解
APIキャッシュがURLなどの情報に依存するのに対し、Idempotency-Keyはクライアントが能動的にキーを発行するため、「通信エラーによる事故的な再送」と「ユーザーが意図して金額を変えて再入金した操作」を明確に区別できるという点で、この課題解決に適した技術といえる。また、Idempotency-Keyヘッダは自前で構築する形になるため、処理中や未処理といったステータスも細かくハンドリングできるという利点がある。
競合を防ぐもう1つのアプローチ、「ETagとIf-Match」による楽観的ロック
三谷氏が最後に紹介した防御策が、ETag(エンティティタグ)とIf-MatchというHTTPヘッダーを活用した条件付き更新リクエストの仕組みだ。これは一般的に「楽観的ロック」と呼ばれる手法にあたる。
「ETagとIf-Match」による楽観的ロック
この仕組みにおいて、クライアントがGETリクエストで対象リソースを取得した際、サーバーはその時点のバージョン情報(更新日時やハッシュ値など)をETagとして返す。クライアントが更新を行う際には、このバージョン情報をIf-Matchヘッダーに付与して送信する。サーバー側は、現在のデータのバージョンと送信されたバージョンを比較し、一致すれば処理を実行し、別のリクエストによってすでに更新されていればエラーを返す。
ETagによる楽観的ロックは、ECサイトの在庫のように複数人が同時に更新を行うリソースには不向きだが、ユーザープロフィールの更新やブログの編集など、基本的に1人のユーザーが操作する処理において、同時更新による意図しない上書きを防ぐのに効果的だ。常に正常な結果を返すIdempotency-Keyとは異なり、重複をエラーとして返すことでクライアント側での同時更新を防ぐという明確な目的の違いがある。
