ファントムリーディングとギャップロック(ギャップロック)

ファントムリーディング

ファントムリーディングとは

例えば:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

注:InnoDBのデフォルトのトランザクション分離レベルは反復可能読み取り分離レベルであるため、この記事で具体的に説明されていない部分は、反復可能読み取り分離レベルの下に設定されます。

    

Q3 id = 1の行を読み取る現象を「ファントム読み取り」と呼びます。言い換えると、ファントムリードとは、トランザクションが前後に2回同じ範囲をクエリする場合、後者のクエリは前のクエリでは表示されなかった行を表示することを指します。そして:

  • ファントムの読み取り値は、「現在の読み取り値」の下にのみ表示されます。
  • ファントム読み取り専用は、「新しく挿入された行」を指します。

これらの3つのクエリは更新のために追加されているため、現在すべて読み取られています。現在の読み取りルールは、送信されたすべてのレコードの最新の値を読み取ることができるようにすることです。さらに、セッションBとセッションCの2つのステートメントは実行後に送信されるため、Q2とQ3は、これら2つのトランザクションの運用上の影響を確認し、これがトランザクションの可視性ルールと矛盾しないことも確認する必要があります。

ファントムリーディングの何が問題になっていますか

1.セマンティックの問題:

セッションAはT1で宣言され、「d = 5ですべての行をロックしたいのですが、他のトランザクションで読み取りおよび書き込み操作を実行することはできません。」実際、このセマンティクスは破壊されています。

    

セッションBとセッションCの2番目のステートメントは、セッションAのQ1ステートメントのロックステートメントを破棄して、すべてのd = 5行をロックします。

2.データの整合性の問題:

    

updateのロックセマンティクスはselect…forupdateと同じであるため、この時点でこのupdateステートメントを追加するのが妥当です。セッションAは、データを更新するために「d = 5のステートメントにロックを追加する必要がある」と述べました。新しく追加された更新ステートメントは、ロックされていると見なされる行のd値を100に変更することです。

データベースの結果がどうなるか見てみましょう。

  1. T1の後、行id = 5は(5,5,100)になります。もちろん、この結果は最終的にT6で正式に送信されます。
  2. T2の後、行id = 0は(0,5,5)になります。
  3. T4時間の後、テーブルにもう1つの行があります(1,5,5)。
  4. 他の行はこの実行シーケンスとは関係がなく、変更されません。

このように見ると、データに問題はありませんが、現時点でbinlogの内容を見てみましょう。

  1. T2で、セッションBトランザクションがコミットされ、2つのステートメントが書き込まれます。
  2. T4で、セッションCトランザクションがコミットされ、2つのステートメントが書き込まれます。
  3. T6で、セッションAトランザクションがコミットされ、ステートメント更新t set d = 100(d = 5が書き込まれます)。

この一連の文は、スタンバイデータベースを実行する場合でも、binlogを使用して後でデータベースのクローンを作成する場合でも、これら3行の結果は(0,5,100)、(1,5,100)、および(5,5,100)になります。つまり、2つの行id = 0とid = 1にデータの不整合があります。この問題は非常に深刻で、機能しません。

ファントムリーディングを解決する方法:

ここで、ファントム読み取りの理由は、行ロックでのみ行をロックできるためですが、新しいレコードを挿入するアクションでは、レコード間の「ギャップ」を更新する必要があります。したがって、ファントム読み取りの問題を解決するために、InnoDBは新しいロック、つまりギャップロック(ギャップロック)を導入する必要がありました。

ギャップロック

ギャップロックとは

名前が示すように、ギャップロックは2つの値の間のギャップをロックします。

更新のためにd = 5であるselect * from tを実行すると、データベース内の6つの既存のレコードに行ロックが追加されるだけでなく、7つのギャップロックも追加されますこれにより、新しいレコードを挿入できなくなります。

私たちは皆知っています。行ロックは、読み取りロックと書き込みロック、読み取り/書き込み除外、書き込み/書き込み除外、およびロックとロック間の競合に分けられます。

ただし、ギャップロックは異なります。ギャップロックとの矛盾する関係は、「このギャップにレコードを挿入する」操作です。ギャップロック間に競合関係はありません。

    

 テーブルtにc = 7のレコードがないため、セッションBはブロックされませんセッションAはギャップロック(5,10)を追加します。また、セッションBは、このギャップに追加されたギャップロックでもあります。それらには同じ目標があります。つまり、このギャップを保護し、値を挿入できないようにすることです。ただし、それらの間に競合はありません。

ネクストキーロック

ギャップロックと行ロックを総称してネクストキーロックと呼び、各ネクストキーロックは開閉までの間隔です。

テーブルtが初期化された後、更新にselect * from tを使用してテーブル全体のすべてのレコードをロックすると、7つの次のキーロックが形成されます。これらは(-∞、0]、(0,5]、( 5,10]、(10,15]、(15,20]、(20、25]、(25、+上限]。

ギャップロックとネクストキーロックの導入は、ファントム読み取りの問題を解決するのに役立ちましたが、いくつかの「困難」ももたらしました。

ビジネスロジックは次のようになります。行を任意にロックし、行が存在しない場合は挿入し、行が存在する場合はデータを更新します。このロジックに同時実行性があると、デッドロックが発生します。

    

  • セッションAはselect…forupdateステートメントを実行します。行id = 9が存在しないため、ギャップロック(5,10)が追加されます。
  • セッションBはselect…forupdateステートメントを実行し、ギャップロック(5、10)も追加されます。ギャップロック間で競合が発生しないため、このステートメントを正常に実行できます。
  • セッションBは、セッションAのギャップロックによってブロックされた行(9,9,9)を挿入しようとし、待機する必要がありました。
  • セッションAは、セッションBのギャップロックによってブロックされた行(9,9,9)を挿入しようとしました。

もちろん、InnoDBのデッドロック検出により、このデッドロック関係のペアがすぐに検出され、セッションAの挿入ステートメントがエラーを報告して戻ります。ギャップロックの導入により、同じステートメントがより広い範囲をロックする可能性があり、これは実際には同時実行の程度に影響します。実際、これは単純な例です

注:記事の冒頭で、繰り返し可能な読み取りレベルを前提として、ギャップロックは繰り返し可能な読み取り分離レベルでのみ有効になることを強調しました。

分離レベルを読み取りコミット設定した場合、ギャップロックはありません。ただし、同時に、考えられるデータとログの不整合解決する場合は、binlog形式をrowに設定する必要があります読み取り送信の分離レベルが十分である場合、つまり、ビジネスで繰り返し可能な読み取り保証が必要ない場合、読み取り送信中の操作データのロック範囲が小さい(ギャップロックがない)ことを考慮すると、この選択は妥当です。

ロックルール

前提条件:MySQLの新しいバージョンではロック戦略が変更される可能性があるため、このルールは現在までの最新バージョン、つまり5.xシリーズ<= 5.7.24、8.0シリーズ<= 8.0.13に制限されています。

このロックルールには、2つの「原則」、2つの「最適化」、および1つの「バグ」が含まれています。

  • 原理1:ロックの基本単位は、フロントの開閉間隔であるネクストキーロックです。
  • 原則2:検索プロセス中にアクセスされるオブジェクトはロックされます。
  • 最適化1:インデックスに対する同等のクエリ。一意のインデックスがロックされると、次のキーのロックが行ロックに縮退します。
  • 最適化2:インデックスの等価クエリ。右に移動し、最後の値が等価条件を満たさない場合、次のキーのロックはギャップロックに縮退します。
  • バグ:一意のインデックスの範囲クエリは、条件を満たさない最初の値までアクセスします。

ケーススタディ

以下の場合は、上記の表とデータの例です。

1.1。

    

テーブルtにはid = 7のレコードがないため、上記のロックルールを使用して次のことを判断します。

  • 原理1によれば、ロックユニットはネクストキーロックであり、セッションAのロック範囲は(5,10)です。
  • 同時に、最適化2によれば、これは同等のクエリ(id = 7)であり、id = 10はクエリ条件を満たさず、次のキーのロックはギャップロックに縮退するため、最終的なロック範囲は次のようになります。 (5,10)。

2.2。 

    

セッションAは、インデックスcの行c = 5に読み取りロックを追加する必要があります。

  • 原理1によれば、ロックの単位はネクストキーロックであるため、ネクストキーロックが(0,5)に追加されます。
  • cは通常のインデックスであるため、レコードc = 5へのアクセスのみをすぐに停止することはできません。c= 10の場合は、右にトラバースしてあきらめる必要があります。原則2によれば、すべてのアクセスをロックする必要があるため、(5,10)に次のキーのロックを追加します。
  • しかし同時に、これは最適化2に準拠しています。同等の判断、右に移動すると、最後の値はc = 5の同等の条件を満たさないため、ギャップロック(5、10)に縮退します。
  • 原則2に従って、アクセスされたオブジェクトのみがロックされます。このクエリはカバーインデックスを使用し、主キーインデックスにアクセスする必要がないため、主キーインデックスにロックはありません。これがセッションBの更新ステートメントの理由です。実行することができます。

この例では、共有モードでのロックはカバーするインデックスのみをロックしますが、更新用であるかどうかは異なることに注意してください更新を実行すると、システムは次にデータを更新する必要があると判断するため、主キーインデックスの条件を満たす行に行ロックを追加します。さらに、この例は、ロックがインデックスに追加されることを示しています。同時に、共有モードでロックを使用して行に読み取りロックを追加し、データが更新されないようにする場合のガイダンスを提供します。カバーするインデックスの最適化をバイパスし、クエリフィールドのインデックスに存在しないフィールドを追加する必要があります。

3.3。 

      

左の図は、一意のインデックス範囲クエリです。

  • 実行の開始時に、id = 10の最初の行を見つける必要があるため、next-key lock(5,10]である必要があります。最適化1によると、主キーidの同等の条件が行に縮退します。 IDのみが追加されたロック= 10この行の行ロック。
  • 範囲検索は引き続き検索し、id = 15行を見つけて停止するため、next-key lock(10,15]を追加する必要があります。

右の図は、一意ではないインデックス範囲クエリです。

  • 次のキーロック(5,10)がセッションAのインデックスcに追加された後、インデックスcは一意ではないインデックスであるため、最適化ルールはありません。つまり、行ロックに縮退しません。セッションAによって追加された最後のロックはインデックスです。cの2つの次のキーロック(5,10]と(10,15)

4.4。 

    

セッションAは範囲クエリです。原則1に従って、次のキーのロック(10,15)のみをインデックスIDに追加する必要があり、idは一意のキーであるため、ループはid = 15に達すると停止する必要があります。 。ただし、実装に関しては、InnoDBは、条件を満たさない最初の動作、つまりid = 20まで前方にスキャンします。これは範囲スキャンであるため、インデックスの次のキーのロック(15,20)です。 idもロックされます。

5. 上記の例では、テーブルtに新しいレコードを挿入します。tvalues(30,10,30);に挿入します。

これで、テーブルにc = 10の2つの行がありますが、それらの主キー値idは異なり(それぞれ10と30)、c = 10の2つのレコード間にギャップがあります。今回は、deleteステートメントを使用して確認します。deleteステートメントのロックロジックは、実際にはselect ... for updateに似ていることに注意してください。これは、記事の冒頭で要約した2つの「原則」、2つの「最適化」、および1つの「バグ」です。

    

  • セッションAがトラバースしているときは、最初にc = 10の最初のレコードにアクセスします。同様に、原則1によれば、次のキーのロック(c = 5、id = 5)から(c = 10、id = 10)があります。
  • 次に、セッションAは、ライン(c = 15、id = 15)に到達するまで右を向いており、ループは終了しません。最適化2によると、これは同等のクエリであり、条件を満たさない行が右側にあるため、(c = 10、id = 10)から(c = 15、 id = 15)。

 この青い領域の左側と右側に点線があり、開いている間隔を示しています。つまり、2本の線(c = 5、id = 5)と(c = 15、id = 15)にロックがありません。 。

     

6. 上記のケース5に進みます

    

セッションAのdeleteステートメントに制限2が追加されます。テーブルtには実際にはc = 10のレコードが2つしかないため、制限2が追加されているかどうかに関係なく、削除の効果は同じですが、ロックの効果は異なります。ご覧のとおり、セッションBの挿入ステートメントが通過しました。したがって、行(c = 10、id = 30)に移動した後、条件を満たすステートメントがすでに2つあり、ループは終了します。

したがって、次の図に示すように、インデックスcのロック範囲は(c = 5、id = 5)から(c = 10、id = 30)になります。これは、オープンとクローズの間隔です。

    

7.デッドロックの例

    

  • セッションAがトランザクションを開始した後、クエリステートメントと共有モードでのロックを実行し、インデックスcにnext-key lock(5,10)とgap lock(10,15)を追加します。
  • セッションBの更新ステートメントでは、次のキーのlock(5,10]をインデックスcに追加して、ロック待機に入ります。
  • 次に、セッションAは、セッションBのギャップロックによってロックされている行(8,8,8)を再度挿入する必要があります。デッドロックのため、InnoDBはセッションBをロールバックさせます。

8.8。 

    

  • c descによる順序であるため、最初に配置される行はインデックスcの「右端」のc = 20行であり、ギャップロック(20,25)とネクストキーロック(15,20]が追加されます。
  • インデックスcを左にトラバースし、c = 10まで停止すると、次のキーのロックが(5,10)に追加されます。これが、セッションBの挿入ステートメントがブロックされる理由です。
  • スキャンプロセス中、3つの行c = 20、c = 15、およびc = 10にはすべて値があります。これはselect *であるため、3つの行ロックが主キーIDに追加されます。

したがって、セッションAのselectステートメントのロック範囲は、インデックスcでは(5、25)、主キーインデックスではid = 15および20です。

 

 

コンテンツソース:LinXiaobin「MySQLの実際の戦闘に関する45の講義」

 

 

おすすめ

転載: blog.csdn.net/qq_24436765/article/details/112783491