インデックスレコード間にあるギャップのロック、または先頭のインデックスレコードの前や末尾のインデックスレコードのあとにあるギャップのロックです。1
ギャップロックは明示的に無効化できます。これは、トランザクション分離レベルを READ COMMITTED に変更するか、または innodb_locks_unsafe_for_binlog システム変数 (現在は非推奨です) を有効にすると発生します。2
通常はレコードロックで単一の行にのみロックがかかるが、範囲指定や存在しないキーに対してロックを取得しようとしたときに存在しないレコード(ギャップ)にもロックを取得します。
例えば、companiesテーブルにid(PK)が1~10のデータが存在しているとして、id=10に対してトランザクションを張ってデータを削除してみます。
-- sample sql BEGIN; DELETE FROM `companies` WHERE `companies`.`id` = 10;
そうすると、ロック状態は以下のようになります。
ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
INNODB | 139925786582232:1362:139925672503424 | 974612 | 48 | 119 | app_development | companies | NULL | NULL | NULL | 139925672503424 | TABLE | IX | GRANTED | NULL |
INNODB | 139925786582232:297:4:11:139925672500432 | 974612 | 48 | 119 | app_development | companies | NULL | NULL | PRIMARY | 139925672500432 | RECORD | X,REC_NOT_GAP | GRANTED | 10 |
着目するのは、2行目のLOCK_TYPE、LOCK_MODE、LOCK_DATA
となっており、レコードロックになっていることがわかります。 1行目はインテンションロックなのでここでは気にしない
次に存在しないレコードに対して削除処理を行ってみます。
-- sample sql BEGIN; DELETE FROM `companies` WHERE `companies`.`id` = 9999999;
そうすると、ロック状態は以下のようになります。
ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
INNODB | 139925786582232:1362:139925672503424 | 974618 | 48 | 129 | app_development | companies | NULL | NULL | NULL | 139925672503424 | TABLE | IX | GRANTED | NULL |
INNODB | 139925786582232:297:4:1:139925672500432 | 974618 | 48 | 129 | app_development | companies | NULL | NULL | PRIMARY | 139925672500432 | RECORD | X | GRANTED | supremum pseudo-record |
同じく2行目のLOCK_TYPE、LOCK_MODE、LOCK_DATAに着目する
となっておりギャップロックを確認できます。 次に実際に起きたギャップロックによる問題を紹介します。
アプリケーション側でDELETE-INSERTを実行していた時にデッドロックが頻発する問題が発生しました。 サンプルのSQLとしては下記のような処理になります。
-- sample sql BEGIN; DELETE FROM `companies` WHERE `companies`.`id` = 20; -- 存在しないデータの削除。 INSERT INTO companies (name, created_at, updated_at) values ('insert', SYSDATE(), SYSDATE());
これを複数のトランザクション(tx1、tx2)で順番に実行します。トランザクションの時系列としては下記のようになります。
上から順番にロックの状況を確認していきます。
ギャップロック取得
ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
INNODB | 139925786582232:1362:139925672503424 | 974621 | 48 | 173 | app_development | companies | NULL | NULL | NULL | 139925672503424 | TABLE | IX | GRANTED | NULL |
INNODB | 139925786582232:297:4:1:139925672500432 | 974621 | 48 | 173 | app_development | companies | NULL | NULL | PRIMARY | 139925672500432 | RECORD | X | GRANTED | supremum pseudo-record |
別のトランザクションでもギャップロック取得 ロック取得待ちにはならない
さまざまなトランザクションによってギャップ上に競合するロックを保持できることも、ここで注目するべき点です。たとえば、トランザクション A はギャップ上に共有ギャップロック (ギャップ S ロック) を保持できる一方で、トランザクション B は同じギャップ上に排他ギャップロック (ギャップ X ロック) を保持します。 競合するギャップロックが許可される理由は、レコードがインデックスからパージされる場合に、さまざまなトランザクションによってレコード上に保持されたギャップロックをマージする必要があるためです。 InnoDB のギャップロックは、「単に抑制的」です。つまり、ほかのトランザクションによるギャップへの挿入が停止されるだけです。したがって、ギャップ X ロックの効果はギャップ S ロックと同じです。3
ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
INNODB | 139925786583848:1362:139925672515504 | 974622 | 54 | 29 | app_development | companies | NULL | NULL | NULL | 139925672515504 | TABLE | IX | GRANTED | NULL |
INNODB | 139925786583848:297:4:1:139925672512592 | 974622 | 54 | 29 | app_development | companies | NULL | NULL | PRIMARY | 139925672512592 | RECORD | X | GRANTED | supremum pseudo-record |
INNODB | 139925786582232:1362:139925672503424 | 974621 | 48 | 173 | app_development | companies | NULL | NULL | NULL | 139925672503424 | TABLE | IX | GRANTED | NULL |
INNODB | 139925786582232:297:4:1:139925672500432 | 974621 | 48 | 173 | app_development | companies | NULL | NULL | PRIMARY | 139925672500432 | RECORD | X | GRANTED | supremum pseudo-record |
INSERT_INTENTIONのロックを取得 ここでtx2のロック解放待ちになる
この状況でtx2でINSERTを実行すると、tx1はtx2のギャップロックがあるので待ちになり、tx2もtx1のギャップロックがあり待ちになるのでDeadlockになる
ENGINE | ENGINE_LOCK_ID | ENGINE_TRANSACTION_ID | THREAD_ID | EVENT_ID | OBJECT_SCHEMA | OBJECT_NAME | PARTITION_NAME | SUBPARTITION_NAME | INDEX_NAME | OBJECT_INSTANCE_BEGIN | LOCK_TYPE | LOCK_MODE | LOCK_STATUS | LOCK_DATA |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
INNODB | 139925786583848:1362:139925672515504 | 974622 | 54 | 29 | app_development | companies | NULL | NULL | NULL | 139925672515504 | TABLE | IX | GRANTED | NULL |
INNODB | 139925786583848:297:4:1:139925672512592 | 974622 | 54 | 29 | app_development | companies | NULL | NULL | PRIMARY | 139925672512592 | RECORD | X | GRANTED | supremum pseudo-record |
INNODB | 139925786582232:1362:139925672503424 | 974621 | 48 | 173 | app_development | companies | NULL | NULL | NULL | 139925672503424 | TABLE | IX | GRANTED | NULL |
INNODB | 139925786582232:297:4:1:139925672500432 | 974621 | 48 | 173 | app_development | companies | NULL | NULL | PRIMARY | 139925672500432 | RECORD | X | GRANTED | supremum pseudo-record |
INNODB | 139925786582232:297:4:1:139925672500776 | 974621 | 48 | 178 | app_development | companies | NULL | NULL | PRIMARY | 139925672500776 | RECORD | X,INSERT_INTENTION | WAITING | supremum pseudo-record |