两个间隙锁后,插入语句导致死锁
假设有这么一个表和对应的数据
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);从 行锁 中对于间隙锁的介绍,间隙锁之间并不冲突。
由于表中没有 c=7 数据,所以下面两个 session 都是持有间隙锁,并不会互相堵住。
| session A | session B |
|---|---|
begin;select * from t where c=7 lock in share mode; | |
begin;select * from t where c=7 for update; |
但是,如果有个逻辑是,如果不存在某一行,那么插入这行数据,否则更新。
假如没有使用 insert …on duplicate key update 而是下面方式,那么就会存在 死锁。
| session A | session B |
|---|---|
begin;select * from t where id=9 for update; | |
begin;select * from t where id=9 for update; | |
insert into values(9,9,9);(blocked) | |
insert into values(9,9,9);(ERROR 1213 Deadlock found) |
这个原因在于:
- 首先,间隙锁是排斥插入记录的,而 sessionA 持有间隙锁,所以在 sessionB 尝试插入数据时,被 sessionA 的间隙锁挡住了,进入等待。
- 其次,sessionA 也长孺插入记录,被 sessionB 的间隙锁挡住了,进入等待。
- 这个时候,两个 session 都在尝试等待对方释放间隙锁,从而导致了死锁。
间隙锁申请顺序相反,导致死锁
我们的讨论还是基于下面这个表 t:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);与上面这个例子对应的,是 @发条橙子同学提出的问题:下面这个语句的加锁范围是什么?
begin;
select id from t where c in(5,20,10) lock in share mode;这条查询语句里用的是 in,我们先来看这条语句的 explain 结果。

可以看到,这条 in 语句使用了索引 c 并且 rows=3,说明这三个值都是通过 B+ 树搜索定位的。
在查找 c=5 的时候,先锁住了 (0,5]。但是因为 c 不是唯一索引,为了确认还有没有别的记录 c=5,就要向右遍历,找到 c=10 才确认没有了,这个过程满足优化 2,所以加了间隙锁 (5,10)。
同样的,执行 c=10 这个逻辑的时候,加锁的范围是 (5,10] 和 (10,15);执行 c=20 这个逻辑的时候,加锁的范围是 (15,20] 和 (20,25)。
通过这个分析,我们可以知道,这条语句在索引 c 上加的三个记录锁的顺序是:先加 c=5 的记录锁,再加 c=10 的记录锁,最后加 c=20 的记录锁。
你可能会说,这个加锁范围,不就是从 (5,25) 中去掉 c=15 的行锁吗?为什么这么麻烦地分段说呢?
因为我要跟你强调这个过程:这些锁是“在执行过程中一个一个加的”,而不是一次性加上去的。
理解了这个加锁过程之后,我们就可以来分析下面例子中的死锁问题了。
如果同时有另外一个语句,是这么写的:
select id from t where c in(5,20,10) order by c desc for update;此时的加锁范围,又是什么呢?
这里你需要注意一下,由于语句里面是 order by c desc,这三个记录锁的加锁顺序,是先锁 c=20,然后 c=10,最后是 c=5。
也就是说,这两条语句要加锁相同的资源,但是加锁顺序相反。当这两条语句并发执行的时候,就可能出现死锁。