上周有同学甩过来一条报错,就一行:
vbnet
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
面试里这题经常被答成「四个必要条件」。线上真正要做的是读日志。InnoDB 默认会做死锁检测,挑一个事务回滚。应用侧就算 SQL 逻辑没写错,也得能重试。这是 MySQL 8.4 手册里写明的。
下面用两行账户表,把交叉加锁跑一遍。环境是 MySQL 8.4.11,innodb_deadlock_detect=ON。
sql
CREATE TABLE account (
id INT PRIMARY KEY,
balance INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO account VALUES (1, 100), (2, 200);
两个会话同时开事务:
sql
-- 会话 A
BEGIN;
UPDATE account SET balance = balance - 10 WHERE id = 1;
-- 隔三秒
UPDATE account SET balance = balance + 10 WHERE id = 2;
-- 会话 B
BEGIN;
UPDATE account SET balance = balance - 10 WHERE id = 2;
-- 隔一秒
UPDATE account SET balance = balance + 10 WHERE id = 1;
我这次 A 被回滚,报了 1213。B 提交成功,表变成 (1, 110)、(2, 190)。

然后跑:
sql
SHOW ENGINE INNODB STATUS\G
翻到 LATEST DETECTED DEADLOCK。我这次现场是这样的(物理记录只留关键行):
perl
*** (1) TRANSACTION:
TRANSACTION 1815, ACTIVE 3 sec starting index read
UPDATE account SET balance = balance + 10 WHERE id = 1
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS ... index PRIMARY of table `demo`.`account`
trx id 1815 lock_mode X locks rec but not gap
Record lock, heap no 3 ... hex 80000002
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
... lock_mode X locks rec but not gap waiting
Record lock, heap no 2 ... hex 80000001
*** (2) TRANSACTION:
TRANSACTION 1814, ACTIVE 3 sec starting index read
UPDATE account SET balance = balance + 10 WHERE id = 2
*** (2) HOLDS THE LOCK(S):
... heap no 2 ... hex 80000001
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
... heap no 3 ... hex 80000002 waiting
*** WE ROLL BACK TRANSACTION (2)
读法就三处:
HOLDS 是已经拿到的锁。(1) 拿着 hex 80000002,InnoDB 把有符号整数的最高位翻掉存,所以这是 id=2。它正在等 80000001,也就是 id=1。
(2) 正好反过来:拿着 id=1,等 id=2。环对上了。
最后一行 WE ROLL BACK TRANSACTION (2) 对上了会话 A 的 1213。InnoDB 会挑「更小」的事务回滚,大小按插入、更新、删除的行数算。两边都只改过一行,它回滚了标成 (2) 的那个。
lock_mode X locks rec but not gap 是排他记录锁,没有间隙。这次是主键等值更新,跟可重复读里的间隙锁不是一回事。别一上来把死锁甩给幻读。
我后来把这段日志当口述提纲,在面灵里过了两遍。面试官能听懂的版本其实就这三句:谁 HOLDS、谁 WAITING、谁被回滚。
还有两个容易背错的点。
很多笔记还在写 information_schema.INNODB_LOCKS。MySQL 8.0 起这张表没了。活着的锁看 performance_schema.data_locks。死锁现场本身还是 status 这段。
而且 SHOW ENGINE INNODB STATUS 只留最近一次。默认 innodb_print_all_deadlocks=OFF。死锁如果一阵一阵来,要把这个打开,写到错误日志里,不然下一次会把上一次盖掉。
排查讲到这里就够了。交叉更新顺序会成环;InnoDB 会回滚其中一个;应用必须接住 1213 再重试;统一各事务的加锁顺序能少踩很多。