OceanBaseデータベースは、マルチバージョン2フェーズロックを使用して、その並行制御モデルの正確性を維持しています。ロックメカニズムは、データの正しい並行性と一貫性を保証するために重要な要素です。
OceanBaseデータベースのロックメカニズムは、データ行レベルでのロック粒度を採用しています。同一行内の異なる列間での変更は同一のロックによって排他されますが、異なる行間の変更はそれぞれ別のロックによって行われるため、互いに関係ありません。他のマルチバージョン2フェーズロックを採用するデータベースと同様に、OceanBaseデータベースでは読み取り操作においてロックがかけられないため、読み取りと書き込みの間で排他が発生せず、ユーザーの読み書きトランザクションの並行処理能力を向上させることができます。また、ロックの格納方式として、ロックを行単位(メモリおよびディスク上に格納される可能性あり)で管理することで、メモリ上で大量のロックデータ構造を維持する必要がなくなります。さらに、メモリ内ではロック間の待機関係を管理し、ロックが解放される際にそのロックを待機している他のトランザクションを再開します。
注意
SELECT ... FOR UPDATEでは、読み取りと書き込みの間で排他が発生しません。トランザクションのコミットプロセスにおいて、トランザクションの一貫性スナップショットを維持するため、一時的な読み書きの排他が発生します。これを「lock for read」と呼びます。
ロックメカニズムの使用
詳しく見る前に、まずOceanBaseデータベースの行ロック機能をどのように使用するかを見てみましょう。以下は、貨物情報を更新するための一般的な業務SQLです。
説明
以下のSQLは表示用であり、現時点では実行できません。
UPDATE GOODS
SET PRICE = ?, AMOUNT = ?
WHERE GOOD_ID = ?
AND LOCATION = ?;
上記のSQLでは、ユーザーが入力した貨物IDと住所に基づいて、対応する価格と在庫を更新します。トランザクション内の特定のSQL文では、トランザクション終了前に、対応する貨物IDと住所のデータ行に行ロックがかけられ、すべての同時実行される更新がブロックされて待機します。これにより、並行処理による変更によって引き起こされるダーティライト(Dirty Write)を防ぐことができます。このように、ユーザーがデータを更新する際に、変更されるデータ行に対して暗黙的にロックがかけられます。ユーザーはロック範囲などを明示的に指定する必要がなく、OceanBaseデータベース内部の仕組みに依存して並行制御を実現できます。
もちろん、ユーザーはロックメカニズムを明示的に指定することも可能です。以下は、貨物情報を排他的に取得するための一般的な業務SQLです。
説明
以下のSQLは表示用であり、現時点では実行できません。
SELECT PRICE, AMOUNT
FROM GOODS
WHERE GOOD_ID = ?
AND LOCATION = ?
FOR UPDATE;
上記のSQLでは、ユーザーが入力した貨物IDと住所に基づいて、対応する価格と在庫を取得します。トランザクション内の特定のSQL文では、トランザクション終了前に、対応する貨物IDと住所のデータ行に行ロックがかけられ、すべての同時実行される更新がブロックされて待機します。これにより、ユーザーが指定した明示的なロックが実現されます。異なる業務要件において、これは非常に重要なポイントです。
ロックメカニズムの粒度
OceanBaseデータベースは、テーブルロックと行ロックをサポートしており、行ロックは排他的です。
テーブルロックは主に複雑なDDL操作を実現するために使用され、操作中にデータベース全体のテーブルへの並行アクセスを阻止し、トランザクションの原子性と一貫性を保証します。テーブルロックの粒度は大きいため、行ロックよりも多くのオブジェクトをロックできますが、同時に他のすべてのアクセス試行もブロックされるため、並行性が低下します。このテーブルロックが解除されるまで、他のすべてのアクセス試行はブロックされます。テーブルロックは通常、ロックレベル(行ロックやページロックなど)を細分化できない場合、または次の操作がテーブル内の大部分のデータに影響を与えることが分かっている場合に使用されます。この種のロックは、テーブル構造の作成や変更など、データ定義言語(DDL)操作で一般的に使用されます。
トランザクション処理に関しては、同一行の異なる列を更新する際、OceanBaseが行ロックを使用するため、異なるトランザクション間で相互にブロックし合います。これは、行上のデータ構造に対するロックストレージコストを削減するためです。一方、異なる行のデータを更新する場合、トランザクション間で相互に影響を与えることはなく、トランザクションの並行実行が可能になります。
ロックメカニズムの排他性
OceanBaseデータベースはマルチバージョン2フェーズロックを採用しており、トランザクションによる変更は毎回元のデータを直接書き換えるのではなく、新しいバージョンを生成します。そのため、読み取り処理では一貫性スナップショットを利用して古いバージョンのデータを取得できるため、行ロックをかけなくても対応する並行制御機能を維持できます。これにより、実行中の読み取りと書き込みが互斥にならず、OceanBaseデータベースの並行処理性能が大幅に向上します。ただし、SELECT ... FOR UPDATE のような処理では依然として行ロックがかかり、変更や SELECT ... FOR UPDATE との間で排他性と待機が発生します。また、変更操作は行ロックを必要とするすべての操作と排他的になります。
ロックメカニズムのストレージ
OceanBaseデータベースでは、ロックは行単位で格納されるため、メモリ内で維持する必要があるロックデータ構造によるオーバーヘッドを削減できます。メモリ内では、トランザクションが行ロックを取得すると、対応する行にそのトランザクションのマーカー、すなわち行ロック保持者を設定します。トランザクションが行ロックを取得しようとする際、対応するトランザクションのマーカーを通じて自身が行ロック保持者でないことを認識し、取得を諦めて待機するか、または自身が行ロック保持者であることを確認した後に行の使用権限を得ます。トランザクションが行ロックを解放すると、すべてのトランザクションが関与する行から対応するトランザクションのマーカーが解除され、その後のトランザクションが再び取得を試みることが可能になります。
データがSSTableにダンプされると、マクロブロック内部のデータには対応するトランザクションのマーカーが記録されます。他のトランザクションは依然としてトランザクション識別子を通じて、対応するデータへのアクセスを許可するかどうかを判断する必要があります。メモリ内のロックメカニズムと異なり、SSTableは変更不可能な特性を持つため、トランザクションが行ロックを解放した後も、マクロブロック内部のデータ上のトランザクションのマーカーを即座に削除することはできません。もちろん、トランザクション識別子を通じて対応するトランザクション情報を確認し、トランザクションが既にロックを解除したかどうかを確認することは可能です。
ロックメカニズムの解放
ほとんどの2段階ロック実装と同様に、OceanBaseデータベースのロックはトランザクション終了時(コミットまたはロールバック)に解放され、データの不整合の影響を回避します。また、OceanBaseデータベースには他の解放タイミングも存在します。それはSAVEPOINTです。ユーザーがSAVEPOINTまでロールバックすることを選択した場合、トランザクション内部ではSAVEPOINT以降に関与するすべてのデータ行のロックを、OceanBaseデータベースのロックメカニズムの排他性 で説明されている仕組みに基づいて解放します。
ロックメカニズムのアイドル化解除
トランザクションをアイドル状態から復帰させるために、排他ロックが発生すると、メモリ内で行とトランザクションの待機関係が維持されます。図に示すように、行AはトランザクションBによって保持され、トランザクションCとトランザクションDによって待機されています。この待機関係を維持することで、行ロックが解放された際に対応するトランザクションCとDをアイドル状態から復帰させることが可能になります。トランザクションBが行Aを解放した後、順番に従ってトランザクションCをアイドル状態から復帰させ、その後トランザクションCに依存してトランザクションDもアイドル状態から復帰します。

また、行とトランザクションの待機関係に加えて、OceanBaseデータベースではトランザクション間の待機関係も維持される場合があります。メモリ使用量を削減するため、OceanBaseデータベース内部では行とトランザクションの待機関係をトランザクション間の待機関係に変換することがあります。図に示すように、行AはトランザクションBによって保持され、トランザクションCとトランザクションDによって待機されており、これがトランザクションBがトランザクションCとトランザクションDによって待機される形に変換されます。トランザクションBが終了した後、行間のロック待機関係が不明であるため、トランザクションCとトランザクションDが同時にアイドル状態から復帰します。

ロック機構によるデッドロック
ロック機構の実装により、デッドロックが発生する可能性があります。デッドロックとは、リソースに対する循環依存を指します。例えば、トランザクションAとトランザクションBが同時にリソースCとDを取得した場合、トランザクションAがリソースCを優先的に取得し、次にリソースDを取得しようとします。一方、トランザクションBはリソースDを優先的に取得し、次にリソースCを取得しようとします。このとき、どちらのトランザクションも既に取得したリソースを放棄する意思がない場合、どのトランザクションも正常に終了できなくなります。
タイムアウトに基づくデッドロックの解決
OceanBaseデータベースV3.2以前では、アクティブなデッドロック検出機能は含まれていませんでした。そのため、業務ロジック上のデッドロックを解決するためには、主にタイムアウトによるロールバックメカニズムに依存していました。
対応する問題を解決するために、3種類のタイムアウトメカニズムが用意されています:
ロックタイムアウトメカニズム:構成パラメータ名は
ob_trx_lock_timeoutで、デフォルトはステートメントのタイムアウト時間です。ロック待機がロックタイムアウト時間を超えると、対応するステートメントをロールバックし、ロックタイムアウトに対応するエラーコードを返します。この時点で、ある循環依存関係内のリソース依存が消滅しているため、デッドロックは存在しなくなります。例えば、トランザクションBがリソースCの取得に失敗してタイムアウトした場合、トランザクションBが終了すれば、トランザクションAは対応するリソースDを取得できるようになります。ステートメントタイムアウトメカニズム:構成パラメータ名は
ob_query_timeoutで、デフォルトは10sです。ロック待機がステートメントのタイムアウト時間を超えると、対応するステートメントをロールバックし、ステートメントタイムアウトに対応するエラーコードを返します。この時点で、ある循環依存関係内のリソース依存が消滅しているため、デッドロックは存在しなくなります。例えば、トランザクションBがリソースCの取得に失敗してタイムアウトした場合、トランザクションBが終了すれば、トランザクションAは対応するリソースDを取得できるようになります。トランザクションタイムアウトメカニズム:構成パラメータ名は
ob_trx_timeoutで、デフォルトは86400sです。ロック待機がトランザクションのタイムアウト時間を超えると、対応するトランザクションをロールバックし、ステートメントトランザクションに対応するエラーコードを返します。ある循環依存関係内のリソース依存が消滅しているため、デッドロックは存在しなくなります。例えば、トランザクションBがタイムアウトした場合、トランザクションBが終了することで、トランザクションAは対応するリソースDを取得できるようになります。
主動的なデッドロック検出
OceanBaseデータベースV3.2バージョン以降では、上記のタイムアウトに基づくデッドロック解決メカニズムに加えて、主動的なデッドロック検出メカニズムも実装されました。
現在、OceanBaseデータベースで実装されているデッドロック検出は、LCL(Lock Chain Length)デッドロック検出方式と呼ばれ、優先順位に基づくマルチアウトグレード分散型デッドロック検出方式です。OceanBaseデータベースのデッドロック検出アルゴリズムは、誤ってトランザクションを殺したり、複数のトランザクションを殺したりすることがないように保証します。
優先順位に基づくとは、相互にデッドロックを形成している複数のトランザクションの中で、LCLデッドロック検出方式は常に優先順位が最も低いトランザクションを殺してデッドロックを解除する傾向があります。現在、デッドロック検出におけるトランザクションの優先順位指標は主にトランザクションの開始時間であり、遅くに開始されたトランザクションほど優先順位が低くなります。
マルチアウトグレードとは、各トランザクションが同時に1つ以上の他のトランザクションを待機できることを指します。
分散型デッドロック検出とは、各トランザクションを表すデッドロック検出ノードが自身の依存情報のみを知っており、グローバルなロックマネージャーを必要とせずにノード間のデッドロックを検出できることを指します。
実装原理
分散トランザクションは実行効率を向上させるために、通常複数のパーティションのデータに同時にアクセスする必要があり、同時に複数のロック競合イベントが発生する可能性があります。この場合、デッドロック検出の効率を向上させるために、あるトランザクションが複数のトランザクションから一方向に依存する有向エッジを同時に待機している状況を記述することができ、これをマルチアウトデグリーと呼びます。
一般的なデッドロック検出方式では、パスプッシュアルゴリズム(path-pushing algorithm)が多く採用されていますが、このアルゴリズムはマルチアウトデグリーのシナリオでは、多殺や誤殺の問題が多く見られます。LCLデッドロック検出方式では、特別に設計されたエッジ追跡アルゴリズム(edge-chasing algorithm)を採用しています。LCLデッドロック検出方式では、各ノードが2つの状態を維持しており、それぞれ深さ値とトークン値と呼ばれています。多殺を防ぐために、1つのノードが保持するトークン値の数は1を超えてはならず、トークン値間で比較が可能であり、大きなトークン値が小さなトークン値を上書きすることで、ループ内で最大トークン値を持つノードのみがデッドロックを検出できるようにし、多殺の問題を回避します。
エッジ追跡アルゴリズムにおけるデッドロック検出の基本原理は、トークン値が自分自身から発行されて自分自身に戻ることができるというものですが、マルチアウトデグリーのシナリオで単一トークン値設計を採用した場合、デッドロックループ内で最大のトークン値がそのループ内のどのノードにも属していない可能性があります。この場合、デッドロックは検出されません。このシナリオを「環外汚染」と呼びます。そのため、LCLデッドロック検出方式では「パス深さ」という概念を導入し、各ノードがパス深さ値を維持しています。ループ内のノードのパス深さ値は時間の経過とともに無限に増加することができますが、ループ外でループ内のノードによって到達されないノードのパス深さ値には増加上限があります。ノードは「環外汚染」を避けるために、パス深さが少なくとも自分自身と同じ大きさのノードから送信されるトークンのみを受け取ることができるよう制約されています。また、アルゴリズムの初期段階で既に発生した「環外汚染」を排除するために、定期的にノード上の現在のトークン値をクリーンアップすることで、アルゴリズムがマルチアウトデグリー下で正しく動作することを保証します。
具体実装
トランザクションAが行ロックの追加に失敗した場合、トランザクションAは行ロックの解除を待機しながら、行ロックを保持するトランザクションBのIDを取得し、デッドロック検出ノードa(以下、Detector(a)と呼ぶ)を作成し、Detector(a)にトランザクションBのDetector(b)への一方向依存関係を記録します。
各ノードは以下の状態を維持する必要があります:
あるDetectorノードが作成されると、2つのトークン状態が生成されます。1つは公共トークン値(public label)、もう1つはプライベートトークン値(private label)です。それぞれのノードは、自分自身の優先順位に基づいてグローバル一意のトークンを生成します(優先順位が高いノードほど、トークン値は大きくなります)。そして、そのトークンを用いて2つのトークン値を初期化し、同時に0から始まる深さ値lclv(Lock Chain Length)も生成します。
各Detectorノードは依存リストを維持しており、そのリストには依存する他のノードのネットワーク位置情報が記録されています。
時間軸で分類すると、1.4秒ごとに1つのLCLサイクルが区切られます。
各サイクルの開始時に、各ノードは自分自身のpublic labelをprivate labelにリセットします。
LCLサイクルの最初の700msはLCLPサイクル(Lock Chain Length Proliferating)と呼ばれ、各ノードはこのサイクル内で定期的に自分自身の状態のlclv値を依存リスト内のすべての下流ノードに送信します。各ノードは、上流ノードから送信されてきたlclv値を受信すると、自分自身のlclv値をmax(lclv, received_lclv + 1)に更新します。
LCLサイクルの残りの700msはLCLSサイクル(Lock Chain Length Spreading)と呼ばれ、各ノードはこのサイクル内で定期的に自分自身の状態の{lclv, public label}を依存リスト内のすべての下流ノードに送信します。各ノードは、上流ノードから送信されてきた値を受信した後、自分自身のlclv値が現在のlclv値以上である場合、自分自身のpublic labelをmin(public label, received public label)に更新し、lclv値をreceived lclvに更新します。
デッドロックの検出:あるノードが受信したpublic labelが自分自身のprivate lavelである場合、デッドロックが検出されました。
ビュー
ビューCDB_OB_deadLOCK_EVENT_HISTORYは、これまでに発生したすべてのデッドロックイベントとこれらのイベントに参加したトランザクションを記録し、デッドロックイベント内で最終的にどのトランザクションがkillされたかを示しています。