インフィニットループ 技術ブログ

2024年06月20日 (木)

著者 : nob

MySQL/Aurora/TiDBロック入門 – 第4回 INSERTを止めるインテンションロック【解説動画付】

MySQL とその互換 DB のロックの挙動を紹介する入門シリーズ、第4回はギャップロックとインテンションロックによって INSERT をブロックする仕組みについて解説します。

第1回 トランザクション分離レベル で解説したように、MySQL の特徴でもある REPEATABLE READ によるファントムリードの防止に関わっているところですので、ぜひお手元でも観察してみてください!

第1回 トランザクション分離レベル
第2回 ロックモニターの読み方
第3回 ロック読取りも SELECT は止められない
第4回 INSERT を止めるインテンションロック
★ 第5回 WHERE 条件と違うレコードロック
★ 第6回 performance_schema でロックを測定
★ 第7回 ギャップロックがあっても更新される
★ 第8回 ネクストキーロックと降順インデックス
★ 第9回 共有ロックで発生しやすいデッドロック
★ 第10回 ロック読み取りが READ COMMITTED になる詳細
★ 第11回 AWS Aurora と MySQL の違い
★ 第12回 PingCAP TiDB と MySQL の違い

第4回の解説動画

第4回 INSERTを止めるインテンションロック

インテンションロックとは

MySQL (InnoDB) のロックはテーブルと行という2つの粒度があり、ロックの方式も普通のロックのほかインテンションロックという仕組みがあり使い分けています。

インテンションロックは行単位のロックの前にテーブル単位で先行して取得される形で使われています。実戦ではテーブル単位でのロックは使うことはないですが、 インテンションロックのもう一つの使われ方として、INSERT に先行して行単位で取得しています。

INSERT をブロックしている仕組みは MySQL InnoDB の特徴的なところになりますので、その挙動を詳しく見てみます。

テーブル単位のインテンションロック

行単位の INSERT における挙動の前に、テーブル単位でのインテンションロックの挙動について見てみます。

テーブル単位の排他ロック (X) や共有ロック (S)LOCK TABLES によって取得できます。また、行単位の FOR UPDATELOCK IN SHARE MODEXS のロックを取得しようとする場合も、先行してテーブル単位で排他や共有のインテンションロック、IXIS を取得します。

テーブルまたは行という同一レベルで、通常のロックとインテンションロックにおいて競合する (☓) しない (○) かの関係性は以下の表になります

XIXSIS
X
IX
S
IS
ロック・インテンションロック

特徴的なところとしてはインテンションの排他ロック IX は、インテンションロックとは衝突しない、という点になります。

INSERT 時のインテンションロック

今回の本題になりますが、INSERT 時に先行して取得されるインテンションロックについて見て行きます。

k = 1, 10, 100 の3行が入っている t2 テーブルを使います。

+-----+------+
| k   | v    |
+-----+------+
|   1 |    1 |
|  10 |   10 |
| 100 |  100 |
+-----+------+

トランザクションを開始し、select * from t2 where k > 1 and k < 10 for update で 1 < k < 10 のギャップにロックをかけます。

---TRANSACTION 6438, ACTIVE 71 sec
2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 8, OS thread handle 137085495346880, query id 46 localhost root starting
show engine innodb status
TABLE LOCK table `learning_mysql`.`t2` trx id 6438 lock mode IX
RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `learning_mysql`.`t2` trx id 6438 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 000000001918; asc       ;;
 2: len 7; hex 82000001180110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;

トランザクション 6438 で前方のギャップに排他ロック lock_mode X locks gap beofre rec がかけられていることがわかります。 この状態で別端末でトランザクションを開始し、insert into t2 values (5, 0); を実行します。 トランザクションはブロックされてハングアップ状態になりますが、その間にロックモニターを観察すると

RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `learning_mysql`.`t2` trx id 6439 lock_mode X locks gap before rec insert intention waiting
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 000000001918; asc       ;;
 2: len 7; hex 82000001180110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;

k = 5INSERT しようとしているトランザクションは、k = 10 の前方ギャップに対して挿入インテンションロック insert intention を取得しようとし、既にトランザクション 6438 で X が取得されており、X と IX は衝突するので待ち waiting になっていることがわかります。

挿入インテンション以外と衝突しないギャップロック

前述のトランザクション 6438 が 1 < k < 10 の排他ギャップロックをかけたまま、 別端末で同じ場所に対して排他のロック読み取りを実行するとどうなるでしょうか?

mysql> select * from t2 where k = 5 for update;
Empty set (0.00 sec)

ブロックされることはなくクエリは実行されました!ロックモニターで観察すると

---TRANSACTION 6440, ACTIVE 5 sec
2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 9, OS thread handle 137085494290112, query id 52 localhost root
TABLE LOCK table `learning_mysql`.`t2` trx id 6440 lock mode IX
RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `learning_mysql`.`t2` trx id 6440 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 000000001918; asc       ;;
 2: len 7; hex 82000001180110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;

---TRANSACTION 6438, ACTIVE 846 sec
2 lock struct(s), heap size 1128, 1 row lock(s)
MySQL thread id 8, OS thread handle 137085495346880, query id 53 localhost root starting
show engine innodb status
TABLE LOCK table `learning_mysql`.`t2` trx id 6438 lock mode IX
RECORD LOCKS space id 7 page no 4 n bits 72 index PRIMARY of table `learning_mysql`.`t2` trx id 6438 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 000000001918; asc       ;;
 2: len 7; hex 82000001180110; asc        ;;
 3: len 4; hex 8000000a; asc     ;;

6438 と 6440 という2つのトランザクションの両方で k = 10 の行の前方の排他ギャップロック lock_mode X locks gap before rec が存在していることがわかります。このようにギャップロックは異なるトランザクションでいくつでも取得出来るようになっています!

まとめ

ギャップロックはインテンションロックとのみ衝突し、 他のギャップロック要求とは作用しないため同じ場所のギャップにいくつでも取得可能になっています。FOR UPDATE をつけていても該当の値がなければそれはギャップへのロックとなり、 そのギャップロックは INSERT の阻止でしか作用しないため要注意です!

次回「第5回 実戦で役立つロック読取り」はロックをかける主な方法である ロック読取りREAD COMMITTED REPEATABLE READ で異なってくる細かい挙動について詳しく解説します!

実戦で役立つ知識ですのでご期待ください!

ブログ記事検索

このブログについて

このブログは、札幌市・仙台市の「株式会社インフィニットループ」が運営する技術ブログです。 お仕事で使えるITネタを社員たちが発信します!