背景

由于最近在准备换工作,所以开始补充一些基础知识,以前准备的时候总是去硬背一些知识点,这次花了不少时间去问了问为什么,年前对于幻读的内容有了点心得,为了不遗忘,也是为了只有能讲出来才算是真的理解了,借着这边博客自己在复习一下。

幻读的定义

至于Mysql的InnoDB存储引擎的事务的四个隔离级别具体内容我这里就不在赘述了,这里主要说一下幻读这个词,之前我也一直不太明白这个词,现在我尝试这去解释一下。在《高性能Mysql》里对于幻读是这样定义的:

幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。

虽然这本书里是这么说的,而且Myql官方文档里也大概是这个意思,但是我个人来讲的话我觉得这更符合不可重复读的特征,所以更倾向于将上面的理解为不可重复读的一种,至于幻读呢,我更愿意理解为

某一次的select出的结果集不能支撑下一次的操作(这里的操作主要是指插入),更具体些,当前的事务下select某个记录发现不存在,此时进行插入操作,提示记录已经存在,再次select发现记录还是不存在。对于上面的这个现象我认为是发生了幻读。以上是我对幻读概念的理解。

什么情况下会出现幻读

根据事务的四个隔离级别的定义,读未提交,读已提交,可重复读,串行化,前面三个级别下都不能避免幻读的发生,但是在现实中各个不同的数据库在可重复读(RR)的级别下都实现了避免幻读现象的发生。对于Mysql而言是采用的是MVCC+Next-Key实现避免幻读的。那什么是MVCC和Next-Key呢。我们一个一个来说:

MVCC

首先MVCC的中文名字叫做多版本并发控制 (Multiversion concurrency control),这个是用来解决重复读的,保证同一个事务下读取到的数据是一致的,那么它是怎么做到的呢(这里不提供具体的代码,只讲思路)?幻读幻读,强调的是读,在MVCC里有两种读快照读和当前读,

快照读:读取数据的某个版本

当前读:读取当前数据库最新的数据

其中Select操作读取的是快照读,Insert,Update,Delete是当前读。简单来说就是数据库引擎会隐式的给每条记录里存储引擎会给你自动加上两个字段,类似创建时间和删除时间只不过这里存的不是时间戳而是系统的某个版本(可以理解为事务id)。

虽然Select操作读取的是快照,可以理解为每次读取都是读取的历史版本,如此一来表面上看是解决了幻读的问题,但是仔细想想却并非如此,我们假设目前只用MVCC来实现避免幻读的情况,比如说下面的这种情况:

当前的数据;

idnameage1John13

2Mike14

3Bob12

这时有两个事务到来

事务/步骤事务1事务21begin;

2select * from table;begin;
3insert into table(name,age) values('Bruce', 15);
4commit;
5select * from table;
6update table set age = 16 where id = 4;
7select * from table;

数据库的隔离级别为可重复读,我们看到,事务1在第2部和第5步的select的结果是一致的,都是看不到事务2提交的数据的,当前为止确实是避免了可重复读,但是如果此时事务1对后续的id进行修改的话我们发现是可以更新的,更新完成以后再次进行select操作,发现是可以看到事务2提交的那条数据了,并且是经过事务1更新后的版本。

注:以上所描述的场景是了解了MVCC的实现原理以后模拟出得场景,并没有经过实际检验。

虽说上面的场景并没有检验,但是理论上是存在这种情况的,所以,结论是:单独依靠MVCC是不能够解决幻读问题的。

Next-Key Lock

既然MVCC单独不能解决幻读问题,所以Mysql引入了Next-key锁的概念,什么是Next-key锁呢,实际上,他不是一个单独的锁,他是由Record Lock和Gap Lock两个锁共同组成的。

Record Lock: Mysql 索引记录的锁

Gap Lock: 间隙锁