2相ロックと直列化可能性
ロックを正しく取れば並行実行が「1件ずつ順番に流したのと同じ」結果になる――その理論的な裏付けを、2PL と先行グラフで腹落ちさせます。
- 1.2PL は各トランザクションのロックを「取得フェーズ」と「解放フェーズ」に分け、一度でも解放したら以後は取得しないルール。これだけで競合直列化可能性が保証される。
- 2.正しさの判定は先行グラフ(precedence graph)で行う。グラフに閉路がなければ直列化可能、閉路があれば不可能。
- 3.基本 2PL はカスケードアボートやデッドロックを残す。実務は Strict 2PL(書き込みロックをコミットまで保持)で回復可能性まで担保するのが定番。
直列化可能性とは「何と同じ結果か」
並行実行の正しさの基準は、直列スケジュール(serial schedule)と同じ結果になることです。直列スケジュールとは、トランザクションを途中で混ぜず、1件ずつ最後まで流したスケジュールを指します。直列なら相互干渉が原理的に起きないので「正しい」とみなせる――これを基準点にします。
並行スケジュールが、ある直列スケジュールと等価なら、それは直列化可能(serializable)です。注意したいのは「元の到着順と同じ」である必要はない点で、T1→T2 でも T2→T1 でも、どれか1つの直列順と等価でありさえすればよい、という緩い条件です。
実務で扱う等価性はほぼ常に競合等価(conflict equivalence)です。2つの操作が「同じデータ項目を対象とし、少なくとも一方が書き込み」のとき、その2操作は競合します。2つのスケジュールが競合等価とは、すべての競合ペアの相対順序が両者で一致することを指します。そして並行スケジュールがある直列スケジュールと競合等価なら、それを**競合直列化可能(conflict serializable)**と呼びます。読み×読みは競合しないので順序を入れ替えても結果は変わりません。
競合は順序を変えると結果が変わる組み合わせです。r1(X) と w2(X)(read-write)、w1(X) と r2(X)(write-read)、w1(X) と w2(X)(write-write)の3種。共通点は「異なるトランザクション・同一項目・一方以上が書き込み」です。同一トランザクション内の操作はそもそも順序固定なので競合の対象外です。
先行グラフ:閉路の有無で判定する
競合直列化可能かどうかは、**先行グラフ(precedence graph、別名 serializability graph)**で機械的に判定できます。作り方は単純です。
- トランザクションごとに頂点を1つ作る。
Tiの操作とTjの操作が競合し、スケジュール上でTiの操作が先なら、辺Ti → Tjを引く。
そして次の定理が成り立ちます。
スケジュールが競合直列化可能であることと、その先行グラフが閉路(cycle)を持たないことは同値です。閉路がなければ、グラフをトポロジカルソートした順序が、等価な直列スケジュールそのものになります。
なぜ閉路が「不可能」を意味するのか。辺 Ti → Tj は「等価な直列順では Ti が Tj より前でなければならない」という制約です。閉路 T1 → T2 → T1 があれば「T1 は T2 より前、かつ T2 は T1 より前」という矛盾した要求になり、それを満たす直列順は存在しません。だから閉路の有無がそのまま判定基準になります。
スケジュール S: r1(A) w2(A) w2(B) r1(B)
r1(A)→w2(A): A で read-write 競合、T1 が先 ⇒ 辺 T1→T2
w2(B)→r1(B): B で write-read 競合、T2 が先 ⇒ 辺 T2→T1
先行グラフ: T1 ⇄ T2 (閉路あり)⇒ 直列化不可能
上の S は、どちらの直列順(T1→T2 でも T2→T1)とも結果が一致しません。だから「速いが正しくない」スケジュールです。隔離レベルの議論(→ トランザクション分離レベル)で出てくる各種アノマリーは、この先行グラフに閉路を生む並びを許してしまうことに対応します。
2相ロック(2PL)が閉路を作らせない理由
先行グラフは「正しさの判定器」ですが、DB は実行前に閉路を防ぎたい。そこで使うのが2相ロックです。ルールは2つだけです。
- 取得(growing)フェーズ:必要なロックを取得していく。この間は解放しない。
- 解放(shrinking)フェーズ:いったんどれか1つでもロックを解放したら、以後は新たなロックを取得してはいけない。
つまり各トランザクションのロック保有数は「単調に増えてから単調に減る」山型になります。基本ロックは共有ロック(S、読み取り用)と排他ロック(X、書き込み用)で、S は互いに両立し、X は誰とも両立しません(→ ロックと MVCC)。
各トランザクションにはロックポイント――最後のロックを取得した瞬間(取得フェーズの終端)――が定義できます。Ti → Tj の競合辺が存在するなら、Ti は対象項目のロックを解放し、Tj がそれを取得しています。2PL では Ti は「すべてのロックを取り終えた後」にしか解放しないので、Ti のロックポイントは Tj のロックポイントより前になります。よって先行グラフの辺の向きはロックポイントの時間順と一致し、ロックポイントは実数の時刻なので閉路を作れません。これが2PL が直列化可能性を保証する理論的根拠です。
言い換えると、2PL は「ロックポイントの順序」という1本の直列順を暗黙に決めており、全トランザクションをその順に並べたものが等価な直列スケジュールになります。
T1: lock-X(A) ... lock-S(B) ... | unlock(A) ... unlock(B) ← | がロックポイント
T2: ......... lock-X(A) を待つ(T1 が unlock するまでブロック)
基本 2PL の弱点と Strict 2PL
2PL は直列化可能性を保証しますが、それだけでは実務に足りません。2つの問題が残ります。
| 問題 | 基本 2PL での状況 | Strict 2PL での扱い |
|---|---|---|
| カスケードアボート | 解放フェーズで早めに X ロックを手放すと、未コミットの書き込みを他者が読み、後で自分が abort すると連鎖的に巻き戻る | X ロックをコミット/アボートまで保持するので、未コミット値を他者が読めず連鎖が起きない |
| 回復可能性 | コミット順とデータ依存の順が食い違い、回復不能スケジュールが生じうる | 書き込みロックを最後まで握るため回復可能(recoverable)かつカスケードレス |
| デッドロック | 取得順次第で発生しうる | 同様に発生しうる(2PL では解消できない別問題) |
そこで実装の定番が **Strict 2PL(厳格2相ロック)**です。追加ルールはひとつ、「すべての排他(X)ロックは、コミットまたはアボートの瞬間まで保持する」。S ロックは早めに解放する変種が一般的です。さらに S ロックも含めて全ロックをコミットまで持つ方式を **Rigorous 2PL(強2相ロック)**と呼びます。
Strict 2PL の狙いは、直列化可能性に加えて「未コミットの書き込みを誰にも見せない」を徹底することです。これにより、(1) 確定前の値を読んでしまうダーティリードと、(2) abort が連鎖するカスケードアボートの両方を構造的に封じます。「コミットまで X ロックを離さない」という一手で、正しさと回復可能性を同時に満たせるため、商用 RDB のロック方式は実質これが基準です。
2相ロックは直列化可能性は保証しますが、デッドロックは別問題で残ります。T1 が A→B、T2 が B→A の順でロックを取ろうとすれば、互いの解放を待って膠着します。2PL 系はこれを検知(待ちグラフの閉路検出)して片方を犠牲者にアボートする、あるいはタイムアウトで打ち切る前提です。先行グラフ(正しさの判定)と、デッドロック検出に使う**待ちグラフ(wait-for graph)**は別物なので混同しないでください。
ロック方式と MVCC の関係
近年の RDB は読み取りを止めない MVCC を併用するため、「読み取りは S ロックを取らずスナップショットを見る」のが標準です(→ MVCC の内部実装)。この場合、純粋な2PL とは正しさの保証経路が変わります。スナップショット分離(SI)は直列化可能性より弱く、**書き込みスキュー(write skew)**という、先行グラフに閉路を作るアノマリーを許します。
そこで PostgreSQL の SERIALIZABLE などは、MVCC の上に SSI(Serializable Snapshot Isolation)を載せ、実行時に危険な競合パターン(先行グラフの閉路につながる依存)を検出して、片方を直列化失敗でアボートします。考え方は先行グラフそのもので、「閉路ができそうなら未然に1つ落とす」楽観的な手法です。基本 2PL がロックで事前に閉路を防ぐのに対し、SSI は事後検出で防ぐ、という対比で押さえると見通しが良くなります。
まとめ
- 正しさの基準は競合直列化可能性で、その判定は先行グラフの閉路の有無に帰着する。
- 2PL は取得→解放の山型ルールにより、ロックポイントの時間順という1本の直列順を作り、先行グラフに閉路を作らせない。これが直列化可能性の理論的根拠。
- 実務は Strict 2PL(X ロックをコミットまで保持)で、ダーティリードとカスケードアボートまで封じ、回復可能性を担保する。
- 2PL はデッドロックを防がないので、検出・アボートとリトライが前提。MVCC 系では SSI が先行グラフの閉路を事後検出して同じ正しさを実現する。分散環境のコミット調整は別レイヤの話(→ 2相コミット(2PC))。
データベース Article
2相ロックと直列化可能性を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データベース
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 4
導入後に効く点
正しさの判定は先行グラフ(precedence graph)で行う。グラフに閉路がなければ直列化可能、閉路があれば不可能。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 4
判断チェックリスト
- 自社の用途が「データベース / トランザクション」に近いか確認する。
- 強みである「2PL は各トランザクションのロックを「取得フェーズ」と「解放フェーズ」に分け、一度でも解放したら以後は取得しないルール。これだけで競合直列化可能性が保証される。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。