MySQL(InnoDB)で UPDATE や SELECT … FOR UPDATE がしばらく返らず、次のエラーになることがあります。
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
別のトランザクションが持っている行ロックを、innodb_lock_wait_timeout(既定 50 秒)待っても取れなかったときのエラーです。この記事のログは MySQL 8.4.6 で採りました(mysql クライアントをバッチで実行したため実際のログではエラーに at line N が付きますが、ここでは省いています)。
まず見る(結論)
待たされている間に、別のセッションから次を実行します。
SELECT * FROM sys.innodb_lock_waits\G
blocking_pid がロックを持っているセッションです。blocking_query が NULL なら、そのセッションは更新した後にコミットせず止まっています。KILL <blocking_pid>; で切ると、そのトランザクションはロールバックされて待ちが解けます。切る前に、その接続がどのアプリのものか、更新が取り消されて困らないかを確かめます。
再現と待ち時間
セッション A で行を更新したまま、セッション B で同じ行を更新しました。
-- セッション A
BEGIN;
UPDATE accounts SET balance=balance-10 WHERE id=1;
-- コミットしない
-- セッション B
BEGIN;
UPDATE accounts SET balance=balance-10 WHERE id=1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
B のエラーが返るまで 50.18 秒でした。SELECT @@innodb_lock_wait_timeout; は 50 です。
この値はセッションごとに変えられます。B で SET SESSION innodb_lock_wait_timeout = 3; にすると、3.10 秒でエラーが返りました。バッチなど長く待たせたくない処理だけ短くできます。
エラーの後もトランザクションは続いている
1205 で取り消されるのは、待っていた SQL 1文だけです。同じトランザクションで先に実行した更新は残ります。B で id=2 を更新してから id=1 で 1205 を受けたときの結果です。
-- セッション B
BEGIN;
UPDATE accounts SET balance=balance-10 WHERE id=2;
UPDATE accounts SET balance=balance-10 WHERE id=1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
SELECT * FROM accounts;
+----+---------+
| id | balance |
+----+---------+
| 1 | 100 |
| 2 | 90 |
+----+---------+
B は id=2 のロックも持ったままです。ROLLBACK すると 100 に戻りました。1205 を受けたら、アプリは自分で ROLLBACK してからトランザクションをやり直します。そのまま COMMIT すると、id=2 だけが更新されます。
エラー時にトランザクション全体を取り消す設定は innodb_rollback_on_timeout で、既定は 0(OFF)です。起動時にしか設定できず、SET GLOBAL は次のエラーになりました。
ERROR 1238 (HY000): Variable 'innodb_rollback_on_timeout' is a read only variable
ロックを持っている相手を探す
B が待っている間に、3本目のセッションから見た結果です。
SELECT * FROM sys.innodb_lock_waits\G
*************************** 1. row ***************************
wait_started: 2026-10-17 07:22:28
wait_age: 00:00:01
locked_table: `lwdb`.`accounts`
locked_index: PRIMARY
locked_type: RECORD
waiting_pid: 139
waiting_query: UPDATE accounts SET balance=balance-10 WHERE id=1
waiting_lock_mode: X,REC_NOT_GAP
blocking_pid: 138
blocking_query: NULL
blocking_lock_mode: X,REC_NOT_GAP
blocking_trx_age: 00:00:03
blocking_trx_rows_modified: 1
sql_kill_blocking_query: KILL QUERY 138
sql_kill_blocking_connection: KILL 138
(列は抜粋です)138 が A、139 が B です。SHOW PROCESSLIST でも A は次のように見えます(A と B の行だけ抜粋)。
+-----+------+-----------+------+---------+------+----------+---------------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+-----+------+-----------+------+---------+------+----------+---------------------------------------------------+
| 138 | root | localhost | lwdb | Sleep | 4 | | NULL |
| 139 | root | localhost | lwdb | Query | 2 | updating | UPDATE accounts SET balance=balance-10 WHERE id=1 |
+-----+------+-----------+------+---------+------+----------+---------------------------------------------------+
ロックを持っているかどうかは、sys.innodb_lock_waits か information_schema.innodb_trx で確かめます。innodb_trx では B が LOCK WAIT、A が RUNNING でした。
sql_kill_blocking_connection の列に、相手を切るための SQL が入っています。別の回で A を KILL したところ、B の UPDATE はその場で通り、A の未コミットの更新はロールバックされました。
コミットされないまま放置された接続は、アプリのコネクションプールや、手作業で BEGIN したままのクライアントでよく見られます。blocking_pid を SHOW PROCESSLIST の Id と突き合わせ、Host と User からどのアプリの接続かを特定します。
索引の無い列で絞った更新は別の行でも待つ
InnoDB は、更新対象を探すために読んだ索引のレコードにロックを掛けます。WHERE の列に索引が無いと全行を読むので、関係のない行の更新まで止まります。
-- code に索引が無い表 noidx
-- セッション A
BEGIN;
UPDATE noidx SET code=code+1 WHERE code=10;
-- セッション B(innodb_lock_wait_timeout = 3)
UPDATE noidx SET code=code+1 WHERE code=30;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
別の行なのに、B は 3.17 秒待ってエラーになりました。code に索引がある表 items で同じ操作をすると、B の UPDATE は 0.01 秒で通りました。
関係のない行の更新で 1205 が出るなら、EXPLAIN で両方の SQL が索引を使っているかを確かめます。
待たずに返す(NOWAIT / SKIP LOCKED)
A が id=1 を更新したままの状態で、B から読んだ結果です(B の innodb_lock_wait_timeout は 3)。
| B で実行した SQL | 結果 |
|---|---|
SELECT * FROM accounts WHERE id=1; |
待たずに更新前の値(100)を返す |
... WHERE id=1 FOR SHARE; |
3 秒待って 1205 |
... WHERE id=1 FOR UPDATE NOWAIT; |
すぐに ERROR 3572 (HY000) |
... WHERE id=1 FOR UPDATE SKIP LOCKED; |
すぐに Empty set |
SELECT * FROM accounts FOR UPDATE SKIP LOCKED; |
すぐに id=2 の1行だけ返す |
NOWAIT のエラーは Statement aborted because lock(s) could not be acquired immediately and NOWAIT is set. です。待たせたくない処理は、NOWAIT でエラーを受けて後で再試行します。キューのように空いている行だけ処理すればよい場合は SKIP LOCKED を使います。
デッドロック(ERROR 1213)との違い
2つのトランザクションが逆の順番で行を更新すると、1205 ではなく 1213 が返ります。
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
1213 はタイムアウトを待たずに返ります。A が id=1 → id=2、B が id=2 → id=1 の順で更新したところ、B がエラーを受けました。A の id=2 の UPDATE は 0.53 秒で通りました。1213 を受けた側のトランザクションは全体がロールバックされます。直近のデッドロックは SHOW ENGINE INNODB STATUS\G の LATEST DETECTED DEADLOCK 節に残ります。
*** (1) TRANSACTION:
TRANSACTION 2754, ACTIVE 2 sec starting index read
UPDATE accounts SET balance=balance-10 WHERE id=2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY of table `lwdb`.`accounts` trx id 2754 lock_mode X locks rec but not gap
*** (2) TRANSACTION:
TRANSACTION 2755, ACTIVE 1 sec starting index read
UPDATE accounts SET balance=balance-20 WHERE id=1
...
*** WE ROLL BACK TRANSACTION (2)
(抜粋です)どちらのエラーも、アプリは ROLLBACK してトランザクションを最初からやり直せば同じ処理で扱えます。