コンテンツにスキップ

Web APIのトランザクション

チェック

  • [ ] 本文を確認した
  • [ ] 概要を確認した
  • [ ] タグを確認した
  • [ ] inbox/ 直下へ移行した

概要

Web APIでリソース更新を扱うとき、HTTP越しではクライアントとサーバが同じエラーを同時に検知できない。 そのため、単純なリトライは二重作成や更新ロストを生む。 基本戦略は、冪等な仕組みを作って成功するまでリトライできるようにすること。ETag、セカンダリキー、冪等キー、メッセージ配信保証が論点になる。

本文

ネットワークの向こう側にあるリソースを更新するのは難しい。TCP/IPとHTTPの上では、リクエストやレスポンスの途中で失敗したとき、クライアントとサーバが同じ状態を同じように検知できないケースがある。

エラーの発生箇所は次のように整理される。

番号 発生箇所 クライアント サーバ
1 クライアントからサーバへの接続エラー コネクションタイムアウトなど 検知不能
2 リクエスト送信したがサーバに届かない ソケットタイムアウトなど 検知不能
3 サーバが不完全なメッセージを受信 400 400
4 メッセージが処理キューに入らない 503 503
5 サーバ処理が正常完了しない 5xx 5xx
6 サーバがレスポンス返却しようとしたが接続断 検知不能 コネクションリセットなど
7 サーバはレスポンスを返したがクライアントに届かない 検知不能 ソケットタイムアウトなど

冪等な仕組みがない場合、安全にリトライできるのは基本的に 1、4、場合によって5 くらい。6や7では、サーバ側で処理が成功したかもしれないのに、クライアントからは失敗に見える。

基本戦略は、更新APIを冪等にし、成功するまでリトライできるようにすること。

冪等化したいユースケース

ゾンビリソースを防ぐ

同じ内容で複数レコードが作られることを防ぐ。商品注文APIで、クライアント側の事情により同じ注文リクエストが複数回送られても、サーバ側ではただ1つの注文として扱う。

同時更新で更新内容が失われるのを防ぐ

AさんとBさんが同じデータを同時に参照し、別々にPUTするケース。Aさんが名前を変更した直後に、Bさんが古いデータをもとに年齢だけ変更してPUTすると、Aさんの名前変更が戻ってしまう。これはlost update。

安全にリトライできる

商品注文APIで最初のリクエストがソケットタイムアウトした場合、実際にはサーバ側で注文処理が完了しているかもしれない。リトライで二重注文にならないようにしたい。

同じリクエストには同じレスポンスを返す

冪等キーが同じなら、同じ処理として扱い、同じ結果を返す。クライアントはレスポンス欠落時も同じリクエストを再送できる。

対処の仕組み

ETag / If-Match

同時更新によるlost update対策。リソースをGETしたときにETagを返し、更新時に If-Match でETagを送る。

GET /users/123

HTTP/1.1 200 OK
ETag: "v1"
PUT /users/123
If-Match: "v1"
Content-Type: application/json

{"name":"D","age":30}

サーバはETagが現在のリソース状態と一致すれば更新する。一致しなければ 412 Precondition Failed を返す。これにより、古い状態をもとにしたPUTで新しい変更を上書きすることを防げる。

セカンダリキー

同内容のレコードが複数作られることを防ぐため、エンティティに一意のキーを付ける。POSTリクエストが来たときに、そのセカンダリキーで検索し、既に存在すればConflictを返す。

注意点として、クライアントが通常のIDを付けるだけでは、別IDで同じ内容が飛んでくる可能性がある。業務的な一意性をどこで表すかが重要になる。

冪等キー

クライアントが一意なキーを生成し、HTTPヘッダに付ける。

POST /orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json

{"sku":"book-1","quantity":1}

サーバ側では Idempotency-Key を保存し、同じキーの処理が二回以上実行されないようにする。多くの場合、DBテーブルに idempotency_key カラムを追加し、一意制約を張る。

実装イメージ。

CREATE TABLE idempotency_keys (
  key text PRIMARY KEY,
  request_hash text NOT NULL,
  response_status integer,
  response_body jsonb,
  created_at timestamptz NOT NULL DEFAULT now()
);

処理の流れ。

1. リクエストから Idempotency-Key を読む
2. key が未登録なら、処理中として登録する
3. 業務処理を実行する
4. レスポンス内容を key に紐づけて保存する
5. 同じ key の再リクエストでは、保存済みレスポンスを返す

同じkeyで異なるpayloadが来た場合は、リクエストハッシュを比較して拒否する設計が必要になる。

メッセージ配信保証

APIの冪等化はメッセージ配信保証とも関係する。

  • at-most-once: 届かないかもしれないが、重複しない
  • at-least-once: 重複するかもしれないが、届くことは保証する
  • exactly-once: 重複なく一度だけ届く

冪等キーが実装されていれば、重複処理は抑えられる。次に考えるべきは、届くことをどう保証するか。

要点

  • Web APIの更新処理では、クライアントとサーバが同じ失敗を同時に観測できない。
  • レスポンス欠落時、サーバ処理が成功している可能性があるため単純リトライは危険。
  • 更新APIは冪等化し、成功するまで安全にリトライできるようにする。
  • lost updateにはETag/If-Matchが効く。
  • 二重作成にはセカンダリキーやIdempotency-Keyが効く。
  • 冪等キーはDB一意制約とレスポンス保存まで含めて設計する。

タグ

web-api #transaction #idempotency #http #concurrency-control #distributed-systems