ERROR 1213 死锁。SHOW ENGINE INNODB STATUS 和错误日志读取死锁现场。安装 Docker,本机 3307 端口空闲。实验使用独立容器和 lab 数据库,不影响现有 MySQL。
docker run -d --name lab-mysql \
-e MYSQL_ROOT_PASSWORD=labpass \
-e MYSQL_DATABASE=lab \
-p 3307:3306 \
mysql:8.0 --innodb-print-all-deadlocks=ON
until docker exec lab-mysql mysqladmin ping -uroot -plabpass --silent; do sleep 2; done
docker exec -i lab-mysql mysql -uroot -plabpass lab <<'SQL'
CREATE TABLE accounts (id INT PRIMARY KEY, balance INT NOT NULL);
INSERT INTO accounts VALUES (1, 100), (2, 100);
SQL
innodb_print_all_deadlocks=ON 会把每次死锁写入错误日志,避免只看到最后一次。生产环境也建议开启,代价是日志量增加。
两个事务以相反顺序更新同一批行,并用 SLEEP 保证持锁时间重叠:
docker exec lab-mysql mysql -uroot -plabpass lab -e \
"BEGIN; UPDATE accounts SET balance=balance+1 WHERE id=1; SELECT SLEEP(2); UPDATE accounts SET balance=balance+1 WHERE id=2; COMMIT;" \
> /tmp/lab-mysql-a.log 2>&1 &
sleep 0.5
docker exec lab-mysql mysql -uroot -plabpass lab -e \
"BEGIN; UPDATE accounts SET balance=balance+1 WHERE id=2; SELECT SLEEP(2); UPDATE accounts SET balance=balance+1 WHERE id=1; COMMIT;" \
> /tmp/lab-mysql-b.log 2>&1 &
wait
cat /tmp/lab-mysql-a.log /tmp/lab-mysql-b.log
其中一个事务应报 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction,另一个事务提交成功。InnoDB 会自动选择一个事务作为牺牲者回滚。
docker exec lab-mysql mysql -uroot -plabpass -e 'SHOW ENGINE INNODB STATUS\G' \
| sed -n '/LATEST DETECTED DEADLOCK/,/TRANSACTIONS/p'
docker logs lab-mysql 2>&1 | grep -A40 'LATEST DETECTED DEADLOCK' | tail -45
docker exec lab-mysql mysql -uroot -plabpass lab \
-e 'SELECT * FROM accounts ORDER BY id;'
预期死锁段中能看到两个事务、各自持有的行锁和等待的锁,以及 WE ROLL BACK TRANSACTION 标记;accounts 中只有一个事务的更新生效。失败事务的 COMMIT 不会执行,连接关闭后自动回滚。
LATEST DETECTED DEADLOCK 会列出两个事务的 SQL、持有锁和等待锁,按“谁持有、谁等待”画出等待环即可确认。innodb_lock_wait_timeout,不会自动产生死锁日志。performance_schema.data_locks 和 data_waits 可以在事务仍在运行时观察实时锁;事务结束后这些视图会清空。SHOW ENGINE INNODB STATUS 只保留最近一次死锁,历史死锁要靠 innodb_print_all_deadlocks 的日志。统一加锁顺序,按主键从小到大更新,并缩短事务:
docker exec lab-mysql mysql -uroot -plabpass lab -e \
"BEGIN; UPDATE accounts SET balance=balance+1 WHERE id=1; UPDATE accounts SET balance=balance+1 WHERE id=2; COMMIT;" &
docker exec lab-mysql mysql -uroot -plabpass lab -e \
"BEGIN; UPDATE accounts SET balance=balance+1 WHERE id=1; UPDATE accounts SET balance=balance+1 WHERE id=2; COMMIT;" &
wait
两个事务按相同顺序加锁时,后到者会在第一把锁上等待,不会形成环,两个事务最终都会提交。生产修复还包括:给条件列补索引减少锁范围、把外部调用移出事务、捕获 1213 后退避重试并保证幂等。注意 SHOW ENGINE INNODB STATUS 仍保留上一次死锁段,不能用它判断是否复发,应看日志中是否有新增记录。
docker rm -f lab-mysql
rm -f /tmp/lab-mysql-a.log /tmp/lab-mysql-b.log
容器删除后数据库数据一并消失。若需要保留现场,可先 docker cp lab-mysql:/var/lib/mysql ./lab-mysql-data 再删除。
ERROR 1213 和 ERROR 1205 的关键区别是什么?innodb_print_all_deadlocks 有什么代价?