如果对 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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 然后,插入相关的数据后,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 此时加了什么锁。
因此,此时事务 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
从上图可以看到,行锁是 X 类型的间隙锁,间隙锁的范围是(20, 30)。
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 就陷入了等待状态。
可以看到,事务 A 的状态为等待状态(LOCK_STATUS: WAITING),因为向事务 B 生成的间隙锁(范围 (20, 30))中插入了一条记录,所以事务 A 的插入操作生成了一个插入意向锁(LOCK_MODE:INSERT_INTENTION)。
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 就陷入了等待状态。
可以看到,事务 B 在生成插入意向锁时而导致被阻塞,这是因为事务 B 向事务 A 生成的范围为 (20, 30) 的间隙锁插入了一条记录,而插入意向锁和间隙锁是冲突的,所以事务 B 在获取插入意向锁时就陷入了等待状态。
最后回答,为什么会发生死锁?
本次案例中,事务 A 和事务 B 在执行完后 update 语句后都持有范围为 (20, 30) 的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。
总结
两个事务即使生成的间隙锁的范围是一样的,也不会发生冲突,因为间隙锁目的是为了防止其他事务插入数据,因此间隙锁与间隙锁之间是相互兼容的。
在执行插入语句时,如果插入的记录在其他事务持有间隙锁范围内,插入语句就会被阻塞,因为插入语句在碰到间隙锁时,会生成一个插入意向锁,然后插入意向锁和间隙锁之间是互斥的关系。
如果两个事务分别向对方持有的间隙锁范围内插入一条记录,而插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,满足了死锁的四个条件:互斥、占有且等待、不可强占用、循环等待,因此发生了死锁。