InnoDB ロック機構の内部構造を徹底解説:Gap Lock、Next-Key Lock、デッドロック検出まで

はじめに

この記事では、MySQL InnoDBのロック機構の内部実装を深掘りします。

MVCCの記事で「読み取りと書き込みは互いにブロックしない」と説明しました。しかし書き込み同士は競合します。また、SERIALIZABLE分離レベルやSELECT ... FOR UPDATEでは読み取りもロックを取ります。MySQL 8.0のソースコードを参照しながら、以下を解説します:

  • ロックの種類(共有/排他、テーブル/行)
  • Record Lock、Gap Lock、Next-Key Lock
  • Insert Intention Lock
  • デッドロック検出
  • ロックの確認方法

ロックの基本:共有と排他

ロック 略称 用途 競合
共有ロック(Shared) S 読み取り(SELECT ... FOR SHARE Xと競合
排他ロック(Exclusive) X 書き込み(INSERT/UPDATE/DELETE S, X両方と競合
-- 共有ロック
SELECT * FROM employees WHERE id = 1 FOR SHARE;

-- 排他ロック
SELECT * FROM employees WHERE id = 1 FOR UPDATE;

テーブルロックとインテンションロック

InnoDBは行レベルロックが基本ですが、テーブルレベルのインテンションロック(Intention Lock)も使います。

テーブルに対して:
  IS (Intention Shared)  — 「これから行のSロックを取る予定」
  IX (Intention Exclusive) — 「これから行のXロックを取る予定」

行に対して:
  S — 共有ロック
  X — 排他ロック

インテンションロックは、テーブルロック(LOCK TABLES)と行ロックの競合を効率的に検出するためのものです。行ロック同士は競合しません。

行ロックの3種類

InnoDBの行ロックはインデックスレコードに対してかかります。3種類あります。

Record Lock

インデックスレコードそのものをロックします。

-- id=1のレコードにRecord Lock (X)
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
Index: ... [id=1] ...
             ↑ Record Lock

Gap Lock

インデックスレコード間の隙間をロックします。他のトランザクションがその隙間にINSERTするのを防ぎます。

-- id=1とid=5の間にGap Lock
-- (id=3が存在しない場合)
SELECT * FROM employees WHERE id = 3 FOR UPDATE;
Index: ... [id=1]  (gap)  [id=5] ...
                ↑ Gap Lock
                (ここにINSERTできない)

Gap Lockの目的はファントムリードの防止です。REPEATABLE READ分離レベルで、同じ範囲クエリを2回実行したときに結果が変わらないことを保証します。

Next-Key Lock

Record Lock + Gap Lockの組み合わせです。レコード自体とその前の隙間をロックします。

Index: ... [id=1]  (gap)  [id=5] ...
                ↑─────────↑ Next-Key Lock
                (gap + id=5のレコード)

InnoDBのデフォルトのロック方式です。REPEATABLE READでの範囲検索は、スキャンした各レコードにNext-Key Lockをかけます。

-- id BETWEEN 3 AND 7 の範囲検索
SELECT * FROM employees WHERE id BETWEEN 3 AND 7 FOR UPDATE;

-- 実際のロック(id=1,5,10が存在する場合):
-- (1, 5] — Next-Key Lock on id=5
-- (5, 10] — Next-Key Lock on id=10

Insert Intention Lock

INSERTしようとするとき、Gap Lock内の異なる位置への挿入は互いにブロックしません。これを実現するのがInsert Intention Lockです。

Gap (1, 5) にGap Lockがかかっている状態:

Tx A: INSERT id=2 → Insert Intention Lock (2) → Gap Lockと競合 → 待機
Tx B: INSERT id=3 → Insert Intention Lock (3) → Gap Lockと競合 → 待機

ただし、Insert Intention Lock同士は競合しない:
Tx A: INSERT id=2 → OK
Tx B: INSERT id=3 → OK(Aとは異なる位置なので競合しない)

デッドロック検出

InnoDBはWait-for Graph(待機グラフ)でデッドロックを検出します。

Tx A: id=1のXロック保持 → id=2のXロック待ち
Tx B: id=2のXロック保持 → id=1のXロック待ち
→ 循環 → デッドロック検出 → コストの低いTxをロールバック
-- デッドロック情報の確認
SHOW ENGINE INNODB STATUS;
-- → LATEST DETECTED DEADLOCK セクション

-- デッドロック検出の無効化(タイムアウトに頼る場合)
SET GLOBAL innodb_deadlock_detect = OFF;
SET GLOBAL innodb_lock_wait_timeout = 50;  -- 秒

高負荷環境ではデッドロック検出自体がボトルネックになることがあります(Wait-for Graphの探索コスト)。その場合innodb_deadlock_detect = OFFにしてタイムアウトで対処する選択肢もあります。

ロックの確認方法

-- 現在のロック状況
SELECT * FROM performance_schema.data_locks;

ENGINE  LOCK_TYPE  LOCK_MODE  LOCK_DATA  LOCK_STATUS
InnoDB  RECORD     X          1          GRANTED
InnoDB  RECORD     X,GAP      5          WAITING

-- ロック待ちの状況
SELECT * FROM performance_schema.data_lock_waits;

まとめ

ロック種類 対象 目的
Record Lock レコード自体 同一レコードの同時更新防止
Gap Lock レコード間の隙間 ファントムリード防止
Next-Key Lock レコード + 前の隙間 デフォルト。Record + Gap
Insert Intention Lock 挿入位置 異なる位置への並列INSERT許可
Intention Lock (IS/IX) テーブル テーブルロックとの競合検出

次回はPostgreSQLのロック機構を同じ切り口で解説します。

参考文献

コメントする