代表的な面接トピック

バックエンド面接:ETag と If-Match はどのようにして更新の喪失(Lost Update)を防ぐのか?

バックエンド普通
Offer.cc 編集チーム公開日 更新日

質問

2 人のエディタが同じドキュメントを読み込み、両方が更新を送信します。2 回目の書き込みが 1 回目の書き込みを暗黙的に上書きしないようにするために、ETag と If-Match をどのように使用しますか?

課題と設定

ある API がバージョンバリデータを備えたドキュメントを提供しています。複数のクライアントが同時にそれを読み取り、編集できる状況において、API のリトライ可能性と可観測性を維持しながら、古い(stale な)書き込みを拒否することが求められます。

面接官がテストするポイント

  • 表現(representation)のキャッシングと書き込み事前条件の区別。
  • 強バリデータ(strong ETag)を選択し、ミューテーションメソッドで If-Match を強制すること。
  • 古いバージョンに対して 412 を返し、安全なクライアントリカバリパスを定義すること。

回答前に確認すべき質問

  • ETag は保存されている正確な表現を表しますか、それとも弱いセマンティックバージョンのみを表しますか?
  • どのメソッドが事前条件を必要としますか:PUT、PATCH、DELETE、またはすべての書き込みですか?
  • クライアントはフィールドを自動的にマージすべきですか、それとも人間が競合を解決する必要がありますか?
  • リトライはロードバランスされた API と単一のトランザクショナルデータストアを介して送信されますか?

30秒の回答フレームワーク

編集可能なすべての表現とともに強 ETag を返します。クライアントは PUT、PATCH、または DELETE 時にその値を If-Match で送信します。サーバーは更新と同じトランザクション内でそれを比較し、不一致の場合はミューテーションを適用せずに 412 を返します。レスポンスには現在の表現または再取得(refetch)のシグナルを含めるべきであり、メトリクスによって競合や事前条件の欠落を追跡します。その後、クライアントは再取得し、意図的にマージして、新しい ETag でリトライします。

ステップごとの詳細解説

1. バリデータの発行

GET 時に、ドキュメントと正規の保存バージョンから導出された ETag を返します。編集に関連する表現が変更されるたびに値が変わる限り、大規模なペイロードをハッシュ化するよりもデータベースのリビジョン番号を使用する方がシンプルになる場合があります。If-Match には強バリデータを使用してください。弱バリデータは正確な書き込み状態を保護するのには適していません。

2. 事前条件のアトミックな適用

更新処理では、期待されるバージョンを確認し、新しいバージョンを書き込む処理を 1 つの条件付き操作として実行する必要があります。概念的には以下のとおりです。

sql
UPDATE documents
SET body = :new_body, version = version + 1
WHERE id = :id AND version = :expected_version;

影響を受けた行数がゼロの場合、412 を返して副作用を実行しません。アプリケーションメモリ内で ETag をチェックしてから後で書き込みを行うと、チェックとコミットの間に競合状態(race condition)が発生します。

3. 412 を他の失敗と区別する

412 は提供された事前条件が false であることを意味します。これは並行性の競合であり、不正な JSON や認証の失敗ではありません。無効なリクエスト形式には 400 を、認可には 401 または 403 を、API ポリシーに基づいてリソースが利用できない場合は 404 を返します。この区別により、クライアントは「再取得してマージ」か「ユーザーによる修正」かを選択できます。

4. クライアントリカバリの設計

412 の後、現在のドキュメントを取得し、競合するフィールドまたは差分(diff)を表示します。自動マージは、フィールドレベルのセマンティクスと認可ルールによって安全であると判断できる場合にのみ安全です。リトライは新しく返された ETag を使用し、リソースレベルで冪等性を維持する必要があります。古いリクエストを盲目的に再実行してはいけません。

5. キャッシュとレプリカの整合性を保つ

書き込みパスから見えるコミット済み状態からバリデータを生成します。遅延しているレプリカからの読み取りは古い ETag を返す可能性があり、回避可能な競合を引き起こします。一方、別のノードにルーティングされた書き込みも、プライマリトランザクション内でバージョンを強制する必要があります。競合率、If-Match 欠落率、リトライ成功率のメトリクスを出力します。

高品質な回答例

「編集可能なドキュメントごとに強 ETag を返し、ミューテーション時に If-Match を必須とします。サーバーは条件付きバージョン更新を使用して、行を更新するのと同じトランザクション内でタグを比較します。一致する行がない場合は 412 を返し、副作用を適用しません。クライアントは再取得し、差分を提示するか厳密に定義されたマージを実行してから、新しい ETag でリトライします。412 をバリデーションエラーや認可エラーと区別し、古い読み取り率、競合率、リトライ成功率を監視します。」

よくある間違い

  • ETag をチェックしてから別々の操作で更新する → 競合が残る → バージョンの述語と書き込みをアトミックに実行する。
  • 正確な書き込みに弱バリデータを使用する → セマンティクス上類似したコンテンツが誤って比較される可能性がある → 強バリデータを使用する。
  • すべての古い書き込みに対して 409 を返す → クライアントがプロトコルの事前条件を区別できない → If-Match 条件の失敗には 412 を使用する。
  • 古いペイロードを盲目的にリトライする → 最初のエディタの変更が失われる可能性がある → 再取得し、意図的にマージして、新しいタグでリトライする。

フォローアップの質問と回答

ETag はキャッシュ専用ですか?

いいえ。If-None-Match は一般にキャッシュ検証を可能にしますが、If-Match は書き込みを現在の表現に条件付けます。比較強度と生成ポリシーが正しければ、同じバリデータが両方の役割を果たすことができます。

クライアントが If-Match を省略した場合はどうなりますか?

楽観的並行性を必要とするリソースの場合、ドキュメント化された precondition-required レスポンスまたは明確な 400 ポリシーでミューテーションを拒否します。無条件の書き込みを暗黙的に受け入れると、更新の喪失が再び発生します。ポリシーはメソッド全体で一貫している必要があります。

サーバーは 412 レスポンスで最新のドキュメントを返すべきですか?

認可とペイロードサイズが許せば返すことも可能ですが、コントラクトとしては依然として再取得または明示的な競合解決を要求すべきです。データを返したからといって、最新のバリデータを使用せずにそれを上書きすることをクライアントに許可するわけではありません。

公開情報ソース

関連する質問