+ 作るログイン
図を読み込んでいます…
1 / 7
動きでわかる Webとデータの仕組み / 全10章

同時更新:最後の在庫を、二人が買おうとしたら

発行
アニメモ公式
版
第1版
改訂
コマ
全7

残り1個の在庫を題材に、読み取り後の更新が競合する理由、条件付きUPDATE、行ロック、COMMITとROLLBACKを追う。

二人が、同じ残り1個を見る

最後の一冊をAとBが同時に購入しようとしています。二つのトランザクションがそれぞれ SELECT stock を実行し、どちらも1を得ました。

ここではPostgreSQLの既定の分離レベルであるRead Committedを前提にします。通常のSELECTは、その文が始まった時点のスナップショットを読みます。他の取引がまだ確定していなければ、同じ残数が返ることはあります。

「画面で在庫1と見えた」ことだけでは、後の購入時にも残っている約束にはなりません。PostgreSQL 17:Read Committed

  • 06 / WEB SYSTEMS
  • 1 / 7
  • 残り1個を、AもBも見ている
  • 購入 A
  • 在庫テーブル
  • 購入 B
  • 別のトランザクション
  • 残り 1 個
  • 別のトランザクション
  • 読んだ値:1
  • 読んだ値:1
  • 別々の読み取りが、同じ「残り1」を返すことはある。

古い読み取り結果から、同じ0を書き込む

アプリ側でそれぞれ 1 - 1 を計算し、条件なしで stock=0 を保存したらどうなるでしょう。Aが0にして予約を作り、その後Bも0にして別の予約を作れてしまいます。

更新が行ロックで順番に実行されても、Bの更新値が「以前読んだ1から計算した0」のままなら、この設計の問題は消えません。最後の在庫表示は0でも、予約は二つです。

BEGINとCOMMITで囲むことは必要な道具ですが、取引の中の確認方法と分離レベルも重要です。トランザクションという名前だけで、競合が自動的に意図どおり解消されるわけではありません。

  • 06 / WEB SYSTEMS
  • 2 / 7
  • 二人とも「0にする」だけでは、売り過ぎる
  • 購入 A
  • 在庫テーブル
  • 購入 B
  • 別のトランザクション
  • 残り 0 個
  • 別のトランザクション
  • 1 − 1 = 0 を保存
  • 1 − 1 = 0 を保存
  • Aの予約が成立
  • Bの予約も成立
  • BEGINとCOMMITで囲むだけでは、この競合は防げない。

条件と減算を、一つのUPDATEにまとめる

在庫が正の行だけ、その場で1減らします。購入処理は、この文が行を返した場合だけ予約追加へ進めます。

sql
UPDATE inventory
SET stock = stock - 1
WHERE sku = 'BOOK' AND stock > 0
RETURNING stock;

図の前提としてskuは一意に一つの在庫行を指します。0行だった場合は、在庫不足などとして扱い、予約を作りません。更新成功と予約追加を同じトランザクションに入れることも、この方法の一部です。

これは説明用SQLです。実際の購入APIには、所有者や権限、重複した購入依頼などの別の確認も必要です。

  • 06 / WEB SYSTEMS
  • 3 / 7
  • 条件の確認と減算を、同じUPDATEで行う
  • 一つのUPDATE
  • 条件が合った行だけ変更する
  • SET stock = stock - 1
  • WHERE sku = 'BOOK' AND stock > 0
  • RETURNINGで結果を確認
  • stock > 0 の行だけ減らし、更新できたかを確認する。

Aが更新している間、Bは同じ行で待つ

Aの条件付きUPDATEが先に対象行を更新したとします。Aの取引内では在庫が0になりましたが、まだCOMMITしていません。図の0はAが更新中の値であり、他の通常のSELECTへ未確定値が見えるという意味ではありません。

Bも同じ行をUPDATEしようとすると、先に更新したAの完了を待ちます。その間にAは、同じトランザクション内で予約を追加します。

図は、一行を競合して更新する単純な場面です。複数行を別の順序でロックする処理などでは、デッドロックや再試行も考慮します。PostgreSQL 17:同時更新

  • 06 / WEB SYSTEMS
  • 4 / 7
  • Aが更新中なら、Bの更新は待つ
  • 購入 A
  • 在庫テーブル
  • 購入 B
  • 別のトランザクション
  • 残り 0* 個
  • 別のトランザクション
  • * Aの未確定変更
  • A:1行を更新
  • Aがロック中
  • B:行の更新を待つ
  • 同じ取引で予約を追加
  • 同じ行への更新は競合する。Aは予約の作成も同じ取引で行う。

Aが確定すると、Bは新しい行で条件を再評価する

AがCOMMITしました。待っていたBは、更新された行に対してWHERE条件を確かめ直します。残数0なので stock > 0 は成立せず、この例では更新結果が0行になります。

アプリはその結果を見て、Bの予約を追加しません。AがROLLBACKしたなら、Bが処理を続けられる結果も変わります。

この説明はPostgreSQLのRead Committedにおける動作です。他の分離レベルではエラーや再試行が必要になる場合があります。特定のDBと分離レベルの振る舞いを確認して設計することが大切です。

  • 06 / WEB SYSTEMS
  • 5 / 7
  • Aの確定後、Bは条件を確かめ直す
  • 購入 A
  • 在庫テーブル
  • 購入 B
  • 別のトランザクション
  • 残り 0 個
  • 別のトランザクション
  • 予約を追加しない
  • B:更新は0行
  • A:COMMIT済み
  • stock > 0 を満たさない
  • PostgreSQLのRead Committedでは、更新後の行で条件を再評価。

在庫と予約を、一つの成功か失敗にする

Aが在庫を減らした後、予約の追加に失敗した別の経路を考えます。両方を同じトランザクションに入れていれば、ROLLBACKで在庫の変更も取り消せます。在庫だけ減った状態を確定させずに済みます。

トランザクションは、DB内の変更をまとめて確定するか、取り消すかを選ぶための境界です。既に送ったメールや、別サービスに確定させた決済まで、DBのROLLBACKで取り消せるわけではありません。

外部処理を含む場合は、冪等性やアウトボックスなどを別に検討します。PostgreSQL 17:トランザクション

  • 06 / WEB SYSTEMS
  • 6 / 7
  • 途中で失敗したら、両方を取り消す
  • 変更中
  • ROLLBACK後
  • 在庫を減らしたが、予約に失敗
  • 在庫1・予約なし
  • 在庫の変更も取り消す
  • まとめて元の状態へ
  • 在庫の減算と予約の追加を、同じトランザクションに入れる。

先に在庫を読めば、購入を約束できる?

答えはいいえです。確認から更新までの間に、別の取引が状態を変えられます。この作品では、条件付きUPDATE、更新結果の確認、在庫と予約をまとめるトランザクションで対処しました。

複雑な条件には SELECT FOR UPDATE でのロックや、Serializableと再試行など、別の設計が合うこともあります。一行の在庫減算の方法が、複数行にまたがるすべての整合性を守るわけではありません。

考える順序は、いつ条件を確かめるか、何を一緒に確定するか、競合した結果をどう扱うか。この三点を図にすると、同時更新の抜け道を見つけやすくなります。

  • 06 / WEB SYSTEMS
  • 7 / 7
  • 確認:在庫を先に読めば、購入を約束できる?
  • 更新時に確認
  • 変更をまとめる
  • 結果で分岐
  • 確認と減算を一つにする
  • 在庫と予約を同じ取引に
  • 0行なら予約を作らない
  • 確認 → 更新 → 結果判定
  • 答えは NO。更新時の条件と、確定する変更のまとまりが大切。

0 件のコメント

読み込み中…

権利侵害・不適切な内容の連絡: [email protected]