楽観ロックと悲観ロックの違い ― 使い分け
楽観ロックは
楽観ロックと悲観ロックの違いは、衝突を先に防ぐか後で見つけるか
悲観ロックは、データを読む時点でロックを取り、ほかの人の更新を待たせて衝突を防ぐ方式です。楽観ロックは、ロックを取らずに読み、更新の時点でバージョン番号を比べて衝突を見つける方式です。在庫や座席のように、同じ行を大勢が同時に書き換えるシステムを作るとき、どちらで更新の衝突を防ぐかを選びます。選び方しだいで、待たされる時間とやり直しの回数が変わります。
名前は、衝突をどう見込むかから来ています。悲観ロックは衝突がよく起きると見込み、楽観ロックはめったに起きないと見込みます。排他制御とデッドロックの記事で説明した専有ロックの使い方は、悲観ロックに当たります。
ロストアップデートは、先に書いた更新が後の更新で消えること
どちらの方式も、同じ問題を防ぐためにあります。在庫が10個ある商品を、Aさんが3個、Bさんが2個、ほぼ同時に買う場面で考えます。2人のプログラムは、在庫を読み、買った数を引いた値を書き戻します。
-- Aさんのプログラム
-- Bさんも同じ形で、引く数が2
SELECT 在庫数 FROM 在庫
WHERE 商品番号 = 'A01';
-- 10が返る
UPDATE 在庫 SET 在庫数 = 7
WHERE 商品番号 = 'A01';
-- 10−3を書く2人がどちらも10を読むと、Aさんは7を書き、Bさんは8を書きます。後から書いたBさんの8が残り、Aさんの3個分の更新は消えます。このように、先に書いた更新が後の更新で上書きされて消えることを、ロストアップデートと呼びます。
起きる原因は、読んでから書くまでの間に、ほかの人が同じ行を書き換えられることです。MySQLの文書も、普通のSELECTで読んだ行は、ほかのトランザクションが更新できると説明しています。トランザクションの意味は、トランザクションとACID特性の記事で説明しています。
悲観ロックは、SELECT ... FOR UPDATEで読む時点から行をロックする
悲観ロックでは、読む時点で行をロックします。SQLでは、SELECT文の最後にFOR UPDATEを付けます。FOR UPDATEで読んだ行には専有ロックがかかり、コミットかロールバックで解放されるまで、ほかの人は書き換えられません。
BEGIN;
SELECT 在庫数 FROM 在庫
WHERE 商品番号 = 'A01'
FOR UPDATE;
-- 10が返り、行をロック
UPDATE 在庫 SET 在庫数 = 7
WHERE 商品番号 = 'A01';
COMMIT;
-- ロックを解放Bさんのプログラムも、同じようにFOR UPDATEを付けて読みます。Aさんがロックを持っている間、BさんのSELECTは待たされます。Aさんのコミットの後にBさんは7を読み、5を書くので、2人分の更新が両方残ります。
読む側にもFOR UPDATEが必要なのは、普通のSELECTは行のロックで止まらないからです。PostgreSQLの文書は、行のロックは問い合わせに影響せず、書き込みとロックだけを止めると説明しています。MySQLの文書も、読む時点でロックを取るにはFOR UPDATEかFOR SHAREを付けると説明しています。
悲観ロックの弱点は、待ちが生まれることです。ロックを持つ時間が長いほど、ほかの人は長く待たされます。待たせたくないときは、FOR UPDATEの後にNOWAITを付けます。すると、ロックを取れない行があった時点で、待たずにエラーになります。複数の行を逆の順でロックし合うと、デッドロックにもなります。
楽観ロックは、バージョン番号をUPDATEの条件にして衝突を見つける
楽観ロックでは、表にバージョン番号の列を1つ足します。行を更新するたびに、この番号を1つ増やします。読むときはロックを取らず、在庫数と一緒にバージョン番号も読んでおきます。
SELECT 在庫数, バージョン
FROM 在庫
WHERE 商品番号 = 'A01';
-- 10, 1 が返る
UPDATE 在庫
SET 在庫数 = 7,
バージョン = バージョン + 1
WHERE 商品番号 = 'A01'
AND バージョン = 1;
-- UPDATE 1書くときは、読んだときのバージョン番号をUPDATE文の条件に入れます。だれも更新していなければ1件が更新され、先にだれかが更新していれば0件になります。0件なら、読んだ後に衝突が起きたと分かります。更新した件数は、PostgreSQLでは実行結果の「UPDATE 1」のように返ります。
例では、Aさんの更新でバージョンは2になります。そのため、バージョン1を条件にしたBさんのUPDATEは0件です。Bさんのプログラムは、読み直してバージョン2を条件にやり直すか、利用者に「ほかの人が先に更新しました」と知らせます。
UPDATE 在庫
SET 在庫数 = 8,
バージョン = バージョン + 1
WHERE 商品番号 = 'A01'
AND バージョン = 1;
-- UPDATE 0(衝突)バージョン番号の代わりに、行を最後に更新した日時を比べる方法もあります。JavaのJakarta Persistenceも、バージョンを表す列に番号か日時を使えると定めています。
楽観ロックでも、UPDATE文で書き換えた行は、そのトランザクションが終わるまでロックされます。相手のUPDATEがまだコミットされていなければ、後のUPDATEはコミットかロールバックを待ちます。PostgreSQLの初期設定の分離レベルでは、相手がコミットすると、後のUPDATEは新しい行で条件を確かめ直し、0件になります。初期設定の分離レベルが製品ごとにどう違うかは、分離レベルの記事で比べています。
2人が同時に更新すると、悲観ロックは待たせ、楽観ロックは弾く
同じ在庫の例を、2つの方式で時間の順に並べます。違いが出るのは、Bさんが行を操作する時点です。
悲観ロックでは、BさんはAさんのコミットまで待ちます。楽観ロックでは、Bさんは待たずに読めますが、更新の時点で0件になり、読み直しが必要になります。楽観ロックでBさんがやり直せば、どちらの方式でも最後の在庫は正しく5になります。
次の図で方式を選ぶと、2人の操作を1段ずつ進めて確かめられます。ロック無しでは、最後にAさんの更新が消えます。
- Aさん在庫を
読む 在庫10と分かる - Bさん在庫を
読む 在庫10と分かる - Aさん10−3=7を
書く 在庫7になる - Bさん10−2=8を
書く Aさんの7を 上書きして 在庫8に なる
最後の
- AさんFOR UPDATEを
付けて 在庫10。読む 行を ロックする - BさんFOR UPDATEを
付けて Aさんが読もうと する ロック中なので 待たされる - Aさん7を
書いて 在庫7。コミットする ロックを 解放する - Bさん待ちが
解けて 在庫7と読む 分かる - Bさん7−2=5を
書いて 在庫5にコミットする なる
最後の
- Aさん在庫と
バージョンを 在庫10・バージョン1読む - Bさん在庫と
バージョンを 在庫10・バージョン1読む - Aさんバージョン1なら
7に 1件を更新する 更新。 バージョン2に なる - Bさんバージョン1なら
8に 0件。更新する 衝突と して 弾かれる - Bさん読み直す在庫7・バージョン2
- Bさんバージョン2なら
5に 1件を更新する 更新。 バージョン3に なる
最後の
使い分けは、更新が重なる頻度とやり直せるかで決める
同じ行の更新がよく重なるなら悲観ロック、めったに重ならないなら楽観ロックが向きます。Java EEの解説書も、多くのトランザクションが同じデータを頻繁に更新するときは悲観ロックがよいと書いています。逆に、あまり更新されないデータに悲観ロックを使うと、性能が落ちることがあるとしています。
| 観点 | 悲観ロック | 楽観ロック |
|---|---|---|
| 衝突を | 読む | 更新の |
| 相手が | 終わるまで | 後から |
| 向く | 同じ | 更新が |
| 欠点 | 待ちが | 衝突したらやり直す |
楽観ロックの弱点は、衝突が後で分かることです。衝突した側は、それまでの作業をやり直します。衝突が多いとやり直しが増えるので、やり直しの手順をプログラムに用意できるかも判断の材料になります。
Webアプリでは、画面を開いてから保存するまでに数分かかることがあります。その間ずっとロックを持つと、ほかの人を長く待たせます。Martin Fowlerは、複数の画面にまたがる処理ではまず楽観ロックを試すとしています。
この場合は、画面を開いたときに読んだバージョン番号を、保存のときのUPDATE文の条件に使います。保存が0件なら、画面を開いている間にほかの人が更新したと分かります。UPDATE文の書き方はINSERT・UPDATE・DELETE文の記事で説明しています。
参考にした資料
- PostgreSQL 16 日本語ドキュメント「13.3. 明示的ロック」(行レベルロックとFOR UPDATE、ロックの解放)
- PostgreSQL 18 Documentation「UPDATE」(更新件数を返す「UPDATE count」)
- PostgreSQL 18 Documentation「Transaction Isolation」(更新中の行を待った後、WHERE句を確かめ直す動き)
- PostgreSQL 18 Documentation「SELECT」(待たずにエラーにするNOWAIT)
- MySQL 8.4 Reference Manual「Locking Reads」(普通のSELECTで起きる問題とSELECT ... FOR UPDATE)
- Jakarta Persistence 3.2「Version」(バージョン番号か日時による衝突の検出)
- The Java EE 7 Tutorial「Overview of Entity Locking and Concurrency」(バージョン列、悲観ロックが向く場面)
- Martin Fowler「Optimistic Offline Lock」・「Pessimistic Offline Lock」(複数の画面にまたがる処理での使い分け)
練習問題(全4問)
正解:8
2人とも
正解:データを
悲観ロックは、
正解:読んだ後に、
読んだ
正解:悲観ロック。
更新が
選んでから「採点する」を押すと、解説つきで答え合わせができます。
関連記事
おすすめの本 PR
セキュリティとデータベースは、基本情報技術者試験の対策本でまとめて学べます。

