🐬

MySQL8.0コードリーディング: ロックの表し方

に公開

コードリーディングの対象

現行のAurora MySQL LTSバージョンである3.04系と互換のあるMySQL8.0のコードを読んでいきます。

https://github.com/mysql/mysql-server/tree/8.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_GAPLOCK_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: 106k_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