MySQL のロックとデッドロック対策
複数のトランザクションが同時にデータへアクセスする環境では、データの整合性を保つためにロックの仕組みが欠かせない。MySQL の InnoDB ストレージエンジンは行レベルロックを採用しており、高い並行性を実現しているが、ロックの仕組みを正しく理解していないとパフォーマンスの低下やデッドロックの原因となる。
InnoDB のロックの種類
InnoDB が提供するロックにはいくつかの種類があり、それぞれ異なる目的で使い分けられている。
行の読み取り時に取得されるロック。他のトランザクションからの共有ロックは許可されるが、排他ロックはブロックされる。SELECT ... FOR SHARE で明示的に取得できる。
行の更新・削除時に取得されるロック。他のトランザクションからの共有ロック・排他ロックの両方をブロックする。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;ロック競合が頻発している場合は、クエリの実行計画を見直してインデックスの最適化を行うか、トランザクションの粒度を細かくすることで改善できるケースが多い。ロックはデータの整合性を守るための仕組みだが、その範囲と期間を必要最小限に留めることが、高い並行性を維持するための鍵となる。












