※この記事にはアフィリエイト広告(プロモーション)が含まれます。
2つのトランザクションが互いの持つロックを待つと、InnoDB は片方を取り消して次のエラーを返します。
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
この記事のログは MySQL 8.4.11(Ubuntu 26.04 の標準パッケージ)で採りました。表は次のとおりです。
CREATE TABLE acct (id int PRIMARY KEY, bal int);
INSERT INTO acct VALUES (1,100), (2,100), (20,100);
まず見る(結論)
- 1213 を受けた側は、トランザクション全体が取り消されている。エラーになった文だけではない。アプリはトランザクションを最初からやり直す
- 原因は
SHOW ENGINE INNODB STATUS\GのLATEST DETECTED DEADLOCKで見る。残るのは最後の1件だけ - 全件残すなら
innodb_print_all_deadlocks=ON。log_error_verbosityが既定の 2 だと、エラーログで時刻と見出しが欠ける
逆の順で更新すると起きる
A は id=1 → id=2、B は id=2 → id=1 の順に更新します。B は A の1秒後に始めました。
-- A
START TRANSACTION;
UPDATE acct SET bal = bal - 10 WHERE id = 1;
DO SLEEP(2);
UPDATE acct SET bal = bal + 10 WHERE id = 2; -- B が持つ id=2 を待つ
COMMIT;
-- B
START TRANSACTION;
UPDATE acct SET bal = bal - 5 WHERE id = 2;
DO SLEEP(2);
UPDATE acct SET bal = bal + 5 WHERE id = 1; -- A が持つ id=1 を待つ → 1213
COMMIT;
B が 1213 を受け、A はそのままコミットしました。
SELECT * FROM acct;
+----+------+
| id | bal |
+----+------+
| 1 | 90 |
| 2 | 110 |
| 20 | 100 |
+----+------+
B が先に実行した id=2 の -5 も残っていません。ロック待ちのタイムアウト(ERROR 1205)は既定では待っていた1文だけを取り消すので、扱いが違います(「Lock wait timeout exceeded」)。
LATEST DETECTED DEADLOCK の読み方
*** (1) TRANSACTION:
TRANSACTION 3778, ACTIVE 3 sec starting index read
update acct set bal=bal+10 where id=2
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY of table `dldb`.`acct` trx id 3778 lock_mode X locks rec but not gap
0: len 4; hex 80000001; asc ;;
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS ... index PRIMARY of table `dldb`.`acct` trx id 3778 lock_mode X locks rec but not gap waiting
0: len 4; hex 80000002; asc ;;
*** (2) TRANSACTION:
TRANSACTION 3779, ACTIVE 2 sec starting index read
update acct set bal=bal+5 where id=1
*** (2) HOLDS THE LOCK(S):
... hex 80000002 ...
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
... hex 80000001 ...
*** WE ROLL BACK TRANSACTION (2)
(ロックの行は抜粋)
(1)(2)の下の SQL は、それぞれが待っていた文HOLDSが持っているロック、WAITINGが待っているロック。index PRIMARYと、行の値(hex 80000001は int の 1)でどの行かが分かるWE ROLL BACK TRANSACTION (2)が 1213 を受けた側
ロックを取った文は出ないので、アプリのコードで「その前に何を更新したか」を確かめます。
同じ順で更新すれば起きない
B も id=1 → id=2 の順に変えると、B は A のコミットまで待ち、A の2秒後に正常に終わりました。最終的な残高は id=1 が 85、id=2 が 115 で、両方の更新が入っています。複数の行を更新する処理は、主キーの順など決まった順で更新します。
無い行への SELECT … FOR UPDATE → INSERT
「無ければ入れる」を SELECT ... FOR UPDATE で確かめてから INSERT する書き方でも起きます。A は id=10、B は id=11 と、別々の値を扱っています。
-- A -- B(1秒後)
START TRANSACTION; START TRANSACTION;
SELECT * FROM acct WHERE id = 10 SELECT * FROM acct WHERE id = 11
FOR UPDATE; -- 0行 FOR UPDATE; -- 0行
INSERT INTO acct VALUES (10, 0); INSERT INTO acct VALUES (11, 0); -- 1213
id=2 と id=20 の間には行が無いので、2つの SELECT ... FOR UPDATE はどちらもその隙間にロック(ギャップロック)を取ります。ギャップロック同士はぶつからないので、どちらも待たずに進みます。その後の INSERT が、相手の持つギャップロックを待ちます。
*** (1) HOLDS THE LOCK(S):
... lock_mode X locks gap before rec
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
... lock_mode X locks gap before rec insert intention waiting
0: len 4; hex 80000014; asc ;;
hex 80000014 は id=20 です。locks gap before rec と insert intention が並んでいたら、この型です。存在の確認を SELECT ... FOR UPDATE でせず、先に INSERT して重複エラーで判断する書き方もあります。重複の扱い方は「ERROR 1062」にまとめています。
全件をエラーログに残す
LATEST DETECTED DEADLOCK は次のデッドロックで上書きされます。全件残すには innodb_print_all_deadlocks を有効にします。
SET GLOBAL innodb_print_all_deadlocks = ON;
ただし log_error_verbosity が既定の 2 のままだと、エラーログに書かれるのはロックの行だけで、時刻の付いた見出しが出ませんでした。grep -i deadlock /var/log/mysql/error.log では1行も引っかかりません。log_error_verbosity を 3 にすると、次の見出しが付きます。
SET GLOBAL log_error_verbosity = 3;
2026-10-21T01:22:20.634751Z 0 [Note] [MY-012468] [InnoDB] Transactions deadlock detected, dumping detailed information.
2026-10-21T01:22:20.634794Z 0 [Note] [MY-012469] [InnoDB] *** (1) TRANSACTION:
3 にすると他の [Note] も出るようになるので、ログの量が増えます。どちらも SET GLOBAL で変えた値は再起動で戻ります。常に有効にするなら設定ファイルにも書きます。
発生件数を数える
SELECT name, count FROM information_schema.innodb_metrics WHERE name = 'lock_deadlocks';
+----------------+-------+
| name | count |
+----------------+-------+
| lock_deadlocks | 2 |
+----------------+-------+
起動してからのデッドロックの件数です。SHOW GLOBAL STATUS LIKE 'Innodb_deadlocks' は 8.4.11 では何も返しませんでした。