コンテンツにスキップ

【比較】楽観ロックと悲観ロックの違い

概要

同じデータを複数のトランザクション(処理)が同時に更新しようとしたとき、上書きで一方の変更が消える「更新の喪失(lost update)」をどう防ぐか。その戦略が「楽観ロック(optimistic locking)」と「悲観ロック(pessimistic locking)」です。悲観ロックは「衝突は起こるものだ」と悲観的に構え、読む時点で対象行にロックを取得して他者をブロックし、更新が終わるまで触らせません(SELECT ... FOR UPDATE が典型)。楽観ロックは「衝突はめったに起きない」と楽観的に構え、ロックを取らずに読み書きを進め、コミット時にバージョン番号やタイムスタンプを照合して、読んだ後に他者が変更していたら更新を弾きます(衝突検出 → 再試行)。

どちらが優れているという話ではなく、競合の頻度で選びます。Jakarta EE のチュートリアルも「悲観ロックは多数のトランザクションが頻繁にアクセス・更新する場合に向き、更新がまれな相手に悲観ロックをかけると性能が落ちる」と述べています。なお、楽観/悲観ロックはトランザクション分離レベル(READ COMMITTED / SERIALIZABLE など)と関連はあるが別概念です。分離レベルは「1 トランザクションが他のトランザクションの未確定変更をどこまで見えるか」を規定する仕組みで、楽観/悲観ロックは「アプリ/ORM が更新喪失をどう防ぐか」という戦略です。実際には組み合わせて使い、たとえば SERIALIZABLE 分離は内部的に楽観的に動作することがあります(後述)。

例えるなら:図書館の本の借り方

悲観ロックは「先に本を貸出手続きして自分の机に持っていく」やり方です。あなたが読んでいる間、その本は棚にないので誰も触れません(ブロック)。確実ですが、人気の本を長時間占有すると他の人が待たされ、互いに相手の本を待って動けなくなることもあります(デッドロック)。楽観ロックは「棚で立ち読みして、必要なら最後にコピーを取りに行く」やり方です。読んでいる間は誰でも本に触れますが、コピーを取る瞬間に「あなたが見た版」と「今の版」が違えば(誰かが書き込んでいたら)「最新版で取り直してください」と弾かれます(衝突検出 → 再試行)。混んでいなければ快適ですが、人気の本だと毎回やり直しになり、いつまでも取れない人が出ます(スターベーション)。比喩は戦略の直感には有効ですが、実際の照合粒度(行・バージョン列単位)やデッドロック検出の挙動は DBMS の実装に依存します。

詳細比較

項目 楽観ロック (Optimistic) 悲観ロック (Pessimistic)
基本の構え 衝突はまれと仮定。ロックを取らず進める 衝突は起こると仮定。先にロックを取る
ロック取得タイミング 取らない(コミット時に照合のみ) 読み取り時点で即取得し、トランザクション終了まで保持
衝突への対処 コミット/フラッシュ時にバージョン・タイムスタンプを照合し、不一致なら更新を拒否(例外)→ 呼び出し側が再試行 他トランザクションをブロックし、ロック解放まで待たせる(衝突自体を起こさせない)
代表的な実装 バージョン列/タイムスタンプ列の比較、CAS(compare-and-swap)、JPA の @Version/OptimisticLockException SELECT ... FOR UPDATE、行/テーブルロック、アドバイザリロック、JPA の PESSIMISTIC_WRITE
主なリスク 再試行の発生、競合が多いとリトライ多発・スターベーション、ユーザー操作のやり直し デッドロック、ロック待ちによるスループット低下・待ち時間増、ロック保持中のリソース占有
向いている状況 競合が低頻度、読み取り主体、長時間の「考える時間」を挟む画面更新 競合が高頻度、短時間で確実に更新したい、在庫引き当てなど取りこぼし厳禁の処理
スケーラビリティ 競合が低ければ高い(ロック待ちがない) ロック競合が増えると低下しやすい

PostgreSQL の公式ドキュメントは SELECT ... FOR UPDATE を「取得した行が更新対象であるかのようにロックし、現トランザクションが終わるまで他トランザクションによるロック・変更・削除を防ぐ」と説明しており、これが悲観ロックの中核です。同時に「明示ロックの使用はデッドロックの可能性を高める」とも明記しています。一方、Jakarta Persistence の楽観ロックはバージョン属性(@Version)を持つエンティティで、更新時にバージョンを照合し、読み取り後に他者が変更していれば OptimisticLockException を投げてトランザクションをロールバックする、と仕様で定義されています。

よくある誤解

  • 「楽観ロックも内部ではロックを取っている(名前にロックと付くから)」は誤り。 楽観ロックは通常、ロックを取得しません。「ロック」という語は「更新喪失を防ぐ仕組み」という広義の用法で、実体はバージョン照合による衝突検出です。Jakarta Persistence でも、楽観ロックはコミット/フラッシュ時にバージョンを照合し不一致なら OptimisticLockException を投げる仕組みで、行ロックの保持とは異なります(衝突を「防ぐ」のではなく「検出して弾く」)。

  • 「悲観ロックの方が安全だから常にこちらを使うべき」は誤り。 安全側に倒れるのは事実ですが、Jakarta EE チュートリアルが述べるとおり、更新がまれなデータに悲観ロックをかけると無用なロック待ちで性能が落ちます。さらに PostgreSQL 公式が警告するように明示ロックはデッドロックの危険を高めます。競合頻度を見ずに一律で悲観ロックを選ぶのは不適切で、低競合では楽観ロックの方が速くスケールします。

  • 「楽観ロック=衝突が起きない」は誤り。 楽観ロックは衝突を防ぎません。衝突は起こり得るのを前提に、コミット時に検出して更新を拒否し、呼び出し側が再試行する設計です。再試行ロジックや、ユーザーに「他の人が更新しました」と返す UX を用意しないと、衝突時に変更が失われたように見えます。

  • 「楽観/悲観ロックとトランザクション分離レベルは同じもの」は誤り。 関連はありますが別概念です。分離レベル(READ COMMITTED / SERIALIZABLE 等)は「他トランザクションの未確定変更がどこまで見えるか」を規定し、楽観/悲観ロックは「更新喪失をどう防ぐか」というアプリ/ORM の戦略です。実際 PostgreSQL では SERIALIZABLE/REPEATABLE READ 中に対象行が変化していると FOR UPDATE がエラーになる(楽観的な動作になる)など、両者は組み合わさって挙動します。分離レベルを上げれば楽観/悲観ロックの設計が不要になるわけではありません。

実務での選び分け

  • 競合がまれ・読み取り主体・画面で長く編集してから保存(人間の考える時間が入る) → 楽観ロック。バージョン列/@Version を持ち、衝突時に「他の人が更新しました。再読み込みしてください」と返す UX と再試行を必ず用意する。
  • 競合が頻繁・在庫引き当てや残高更新など取りこぼし厳禁・処理が短時間 → 悲観ロック(SELECT ... FOR UPDATE)。ロック保持時間を最短にし、デッドロック回避のため全アプリで同じ順序でロックを取得する(PostgreSQL 公式の推奨)。
  • デッドロックが頻発する → ロック取得順序の統一、ロック範囲の縮小、必要なら悲観→楽観への切り替えを検討。明示ロックはデッドロック確率を上げる前提で設計する。
  • 楽観ロックでリトライが多発・特定処理が永遠に成功しない(スターベーション) → その箇所だけ悲観ロックに切り替える、競合粒度を下げる(更新対象列を分割)、リトライにバックオフを入れる。
  • 「分離レベルを上げれば解決」と考える前に → 分離レベルは更新喪失対策の代替にならない場面がある。更新喪失を確実に防ぐにはバージョン照合(楽観)か行ロック(悲観)を明示的に設計する。

ひとことまとめ

悲観ロックは「衝突する前提でロックを取り他者をブロックして衝突を起こさせない(待たせる・デッドロックの危険)」、楽観ロックは「衝突はまれな前提でロックを取らず進め、コミット時にバージョン照合で衝突を検出して弾く(再試行・スターベーションの危険)」であり、競合の頻度で選びます。トランザクション分離レベルとは関連しつつも別概念です。

出典・参考

  • Controlling Concurrent Access to Entity Data with Locking — Jakarta EE Tutorial — 楽観ロックは「コミット前に他トランザクションがデータを変更/削除していないか確認」、悲観ロックは「完了まで長期ロックを取得し他者の変更/削除を防ぐ」、頻繁に競合する場合は悲観ロックが適し、更新がまれなデータに悲観ロックをかけると性能低下(Jakarta EE チュートリアルの記述に基づく)
  • OptimisticLockException — Jakarta Persistence API documentation — 楽観ロック衝突時に投げられ、現トランザクションはロールバック対象になる。バージョン属性で並行変更を制御
  • Version — Jakarta Persistence API documentation — @Version の許容型(int/Long/Timestamp/Instant 等)、エンティティ階層につき 1 つ、行更新時にバージョンをインクリメントして照合
  • PostgreSQL Documentation: 13.3 Explicit Locking — SELECT ... FOR UPDATE は行を更新対象としてロックし他トランザクションのロック/変更/削除をブロック、明示ロックはデッドロックの可能性を高める、回避策は全アプリで一貫した順序でロック取得、アドバイザリロックは悲観ロック戦略の模倣に使える