MySqlのネクストキーロックについて
#mysql
MySQLのネクストキーロックについてなかなか覚えられないので、手元で試しながら頭の中を整理してみる。
手元のMySQLのバージョンは5.7。
テーブルに登録されているデータの状態
code: ddl.sql
CREATE TABLE test (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
val varchar(10) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
;
code: registered
mysql> INSERT INTO test VALUES (1, 'a'), (2, 'b'), (3, 'c'), (10, 'd'), (15, 'e');
Query OK, 5 rows affected (0.01 sec)
Records: 5 Duplicates: 0 Warnings: 0
mysql> select * from test;
+----+-----+
| id | val |
+----+-----+
| 1 | a |
| 2 | b |
| 3 | c |
| 10 | d |
| 15 | e |
+----+-----+
5 rows in set (0.00 sec)
各クライアントの状態
トランザクション分離レベルはREPEATABLE-READ
MySQLのトランザクション分離レベルについて
MySQLはREPEATABLE-READとかの分離レベルでロッキングリードした場合は、ネクストキーロックの仕組みによってファントムリードを防げるようになっている
https://dev.mysql.com/doc/refman/5.6/ja/innodb-next-key-locking.html
特定のインデックス間でネクストキーロックがかかるので、そのロックの範囲の要素に対してのinsert / deleteはロックがかかり待ち状態になる。よって先行してネクストキーロックをかけたトランザクション側からはトランザクションが終了するまで
ちなみにREPEATABLE-READでロッキングリードしない場合も、MySQLのMVCCによってファントムリードは防がれてる
Pattern1: あるインデックスの直前までロック
code: select(client1).sql
begin;
-- 今回はselect-for updateでやってるけど、delete, updateでも同じようにロックかかる
select * from test where id >= 3 and id < 10 for update;
+----+-----+
| id | val |
+----+-----+
| 3 | c |
+----+-----+
1 row in set (0.00 sec)
code: insert(client2).sql
begin;
insert test (id, val) values (3, 'xxx');
-- 返ってこない
SHOW ENGINE INNODB STATUS \G
...
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 59, OS thread handle 140442620094208, query id 943 localhost test update
insert test (id, val) values (3, 'xxx')
------- TRX HAS BEEN WAITING 44 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 80 index PRIMARY of table test.test trx id 2438 lock mode S locks rec but not gap waiting
insert test (id, val) values (4, 'xxx');
-- 返ってこない
SHOW ENGINE INNODB STATUS \G
...
MySQL thread id 59, OS thread handle 140442620094208, query id 748 localhost test update
insert test (id, val) values (4, 'xxx')
------- TRX HAS BEEN WAITING 3 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 72 index PRIMARY of table test.test trx id 2379
lock_mode X locks gap before rec insert intention waiting
insert test (id, val) values (10, 'xxx');
-- 返ってこない
SHOW ENGINE INNODB STATUS \G
...
MySQL thread id 59, OS thread handle 140442620094208, query id 750 localhost test update
insert test (id, val) values (10, 'xxx')
------- TRX HAS BEEN WAITING 2 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 72 index PRIMARY of table test.test trx id 2380 lock mode S locks rec but not gap waiting
mysql> insert test (id, val) values (11, 'xxx');
Query OK, 1 row affected (0.00 sec)
この場合、直感通りid: 3〜10の間でロックが掛かる。
table: ロック範囲のイメージ
登録されてるレコード 1 2 3 10 15
ロック範囲 □ → ← □
→の範囲はギャップロック、□はレコードロック
ネクストキーロックが以下の組み合わせで起こっていそう
ギャップロック
index(indexが張られていて登録されているカラム)の合間で張られるので、4~9の範囲で張られる
レコードロック
条件範囲内のレコードロック (3)
ギャップロックの次のindexのレコード (10)
Pattern2: あるインデックスまでロック
code: select(client1).sql
begin;
-- 今回はselect-for updateでやってるけど、delete, updateでも同じようにロックかかる
select * from test where id > 3 and id <= 10 for update;
+----+-----+
| id | val |
+----+-----+
| 10 | d |
+----+-----+
2 rows in set (0.00 sec)
code: insert(client2).sql
begin;
insert test (id, val) values (3, 'xxx');
ERROR 1062 (23000): Duplicate entry '3' for key 'PRIMARY'
insert test (id, val) values (4, 'xxx');
-- 返ってこない
SHOW ENGINE INNODB STATUS \G
...
LOCK WAIT 3 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 59, OS thread handle 140442620094208, query id 956 localhost test update
insert test (id, val) values (4, 'xxx')
------- TRX HAS BEEN WAITING 3 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 80 index PRIMARY of table test.test trx id 2438 lock_mode X locks gap before rec insert intention waiting
insert test (id, val) values (10, 'xxx');
-- 返ってこない
SHOW ENGINE INNODB STATUS \G
...
LOCK WAIT 3 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 59, OS thread handle 140442620094208, query id 960 localhost test update
insert test (id, val) values (10, 'xxx')
------- TRX HAS BEEN WAITING 2 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 80 index PRIMARY of table test.test trx id 2438 lock mode S locks rec but not gap waiting
insert test (id, val) values (11, 'xxx');
-- 返ってこない
SHOW ENGINE INNODB STATUS \G
...
LOCK WAIT 3 lock struct(s), heap size 1136, 5 row lock(s)
MySQL thread id 59, OS thread handle 140442620094208, query id 964 localhost test update
insert test (id, val) values (11, 'xxx')
------- TRX HAS BEEN WAITING 2 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 80 index PRIMARY of table test.test trx id 2438 lock_mode X locks gap before rec insert intention waiting
insert test (id, val) values (15, 'xxx');
-- 返ってこない
SHOW ENGINE INNODB STATUS \G
...
LOCK WAIT 3 lock struct(s), heap size 1136, 7 row lock(s)
MySQL thread id 59, OS thread handle 140442620094208, query id 970 localhost test update
insert test (id, val) values (15, 'xxx')
------- TRX HAS BEEN WAITING 2 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 50 page no 3 n bits 80 index PRIMARY of table test.test trx id 2438 lock mode S locks rec but not gap waiting
insert test (id, val) values (16, 'xxx');
Query OK, 1 row affected (0.00 sec)
この場合、直感的にはid: 4〜10の間でロックが掛かりそうだが、かなり広い範囲でロックがかかる (範囲の次のインデックスまでロックかかるのでこのような広さになるのだなぁ)。
table: ロック範囲のイメージ
登録されてるレコード 1 2 3 10 15
ロック範囲 → ← □ → ← □
→の範囲はギャップロック、□はレコードロック
ネクストキーロックが以下の組み合わせで起こっていそう
ギャップロック
index(indexが張られていて登録されているカラム)の合間で張られるので、4~9, 11~14の範囲で張られる
レコードロック
範囲内のindexレコード (10)
最後のindexの次のindex (15)
Pattern3: あるインデックスより大きい値をロック
こちらは後日mysql8系で操作してる
code: select client1.sql
begin;
select * from test where id > 15 for update;
この状態でパフォーマンススキーマのdata_locksでロック状態を確認する。
code: select performance_schama.data_locks.sql
mysql> select engine_transaction_id as tx_id, index_name, lock_mode, lock_data from performance_schema.data_locks where lock_type = 'RECORD' and object_name = 'test';
+--------+------------+-----------+------------------------+
| tx_id | index_name | lock_mode | lock_data |
+--------+------------+-----------+------------------------+
| 372426 | PRIMARY | X | supremum pseudo-record |
+--------+------------+-----------+------------------------+
1 row in set (0.00 sec)
supremum は ∞ というか上限の値を表現した行 と以下のブログに解説あるように、上限の行までのgapロックと、上限行のレコードロックのネクストキーロックがかかることが確認できる
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-2-lock-monitor/#:~:text=範囲に対する%20FOR%20UPDATE%20のロックを観察
ギャップロックと挿入インテンションロックの挙動について
ギャップロックの目的はドキュメントにもあるように、ギャップ範囲にinsertされないようにすること (ファントムリードを防ぐこと) である
https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#innodb-insert-intention-locks:~:text=つまり%E3%80%81その唯一の目的は%E3%80%81他のトランザクションがギャップに挿入されないようにすることです%E3%80%82
REPEATABLE READの分離レベルで使われる
そのためギャップロックがかぶっても特に競合は起きない
反面、ギャップロックの範囲にinsertが行われると、挿入インテンションロックと競合するようになっている
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-4-intention-lock/
挿入インテンションロックはinsert時に事前取得されるギャップロックの一種とのことで、これがギャップロックと範囲がかぶると競合して、insertが待たされる (これによってREPEATABLE-READでロッキングリードした場合にファントムリードが防がれている)。
https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#:~:text=挿入意図ロックは%E3%80%81行の挿入前に%20INSERT%20操作によって設定されるギャップロックのタイプです%E3%80%82%20このロックは%E3%80%81同じインデックスギャップに挿入する複数のトランザクションは%E3%80%81そのギャップ内の同じ場所に挿入しなければ相互に待機する必要がないように%E3%80%81意図的に挿入することを示しています%E3%80%82
SNOW INNODB ENGINE STATUSで見たときのネクストキーロックの表示のされ方
ロックモードが lock_mode X と表示される
単純な行ロックは lock_mode X、前方へギャップロックは lock_mode X locks gap before rec と表示される
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-2-lock-monitor/#:~:text=lock_mode%20X%20行ロックと前方ギャップロックが組み合わさったネクストキーロック
ネックストキーロックがかかっていた際は、ロックがかかったレコードの情報 (Record lockで説明される)のキーの値が supremum と表示されることがある (一番大きいIDより後ろに対してネクストキーロックがかかった場合とおもう)
superemumは上限値を表している特殊な行とのこと
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-2-lock-monitor/#:~:text=その場所ですが%E3%80%81キーが%20supremum%20という特殊な行になっています%E3%80%82
ネクストキーロックが上限でないIDにかかっていた場合は、通常通りRecord lockにレコードの値が返される
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-2-lock-monitor/#:~:text=そしてこれらタイプの行ロックがかかっているレコードの場所として%E3%80%81続けて小文字も含んだ%20Record%20lock%20で始まる行があり%E3%80%81下のフォーマットで説明されます%E3%80%82
こんな感じで
SELECT * FROM t2 WHERE k >= 1 AND k <= 10 FOR UPDATE;
code: innodb engine status
RECORD LOCKS space id 69 page no 4 n bits 72 index PRIMARY of table db.t2 trx id 361589 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 00000005846b; asc k;;
2: len 7; hex 81000000990110; asc ;;
3: len 4; hex 80000000; asc ;;
RECORD LOCKS space id 69 page no 4 n bits 72 index PRIMARY of table db.t2 trx id 361589 lock_mode X locks gap before rec
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000005846b; asc k;;
2: len 7; hex 8100000099011d; asc ;;
3: len 4; hex 80000000; asc ;;
key a(10)の部分はネクストキーロックかかっているが、Record lockにちゃんとレコードの情報が出ている
また共有ロックがロックとっているばあい、他のロックと衝突していない場合はengine innodb statusにも表示されないので注意が必要 (ネクストキーロックに限らずそうなる)
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-3-locking-read/#:~:text=共有ロックのロックモニター上の特別な取り扱い
参考
https://blog.kamipo.net/entry/2013/12/03/235900
トランザクションがブロックする、ブロックしないというのは、つまるところロックの範囲が被ったか被ってないかなので
んーなるほどな……
https://sh2.hatenablog.jp/entries/2009/01/12
https://qiita.com/kenjiszk/items/05f7f6e695b93570a9e1
https://softwarenote.info/p1067/
https://dev.mysql.com/doc/refman/8.0/ja/innodb-locks-set.html
各MySQLの操作でどのようなロックがかかるんだっけ?のはなし
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-2-lock-monitor/
https://www.infiniteloop.co.jp/tech-blog/2024/06/mysql-intro-locking-4-intention-lock/