ITの教科書検索
入門読了 約9分

楽観ロックと悲観ロックの違い ― 使い分け

楽観ロックは更新時にバージョン番号で衝突を見つけ、悲観ロックは読む時点でロックして防ぎます。在庫の更新が消える例から、SQLでの書き方と使い分けまで分かります。

目次
  1. 楽観ロックと悲観ロックの違い
  2. ロストアップデートとは
  3. 悲観ロックの仕組み
  4. 楽観ロックとバージョン番号
  5. 2人が同時に更新したとき
  6. 使い分けと欠点
  7. 練習問題

楽観ロックと悲観ロックの違いは、衝突を先に防ぐか後で見つけるか

悲観ロックは、データを読む時点でロックを取り、ほかの人の更新を待たせて衝突を防ぐ方式です。楽観ロックは、ロックを取らずに読み、更新の時点でバージョン番号を比べて衝突を見つける方式です。在庫や座席のように、同じ行を大勢が同時に書き換えるシステムを作るとき、どちらで更新の衝突を防ぐかを選びます。選び方しだいで、待たされる時間とやり直しの回数が変わります。

名前は、衝突をどう見込むかから来ています。悲観ロックは衝突がよく起きると見込み、楽観ロックはめったに起きないと見込みます。排他制御とデッドロックの記事で説明した専有ロックの使い方は、悲観ロックに当たります。

ロストアップデートは、先に書いた更新が後の更新で消えること

どちらの方式も、同じ問題を防ぐためにあります。在庫が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さんが行を操作する時点です。

悲観ロックAさんロックして読む在庫107に更新在庫7コミットロック解放Bさんロックを要求待つロックして読む在庫75に更新コミット結果:在庫5楽観ロックAさん読む在庫10・v1v1なら更新1件・在庫7Bさん読む在庫10・v1v1なら更新0件=衝突読み直す在庫7・v2v2なら更新1件・在庫5結果:在庫5悲観ロックAさんロックして読む在庫107に更新在庫7コミットロック解放Bさんロックを要求待つロックして読む在庫75に更新コミット結果:在庫5楽観ロックAさん読む在庫10・v1v1なら更新1件・在庫7Bさん読む在庫10・v1v1なら更新0件=衝突読み直す在庫7・v2v2なら更新1件・在庫5結果:在庫5
上から下へ時間が進みます。v1・v2はバージョン番号です。悲観ロックではBさんが待たされ、楽観ロックではBさんの更新が衝突として弾かれます。

悲観ロックでは、BさんはAさんのコミットまで待ちます。楽観ロックでは、Bさんは待たずに読めますが、更新の時点で0件になり、読み直しが必要になります。楽観ロックでBさんがやり直せば、どちらの方式でも最後の在庫は正しく5になります。

次の図で方式を選ぶと、2人の操作を1段ずつ進めて確かめられます。ロック無しでは、最後にAさんの更新が消えます。

  1. Aさん在庫を読む在庫10と分かる
  2. Bさん在庫を読む在庫10と分かる
  3. Aさん10−3=7を書く在庫7になる
  4. Bさん10−2=8を書くAさんの7を上書きして在庫8になる

最後の在庫は8です。Aさんの3個分の更新が消えました。これがロストアップデートで、正しい在庫は5です。

使い分けは、更新が重なる頻度とやり直せるかで決める

同じ行の更新がよく重なるなら悲観ロック、めったに重ならないなら楽観ロックが向きます。Java EEの解説書も、多くのトランザクションが同じデータを頻繁に更新するときは悲観ロックがよいと書いています。逆に、あまり更新されないデータに悲観ロックを使うと、性能が落ちることがあるとしています。

悲観ロックと楽観ロックの比較
観点悲観ロック楽観ロック
衝突を扱う時点読む時点でロックを取って防ぐ更新の時点で見つける
相手が同じ行を使うと終わるまで待たされる後から更新した側が0件になる
向く場面同じ行の更新がよく重なる更新が重なることが少ない
欠点待ちが増える。デッドロックが起きうる衝突したらやり直す作りが必要

楽観ロックの弱点は、衝突が後で分かることです。衝突した側は、それまでの作業をやり直します。衝突が多いとやり直しが増えるので、やり直しの手順をプログラムに用意できるかも判断の材料になります。

Webアプリでは、画面を開いてから保存するまでに数分かかることがあります。その間ずっとロックを持つと、ほかの人を長く待たせます。Martin Fowlerは、複数の画面にまたがる処理ではまず楽観ロックを試すとしています。

この場合は、画面を開いたときに読んだバージョン番号を、保存のときのUPDATE文の条件に使います。保存が0件なら、画面を開いている間にほかの人が更新したと分かります。UPDATE文の書き方はINSERT・UPDATE・DELETE文の記事で説明しています。

参考にした資料

練習問題(全4問)

Q1在庫10の商品を、AさんとBさんがロックを取らずに同時に読み、Aさんは3個、Bさんは2個減らした値を書いた。Bさんが後から書くと、在庫はいくつになる?
Q2悲観ロックの説明として適切なものはどれ?
Q3楽観ロックで、UPDATE 在庫 SET 在庫数 = 8, バージョン = バージョン + 1 WHERE 商品番号 = 'A01' AND バージョン = 1 を実行すると、更新件数が0件だった。何が分かる?
Q4同じ商品の在庫を、多くの注文が同時に何度も更新する。悲観ロックと楽観ロックのどちらが向く?

選んでから「採点する」を押すと、解説つきで答え合わせができます。

関連記事

おすすめの本 PR

セキュリティとデータベースは、基本情報技術者試験の対策本でまとめて学べます。

目的別の本の選び方を見る

データベースの記事を続けて読む

データベース 全36本中30本目