「MySQL8.0 で起こる謎のデッドロックの条件を調べてみた」を data_locks・data_lock_waits テーブルで確かめてみた

これは **[インフラ勉強会 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/infra-workshop)** 17 日目の記事です。

※忘れていたので後から突っ込みました。

昨日は sahya さんだった…はずです（皆さん忙しそうですね…時期が時期だけに）。

---

インフラ勉強会の Advent Calendar には PostgreSQL の記事を 2 本入れたのですが、本来わたしは MySQL 側の人ですので、ここはひとつ MySQL ネタを突っ込んでおきます。

**[Qrunch](https://qrunch.io/)** に、

 * **[MySQL8.0 で起こる謎のデッドロックの条件を調べてみた](https://bluexxsun.qrunch.io/entries/XnT8c5yNL4qv8rS2/S4AUPwoqwJNZ2Qhb)（bluexxsun さん）**

という記事が掲載されており、面白そうだったので、

 * **[MySQL 5.7と8.0でロック状態を確認する（sys.innodb_lock_waitsビュー）](https://qiita.com/hmatsu47/items/607d176e885f098262e8)**

に書いた、

 * **`performance_schema.data_locks`**テーブル
 * **`performance_schema.data_lock_waits`**テーブル

を見ながら、**「再現１」**の動きを確かめてみます。

※残念ながら（？）`sys.innodb_lock_waits`ビューはあまり役に立ちませんでした。「他を待たせていないロック」を表示しないと意味がないので…。

---

**12/20 追記：**
MySQL のロックの種類について、軽く説明を書きました。

 * **[MySQL のロックについて補足（注：すでに語りつくされている内容です）](https://qiita.com/hmatsu47/items/f5eb64428494686d4ad3)**

---

# 実験

DB とテーブルと初期データを登録しますが、それと同時に、実験の流れが分かりやすいようにデッドロックを検出しない（タイムアウトまで待つ）`innodb_deadlock_detect=0`の設定にしておきます。

```sql:準備
mysql> SET GLOBAL innodb_deadlock_detect=0;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE DATABASE dl_test;
Query OK, 1 row affected (0.06 sec)

mysql> USE dl_test;
Database changed
mysql> CREATE TABLE users
    -> (
    ->   id    INTEGER PRIMARY KEY,
    ->   score INTEGER
    -> );
Query OK, 0 rows affected (0.07 sec)

mysql> INSERT INTO users
    -> (id, score) VALUES
    -> (10, 95),
    -> (15, 67),
    -> (20, 82),
    -> (25, 93),
    -> (30, 87);
Query OK, 5 rows affected (0.07 sec)
Records: 5  Duplicates: 0  Warnings: 0

mysql> CREATE INDEX index_users_on_score ON users(score);
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0
```

次に、トランザクション１で`SELECT ～ FOR UPDATE`します。

```sql:トランザクション１・SELECT_FOR_UPDATE
mysql> USE dl_test;
Database changed
mysql> SET autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM users WHERE id BETWEEN 18 AND 23 FOR UPDATE;
+----+-------+
| id | score |
+----+-------+
| 20 |    82 |
+----+-------+
1 row in set (0.00 sec)
```

ここで、前述の 2 つのテーブルでロックの状況を見てみましょう。

```sql:ロック確認・その１
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:1090
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140541731842104
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:4
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 20
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:5
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 25
3 rows in set (0.00 sec)

mysql> SELECT * FROM performance_schema.data_lock_waits\G
Empty set (0.00 sec)
```

`performance_schema.data_locks`に、

 * 1 行目：トランザクション１の`users`テーブルに対するインテンション排他ロック（テーブル排他ロック）※全体の流れの説明が難しくならないよう、このインテンション排他ロックについては特に触れません
 * 2 行目：同・`id=20`の行に対する排他ロック
 * 3 行目：同・`id=25`の行に対する排他ロック

が表示されました。

これは、

 * 実在するレコードのうち、`id=18`のすぐ下の行はロック対象外
 * 実在するレコードのうち、`id=18`のすぐ上の行はロック対象
 * 実在するレコードのうち、`id=23`のすぐ上の行はロック対象

という動作を示しています（ので、`id=15`の行はロック対象外です）。

ロックで待たせているものはないので`performance_schema.data_lock_waits`は空です。

※余談ですが、MySQL ではトランザクション分離レベルが`REPEATABLE READ`であっても、この`SELECT ～ FOR UPDATE`の`SELECT`処理自体は`READ COMMITTED`相当になります。

 * **[MySQLのデフォルトトランザクション分離レベルはREPEATABLE READだけど…](https://qiita.com/hmatsu47/items/22a0b2a4aece62afbb52)**

次に、トランザクション２で`SELECT ～ FOR UPDATE`します。

```sql:トランザクション２・SELECT_FOR_UPDATE
mysql> USE dl_test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> SET autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM users WHERE id BETWEEN 18 AND 23 FOR UPDATE;
```

ロック待ち状態になりました。

```sql:ロック確認・その２
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16938:1090
ENGINE_TRANSACTION_ID: 16938
            THREAD_ID: 49
             EVENT_ID: 23
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140541731848056
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16938:21:4:4
ENGINE_TRANSACTION_ID: 16938
            THREAD_ID: 49
             EVENT_ID: 23
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731845128
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: WAITING
            LOCK_DATA: 20
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:1090
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140541731842104
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 4. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:4
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 20
*************************** 5. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:5
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 25
5 rows in set (0.00 sec)

mysql> SELECT * FROM performance_schema.data_lock_waits\G
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 16938:21:4:4
REQUESTING_ENGINE_TRANSACTION_ID: 16938
            REQUESTING_THREAD_ID: 49
             REQUESTING_EVENT_ID: 23
REQUESTING_OBJECT_INSTANCE_BEGIN: 140541731845128
         BLOCKING_ENGINE_LOCK_ID: 16937:21:4:4
  BLOCKING_ENGINE_TRANSACTION_ID: 16937
              BLOCKING_THREAD_ID: 48
               BLOCKING_EVENT_ID: 37
  BLOCKING_OBJECT_INSTANCE_BEGIN: 140541731839064
1 row in set (0.00 sec)
```

`performance_schema.data_locks`に（`users`テーブルに対するインテンション排他ロックは省略します）、

 * 2 行目：トランザクション２の`id=20`の行に対する排他ロック（待ち）
 * 4 行目：トランザクション１の`id=20`の行に対する排他ロック
 * 5 行目：同・`id=25`の行に対する排他ロック

が表示されました。

また、`performance_schema.data_lock_waits`には、

 * トランザクション２の`id=20`の行に対する排他ロックがトランザクション１の同じ行に対する排他ロックに待たされている

ことを示す行が表示されました。

※`id=20`の行のロックで待たされているので、トランザクション２はまだ`id=25`の行ロックを取りに行く前の状態です。

ここで、トランザクション１で`id=18`の行を`INSERT`してみます。

```sql:トランザクション１・INSERT
mysql> INSERT INTO users VALUES (18, 75);
```

デッドロックで待ち状態になりました（最初の設定により、タイムアウトするまでエラーは出ないようになっています）。

```sql:ロック確認・その３
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16938:1090
ENGINE_TRANSACTION_ID: 16938
            THREAD_ID: 49
             EVENT_ID: 23
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140541731848056
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16938:21:4:4
ENGINE_TRANSACTION_ID: 16938
            THREAD_ID: 49
             EVENT_ID: 23
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731845128
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: WAITING
            LOCK_DATA: 20
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:1090
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140541731842104
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 4. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:4
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 20
*************************** 5. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:5
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 25
*************************** 6. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:4
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 38
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839408
            LOCK_TYPE: RECORD
            LOCK_MODE: X,GAP,INSERT_INTENTION
          LOCK_STATUS: WAITING
            LOCK_DATA: 20
6 rows in set (0.00 sec)

mysql> SELECT * FROM performance_schema.data_lock_waits\G
*************************** 1. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 16938:21:4:4
REQUESTING_ENGINE_TRANSACTION_ID: 16938
            REQUESTING_THREAD_ID: 49
             REQUESTING_EVENT_ID: 23
REQUESTING_OBJECT_INSTANCE_BEGIN: 140541731845128
         BLOCKING_ENGINE_LOCK_ID: 16937:21:4:4
  BLOCKING_ENGINE_TRANSACTION_ID: 16937
              BLOCKING_THREAD_ID: 48
               BLOCKING_EVENT_ID: 37
  BLOCKING_OBJECT_INSTANCE_BEGIN: 140541731839064
*************************** 2. row ***************************
                          ENGINE: INNODB
       REQUESTING_ENGINE_LOCK_ID: 16937:21:4:4
REQUESTING_ENGINE_TRANSACTION_ID: 16937
            REQUESTING_THREAD_ID: 48
             REQUESTING_EVENT_ID: 38
REQUESTING_OBJECT_INSTANCE_BEGIN: 140541731839408
         BLOCKING_ENGINE_LOCK_ID: 16938:21:4:4
  BLOCKING_ENGINE_TRANSACTION_ID: 16938
              BLOCKING_THREAD_ID: 49
               BLOCKING_EVENT_ID: 23
  BLOCKING_OBJECT_INSTANCE_BEGIN: 140541731845128
2 rows in set (0.00 sec)
```

`performance_schema.data_locks`の 6 行目に`INSERT`による`id=20`に対する排他・ギャップ・挿入インテンションロックが増えました（`id=18`のすぐ上に実在する行に対するロック）。

`performance_schema.data_lock_waits`の 2 行目に、**「`id=20`の行に対して、トランザクション１がトランザクション２に待たされている」**ことを示す情報が増えました。

デッドロックの完成です！

ここで、タイムアウトまで待ってから、ロックの状況を見てみます。

```sql:トランザクション２・タイムアウト
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
```

```sql:トランザクション１・タイムアウト直後
Query OK, 1 row affected (9 min 5.26 sec)
```

```sql:ロック確認・その４
mysql> SELECT * FROM performance_schema.data_locks\G
*************************** 1. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16938:1090
ENGINE_TRANSACTION_ID: 16938
            THREAD_ID: 49
             EVENT_ID: 23
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140541731848056
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 2. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:1090
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: NULL
OBJECT_INSTANCE_BEGIN: 140541731842104
            LOCK_TYPE: TABLE
            LOCK_MODE: IX
          LOCK_STATUS: GRANTED
            LOCK_DATA: NULL
*************************** 3. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:4
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 20
*************************** 4. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:5
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 37
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839064
            LOCK_TYPE: RECORD
            LOCK_MODE: X
          LOCK_STATUS: GRANTED
            LOCK_DATA: 25
*************************** 5. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:4
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 38
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839408
            LOCK_TYPE: RECORD
            LOCK_MODE: X,GAP,INSERT_INTENTION
          LOCK_STATUS: GRANTED
            LOCK_DATA: 20
*************************** 6. row ***************************
               ENGINE: INNODB
       ENGINE_LOCK_ID: 16937:21:4:7
ENGINE_TRANSACTION_ID: 16937
            THREAD_ID: 48
             EVENT_ID: 38
        OBJECT_SCHEMA: dl_test
          OBJECT_NAME: users
       PARTITION_NAME: NULL
    SUBPARTITION_NAME: NULL
           INDEX_NAME: PRIMARY
OBJECT_INSTANCE_BEGIN: 140541731839752
            LOCK_TYPE: RECORD
            LOCK_MODE: X,GAP
          LOCK_STATUS: GRANTED
            LOCK_DATA: 18
6 rows in set (0.00 sec)

mysql> SELECT * FROM performance_schema.data_lock_waits\G
Empty set (0.00 sec)
```

トランザクション１の`INSERT`が完了したため、`performance_schema.data_locks`の 6 行目は実際に挿入した`id=18`の行への排他・ギャップロックになっています。

※まだ`COMMIT`していない状態です。

まとめると、

 * `SELECT ～ FOR UPDATE`では、**実在する行に対して**排他ロックを取る
   * キーの範囲は「指定範囲の下限（この例では`id=18`）のすぐ上に実在する行以上、上限（同・`id=23`）のすぐ上に実在する行以下」
   * 但し、一番下の行（この例では`id=20`）がロック待ちになった場合はそれより上の行（同・`id=25`）のロック待ちにはならない（「ロック待ち」の待ち状態？）
 * `INSERT`では、**同一トランザクションで先に実行した`SELECT ～ FOR UPDATE`とは別に**、排他ロック（ギャップロック・挿入インテンションロック）を取ろうとする
   * キーの範囲は「挿入しようとする行（この例では`id=18`）のすぐ下に実在する行より大きく、すぐ上に実在する行以下」でギャップロックを取る
 * トランザクション１→トランザクション２→トランザクション１の順に同じ行に対して排他ロックを取ろうとすると、対象が 1 行であってもデッドロックする
   * 別々の 2 行に対してお互いに逆順でロックを取ろうとしなくてもデッドロックは成立する

ということになります。

そう考えると、最後にトランザクション１で`INSERT`する行が、

 * `id=14`：`INSERT`でロックを取るのが`id=15`の行になるのでデッドロックしない
 * `id=16・17・19`：`id=18`と同様ロックを取るのが`id=20`の行になるのでデッドロックする
 * `id=21`：ロックを取るのが`id=25`の行になるのでデッドロックしない（トランザクション２は`id=25`のロックを取ろうとする前で止まっているため）

となります。

※インデックスが降順の場合は~~もしかすると上下関係が逆かもしれません（未確認）~~上下関係が逆になります。

# というわけで、

`UPDATE`・`DELETE`でどうなるのかについては、皆さんで試してみてください。

bluexxsun さん、良いネタの提供ありがとうございました。

---

 * **[Qiitaに投稿したMySQL 8.0関連記事](https://hmatsu47.hatenablog.com/qiita_mysql80)**