最近、表題のようなケースに遭遇し、SELECT FOR UPDATEは悲観ロックでしか使わない認識だったのが変わりました。
使用技術
- PostgreSQL 16.4
- トランザクション分離レベル: Read Commited
楽観ロック・悲観ロックの説明はこちらの記事がわかりやすいです。
「楽観ロック」と「悲観ロック」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典
SELECT FOR UPDATEの説明はこちら 13.3. 明示的ロック
楽観ロックでSELECT FOR UPDATEを使う
例えば、下記のような商品テーブルを更新するとします。
id | name | price | version |
---|---|---|---|
1 | 商品A | 1000 | 1 |
2 | 商品B | 1200 | 2 |
BEGIN; -- 1.更新対象のデータのバージョンを取得SELECT id, name, price, version FROM products WHERE id = 1; -- 結果: id=1, name='商品A', price=1000, version=1-- 2.なんらかの権限チェックや計算などの処理-- 3.更新直前に指定データの行ロック取得SELECT version FROM products WHERE id = 1FORUPDATE; -- 結果: version=1(この時点で行ロック取得)-- 結果: 1以外の場合、処理を終了する-- 4.更新処理UPDATE products SET price = 1200, version = version + 1WHERE id = 1AND version = 1; COMMIT;
なぜ楽観ロックでSELECT FOR UPDATEを使うのか
楽観的ロックを使わないと、下記のような処理になる可能性があります。
トランザクションA トランザクションB 1. バージョン確認(version=1) 1. バージョン確認(version=1) 2. 計算やチェック OK 2. 計算やチェック OK 3. バージョン確認(version=1) 3. バージョン確認(version=1) 4. UPDATE実行(version=2に更新) 4. UPDATE実行(version=2に更新)
後から実行されたトランザクションBのUPDATEが成功してしまい、トランザクションAの更新内容が失われる「後勝ち」の状況が発生します。
下記だったら成功しますが、上記のような場合ではロックが生きません。
SELECT FOR UPDATEを使うことで下記のような流れになり、Aの更新が守られます。
トランザクションA トランザクションB 1. バージョン確認(version=1) 1. バージョン確認(version=1) 2. 整合性チェック OK 2. 整合性チェック OK 3. SELECT FOR UPDATE (行ロック取得、version=1) 3. SELECT FOR UPDATE (ロック待ち) 4. UPDATE実行(version=2に更新) 5. トランザクション完了 (ロック解放) 4. SELECT FOR UPDATE (version=2を取得) 5. UPDATE失敗 (version不一致)
※他のトランザクションで実行された更新のコミット後の内容を読み取れるRead Commitedだからできることであって、トランザクション分離レベルがRepeatable Read以上になると他トランザクションの更新内容を読み取れなくなり、トランザクションBの4でversion=1を取得して5でUPDATEが実行されてしまい、楽観ロックを実現できません。