一.几个基本概念

  1. 行锁:给某一行加的锁
  2. 间隙锁:就是两个值之间的间隙。为了解决幻读问题,InnoDB 只好引入新的锁,也就是 间隙锁 (Gap Lock)。
  3. 间隙锁Gap,左右都是开区间,间隙锁+行锁合称next-key lock,每个 next-key lock 是前开后闭区间。间隙锁和next-key lock的引入帮我们解决幻读问题。

二. 什么是幻读?

幻读指的是一个事务在 前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

间隙锁_mysql


可以看到,session A 里执行了三次查询,分别是 Q1、Q2 和 Q3。它们的 SQL 语句相 同,都是 select * from t where d=5 for update。这个语句的意思你应该很清楚了,查 所有 d=5 的行,而且使用的是当前读,并且加上写锁。现在,我们来看一下这三条 SQL 语句,分别会返回什么结果。

  1. Q1 只返回 id=5 这一行;
  2. 在 T2 时刻,session B 把 id=0 这一行的 d 值改成了 5,因此 T3 时刻 Q2 查出来的是
    id=0 和 id=5 这两行;
  3. 在 T4 时刻,session C 又插入一行(1,1,5),因此 T5 时刻 Q3 查出来的是 id=0、
    id=1 和 id=5 的这三行。

其中,Q3 读到 id=1 这一行的现象,被称为“幻读”。也就是说,幻读指的是一个事务在 前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。

PS:说明

  1. 在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因 此,幻读在“当前读”下才会出现。
  2. 上面 session B 的修改结果,被 session A 之后的 select 语句用“当前读”看到,不 能称为幻读。幻读仅专指“新插入的行”

三.为什么会出现幻读

即便你把所有的行都加上锁,一旦有别的会话插入新的记录,还是无法阻止幻读的产生,如上图的session C,可以通过间隙锁来解决这个问题。

间隙锁_死锁_02

  1. session A 执行 select … for update 语句,由于 id=9 这一行并不存在,因此会加上 间隙锁 (5,10);
  2. session B 执行 select … for update 语句,同样会加上间隙锁 (5,10),间隙锁之间不 会冲突,因此这个语句可以执行成功;
  3. session B 试图插入一行 (9,9,9),被 session A 的间隙锁挡住了,只好进入等待; 4. session A 试图插入一行 (9,9,9),被 session B 的间隙锁挡住了。
    至此,两个 session 进入互相等待状态,形成死锁。当然,InnoDB 的死锁检测马上就发 现了这对死锁关系,让 session A 的 insert 语句报错返回了。
    间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了 并发度的。