大厂Java面试:mysql为什么会发生死锁?

张开发
2026/5/31 13:29:09 15 分钟阅读
大厂Java面试:mysql为什么会发生死锁?
之前收到读者面试字节时被问到一个关于 MySQL 的问题。如果对 MySQL 加锁机制比较熟悉的同学应该一眼就能看出会发生死锁。但是具体加了什么锁而导致死锁是需要我们具体分析的。接下来就跟聊聊上面两个事务执行 SQL 语句的过程中加了什么锁从而导致死锁的。准备工作先创建一张 t_student 表假设除了 id 字段其他字段都是普通字段。CREATE TABLE t_student ( id int NOT NULL, no varchar(255) DEFAULT NULL, name varchar(255) DEFAULT NULL, age int DEFAULT NULL, score int DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;然后插入相关的数据后t_student 表中的记录如下开始实验在实验开始前先说明下实验环境MySQL 版本8.0.26隔离级别可重复读RR启动两个事务按照题目的 SQL 执行顺序过程如下表格可以看到事务 A 和 事务 B 都在执行 insert 语句后都陷入了等待状态前提没有打开死锁检测也就是发生了死锁因为都在相互等待对方释放锁。为什么会发生死锁我们可以通过 select * from performance_schema.data_locks\G; 这条语句查看事务执行 SQL 过程中加了什么锁。接下来针对每一条 SQL 语句分析具体加了什么锁。Time 1 阶段加锁分析Time 1 阶段事务 A 执行以下语句# 事务 A mysql begin; Query OK, 0 rows affected (0.00 sec) mysql update t_student set score 100 where id 25; Query OK, 0 rows affected (0.01 sec) Rows matched: 0 Changed: 0 Warnings: 0然后执行 select * from performance_schema.data_locks\G; 这条语句查看事务 A 此时加了什么锁。从上图可以看到共加了两个锁分别是表锁X 类型的意向锁行锁X 类型的间隙锁这里我们重点关注行锁图中 LOCK_TYPE 中的 RECORD 表示行级锁而不是记录锁的意思通过 LOCK_MODE 可以确认是 next-key 锁还是间隙锁还是记录锁如果 LOCK_MODE 为 X说明是 next-key 锁如果 LOCK_MODE 为 X, REC_NOT_GAP说明是记录锁如果 LOCK_MODE 为 X, GAP说明是间隙锁因此此时事务 A 在主键索引INDEX_NAME : PRIMARY上加的是间隙锁锁范围是(20, 30)。Time 2 阶段加锁分析Time 2 阶段事务 B 执行以下语句# 事务 B mysql begin; Query OK, 0 rows affected (0.00 sec) mysql update t_student set score 100 where id 26; Query OK, 0 rows affected (0.01 sec) Rows matched: 0 Changed: 0 Warnings: 0然后执行 select * from performance_schema.data_locks\G; 这条语句查看事务 B 此时加了什么锁。从上图可以看到共加了两个锁分别是表锁X 类型的意向锁行锁X 类型的间隙锁因此此时事务 B 在主键索引INDEX_NAME : PRIMARY上加的是间隙锁锁范围是(20, 30)。事务 A 和 事务 B 的间隙锁范围都是一样的为什么不会冲突两个事务的间隙锁之间是相互兼容的不会产生冲突。在MySQL官网上还有一段非常关键的描述Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from Inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.间隙锁的意义只在于阻止区间被插入因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁共享和排他的间隙锁是没有区别的他们相互不冲突且功能相同。Time 3 阶段加锁分析Time 3事务 A 插入了一条记录# Time 3 阶段事务 A 插入了一条记录 mysql insert into t_student(id, no, name, age,score) value (25, S0025, sony, 28, 90); /// 阻塞等待......此时事务 A 就陷入了等待状态。然后执行 select * from performance_schema.data_locks\G; 这条语句查看事务 A 在获取什么锁而导致被阻塞。可以看到事务 A 的状态为等待状态LOCK_STATUS: WAITING因为向事务 B 生成的间隙锁范围 (20, 30)中插入了一条记录所以事务 A 的插入操作生成了一个插入意向锁LOCK_MODE:INSERT_INTENTION。插入意向锁是什么注意插入意向锁名字里虽然有意向锁这三个字但是它并不是意向锁它属于行级锁是一种特殊的间隙锁。在MySQL的官方文档中有以下重要描述An Insert intention lock is a type of gap lock set by Insert operations prior to row Insertion. This lock signals the intent to Insert in such a way that multiple transactions Inserting into the same index gap need not wait for each other if they are not Inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to Insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with Insert intention locks prior to obtaining the exclusive lock on the Inserted row, but do not block each other because the rows are nonconflicting.这段话表明尽管插入意向锁是一种特殊的间隙锁但不同于间隙锁的是该锁只用于并发插入操作。如果说间隙锁锁住的是一个区间那么「插入意向锁」锁住的就是一个点。因而从这个角度来说插入意向锁确实是一种特殊的间隙锁。插入意向锁与间隙锁的另一个非常重要的差别是尽管「插入意向锁」也属于间隙锁但两个事务却不能在同一时间内一个拥有间隙锁另一个拥有该间隙区间内的插入意向锁当然插入意向锁如果不在间隙锁区间内则是可以的。所以插入意向锁和间隙锁之间是冲突的。另外我补充一点插入意向锁的生成时机每插入一条新记录都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁如果已加间隙锁那 Insert 语句会被阻塞并生成一个插入意向锁 。Time 4 阶段加锁分析Time 4事务 B 插入了一条记录# Time 4 阶段事务 B 插入了一条记录 mysql insert into t_student(id, no, name, age,score) value (26, S0026, ace, 28, 90); /// 阻塞等待......此时事务 B 就陷入了等待状态。然后执行 select * from performance_schema.data_locks\G; 这条语句查看事务 B 在获取什么锁而导致被阻塞。可以看到事务 B 在生成插入意向锁时而导致被阻塞这是因为事务 B 向事务 A 生成的间隙锁范围 (20, 30)中插入了一条记录而插入意向锁和间隙锁是冲突的所以事务 B 在获取插入意向锁时就陷入了等待状态。最后回答为什么会发生死锁本次案例中事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30的间隙锁而接下来的插入操作为了获取到插入意向锁都在等待对方事务的间隙锁释放于是就造成了循环等待满足了死锁的四个条件互斥、占有且等待、不可强占用、循环等待因此发生了死锁。总结两个事务即使生成的间隙锁的范围是一样的也不会发生冲突因为间隙锁目的是为了防止其他事务插入数据因此间隙锁与间隙锁之间是相互兼容的。在执行插入语句时如果插入的记录在其他事务持有间隙锁范围内插入语句就会被阻塞因为插入语句在碰到间隙锁时会生成一个插入意向锁然后插入意向锁和间隙锁之间是互斥的关系。如果两个事务分别向对方持有的间隙锁范围内插入一条记录而插入操作为了获取到插入意向锁都在等待对方事务的间隙锁释放于是就造成了循环等待满足了死锁的四个条件互斥、占有且等待、不可强占用、循环等待因此发生了死锁。

更多文章