zenet_logo

-株式会社ゼネット技術ブログ-

deadlock発生時の読み解き方(mysql5.7) その2 Innodbのロックモードについて

はじめに

システム事業部堤田です。

前回、業務で発生したdeadlockの原因について調査を行い、調べた内容についてブログにて整理を行いました。 今回は、前回の記事では記載しきれなかったインテンションロックについて記載をしていきます。 この記事にて、自身の理解を深めると供に同じように困っている人の一助になれば幸いです。

前回の記事:deadlock発生時の読み解き方(mysql5.7)

今回の記事で、実際発生したdeadlockのログについての解説も行う予定でしたが、その解説に必要なMySQLの用語の整理を含めると記載量が多くなりましたので分けることにしました。 deadloockログを読み解くために、MySQLのロックモードである、インテンションロック、レコードロック、ギャップロック、ネクストキーロック、インサートインテンションロックを説明します。

それぞれのロックモードの特徴を踏まえた上で、次の記事にてインサートインテンションロックが原因で発生したDeadlockについて実例を用いて解説していきます。

インテンションロックについて

公式リファレンスの説明はこちらより。 MySQL5.7の日本語はありませんが、MySQLの8.0だと日本語の説明もありますので、併せてご確認下さい。

インテンション(intention)には意図、目的等の意味があり、これからデータを挿入、更新、削除等をする前に行やテーブルに対してロックをかけようとする(実際にはロックはかけない)事をインテンションロックと呼んでいるそうです。

InnoDBには、2つのロックタイプがありそれぞれ、共有(S)ロック、排他(X)ロックと呼びます。それぞれの説明はこちらの通りです。

共有 (S) ロックでは、ロックを保持するトランザクションによる行の読み取りが許可されます。
排他 (X) ロックでは、ロックを保持するトランザクションによる行の更新または削除が許可されます。

行の読み取りの前にIS(インテンション共有)ロックを、更新・削除時には IX(インテンション排他) ロックをかけてその対象の行が利用できる状態であれば、実際にロックをかけて処理を始めます。 ただ、インテンションロックが競合した場合、そのまま処理が出来る場合と出来ない場合があります。その際のマトリックスが下記のようになります。

テーブルレベルのロックタイプの互換性

今回の事象の場合、2つのトランザクションでどちらもXロックを取得後、お互いのトランザクションでXロックの部分に対してIX(インテンション排他)ロックをかけようとした為、デッドロックとなってしまったという内容となります。

レコードロック (Record Locks)

公式リファレンスの説明はこちら

インデックスレコード上にセットするロックとなります。INSERT, UPDATE, DELETE句で発生するロックとなります。

レコードロックはインデックスレコードに対して常に行われるロックとなり、テーブルにindexを用意しなくともInnodbが用意しているhidden clustered indexを使ってロックがされるようになっています。hidden clustered indexについては詳しく調べていませんので、興味ある方はお調べ下さい。

INSERTでindexを設定しているカラムを追加する際や、updateやdeleteのWHERE句でカラムを指定してそれがindexの場合、そのレコードがロックされます。

ギャップロック (Gap Locks)

公式リファレンスの説明はこちら

A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record. インデックスレコードとインデックスレコードの間(のメモリ空間の)ロックや、最初のインデックスレコードより前、もしくは最後のインデックスレコードの後ろの空間というようにある空間に対してロックをかける事をギャップロックと呼びます。

例として、公式リファレンスに記載の内容を解説します。

  1. 事前準備として10, 20, 30の実際のindex値を持つ行を用意します。途中のindex空間は未だ値が入っていない空白の部分です。
  2. Transactionを開始して、SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE を実行すると、ここでギャップロックが発生します。
  3. 更に、別のTransactionでt.c1に対して、15の値をINSERTしようとすると、10から20までの範囲全てにロックがかかっている(ギャップロック)為、処理がストップされます。

ギャップロックの例

SELECT FOR UPDATEやUPDATE, DELETE句はギャップロックが発生する対象となります。また、ギャップロックは空振り(検索対象が0件だった場合)の場合でもギャップロックが発生しますので注意が必要です。 先ほどの例と同様場合、次の場合にギャップロックが発生します。

  1. SELECT FOR UPDATEを用いて、WHERE id = 5;にて、SQLを実行後、別のトランザクションにてidが9以下ののレコードに対してISNERT処理を行おうとした場合。
  2. SELECT FOR UPDATEを用いて、WHERE id = 15;にて、SQLを実行後、別のトランザクションにてidが11から19までのレコードに対してISNERT処理を行おうとした場合。
  3. SELECT FOR UPDATEを用いて、WHERE id = 35;にて、SQLを実行後、別のトランザクションにてidが31以上のレコードに対してISNERT処理を行おうとした場合。

ギャップロックはトランザクション分離レベルと関わりがあり、こちらを検討することで処理のパフォーマンスが改善したりできます。ただし、データの整合性が崩れる場合もあるので注意となっています。こちらはファジーリードやファントムリード等が関係しますが、今回の内容とは関わりが薄いので割愛します。

ネクストキーロック (Next Key Locks)

公式リファレンスの説明はこちら

ネクストキーロックは、レコードロックとギャップロックの組み合わせとあります。フーンなるほどと思いましたが、イマイチイメージをつかむのが難しかったです。 何故なら、ギャップロックでロックさせる際に、idが15に挿入する処理を行いましたが、当然レコードロックにあたるidが15の部分にもロックがかかっている為です。

何度もリファレンスを読み返しましたが、まるで分からず時間だけが過ぎる日々でしたが、ありがたいことにネット社会。私と同様に悩んだ方がいてその方もnext key lockについて検証を行い、推測ですが違いを解説されていました。その内容では、ギャップロックの場合は、そのギャップが広がる(狭まる場合もあるかも)可能性があり、ネクストキーロックはギャップを固定できるというものでした。ブログの内容はこちら。 ギャップロック、ネクストキーロックを起こして挙動を確認してみた 。 実例も踏まえて、非常に分かりやすい内容となっていますのでこちらもチェックして貰えますと幸いです。

こちらの記事で理解を深めた結果、ギャップロックとネクストキーロックでの特徴をつかむことが出来ました。

ギャップロックは、実データのインデックス値を参照してギャップの範囲が決まる特徴がある。

ネクストキーロックは、ギャップロック同様に実データのインデックス値を参照してギャップの範囲を決めますが、末尾のインデックスには改めてインデックスレコードを置いて、ギャップが広がらないようにする特徴がある。

インサートインテンションロック (Insert Intention Locks)

公式リファレンスの説明はこちら

An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such a way that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same position within the gap.

自分なりにこの分を訳すと次のようになります。

インサートインテンションロックは、ギャップロックの一種で、INSERT句にて、行の挿入を行う直前にsetされるロックです。 このロックは、複数のトランザクションが同じindexギャップ(お互いロック待ちの必要が無い)に対して、同じ位置のindex以外のindexにINSERT処理をしようとした際に発生します。

特に2行目の意味がよく分かりませんね。その後に続く解説も確認して自分なりに解釈しましたが、ギャップロックがかかっている範囲に対して、INSERT処理を行おうとするとインサートインテンションロックとなるという事だと判断しました。

例ですと、id = 4, id = 7のレコードがあるテーブルに対して、2つのトランザクションでそれぞれid = 5とid = 6にINSERTするのは、当然ロックはかかりませんが、id が 5から6の範囲でギャップロックをかけた後に別のトランザクションでid = 5 or 6にINSERTしようとするとインサートインテンションロックになります。という事を記載してありました。

ギャップロックがかかっている領域に対してINSERTで行を挿入しようとすると発生するロックという事です。

ロックモードについて調べてみて

デッドロックが発生する原因を調査して、それぞれの特徴を区別できるまでMySQLのリファレンスを読み漁りましたが、中々説明を読むだけでは腑に落ちる事が少なかったので苦労しました。

DBスペシャリストの勉強はしていたのですが、実践の知識はまだまだ足りないなと思い知りました。今回の調査で初めてインテンションロックという言葉も知りましたし。。まぁただ、それもそのはずで、インテンションロックはストレージエンジンであるInnodbで使われるロックの種類の一つであり、Innodbを使う上では知っておく必要がある知識ではありますが、データベース全般に関する知識ではなかったからです。当たり前ですが、DBスペシャリストはデータベースの知識としての共通項になっていて、実践となると実践用に特化した別の技術や知識が必要になるという事を改めて感じた一件となりました。

今回ストレージエンジンについても非常に勉強になりました。これに取り組む前は、indexが絡むことでデッドロックが発生するケースがある等想像も出来ていませんでした。調査や検証にかなり時間を費やしてしましましたので、願わくばより分かりやすいリファレンスが用意される事、特に例が充実される事を祈ります。

また、この記事で解説したロックモードは次回の記事で必要になるインサートインテンションロックまでです。リファレンスを見ると、他にもAUTO-INCロックや、空間インデックスの述語ロックもあります。今後、もしかしたら解説として付け加えるかもしれませんが、他にもロックモードがあるという事は知っておいてもらえればと思います。

次の記事は、実際にインサートインテンションロックによって、デッドロックが発生した事象についての具体的な説明をしますので、こちらの記事を見返しながら理解を深めていってもらえれば幸いです。

また、翻訳や解釈等記載内容にご指摘ある場合は随時受け付けておりますので、何かありましたら遠慮なくコメントして頂けると幸いです。