幻读

说明

  • 幻读在「当前读」下才会出现

  • 幻读仅专指「新插入的行」

 

什么是幻读?

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

 

image-20200518154707921

其中,Q3读到id=1这一行的现象,称为「幻读」

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

在事务可见性层面并无问题

 

幻读有什么问题?

  • 语义上,sessionA在T1时刻声明「锁住d=5的所有行,不准其它读写操作」。而这个语义被破坏了

  • 数据一致性问题

    image-20200518163725206

由于sessionA把所有的行都加了写锁, 所以sessionB在执行第一个update语句的时候就被锁住了。

需要等到T6时刻,sessionA提交以后,sessionB才能继续执行

所以,上面session B的修改结果,被session A之后的select语句用“当前读”看到,不能称为幻读。

它并不会引发数据一致性问题

binlog执行序列:

 insert into t values(1,1,5); /*(1,1,5)*/
 update t set c=where id=1; /*(1,5,5)*/
 
 update t set d=100 where d=5;/*所有d=5的行,d改成100*/
 
 update t set d=where id=0; /*(0,0,5)*/
 update t set c=where id=0; /*(0,5,5)*/

这个语句序列,拿到备库去执行,或用binlog来克隆一个库:

(5,5,100)、(1,5,100)、 (0,5,5)

id=1这一行,在原数据库里面的结果是(1,5,5),而根据binlog的执行结果是(1,5,100)

新插入的行并不存在,加不了行锁

如何解决幻读?

产生幻读的原因:行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的「间隙」。

因此,为了解决幻读问题,InnoDB引入了「间隙锁」(Gap Lock)

  • 间隙锁,锁的就是两个值之间的空隙。比如文章开头的表t,初始化插入了6个记录,这就产生了7个间隙。

image-20200518175843295

  • 间隙锁和行锁合称next-key lock , Next-key lock前开后闭

如果用select * from t for update要把整个表所有记录锁起来,就形成了7个next-key lock,分别是

(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +suprenum]

  • suprenum

这是因为+∞是开区间。实现上,InnoDB给每个索引加了一个不存在的最大值suprenum,这样才符合我们前面说的“都是前开后闭区间”

  • 间隙锁记为开区间

 

间隙锁引入的性能问题

举例业务逻辑:任意锁住一行,如果这一行不存在插入,存在则更新:

 begin;
 select * from t where id=N for update;
 
 /*如果行不存在*/
 insert into t values(N,N,N);
 /*如果行存在*/
 update t set d=N set id=N;
 
 commit;

问题:一旦有并发,就会碰到死锁

image-20200518170322973

语句分析:

  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语句报错返回了。

 

读提交隔离级别加binlog_format=row的组合

  • 为了解决幻读的问题,我们引入了这么一大串内容,有没有更简单一点的处理方法呢

间隙锁是在可重复读隔离级别下才会生效的

所以,你如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,你要解决可能出现的数据和日志不一致问题,需要把binlog格式设置为row。这,也是现在不少公司使用的配置组合。

posted @ 2020-05-18 17:14  0_0Kelvin  阅读(255)  评论(0)    收藏  举报