MySQL中幻读和幻读存在的问题

 

 

 

 

一、概念

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

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

 

Q1只返回id=5这一行;并发

 

在T2时刻,session B把id=0这一行的d值改为了5,所以T3时刻Q2查出来的是id=0和id=5这两行;设计

 

在T4时刻,session C又插入一行(1,1,5),所以T5时刻Q3查出来的是id=0、id=1和id=5的这三行。3d

 

其中,Q3读到id=1这一行的现象,被称为“幻读”。日志

 

幻读的说明:blog

 

在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。所以,幻读在“当前读”下才会出现。索引

 

上面session B的修改结果,被session A以后的select语句用“当前读”看到,不能称为幻读。幻读仅专指“新插入的行”。事务

二、幻读的问题

(1)首先是语义上面的。io

 

session A在T1时刻就声明了,“我要把全部d=5的行锁住,不许别的事务进行读写操做”。而实际上,这个语义被破坏了。

 

session B的第二条语句update t set c=5 where id=0,语义是“我把id=0、d=5这一行的c值,改为了5”。

 

因为在T1时刻,session A 还只是给id=5这一行加了行锁, 并无给id=0这行加上锁。所以,session B在T2时刻,是能够执行这两条update语句的。这样,就破坏了 session A 里Q1语句要锁住全部d=5的行的加锁声明。

 

session C也是同样的道理,对id=1这一行的修改,也是破坏了Q1的加锁声明。

 

(2)其次,是数据一致性的问题

 

锁的设计是为了保证数据的一致性。而这个一致性,不止是数据库内部数据状态在此刻的一致性,还包含了数据和日志在逻辑上的一致性。

 

为了说明这个问题,我给session A在T1时刻再加一个更新语句,即:update t set d=100 where d=5。

 

 

update的加锁语义和select ...for update 是一致的,因此这时候加上这条update语句也很合理。session A声明说“要给d=5的语句加上锁”,就是为了要更新数据,新加的这条update语句就是把它认为加上了锁的这一行的d值修改为了100。

 

如今,咱们来分析一下上图执行完成后,数据库里会是什么结果。

 

通过T1时刻,id=5这一行变成 (5,5,100),固然这个结果最终是在T6时刻正式提交的;

 

通过T2时刻,id=0这一行变成(0,5,5);

 

通过T4时刻,表里面多了一行(1,5,5);

 

其余行跟这个执行序列无关,保持不变。

 

这样看,这些数据也没啥问题,可是咱们再来看看这时候binlog里面的内容。

 

T2时刻,session B事务提交,写入了两条语句;

 

T4时刻,session C事务提交,写入了两条语句;

 

T6时刻,session A事务提交,写入了update t set d=100 where d=5 这条语句。

 

这个语句序列,不管是拿到备库去执行,仍是之后用binlog来克隆一个库,这三行的结果,都变成了 (0,5,100)、(1,5,100)和(5,5,100)。

图 4 假设扫描到的行都被加上了行锁

因为session A把全部的行都加了写锁,因此session B在执行第一个update语句的时候就被锁住了。须要等到T6时刻session A提交之后,session B才能继续执行。

 

 

这样对于id=0这一行,在数据库里的最终结果仍是 (0,5,5)。在binlog里面,执行序列是这样的:

 

insert into t values(1,1,5); /*(1,1,5)*/

update t set c=5 where id=1; /*(1,5,5)*/

 

update t set d=100 where d=5;/*全部d=5的行,d改为100*/

 

update t set d=5 where id=0; /*(0,0,5)*/

update t set c=5 where id=0; /*(0,5,5)*/

 

能够看到,按照日志顺序执行,id=0这一行的最终结果也是(0,5,5)。因此,id=0这一行的问题解决了。

 

但同时你也能够看到,id=1这一行,在数据库里面的结果是(1,5,5),而根据binlog的执行结果是(1,5,100),也就是说幻读的问题仍是没有解决。为何咱们已经这么“凶残”地,把全部的记录都上了锁,仍是阻止不了id=1这一行的插入和更新呢?

 

缘由很简单。在T3时刻,咱们给全部行加锁的时候,id=1这一行还不存在,不存在也就加不上锁。

 

 

也就是说,即便把全部的记录都加上锁,仍是阻止不了新插入的记录,这也是为何“幻读”会被单独拿出来解决的缘由。

 

 

三、如何解决幻读的问题

产生幻读的缘由是,行锁只能锁住行,可是新插入记录这个动做,要更新的是记录之间的“间隙”。所以,为了解决幻读问题,InnoDB只好引入新的锁,也就是间隙锁(Gap Lock)。

 

 

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

这样,当你执行 select * from t where d=5 for update的时候,就不止是给数据库中已有的6个记录加上了行锁,还同时加了7个间隙锁。这样就确保了没法再插入新的记录。

 

也就是说这时候,在一行行扫描的过程当中,不只将给行加上了行锁,还给行两边的空隙,也加上了间隙锁。

 

如今你知道了,数据行是能够加上锁的实体,数据行之间的间隙,也是能够加上锁的实体。可是间隙锁跟咱们以前碰到过的锁都不太同样。

 

好比行锁,分红读锁和写锁。下图就是这两种类型行锁的冲突关系。

也就是说,跟行锁有冲突关系的是“另一个行锁”。

 

可是间隙锁不同,跟间隙锁存在冲突关系的,是“往这个间隙中插入一个记录”这个操做。间隙锁之间都不存在冲突关系。

 

间隙锁和行锁合称next-key lock,每一个next-key lock是前开后闭区间。也就是说,咱们的表t初始化之后,若是用select * from t for update要把整个表全部记录锁起来,就造成了7个next-key lock,分别是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +suprenum]。

 

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

 

间隙锁和next-key lock的引入,帮咱们解决了幻读的问题,但同时也带来了一些“困扰”。

 

间隙锁的引入,可能会致使一样的语句锁住更大的范围,这实际上是影响了并发度的

 

 

四、小结

 

即便给全部的行都加上行锁,仍然没法解决幻读问题,所以引入了间隙锁的概念。

 

行锁确实比较直观,判断规则也相对简单,间隙锁的引入会影响系统的并发度,也增长了锁分析的复杂度,但也有章可循。

相关文章
相关标签/搜索