Update 计算机操作系统 - 死锁.md

补充锁等待超时只回滚当前语句、死锁日志的读法,附录补充对应实测。
This commit is contained in:
CyC2018
2026-11-02 01:31:14 +08:00
parent d1d0a64068
commit d83b05ec48
+27 -1
View File
@@ -26,6 +26,7 @@
* [A3 先查后插的间隙锁死锁](#a3-先查后插的间隙锁死锁)
* [A4 死锁线程的状态与 CPU 时间](#a4-死锁线程的状态与-cpu-时间)
* [A5 单个线程重复加锁](#a5-单个线程重复加锁)
* [A6 锁等待超时只回滚当前语句](#a6-锁等待超时只回滚当前语句)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
@@ -235,6 +236,10 @@ UPDATE account SET balance = balance + 50 WHERE id = 1; -- 等待事务 1,
InnoDB 默认开启死锁检测,发现环后立即回滚其中一个事务(倾向于回滚修改行数少的事务),另一个事务继续执行。上例中事务 2 被回滚,事务 1 提交后余额为 900 和 1100(MySQL 8.0 实测,见[附录 A2](#a2-mysql-死锁))。执行 SHOW ENGINE INNODB STATUS,在 LATEST DETECTED DEADLOCK 一节中可以看到两个事务各自持有和等待的锁。被回滚的事务需要由应用重试。
锁等待超时与死锁的处理不同。行锁等待达到 innodb_lock_wait_timeout(默认 50 秒)仍未拿到锁时报错 1205,关闭死锁检测后死锁也靠这个超时解开。这时默认只回滚当前这条语句,事务还在,之前的修改和锁都保留,由应用决定回滚还是重试;innodb_rollback_on_timeout 打开后才回滚整个事务。
例如转账事务先给 id = 2 加了 100,再扣 id = 1 时锁等待超时,如果捕获异常后照常提交,就只加了没有扣(MySQL 8.0 实测,见[附录 A6](#a6-锁等待超时只回滚当前语句))。
按相同顺序加锁并不能避免所有死锁。可重复读隔离级别下,常见的“先查有没有,没有再插入”也会死锁,例如两个事务分别为订单 1024 和 1025 写支付记录,表中已有 1000 和 2000 两行,两个事务先后执行 SELECT,再先后执行 INSERT(按编号顺序):
```sql
@@ -255,6 +260,12 @@ INSERT INTO payment VALUES (1025, 50); -- 同样等待事务 1
排查数据库死锁时,SHOW ENGINE INNODB STATUS 只保留最近一次死锁,打开 innodb_print_all_deadlocks 后每次死锁都会写入错误日志。锁等待还没有形成死锁时,可以查询 performance_schema.data_locks 和 data_lock_waits,看到每个事务持有和等待的锁,例如上例中事务 1 的插入意向锁处于 WAITING 状态。
读 MySQL 8.0 的死锁日志时,两个事务的情况下 (2) 是最后开始等待、使等待关系形成环的事务,(1) 是它在等的事务;(1) HOLDS THE LOCK(S) 列出的就是 (2) 在等的那把锁,反之亦然。
每个事务只显示当时正在执行的语句,持有的锁可能是更早的语句加的,例如附录 A3 中两个事务显示的都是 INSERT,间隙锁却来自之前的 SELECT ... FOR UPDATE。
lock_mode X locks gap before rec 是间隙锁,后面再有 insert intention 的是插入意向锁,在 data_locks 中分别显示为 X,GAP 和 X,GAP,INSERT_INTENTION。
减少数据库死锁的方法:
- 按相同的顺序访问行,例如转账时先更新 id 小的账户;
@@ -343,7 +354,7 @@ INSERT INTO payment VALUES (1025, 50)
ERROR 1062 (23000) at line 1: Duplicate entry '1024' for key 'payment.PRIMARY'
```
执行 SET GLOBAL innodb_print_all_deadlocks = ON 后重跑可重复读的例子,错误日志中同样记录了两个事务持有的间隙锁和等待的插入意向锁(lock_mode X locks gap before rec insert intention waiting)。
执行 SET GLOBAL innodb_print_all_deadlocks = ON 后,再执行一遍可重复读的例子,错误日志中同样记录了两个事务持有的间隙锁和等待的插入意向锁(lock_mode X locks gap before rec insert intention waiting)。
### A4 死锁线程的状态与 CPU 时间
@@ -406,10 +417,25 @@ Semaphore(1) 同一线程 acquire 两次,2 秒后状态:WAITING
此时 findDeadlockedThreads() 返回 null
```
### A6 锁等待超时只回滚当前语句
MySQL 8.0.44,innodb_rollback_on_timeout 为默认的 OFF。account 表中 id = 1、2 的余额都是 1000,A 更新 id = 1 后不提交;B 设置 innodb_lock_wait_timeout = 1,在事务中先更新 id = 2,再更新 id = 1。运行 3 次结果相同:
```text
A: BEGIN; UPDATE account SET balance = balance - 100 WHERE id = 1
B: BEGIN; UPDATE account SET balance = balance + 100 WHERE id = 2
B: UPDATE account SET balance = balance - 100 WHERE id = 1 -> ERROR 1205 Lock wait timeout exceeded
B: SELECT balance FROM account WHERE id = 2 -> 1100
information_schema.innodb_trx 中仍有 2 个事务
B: COMMIT; A: ROLLBACK
最终:id = 1 为 1000,id = 2 为 1100
```
## 参考资料
- Tanenbaum A S, Bos H. Modern Operating Systems [M]. Pearson, 2014.
- [Deadlocks in InnoDB | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks.html)
- [InnoDB Locking | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html)
- [The data_locks Table | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-data-locks-table.html)
- [innodb_rollback_on_timeout | MySQL 8.0 Reference Manual](https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_rollback_on_timeout)
- [ThreadMXBean (Java SE 21 & JDK 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/ThreadMXBean.html)