Log目次

【MySQL】delete、updateの空振りのギャップロックに注意

作成日 2022-11-28更新日 2022-11-28

ギャップロックとは

インデックスレコード間にあるギャップのロック、または先頭のインデックスレコードの前や末尾のインデックスレコードのあとにあるギャップのロックです。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;

そうすると、ロック状態は以下のようになります。

ENGINEENGINE_LOCK_IDENGINE_TRANSACTION_IDTHREAD_IDEVENT_IDOBJECT_SCHEMAOBJECT_NAMEPARTITION_NAMESUBPARTITION_NAMEINDEX_NAMEOBJECT_INSTANCE_BEGINLOCK_TYPELOCK_MODELOCK_STATUSLOCK_DATA
INNODB139925786582232:1362:13992567250342497461248119app_developmentcompaniesNULLNULLNULL139925672503424TABLEIXGRANTEDNULL
INNODB139925786582232:297:4:11:13992567250043297461248119app_developmentcompaniesNULLNULLPRIMARY139925672500432RECORDX,REC_NOT_GAPGRANTED10

着目するのは、2行目のLOCK_TYPE、LOCK_MODE、LOCK_DATA

となっており、レコードロックになっていることがわかります。 1行目はインテンションロックなのでここでは気にしない

次に存在しないレコードに対して削除処理を行ってみます。

-- sample sql BEGIN; DELETE FROM `companies` WHERE `companies`.`id` = 9999999;

そうすると、ロック状態は以下のようになります。

ENGINEENGINE_LOCK_IDENGINE_TRANSACTION_IDTHREAD_IDEVENT_IDOBJECT_SCHEMAOBJECT_NAMEPARTITION_NAMESUBPARTITION_NAMEINDEX_NAMEOBJECT_INSTANCE_BEGINLOCK_TYPELOCK_MODELOCK_STATUSLOCK_DATA
INNODB139925786582232:1362:13992567250342497461848129app_developmentcompaniesNULLNULLNULL139925672503424TABLEIXGRANTEDNULL
INNODB139925786582232:297:4:1:13992567250043297461848129app_developmentcompaniesNULLNULLPRIMARY139925672500432RECORDXGRANTEDsupremum pseudo-record

同じく2行目のLOCK_TYPE、LOCK_MODE、LOCK_DATAに着目する

となっておりギャップロックを確認できます。 次に実際に起きたギャップロックによる問題を紹介します。

DELETE-INSERTでデッドロック

アプリケーション側で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)で順番に実行します。トランザクションの時系列としては下記のようになります。

  1. tx1: トランザクション開始。DELETE実行
  2. tx2: トランザクション開始。DELETE実行
  3. tx1: INSERT実行
  4. tx2: INSERT実行=>Deadlock発生!

上から順番にロックの状況を確認していきます。

1の時のロック状況

ギャップロック取得

ENGINEENGINE_LOCK_IDENGINE_TRANSACTION_IDTHREAD_IDEVENT_IDOBJECT_SCHEMAOBJECT_NAMEPARTITION_NAMESUBPARTITION_NAMEINDEX_NAMEOBJECT_INSTANCE_BEGINLOCK_TYPELOCK_MODELOCK_STATUSLOCK_DATA
INNODB139925786582232:1362:13992567250342497462148173app_developmentcompaniesNULLNULLNULL139925672503424TABLEIXGRANTEDNULL
INNODB139925786582232:297:4:1:13992567250043297462148173app_developmentcompaniesNULLNULLPRIMARY139925672500432RECORDXGRANTEDsupremum pseudo-record

2の時のロック状況

別のトランザクションでもギャップロック取得 ロック取得待ちにはならない

さまざまなトランザクションによってギャップ上に競合するロックを保持できることも、ここで注目するべき点です。たとえば、トランザクション A はギャップ上に共有ギャップロック (ギャップ S ロック) を保持できる一方で、トランザクション B は同じギャップ上に排他ギャップロック (ギャップ X ロック) を保持します。 競合するギャップロックが許可される理由は、レコードがインデックスからパージされる場合に、さまざまなトランザクションによってレコード上に保持されたギャップロックをマージする必要があるためです。 InnoDB のギャップロックは、「単に抑制的」です。つまり、ほかのトランザクションによるギャップへの挿入が停止されるだけです。したがって、ギャップ X ロックの効果はギャップ S ロックと同じです。3

ENGINEENGINE_LOCK_IDENGINE_TRANSACTION_IDTHREAD_IDEVENT_IDOBJECT_SCHEMAOBJECT_NAMEPARTITION_NAMESUBPARTITION_NAMEINDEX_NAMEOBJECT_INSTANCE_BEGINLOCK_TYPELOCK_MODELOCK_STATUSLOCK_DATA
INNODB139925786583848:1362:1399256725155049746225429app_developmentcompaniesNULLNULLNULL139925672515504TABLEIXGRANTEDNULL
INNODB139925786583848:297:4:1:1399256725125929746225429app_developmentcompaniesNULLNULLPRIMARY139925672512592RECORDXGRANTEDsupremum pseudo-record
INNODB139925786582232:1362:13992567250342497462148173app_developmentcompaniesNULLNULLNULL139925672503424TABLEIXGRANTEDNULL
INNODB139925786582232:297:4:1:13992567250043297462148173app_developmentcompaniesNULLNULLPRIMARY139925672500432RECORDXGRANTEDsupremum pseudo-record

3の時のロックの状況

INSERT_INTENTIONのロックを取得 ここでtx2のロック解放待ちになる

この状況でtx2でINSERTを実行すると、tx1はtx2のギャップロックがあるので待ちになり、tx2もtx1のギャップロックがあり待ちになるのでDeadlockになる

ENGINEENGINE_LOCK_IDENGINE_TRANSACTION_IDTHREAD_IDEVENT_IDOBJECT_SCHEMAOBJECT_NAMEPARTITION_NAMESUBPARTITION_NAMEINDEX_NAMEOBJECT_INSTANCE_BEGINLOCK_TYPELOCK_MODELOCK_STATUSLOCK_DATA
INNODB139925786583848:1362:1399256725155049746225429app_developmentcompaniesNULLNULLNULL139925672515504TABLEIXGRANTEDNULL
INNODB139925786583848:297:4:1:1399256725125929746225429app_developmentcompaniesNULLNULLPRIMARY139925672512592RECORDXGRANTEDsupremum pseudo-record
INNODB139925786582232:1362:13992567250342497462148173app_developmentcompaniesNULLNULLNULL139925672503424TABLEIXGRANTEDNULL
INNODB139925786582232:297:4:1:13992567250043297462148173app_developmentcompaniesNULLNULLPRIMARY139925672500432RECORDXGRANTEDsupremum pseudo-record
INNODB139925786582232:297:4:1:13992567250077697462148178app_developmentcompaniesNULLNULLPRIMARY139925672500776RECORDX,INSERT_INTENTIONWAITINGsupremum pseudo-record

まとめ

Footnotes

  1. ギャップロックの概要

  2. ギャップロックの無効化

  3. ギャップロックのロックについて