2026年5月31日

2026年5月31日

MySQLのデッドロックの原因と解消方法|ログの読み方と再発を防ぐ対策

MySQL のデッドロックは、二つ以上の処理が互いに相手の持つロックの解放を待ち、どちらも進めなくなる状態です。InnoDB はこれを自動で検出し、片方の処理を取り消して(ロールバックして)もう片方を進めるため、サーバーが止まり続けることはありません。対処の基本は、「取り消された処理をやり直せるようにする」ことと、「ログから衝突している SQL を特定し、ロックの取り方を変えて頻度を下げる」ことの二つです。この記事では、エラーの見分け方、ログの読み方、原因の候補と確認方法、WordPress で起きやすい場面と対策を説明します。

まずエラーの種類を見分ける

ロックに関するエラーには、よく似た二種類があります。対処が異なるので、最初にどちらかを確認します。

  • ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction:デッドロックです。InnoDB が検出し、このエラーを受け取った側のトランザクションは取り消されています。
  • ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction:ロック待ちの時間切れです。デッドロックではなく、長く続いている別の処理がロックを持ったままになっていることが多い状態です。待つ時間の上限は innodb_lock_wait_timeout(標準は50秒)で決まります。

WordPress では、これらのエラーは PHP のエラーログや、WP_DEBUG_LOG を有効にしたときの wp-content/debug.log に「WordPress database error Deadlock found...」のような形で記録されます。どの SQL で起きたかも一緒に出るので、まず記録の中から該当する行を探してください。

1205 の場合は、次の SQL で長時間続いているトランザクションを確認すると、原因の処理が見つかることがあります。

SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.INNODB_TRX
ORDER BY trx_started;

デッドロックの詳細をログで確認する

直近に起きたデッドロックの詳細は、次のコマンドで確認できます。

SHOW ENGINE INNODB STATUS\G

出力の中の「LATEST DETECTED DEADLOCK」という部分に、衝突した二つのトランザクションについて、次の情報が並びます。

  • 各トランザクションが実行していた SQL
  • 「HOLDS THE LOCK(S)」:そのトランザクションがすでに持っていたロック
  • 「WAITING FOR THIS LOCK TO BE GRANTED」:待っていたロック
  • ロックの対象になったテーブル名とインデックス名
  • 「WE ROLL BACK TRANSACTION (1)」のように、どちらを取り消したか

読むときは、「1 が持っているロックを 2 が待ち、2 が持っているロックを 1 が待っている」という輪を探します。どのテーブルのどのインデックスで輪ができているかが、対策を考える手がかりです。ロックの種類に「gap」や「insert intention」と書かれていれば、行そのものではなく行と行の間(範囲)のロックが関係しています。

ただし、ここに表示されるのは最後の1件だけです。繰り返し起きている場合は、すべてのデッドロックを MySQL のエラーログに書き出す設定を有効にしておくと、傾向がつかめます。

SET GLOBAL innodb_print_all_deadlocks = ON;

SET GLOBAL の設定は再起動で元に戻るため、続けて記録したい場合は my.cnf などの設定ファイルにも書きます。MySQL 8.0 以降では、performance_schema.data_locks や sys.innodb_lock_waits で、今まさに起きているロック待ちを確認することもできます(2026年10月現在)。表示される列は版によって異なることがあります。

主な原因と確認方法

デッドロックの原因として多いものを、起こりやすい順に挙げます。

1. 同じ行を逆の順番で更新している

処理Aが行1→行2、処理Bが行2→行1の順に更新すると、互いに相手の行を待つ形になります。最も典型的なデッドロックです。ログの二つの SQL が同じテーブルを対象にしていて、条件の値だけが違う場合はこれを疑います。

対策は、複数の行を更新する処理では、主キーの小さい順など、常に同じ順番で更新するようにそろえることです。

2. インデックスが無く、余分な行までロックしている

InnoDB は、検索に使ったインデックスの範囲に対してロックをかけます。WHERE の条件に合うインデックスが無いと、条件に合わない行まで広くロックしてしまい、無関係に見える処理同士がぶつかります。ログに出ているインデックスが条件の列とは関係のないもの(主キーなど)だったり、条件に合う行は少ないはずなのに衝突が頻繁に起きたりする場合は、この可能性があります。

EXPLAIN で該当の UPDATE や DELETE の実行計画を確認し、条件の列にインデックスがあるかを調べます。インデックスの追加はテーブルの構造を変える作業なので、バックアップを取り、ステージング環境で効果を確かめてから行ってください。

3. 範囲の条件や重複チェックによるギャップロック

MySQL の標準の分離レベル(REPEATABLE READ)では、範囲を指定した UPDATE・DELETE や SELECT ... FOR UPDATE が、行と行の間にもロック(ギャップロック)をかけます。同じ範囲に別の処理が INSERT しようとすると待ちが発生し、デッドロックにつながることがあります。INSERT ... ON DUPLICATE KEY UPDATE を同じキーに対して同時に実行する場合も起きやすい場面です。

4. トランザクションが長い

トランザクションの途中で外部 API を呼ぶ、大量の行を一度に処理する、といった作り方では、ロックを持っている時間が長くなり、ほかの処理と重なる確率が上がります。

解消と再発防止の対策

取り消された処理をやり直す

デッドロックは、同時に多くの処理が動くデータベースではゼロにはできないものとして扱われます。エラー 1213 を受け取ったら、そのトランザクションの最初からやり直すのが基本の対処です。InnoDB は、変更した行数が少ない方を取り消すように選ぶため、やり直しにかかる負担は通常小さく済みます。

WordPress のプラグインなどで、自分でトランザクションを使う処理を書く場合の例です。$wpdb にはトランザクション専用のメソッドが無いため、SQL で開始・確定・取り消しを行います。

/**
 * $work は、途中のクエリが失敗したら直ちに false を返すこと。
 */
function myprefix_run_with_retry( callable $work, $max_attempts = 3 ) {
	global $wpdb;

	for ( $attempt = 1; $attempt <= $max_attempts; $attempt++ ) {
		$wpdb->query( 'START TRANSACTION' );
		$result = $work();

		if ( false !== $result ) {
			$wpdb->query( 'COMMIT' );
			return $result;
		}

		$error = $wpdb->last_error;
		$wpdb->query( 'ROLLBACK' );

		if ( false === stripos( $error, 'Deadlock' ) ) {
			return false; // デッドロック以外のエラーはやり直さない
		}

		usleep( wp_rand( 50000, 200000 ) * $attempt ); // 少し待ってから再試行
	}

	return false;
}

やり直しは数回までにし、待ち時間を少しずつずらします。同じ間隔ですぐにやり直すと、同じ相手と再び衝突しやすくなるためです。

ロックを持つ時間と範囲を小さくする

  • トランザクションの中では、データベースの操作だけを行い、外部への通信やファイル処理は外に出します。
  • 数万件の更新や削除は、数百〜数千件ずつに分けて実行します。1回あたりのロックの範囲が小さくなり、ほかの処理を長く待たせなくなります。
  • 前述のとおり、複数行の更新は同じ順番に、条件の列にはインデックスを用意します。

分離レベルを見直す(影響を確認したうえで)

ギャップロックが原因の場合、分離レベルを READ COMMITTED にするとギャップロックの多くが使われなくなり、デッドロックが減ることがあります。ただし、データの読み取り方が変わるため、アプリケーションの動作に影響しないかを確認する必要があります。また、バイナリログを使っている環境では、行ベースの形式(binlog_format=ROW、MySQL 8.0 の標準)であることが前提です。サーバー全体を変える前に、ステージングで十分に試してください。

なお、デッドロックの自動検出を無効にする設定(innodb_deadlock_detect)もありますが、これは非常に同時処理の多い環境向けのもので、無効にするとデッドロックが時間切れまで解消されなくなります。通常は有効のままにしておきます。

WordPress で起きやすい場面

WordPress 本体は、ほとんどの SQL を個別に確定させる(自動コミットの)形で実行するため、本体だけでデッドロックが頻発することはあまりありません。多いのは次のような場面です。

  • 定期処理の重複:WP-Cron がアクセスのたびに起動し、同じ定期処理が並行して走ると、同じ行を同時に更新しやすくなります。wp-config.php で DISABLE_WP_CRON を true にし、サーバーの cron から一定間隔で実行する構成にすると、重なりを減らせます。
  • 非同期処理のキュー:WooCommerce などが使う Action Scheduler は、処理の予約や状態をデータベースのテーブルで管理しています。大量の処理が同時に走ると、このテーブルでロックの競合が起きることがあります。設定や挙動は版によって異なるため、各プラグインの資料を確認してください。
  • wp_options への頻繁な書き込み:トランジェントやカウンターを頻繁に更新するプラグインがあると、同じ行への更新が集中します。オブジェクトキャッシュ(Redis など)の導入で、データベースへの書き込みが減る場合があります。
  • 一括処理:投稿の一括更新や検索置換、インポートを、アクセスの多い時間帯に実行している。

どのプラグインが原因かは、エラーに記録された SQL のテーブル名や、debug.log に出る呼び出し元の関数名から見当を付けます。

緊急時の対応

1205 のロック待ちが続いてサイトが応答しない場合は、INNODB_TRX で古いトランザクションを特定し、その接続を終了する方法があります。

KILL 12345;  -- trx_mysql_thread_id の値に置き換える

接続を終了すると、そのトランザクションの変更は取り消されます。取り消しに時間がかかる場合もあり、どの処理を止めるのかを確かめてから実行してください。根本的な解決にはならないため、落ち着いたら前述のログで原因を調べます。

まとめ

  • エラー 1213 はデッドロック、1205 はロック待ちの時間切れ。まずどちらかを見分ける。
  • 詳細は SHOW ENGINE INNODB STATUS の「LATEST DETECTED DEADLOCK」で確認し、繰り返す場合は innodb_print_all_deadlocks でエラーログに残す。
  • 原因は、更新順の不一致、インデックス不足による広いロック、ギャップロック、長いトランザクションの順に疑う。
  • 対策の基本は、1213 を受けたらトランザクションの最初からやり直すこと。更新順をそろえ、処理を小分けにし、トランザクションを短くする。
  • WordPress では、WP-Cron の重複実行や非同期処理のキュー、一括処理が引き金になりやすい。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲