MySQL8.0コードリーディング: ロックの表し方
コードリーディングの対象
現行のAurora MySQL LTSバージョンである3.04系と互換のあるMySQL8.0のコードを読んでいきます。
今回読み進めるのはレコードロック周りです。どのようにロックが表現されるのかをコードから読み解いていきます。
関心があるのはストレージエンジン側の実装なので、storage/innobase/
を基本的には読んでいくことになります。今回はロックがどのように表されるのか定数を追っていくだけなので、以下のヘッダファイルを参照します。
ロックの状態
ロックはunsigned-int 32
(32bit固定長整数)で表現されている。
例えば、2851という数字で定義されたロックがあった場合、2進数に変換することでその意味を解釈することができる。ここでは勝手にブロックに分割して名前をつけてみる。
0b( 0000 | 0000 | 0000 | 0000 | 0000 | 1011 | 0010 | 0011 )
B7 B6 B5 B4 B3 B2 B1 B0
- B0(ロックタイプ): 0b0011 → 0d3 → LOCK_X
- B1(ロックモード): 0b 0010 | 0000 → 0d32 → LOCK_REC
- B2(ギャップモード): ここは4bitを分解して考える必要がある
0b 1000 | 0000 | 0000 → 0d2048
→ LOCK_INSERT_INTENTION
0b 0010 | 0000 | 0000 → 0d512
→ LOCK_GAP
0b 0001 | 0000 | 0000 → 0d256
→ LOCK_WAIT - B3(特殊属性): なし
- B4〜B7(予約/未使用): なし
となっており、ギャップに対するINSERT INTENTION LOCKを取得しようとして待機しているのが、このロックの状態だということがわかる。
定数の定義
勝手にまとめているので、実際のコードでの並び順や置き場所は結構バラバラになってて読みにくい。
lock0lock.h
lock0types.h
// マスク系
constexpr uint32_t LOCK_MODE_MASK = 0xF;
constexpr uint32_t LOCK_TYPE_MASK = 0xF0UL;
// B0: ロックモード
enum lock_mode {
LOCK_IS = 0, /* intention shared */
LOCK_IX, /* intention exclusive */
LOCK_S, /* shared */
LOCK_X, /* exclusive */
LOCK_AUTO_INC, /* locks the auto-inc counter of a table
in an exclusive mode */
LOCK_NONE, /* this is used elsewhere to note consistent read */
LOCK_NUM = LOCK_NONE, /* number of lock modes */
LOCK_NONE_UNSET = 255
};
// B1: ロックタイプ
constexpr uint32_t LOCK_TABLE = 16;
constexpr uint32_t LOCK_REC = 32;
// B2: ギャップモード
constexpr uint32_t LOCK_ORDINARY = 0;
constexpr uint32_t LOCK_WAIT = 256;
constexpr uint32_t LOCK_GAP = 512;
constexpr uint32_t LOCK_REC_NOT_GAP = 1024;
constexpr uint32_t LOCK_INSERT_INTENTION = 2048;
// B3: 特殊属性
constexpr uint32_t LOCK_PREDICATE = 8192;
constexpr uint32_t LOCK_PRDT_PAGE = 16384;
マスク
LOCK_MODE_MASK
type_modeフィールドからロックモードを抽出するために使用されるマスク。
/** mask used to extract mode from the type_mode field in a lock */
constexpr uint32_t LOCK_MODE_MASK = 0xF;
0xFを2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 1111 )
になる。
下位4bitからロックモードの情報を取得するために使う。
LOCK_TYPE_MASK
type_modeフィールドからロックタイプを抽出するために使用されるマスク。
/** mask used to extract lock type from the type_mode field in a lock */
constexpr uint32_t LOCK_TYPE_MASK = 0xF0UL;
2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 1111 | 0000 )
になる。
下位の5〜8bit目までの4bitからロックタイプを取得するために使用されるマスク。
ロックモード(B0)
enumなので、数字に直して書き下しておく。
LOCK_IS
これからトランザクションがこのテーブルのレコードに共有ロックを取りますよという意思表示
LOCK_IS = 0
0d0を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 )
になる。
LOCK_IX
これからトランザクションがこのテーブルのレコードに排他ロックを取りますよという意思表示
LOCK_IX = 1
0d1を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0001 )
になる。
LOCK_S
共有ロック
LOCK_S = 2
0d2を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0010 )
になる。
LOCK_X
排他ロック
LOCK_X = 3
0d3を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0011 )
になる。
LOCK_AUTO_INC
AUTO_INCREMENT カラムの連続性を担保するために取得されるロック
LOCK_AUTO_INC = 4
0d4を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0100 )
になる。
LOCK_NONE
ロックが不要な状態を示す(コンシステントリード)
LOCK_NONE = 5
0d5を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0101 )
になる。
LOCK_NUM
有効なロックモードの総数を表すために定義されていて、LOCK_NONE
が必ずenumの末尾にある前提でLOCK_NONE
を入れている
LOCK_NUM = LOCK_NONE
0d6を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0110 )
になる。
LOCK_NONE_UNSET
不定状態を表すために定義されていて、変数の初期化や一時的な状態管理用として使っている。明示的に不定状態を定義することで、変にプログラム上の変数が意味のある数値にならないようにしているっぽい。
LOCK_NONE_UNSET = 255
0d7を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0111 )
になる。
ロックタイプ(B1)
LOCK_TABLE
テーブルロックを表す。
/** table lock */
constexpr uint32_t LOCK_TABLE = 16;
0d16を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0001 | 0000 )
になる。
LOCK_REC
レコードロックを表す。
/** record lock */
constexpr uint32_t LOCK_REC = 32;
0d32を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0010 | 0000 )
になる。
ギャップモード(B2)
以下のフラグはおそらく排反の関係にあり、同時に有効にならないと考えて良さそう。
- LOCK_ORDINARY(0): next-key lock(レコード + 直前ギャップ)
- LOCK_GAP(512): ギャップのみでレコードは含まない
- LOCK_REC_NOT_GAP(1024): レコードのみでGAPは含まない
それ以外は組み合わせて足し算されて使われそう。
LOCK_ORDINARY
ネクストキーロックを表す。
ORDINARYとは?
MySQLのデフォルトのトランザクション分離レベルがREPEATABLE READ
なので、通常はネクストキーロック(レコード + ギャップ)になるよねという意味だと思われる。LOCK_GAP
とLOCK_REC_NOT_GAP
が排反になっており、ネクストキーロックのようなレコードとその前のギャップをセットでロックするという状態を別途用意する必要があったのだと思われる。おそらくデフォルトがREPEATABLE READ
になったタイミングで、既存のコードベースを壊さないようにこれが作られたのかも。※ LOCK_ORDINARY
は定義が0なのでわかりにくいがB2で使われる。
constexpr uint32_t LOCK_ORDINARY = 0;
0d0を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 | 0000 )
になる。
LOCK_WAIT
ロック取得のために待機している状態を表す。
constexpr uint32_t LOCK_WAIT = 256;
0d256を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0001 | 0000 | 0000 )
になる。
LOCK_GAP
ギャップロックを表す。
constexpr uint32_t LOCK_GAP = 512;
0d512を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0010 | 0000 | 0000 )
になる。
LOCK_REC_NOT_GAP
純粋なレコードロックを表す。
constexpr uint32_t LOCK_REC_NOT_GAP = 1024;
0d1024を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 0100 | 0000 | 0000 )
になる。
LOCK_INSERT_INTENTION
INSERT INTENTION LOCKを表す。INSERT時にのみ取得される。
constexpr uint32_t LOCK_INSERT_INTENTION = 2048;
0d2048を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0000 | 1000 | 0000 | 0000 )
になる。
特殊属性(B3)
述語ロックとは、「この条件に一致する行が将来現れた場合でもブロックするよ」というロック。
LOCK_PREDICATE
レコード単位の述語ロックを表す。インデックスレコードの単一エントリに紐づく述語ロックで、条件がそのレコードに該当することがわかっている場合に使われる。LOCK_RECと合わせて現れる。存在しないレコード条件でロックをかけた場合などに用いられる。
constexpr uint32_t LOCK_PREDICATE = 8192;
0d8192を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0010 | 0000 | 0000 | 0000 )
になる。
LOCK_PRDT_PAGE
ページ単位の述語ロックを表す。インデックスページ全体に対する述語ロックで、ページ内のどのキーが将来的に条件に一致するかわからない場合や、ページ範囲全体を条件付きでブロックする場合に使う。SELECT * FROM deals WHERE id > 10000000 FOR UPDATE
とか対象の範囲を全部ロックしてブロックするようなクエリがそれに当たる。
constexpr uint32_t LOCK_PRDT_PAGE = 16384;
0d16384を2進数に直すと、0b( 0000 | 0000 | 0000 | 0000 | 0100 | 0000 | 0000 | 0000 )
になる。
実際に動かして確認する
試しに、この記事の最初に紹介した2851で表現されるロックを実際に起こして、どのように表示されるか確認してみます。
-- 準備
mysql> create table t(id INT PRIMARY KEY, k INT NOT NULL, KEY k_idx(k)) ENGINE=InnoDB;
mysql> insert into t values (10, 10), (20, 20);
-- トランザクションA
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t where k between 15 and 25 FOR UPDATE;
+----+----+
| id | k |
+----+----+
| 20 | 20 |
+----+----+
1 row in set (0.00 sec)
-- トランザクションB
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values (15, 15);
-- 別セッションからロック状況を確認する
mysql> select ENGINE_LOCK_ID, THREAD_ID, OBJECT_SCHEMA, OBJECT_NAME, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_STATUS, LOCK_DATA FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'testdb' AND OBJECT_NAME= 't' ORDER BY ENGINE_LOCK_ID;
+--------------------------------------+-----------+---------------+-------------+------------+-----------+------------------------+-------------+------------------------+
| ENGINE_LOCK_ID | THREAD_ID | OBJECT_SCHEMA | OBJECT_NAME | INDEX_NAME | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
+--------------------------------------+-----------+---------------+-------------+------------+-----------+------------------------+-------------+------------------------+
| 70369520129936:1190:70369508165664 | 106 | testdb | t | NULL | TABLE | IX | GRANTED | NULL |
| 70369520129936:12:5:3:70369507862560 | 106 | testdb | t | k_idx | RECORD | X,GAP,INSERT_INTENTION | WAITING | 20, 20 |
| 70369520130792:1190:70369508166432 | 82 | testdb | t | NULL | TABLE | IX | GRANTED | NULL |
| 70369520130792:12:4:3:70369507865976 | 82 | testdb | t | PRIMARY | RECORD | X,REC_NOT_GAP | GRANTED | 20 |
| 70369520130792:12:5:1:70369507865632 | 82 | testdb | t | k_idx | RECORD | X | GRANTED | supremum pseudo-record |
| 70369520130792:12:5:3:70369507865632 | 82 | testdb | t | k_idx | RECORD | X | GRANTED | 20, 20 |
+--------------------------------------+-----------+---------------+-------------+------------+-----------+------------------------+-------------+------------------------+
6 rows in set (0.01 sec)
ここで、THREAD_ID: 106
がk_idx
の[20, 20]に対して、INSERT INTENTION LOCKを取得しようとして待機している状態であることがわかります。
- LOCK_TYPE: RECORD
- LOCK_MODE: X,GAP,INSERT_INTENTION
- LOCK_STATUS: WAITING
これは、コード上ではLOCK_REC | LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION | LOCK_WAIT
で計算されて一つのロックとして表現されているはずなので、32 + 3 + 512 + 2048 + 256 = 2851
となります。
次に読む場所
ロックの表し方はわかったので、今度は実際にロックを取得する部分を読む必要があります。具体的には、storage/innobase/lock/lock0lock.ccに定義されるlock_rec_lock
です。ここがレコードロック取得のためのエントリポイントとなっており、SQLスレッドがトランザクション内でステートメントを実行してレコードロックを取るタイミングで呼び出される関数です。
次回の記事ではlock_rec_lock
のコードリーディングを進めていきます。
Discussion