ロックの獲得段階と解放段階を分けて直列化可能性を保証する制御方式。
2相ロック(2フェーズロック)とは、ロックの獲得段階(成長期)と解放段階(縮退期)をきっちり2つに分け、いったん解放を始めたら新しいロックは取らないという制御方式です。保持ロック数のグラフは必ず山型(増えてから減る)になります。
身近な例で考えると、料理で必要な道具を全部そろえてから調理を始め、終わったらまとめて片付けるのに似ています。途中で道具を返してはまた借りる、を繰り返すと混乱しますが、「先に全部そろえる→使う→まとめて返す」と分けると整理されます。
上のツールで▶ボタンを押すと、T1がデータA・Bのロックを次々に獲得(成長期)し、頂点を過ぎたら解放だけを行う(縮退期)様子と、保持ロック数が山型になる様子を確認できます。
「2相(2つの段階)」とは、次の2つの時期のことです。
・成長期(lock phase):必要なロックを獲得していく時期。この間は解放してはいけない
・縮退期(unlock phase):取ったロックを解放していく時期。この間は新たに獲得してはいけない
最大のルールは、一度でもロックを解放したら(縮退期に入ったら)、二度と新しいロックを取ってはいけないという点です。これを守ると保持ロック数のグラフが必ず山型になり、途中でギザギザ(増えたり減ったりの繰り返し)にはなりません。グラフの折り返し地点が「頂点」です。
2相ロックを守ると、直列化可能性(serializability)が保証されます。直列化可能性とは、複数のトランザクションを並行で動かしても、結果が「1つずつ順番(直列)に実行したのと同じ正しさ」になる性質のことです。
なぜ保証できるのでしょうか。それは、「必要なロックを全部そろえてから(頂点)使い、まとめて手放す」ことで、あるトランザクションが使っている最中に別のトランザクションが割り込む隙がなくなるからです。割り込みがなければ、結果が入り乱れて矛盾することもありません。
ただし注意点もあります。2相ロックは正しさを保証しますが、デッドロック(互いに相手のロック解放を待ち続けて止まる状態)までは防げません。デッドロックは別の対策(検出・回避)が必要です。上のツールの最終ステップで、T1が完全に解放してからT2が動く流れを確認できます。
「なぜ2相に分けると直列化できるのか?」 — 結論から言うと、必要なロックを全部そろえてから(頂点)作業し、終わったらまとめて解放することで、他のトランザクションが割り込める隙を作らないからです。
2相をなしにすると次のような問題が起きます。
・T1がロックAを取得 → T1がAを解放 → T2がAを取得して値を書き換える → T1がBを取得
この場合、T1がAを使っている最中にT2が割り込んでAを書き換えてしまいます。T1は「書き換え前」の状態で話を進めているのに、実際のデータは変わっている — という矛盾が生まれます。
一方で2相ロックを守ると、T1が頂点(A・B両方確保)に達するまでAを手放しません。T2はAが欲しくても待つしかないため、割り込めません。割り込みがない = T1の処理が「1つずつ順番に実行」したのと同じ流れになるため、正しい結果が保証されます。
身近な例で考えると、試験の答案用紙を書き終わるまで手放さないのに似ています。採点者(他のトランザクション)が途中の答案をのぞいて採点し始めると、変更前・変更後のどちらの答えを採点したのか分からなくなります。提出(縮退期に入って全解放)してから採点させれば、途中介入はなくなります。
2相ロックが保証するのは「直列化可能性」だけです。デッドロックは防げません。
なぜデッドロックが起きるのかというと、2相ロックの「成長期」中はロックを追加取得し続けてよいルールになっているからです。T1がAを持ったままBを要求し、T2がBを持ったままAを要求すると、両者ともに成長期のまま互いの解放を待つ「循環待ち(=デッドロック)」が成立してしまいます。
2相ロックが守っているルールは「解放し始めたら新しいロックを取るな」です。でもこのルールは「取得の順番をどうするか」は何も決めていません。だから、複数のトランザクションが異なる順番でロックを取りに行くと、成長期の途中でぶつかってデッドロックになることがあります。
デッドロックへの対処は別に用意する必要があります。
・検出してロールバック(=待ちグラフに循環が生じたら一方を強制中断)
・ロック取得順序の統一(全員が「必ずA→Bの順」などと決めると循環ができない)
・タイムアウト(一定時間取れなければあきらめて中断)