MySQL 15 min read

MySQLの「Lock wait timeout exceeded」 ― ロックを持っている相手を見つけて止める

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 してトランザクションを最初からやり直せば同じ処理で扱えます。