いろは2986023 views
高校化学2913383 views
教育148875 views
世界の国560595 views
高校日本史189857 views
中学社会667106 views
MathPython491378 views
高校国語785655 views
LaTeX957300 views
高校物理158224 views
Help
Tools

English

MySQL のロックとデッドロック対策

複数のトランザクションが同時にデータへアクセスする環境では、データの整合性を保つためにロックの仕組みが欠かせない。MySQL の InnoDB ストレージエンジンは行レベルロックを採用しており、高い並行性を実現しているが、ロックの仕組みを正しく理解していないとパフォーマンスの低下やデッドロックの原因となる。

InnoDB のロックの種類

InnoDB が提供するロックにはいくつかの種類があり、それぞれ異なる目的で使い分けられている。

共有ロック(S ロック)

行の読み取り時に取得されるロック。他のトランザクションからの共有ロックは許可されるが、排他ロックはブロックされる。SELECT ... FOR SHARE で明示的に取得できる。

排他ロック(X ロック)

行の更新・削除時に取得されるロック。他のトランザクションからの共有ロック・排他ロックの両方をブロックする。SELECT ... FOR UPDATE で明示的に取得可能。

インテンションロック

テーブルレベルで設定されるロックで、行ロックの取得意図を表明するために使われる。IS(intention shared)と IX(intention exclusive)の 2 種類があり、InnoDB が内部的に管理している。

通常の SELECT 文は一貫性読み取り(consistent read)として動作し、ロックを取得しない。これが InnoDB の高い並行性を支える基盤になっている。

レコードロックとギャップロック

InnoDB の行ロックは、実際にはインデックスレコードに対して設定される。ここで重要なのが、ロックの対象範囲に応じた分類だ。

レコードロック

インデックス上の特定のレコードそのものをロックする。WHERE id = 10 のように一意に特定できる条件で排他ロックを取得した場合、そのレコードだけがロック対象になる。

ギャップロック

インデックス上のレコード間の「隙間」をロックする。範囲検索を行った際に、その範囲内に新しい行が挿入されるのを防ぐために使われる。ファントムリードの防止に寄与している。

さらにネクストキーロックという仕組みがあり、これはレコードロックとギャップロックを組み合わせたものだ。InnoDB のデフォルトのトランザクション分離レベル(REPEATABLE READ)では、範囲検索時にネクストキーロックが使われる。

-- レコードロックの例(id がプライマリキー)
BEGIN;
SELECT * FROM products WHERE id = 10 FOR UPDATE;
-- id = 10 の行だけがロックされる

-- ネクストキーロックの例(範囲検索)
BEGIN;
SELECT * FROM products WHERE price BETWEEN 100 AND 200 FOR UPDATE;
-- 該当するレコードとその間のギャップもロック対象になる

ギャップロックの存在を意識していないと、意図しないロック競合が発生することがある。特に範囲条件を伴う UPDATE や DELETE では、想定以上に広い範囲がロックされる場合があるので注意が必要だ。

デッドロックの発生メカニズム

デッドロックとは、2 つ以上のトランザクションが互いに相手のロック解放を待ち合う状態のことだ。どちらも先に進めなくなるため、MySQL が自動的に一方のトランザクションをロールバックして解消する。

典型的なデッドロックのシナリオを見てみよう。

-- トランザクション A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- id=1 をロック
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- id=2 のロック待ち

-- トランザクション B(同時に実行)
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;   -- id=2 をロック
UPDATE accounts SET balance = balance + 50 WHERE id = 1;   -- id=1 のロック待ち

トランザクション A が id=1 をロックし、id=2 のロックを待つ

トランザクション B が id=2 をロックし、id=1 のロックを待つ

互いに相手のロック解放を待ち続け、どちらも進めなくなる(デッドロック)

InnoDB はデッドロックを検知すると、ロールバックのコストが小さい方のトランザクションを選んで強制的にロールバックする。アプリケーション側では、デッドロックによるエラー(エラーコード 1213)を受け取った場合にリトライする仕組みを実装しておく必要がある。

デッドロックの調査方法

デッドロックが発生した場合、直近の情報は SHOW ENGINE INNODB STATUS で確認できる。

SHOW ENGINE INNODB STATUS\G

出力の中に LATEST DETECTED DEADLOCK というセクションがあり、デッドロックに関与したトランザクションの詳細が記録されている。どのテーブルのどのインデックスでロック待ちが発生したのか、実行されていた SQL 文は何かといった情報が含まれるため、原因特定の手がかりになる。

ただし SHOW ENGINE INNODB STATUS は直近の 1 件しか保持しない。頻繁にデッドロックが起きている環境では、ログに出力する設定を有効にしておくべきだ。

-- デッドロック情報をエラーログに記録する
SET GLOBAL innodb_print_all_deadlocks = ON;

この設定を有効にすると、発生したすべてのデッドロック情報がエラーログに記録されるようになる。本番環境では有効化しておくことを推奨する。

デッドロックを防ぐための設計指針

デッドロックを完全に排除することは難しいが、発生頻度を大幅に減らすための設計指針がいくつかある。

複数テーブルや複数行を更新する際は、常に同じ順序でアクセスする
トランザクションはできるだけ短く保ち、不要な処理をトランザクション内に含めない
適切なインデックスを設定し、ロック対象の行数を最小限に抑える
大量データの更新はバッチに分割して実行する

特に重要なのが「同じ順序でアクセスする」というルールだ。先ほどのデッドロック例では、トランザクション A が id=1 → id=2 の順、トランザクション B が id=2 → id=1 の順でアクセスしたことが原因だった。両方とも id の昇順でアクセスすれば、デッドロックは発生しない。

-- 改善例:両方とも id 昇順でアクセス
-- トランザクション A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

-- トランザクション B
BEGIN;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
COMMIT;

この方法であれば、トランザクション B は id=1 のロックが解放されるまで待機するだけで済み、循環的なロック待ちは発生しない。

ロック待ちタイムアウトの設定

デッドロックとは異なり、単純なロック待ちが長時間続くケースもある。長いトランザクションがロックを保持し続けることで、後続のトランザクションが待たされる状況だ。

-- ロック待ちタイムアウトの確認と設定
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- デフォルトは 50 秒

-- 必要に応じて変更
SET GLOBAL innodb_lock_wait_timeout = 10;

タイムアウトが発生するとエラーコード 1205(Lock wait timeout exceeded)が返される。

デッドロック(1213)とは異なり、タイムアウトではトランザクション全体ではなく該当のステートメントだけがロールバックされる点に注意が必要。

アプリケーションの要件に応じて適切なタイムアウト値を設定することが大切だ。Web アプリケーションでは 5〜10 秒程度に設定し、長時間待たせるよりも早めにエラーを返してリトライさせる方針が一般的となっている。

ロック競合の監視

現在のロック状況をリアルタイムで把握するには、performance_schema のロック関連テーブルを参照する。

-- 現在保持されているロックの確認
SELECT * FROM performance_schema.data_locks
ORDER BY ENGINE_TRANSACTION_ID;

-- ロック待ちの状況を確認
SELECT
  r.trx_id AS waiting_trx,
  r.trx_mysql_thread_id AS waiting_thread,
  b.trx_id AS blocking_trx,
  b.trx_mysql_thread_id AS blocking_thread,
  b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;

ロック競合が頻発している場合は、クエリの実行計画を見直してインデックスの最適化を行うか、トランザクションの粒度を細かくすることで改善できるケースが多い。ロックはデータの整合性を守るための仕組みだが、その範囲と期間を必要最小限に留めることが、高い並行性を維持するための鍵となる。