ECサイトの在庫引当における同時実行制御と障害復旧の判断
衣料品ECを運営するD社は、在庫テーブル stock(item_id, warehouse_id, qty, version)で引当を行っている。1日の注文は約8万件、セール時はピークで毎秒100件の引当要求が来る。現行処理は「SELECTで在庫を読む→アプリで充足判定→UPDATE qty = qty - n」の3段階で、分離レベルはREAD COMMITTEDである。version列を追加して楽観ロックにしたところ、ピーク時に更新件数0のエラーが1時間で約1,200件発生し、再試行3回で失敗する注文が約40件出た。同一商品に注文が集中するセール商品が原因と考えられている。また、夜間の引当バッチは倉庫ID→商品IDの順でロックを取り、オンライン処理は商品ID→倉庫IDの順でロックを取るため、デッドロックが週に数回発生している。バックアップは日次でフルを取得し、REDOログのアーカイブは30分ごとに行っている。経営層は「在庫の取り過ぎは絶対に避ける」と指示している。
1. オーバーセル(在庫の取り過ぎ)が起きる条件を、READ COMMITTEDの動作とともに説明し、確実に防ぐ実装を示せ。
Read the worked answer
起きる条件: READ COMMITTEDでは各SQL文がその時点で最新のコミット済みデータを見る。Tx1とTx2が同時に qty=5 を読み、それぞれ3個と3個を引当可能と判断し、Tx1が qty=2 を書いてコミット、Tx2が自分の計算値 5-3=2 を書いてコミットする。結果は qty=2 だが実需は6で、3個のオーバーセルになる。UPDATEが行ロックを取っても、Tx2の更新値はTx1の結果を加味せずに上書きする(ロストアップデート)。
確実な対策:判定と更新を1文にする。
UPDATE stock
SET qty = qty - :n, version = version + 1
WHERE item_id = :i AND warehouse_id = :w AND qty >= :n;
更新件数が0なら在庫不足として扱う。行ロックの取得と充足判定が原子的になり、読んだ値と書く値の間に割り込む余地がない。
代替: SELECT ... FOR UPDATE で行ロックを取ってから判定・更新する。分離レベルを上げるだけでは不十分で、スナップショットを読む以上、アプリ側の判定ロジックと競合検出・再試行が前提になる。経営方針が「取り過ぎは絶対に避ける」なら、条件付きUPDATEを標準とする。
2. デッドロックの原因を特定し、オンラインとバッチの両方に適用できる対策を述べよ。
Read the worked answer
原因: 資源の獲得順序が逆である。オンラインは商品ID→倉庫ID、バッチは倉庫ID→商品IDの順でロックを取るため、互いに相手が持つロックを待つ循環待ちが発生する。
対策:
- 全プログラムでロック順序を統一する。たとえば(item_id, warehouse_id)の昇順と決め、複数行を扱うときはキーでソートしてからロックする。
- 1件ずつロックするのをやめ、対象キーをまとめて昇順にロックする。
- デッドロックのタイムアウトを設定し、検出時はトランザクションをロールバックして指数バックオフ付きで再試行する。DBが victim を選んでロールバックする場合もあるので、アプリはエラーコードを握りつぶさず再試行対象として扱う。
- バッチはオンラインのピーク帯を避ける、または対象を商品単位で分割して衝突面を小さくする。
- ロック順序の統一は設計規約としてリポジトリに明記し、レビューで確認する。
3. 現在のバックアップ方式でのRPOを説明し、目標復旧点を5分にしたい場合の改善策と復旧手順を示せ。
Read the worked answer
現状のRPO: REDOログのアーカイブが30分ごとなので、最悪30分間の更新を失う。日次フルバックアップ+アーカイブREDOでロールフォワードする前提であり、RPOは約30分、RTOはリストア時間+ロールフォワード時間+整合性確認時間になる。
RPOを5分にする改善策:
- アーカイブ間隔を5分に短縮する。ただし障害時に失うのは「アーカイブされていないログ」の分なので、現在のログの保護(別ディスク・別ホストへの退避)も必要。
- 同期または準同期のレプリケーションを standby に張り、RPOをほぼ0に近づける。ただし commit のレイテンシが増えるため、ピーク時の毎秒100件に耐えるか事前に検証する。
復旧手順:
- 障害を検知してサービスを停止し、書き込みを止める。
- 最新のフルバックアップをリストアする。
- アーカイブREDOログを順に適用してロールフォワードする。欠番がないことを確認する。
- 未コミットのトランザクションをUNDOし、DBを整合状態にする。
- 在庫と引当の突合チェックを行い、オーバーセルや二重引当がないか確認する。
- サービスを再開する。
復旧リハーサルを定期的に行い、実際のRTOを計測しておく。目標値は口頭ではなく運用手順書に書く。