说实话,从事开发工作三年,一直对幻读的原理并没有搞懂,之前也有看过一些帖子,各有各的说法,但是都“点到为止”,并没有说清楚其中的原理,我也就这样一知半解过来了,在一次看《凤凰架构》一书中,终于理解了幻读的原理,现记录下来分享给大家,若有错误,轻喷谢谢。
疑问的产生
slecte count(1) from books where price < 100 //时间顺序1 事务T1
insert into books(name,price) values ('深入理解Java虚拟机',90) //时间顺序2 事务T2
slecte count(1) from books where price < 100 //时间顺序3 事务T1
以上情况是否会产生幻读?
不会
此种情况可以在很多帖子中看到,无非就是两个事务并发执行的情况下,会产生何种结果。不乏一些帖子会说此种情况也会产生幻读,或者说幻读产生的情况就是在事务T1执行时,事务T2进行了插入,所以T1事务在第二次快照读时产生了幻读。
所以在此已经出现了一定的观点分歧,这让我感到疑惑,但当我看到《凤凰架构》(此后简称为《凤凰》)一文中描述该过程为:一个例子是MySQL/InnerDB的默认隔离级别为可重复读,但他在只读事务中可完全避免幻读问题,譬如上面例子中,事务T1只有查询语句,是一个只读事务,所以上述问题在MySQL中并不会出现。
读到此作者已经给出很明确的答复:是不会出现幻读的,那么有一部分帖子的结论已经存在错误了。
读到此有两个关键点:
- 可重复读事务隔离级别原理
- 为什么在只存在查询语句的情况下不会出现幻读问题
mvcc(多版本并发控制)
以上问题都指向了该作用机制。其实该机制严格遵循以下规则运行:
不妨将版本理解为数据库中每一行记录都从在两个看不见的字段:CREATE_VERSION,DELETE_VERSION
- 插入数据时:CREATE_VERSION记录插入数据的事务ID,DELETE_VERSION为空
- 删除数据时:DELETE_VERSION记录删除数据的事务ID,CREATE_VERSION为空
- 修改数据时:将修改数据视为“删除旧数据,插入新数据”的组合,即先将原有数据复制一份,原有数据的DELETE_VERSION记录修改数据的事务ID,CREATE_VERSION为空。复制后的新数据CREATE_VERSION记录修改数据的事务ID,DELETE_VERSION为空。
可重复读隔离级别的规则为:
总是读取CREATE_VERSION小于或等于当前事务ID的记录,在这个前提下,如果数据仍有多个版本,则取最新的(事务ID最大)。
那么上面的事务执行的例子可以得到解释,事务T2的事务ID肯定是大于事务T1的,对于事务T1,在可重复读的隔离级别下(从命名也能理解,就是能多次读取到相同的数据),事务T1只会读取到小于等于事务T1的数据,由于事务ID的递增,事务T1是不会读取到事务T2插入的数据的。所以在可重复读隔离级别下,不会也不可能产生能读到事务T2的新插入数据的情况,否则可重复读隔离级别的定义将被打破,唯一的例外为以下情况。
幻读的产生
如果大家注意到MVCC在处理修改数据的时候,那么就能够发现关键所在,先给出幻读的产生必要条件:
- 先:事务T2先进行了插入操作
- 后:事务T1进行了范围修改操作
请注意标红的点,必须按照以上顺序才会出现幻读的情况(不考虑当前读,只是采用快照读的情况下),事务T1在进行范围修改的情况下,由于没有间隙锁的约束,连带把事务T2的新插入数据一并修改了,并且新的数据的CREATE_VERSION记录修改数据的事务ID,也就是相当于把本来事务ID大于自身事务ID的不可见数据,刷新成为了自己可见的数据!这就产生了幻读。
我相信只要明白了以上原理的小伙伴,对于读已提交,读未提交也能很快理解了。看到这里幻读产生的原理已经解释完毕了,无非深刻掌握事务隔离级别的定义和MVCC的作用机制就行。以下为防止幻读产生的做法也可以一起顺带学习下,是不是整个原理都融会贯通了?
间隙锁的作用
间隙锁也可称之为范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被写入。详细点为:不仅不能修改该范围内已有的数据,也不能在该范围内新增或者删除任何数据,后者是一组排他锁的集合无法做到的。
在可重复读的隔离级别下,就是没有范围锁来禁止在该范围内插入新的数据,这是一个事务受到其他事务的影响,隔离性被破坏的表现。