间隙锁的作用

间隙锁是为了解决幻读而出现的。

幻读产生的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的间隙,所以为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)。需要注意的是,幻读是可重读读解决的问题,所以间隙锁也是可重复读级别下的机制。

顾名思义,间隙锁,锁的就是两个值之间的间隙,比如下面的表,有6个记录,那么就有了7个间隙:

就是两个相邻的记录之间的间隙。

在读取一行的时候,不仅给行加上了行锁,还给行两边的空隙,加上了间隙锁

间隙锁之间是不存在冲突的,但是往加了间隙锁的间隙里插入一个记录是与这个间隙锁冲突的

间隙锁和行锁合成next-key lock,每个next-key lock都是前开后闭区间,比如上面的有6个记录的表被锁起来,就有了七个next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。

间隙锁的引入虽然解决了幻读的问题,但是会导致同样的语句锁住更大的范围,这其实是影响了并发度的。

加锁规则

两个原则、两个优化和一个bug。

  • 原则一:加锁的基本单位是next-key lock。
  • 原则二:查询过程中访问到的对象才会加锁。
  • 优化一:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
  • 优化二:索引上的等值查询,向右遍历且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
  • 一个bug:唯一索引上的范围查询会查询到不满足条件的第一个值为止。

以下表为例进行验证:

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);

案例一:等值查询间隙锁

  1. 根据原则1,加锁单位是next-key lock,session A的加锁范围就是(5,10]
  2. 根据优化2,这是一个等值查询,且id=10不满足等值条件,next-key lock退化成间隙锁,因此最终的加锁范围是(5,10)。 所以session B被阻塞,而session C能够执行。

案例二:非唯一索引等值锁

  1. 根据原则 1,加锁单位是 next-key lock,因此会给 (0,5]加上 next-key lock。
  2. 要注意 c 是普通索引,因此仅访问 c=5 这一条记录是不能马上停下来的,需要向右遍历,查到 c=10 才放弃。根据原则 2,访问到的都要加锁,因此要给 (5,10]加 next-key lock。
  3. 但是同时这个符合优化 2:等值判断,向右遍历,最后一个值不满足 c=5 这个等值条件,因此退化成间隙锁 (5,10)。
  4. 根据原则 2 ,只有访问到的对象才会加锁,这个查询使用覆盖索引,并不需要访问主键索引,所以主键索引上没有加任何锁,这就是为什么 session B 的 update 语句可以执行完成。

注意,for update: 会给主键索引加锁, 而 in shared mode: 如果有覆盖索引优化,没有访问到主键索引,那么主键索引就不会加锁。

所以要想通过lock in share mode给行加读锁来避免数据被更新,就需要绕过覆盖索引的优化,在查询字段中引入索引中不存在的字段

案例三:主键索引范围锁

下面两条语句虽然逻辑一致,但是加锁的范围并不一致

mysql> select * from t where id=10 for update;
mysql> select * from t where id>=10 and id<11 for update;

作为主键索引上的等值查询,在找到对应的行之后就会退化为行锁。

而第二条查询语句还会继续往下查不满足条件的第一个记录,是个范围查询,会加上间隙锁。

案例四:非唯一索引范围锁

加锁规则跟案例三唯一的不同是:在第一次用 c=10 定位记录的时候,索引 c 上加了 (5,10]这个 next-key lock 后,由于索引 c 是非唯一索引,没有优化规则,也就是说不会蜕变为行锁,因此最终 sesion A 加的锁是,索引 c 上的 (5,10] 和 (10,15] 这两个 next-key lock。

案例五:唯一索引范围锁bug

按理说,id作为主键,值是唯一的。所以在找到15之后应该就可以停止继续找了,但是实现上,InnoDB 会往前扫描到第一个不满足条件的行为止,也就是 id=20。而且由于这是个范围扫描,因此索引 id 上的 (15,20]这个 next-key lock 也会被锁上。

案例六:非唯一索引上存在等值的例子

比如c字段上建立的索引是可以存在多个相同的c值的,但是二级索引上包含id,而id是唯一的,所以是不可能存在“相同”的两行的。

虽然两个c值相同,但是他们之间也是存在间隙的。也会按照加锁的原则进行加锁,不过会带上主键id。

案例七:limit语句加锁

这里c=10的记录也就两条,但是session B却可以执行成功,这就是因为加上了limit之后,遍历到两条记录之后直接就会结束,不会继续向后找不满足的第一条记录,这样就是的锁的范围缩小了,session B能够顺利执行。

所以在实践中,删除语句尽量加limit

案例八:一个死锁的例子

next-key lock是间隙锁+行锁,并且next-key lock加锁的步骤是分为两步的,先是加上间隙锁,然后才会加行锁。 看下面的例子:

  1. sessionA会加上next-key lock(5,10]和间隙锁(10,15)
  2. sessionB也要在索引C上加上next-key lock (5,10],进入锁等待。
  3. 然后 session A 要再插入 (8,8,8) 这一行,被 session B 的间隙锁锁住。由于出现了死锁,InnoDB 让 session B 回滚。 之所以出现这种情况,是因为sessionB现加上间隙锁(5,10)成功,然后申请行锁进入等待,而session A的insert操作又被session B的间隙锁阻塞,造成死锁。