🔒

MySQLのdata_locksのLOCK_MODEに現れる値について

2023/08/19に公開

MySQLの8.0以降ではロックの挙動を調べる時にはperformance_schema.data_locksテーブルを利用することができます。

また、デットロックの調査ではperformance_schema.data_lock_waitsテーブルにこのdata_locksテーブルとinformation_schema.INNODB_TRXテーブルをjoinして加工したsys.innodb_lock_waitsビューにもお世話になると思います。

これらを利用する際に、data_locksではLOCK_MODEというカラム、sys.innodb_lock_waitsではwaiting_lock_mode/blocking_lock_modeというカラムが出てきますが、これらの値の意味が初見では若干わかりにくいかなと思ったのでそれについての簡単な説明と、それらの値はどのような仕組みで出力されるているのかを書いた記事です。

単なるXやSはレコードロックではなくネクストキーロック

まず適当にテーブルとデータを用意します。

create table t1 (
  id bigint,
  primary key id (id)
);

insert into t1 (id) values (1);
insert into t1 (id) values (3);
insert into t1 (id) values (5);
insert into t1 (id) values (7);
insert into t1 (id) values (9);
insert into t1 (id) values (11);
insert into t1 (id) values (13);

レコードロック、ギャップロック、ネクストキーロックを発生させるために、primary indexに対して範囲選択で排他ロックを取得します。

mysql> begin;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from t1 where id between 3 and 10 for update;
+----+
| id |
+----+
|  3 |
|  5 |
|  7 |
|  9 |
+----+
4 rows in set (0.01 sec)

この状態でdata_locksを見てみると以下のような情報が取得できます。

mysql> select OBJECT_NAME,INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA from performance_schema.data_locks;
+-------------+------------+-----------+---------------+-------------+-----------+
| OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE     | LOCK_STATUS | LOCK_DATA |
+-------------+------------+-----------+---------------+-------------+-----------+
| t1          | NULL       | TABLE     | IX            | GRANTED     | NULL      |
| t1          | PRIMARY    | RECORD    | X,REC_NOT_GAP | GRANTED     | 3         |
| t1          | PRIMARY    | RECORD    | X             | GRANTED     | 5         |
| t1          | PRIMARY    | RECORD    | X             | GRANTED     | 7         |
| t1          | PRIMARY    | RECORD    | X             | GRANTED     | 9         |
| t1          | PRIMARY    | RECORD    | X,GAP         | GRANTED     | 11        |
+-------------+------------+-----------+---------------+-------------+-----------+
6 rows in set (0.01 sec)

LOCK_TYPEがRECORDの行について、LOCK_MODEの値をみるとX,REC_NOT_GAPXX,GAPの3種類の値が表示されています。 これらは全てXなので排他ロックですが、単にXとなっている行はレコードロックではなくネクストキーロックです。そしてX,REC_NOT_GAPがレコードロックで、X,GAPがギャップロックです。上記のように並べてみるとわかりやすいですが、この部分を読み間違えるとなぜデットロックが発生したのか分からなかったりするので注意が必要です。

また、上記の例ではfor updateのため排他ロック(X)でしたが、for share等の場合は共有ロック(S)になります。この場合も同じようにSはネクストキーロックで、S,REC_NOT_GAPがレコードロック、S,GAPがギャップロックです。

なお、ネクストキーロックやギャップロックが何かについては良質な解説記事がたくさんあるためここで触れることはしませんが、MySQLのロックについてに添付されているスライドがオススメです。先のLOCK_MODEの表示には出てきていませんが、MySQLの行ロックには挿入インテンションロックというものもあり、そちらについても言及されています。このスライドが書かれた当時はロックを確認するにはshow engine innodb statusから拾って読んだりする必要があり、それなりに手間でしたが、いまはdata_locksやmetadata_locks等で簡単に見やすい形式で取得できるので、これらを使いながらスライドにある諸々の挙動を確かめるとわかりやすいと思います。

※章題をもう少し厳密に書くと「LOCK_TYPE: RECORDのロックについて、LOCK_MODEが単なるXやSであるものはレコードロックではなくネクストキーロック」です。これだと長すぎるので省略しています。

data_locksのLOCK_MODEをソースコードで追ってみる

次に、より理解を深めるために、data_locksテーブルのLOCK_MODEはMySQL(InnoDB)の内部でどのようにして出力されているのかを見ていきます。この過程で、内部ではロックの種類がどのように表現されているかについても知ることができます。

table_data_locksクラスからlock_get_mode_strまで

まず、performance_schema.data_locksテーブルの実態はtable_data_locksクラスです。

https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/perfschema/table_data_locks.h#L89-L137

このテーブルを参照する際に呼ばれるメソッドはtable_data_locks::read_row_valuesです。

https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/perfschema/table_data_locks.cc#L276-L371

このメソッドの中身をみていくと、LOCK_MODEはm_rowのm_lock_modeを見ていることがわかります。

https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/perfschema/table_data_locks.cc#L351-L353

m_rowはtable_data_locksクラスのプライベートメンバ変数で、型はrow_data_lockです。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/perfschema/table_data_locks.h#L124-L125
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/perfschema/pfs_data_lock.h#L61-L90

m_rowのm_lock_modeを書き込んでいる場所を探すと、PFS_data_lock_container::add_lock_row内にそれっぽい処理が見つかります。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/perfschema/pfs_data_lock.cc#L314

これを読んでいる場所を探すとInnodb_data_lock_iterator::scan_trxの中にそれっぽい箇所が見つかります。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/handler/p_s.cc#L793-L827

また、引数のlock_mode_strはlock_get_mode_strにトランザクションに紐づいているlockを渡して生成されていることがわかります。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/handler/p_s.cc#L741-L742
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/handler/p_s.cc#L770

lock_get_mode_strの定義を探すとlock0lock.ccに見つかります。そして、ここで実際に表示される文字列が生成されています。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/lock/lock0lock.cc#L5944-L5996

lock_tとtype_modeについて

lock_get_mode_strの処理を読む前に、準備としてlock_get_mode_strの引数の型であるlock_tについて少し見ていきます。まず、lock_tとは何かについてですが、これはMySQL(InnoDB)でのロックに関する情報を保持する構造体です。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/include/lock0priv.h#L134-L254

どんなロックなのかについての情報がtype_modeにエンコードされています。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/include/lock0priv.h#L167-L169

エンコードされている情報は、lock0types.hで定義されているlock_modeと、
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/include/lock0types.h#L50-L61

lock0lock.hで定義されている以下の定数です。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/include/lock0lock.h#L959-L1000

これらを見るとtype_modeは以下のようなレイアウトになっていることがわかります。

bit範囲 分類 説明
1~4bit Basic lock modes 排他ロックなのか共有ロックなのか等(lock_modeのどれかの値)
5~8bit Lock types ロック対象がレコードなのかテーブルなのか(LOCK_TABLEもしくはLOCK_REC)
9bit Waiting lock flag ロックを獲得済みかどうか
10bit~ Precise modes より詳細なロックモード

Basic Lock modes以外は単純な定数で定義されており、ビットフラグになっています。Lock typesはどちらかのフラグしか立ちませんが、Precise modesに関しては複数のフラグが立つことがあります(LOCK_GAPとLOCK_INSERT_INTENTION等)。

少し興味深いのは、Precise modesのLOCK_ORDINARYは0と定義されており、これはコメントにあるようにネクストキーロックであることを表すmodeです。つまり、Lock typesがLOCK_RECORDでPrecise modesのフラグが何も立っていないものがネクストキーロックです(判定ロジック)。

このことから、data_locksのLOCK_MODEではPrecise modesで定義されている各種フラグが立っている場合に表示が増え、単にXSとだけ表示されているものはそれらフラグが一切立ってないことを示しており、それはつまりネクストキーロックであるということがなんとなくわかります。

lock_get_mode_strでの文字列生成

lock_tとtype_modeについて説明したので、lock_get_mode_strの処理を見ていきます。

まずこの部分ですが、type_modeのBasic lock modes、Lock types、Precise modesのそれぞれについて、他のbit範囲を0にしたものをmode、type、flagsに入れています。
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/lock/lock0lock.cc#L5950-L5956

次に、以下の部分でmodeに入っているlock_mode(enum)の値をlock_mode_stringに渡して文字列に変換した後に、先頭のLOCK_を消して出力に渡しています。これは例えばX,REC_NOT_GAPXの部分です。

https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/lock/lock0lock.cc#L5964-L5975

https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/include/lock0types.h#L66-L85

最後に、以下の部分でflagsに入ってるPrecise modesの各種フラグをチェックして、フラグが立っていればフラグに対応する文字列をカンマ区切りで出力しています。これは例えば、X,GAP,INSERT_INTENTION,GAP,INSERT_INTENTIONの部分です。

https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/lock/lock0lock.cc#L5978-L5986
https://github.com/mysql/mysql-server/blob/mysql-8.1.0/storage/innobase/lock/lock0lock.cc#L86-L93

以上がdata_locksのLOCK_MODEに値が表示されるまでです。

Discussion