当前位置: 首页 > 科技观察

Innodb的RR能解决幻读吗?我不明白你打我!

时间:2023-03-11 22:58:55 科技观察

关于Innodb中REPEATABLEREAD的隔离级别,是否解决幻读?好像众说纷纭,每个人的看法都不一样。有人说RR肯定不能解决幻读,因为只有Seri??alizable才能解决幻读。也有人说RR解决了幻读问题,因为在RR中加入了gaplock,可以解决幻读问题。还有人说只有间隙锁是没有用的,MVCC也帮助RR解决了幻读问题。那么真实情况是怎样的呢?在我看来,InnoDB中REPEATABLEREAD的隔离级别通过间隙锁+MVCC解决了大部分幻读问题,只有一种特殊的幻读情况无法解决。你为什么这么说?这种特殊情况是怎么回事?本文将澄清这个问题。(本文所有SQL的运行环境均为MySQL5.7.9和MySQL8.0.30)什么是幻读?在介绍如何解决幻读之前,有必要明确一下什么是幻读,保证大家理解一致。幻读是事务在范围查询(SELECT)过程中向范围内添加新记录(INSERT),导致范围查询结果个数不一致的现象。有这样一张表:CREATETABLEusers(idINTUNSIGNEDAUTO_INCREMENT,gmt_createDATETIMENOTNULL,ageINTNOTNULL,nameVARCHAR(16)NOTNULL,PRIMARYKEY(id))ENGINE=InnoDB;INSERTINTOusers(gmt_create,age,name)values(now(),18,'Hollis');插入用户(gmt_create,age,name)values(now(),28,'HollisChuang');INSERTINTOusers(gmt_create,age,name)值(现在(),38,'Hollis666');那么我们进行如下处理:在这个例子中,同一个查询操作在事务1中执行了两次。但是,在两次操作中间的事务2向数据库添加了一条满足事务1查询条件的数据,并且事务1的两次查询结果不同。这种现象就是幻读。MVCC和幻读MVCC是MultiversionConcurrencyControl的缩写。它转化为多版本并发控制。和数据库锁一样,也是一种并发控制方案。主要用来解决读写并发的情况。MVCC的原理可以参考《??再有人问你什么是MVCC,就把这篇文章发给他!???》。我们知道MVCC中有两种读取,一种是快照读取,一种是当前读取。所谓snapshotread就是读取快照数据,也就是快照生成那一刻的数据,像我们经常使用的普通SELECT语句,就是不带锁的snapshotread。SELECT*FROMxx_tableWHERE...在RC中,每次读取都会生成一个新快照,始终读取行的最新版本。在RR中,快照会在事务中执行第一条SELECT语句时产生,只有在本次事务中数据发生变化时才会更新快照。也就是说如果在RR下,一个事务中的多次查询不会查询到其他事务中变化的内容,这样就可以解决幻读。因此,对于上面的例子,如果我们将事务隔离级别设置为RR,那么因为MVCC机制,就可以解决幻读问题:可以看到同一个事务中两次查询的结果是一样的,也就是在RR层面,因为有snapshotread,所以第二次query实际上是读取了一个snapshot数据。间隙锁和幻读我们上面提到MVCC可以解决RR级别以下快照读的幻读问题,那么当前读下幻读问题怎么解决呢?当前读取是读取最新的数据。因此,加锁的SELECT,或者对数据的增删改查,都会执行当前读,如:SELECT*FROMxx_tableLOCKINSHAREMODE;SELECT*FROMxx_tableFORUPDATE;INSERTINTOxx_table...DELETEFROMxx_table...UPDATExx_table...举个例子:像上面的情况,在RR层面,当我们使用SELECT...FORUPDATE时,会加锁,不仅仅是针对行记录被锁定,记录之间的缝隙也被锁定。这叫做间隙锁(参考:数据库锁,到底什么是锁?)。因为记录之间的间隙被加锁,事务2的插入操作被阻塞,需要事务1释放锁才能执行成功。因为事务2无法成功插入数据,所以不会出现幻读。所以在RR层面,通过加间隙锁,避免了幻读的发生。无法解决的幻读我们介绍了快照读(无锁查询)和当前读(锁定查询)下幻读问题的解决方法,但是上面的例子都是幻读的情况吗?很明显不是。我们说了MVCC只能解决快照读的幻读,那么如果当前读发生在一个事务中,来不及在另一个事务插入数据之前加间隙锁怎么办?然后,我们稍微修改一下上面的SQL代码,通过当前的读取方式查询数据:在上面的例子中,在事务1中,我们在事务打开后并没有立即锁定事务,而是进行了一次普通的Query,事务之后2成功插入数据,通过事务1进行2次查询。我们发现事务1之后的两次查询结果完全不同。如果没有锁,就是快照读。读取的数据和第一次查询一样,不会出现幻读。但是第二个查询加了锁,就是当前读,那么读到的数据有其他事务提交的数据,就出现了幻读。好吧,如果你理解了上面的例子,你也理解了当前阅读的概念,那么你很容易想象幻读实际上会发生在下面的情况下:这里的幻读原理和上面的例子其实是一样的同样的,就是MVCC只能解决快照读中的幻读问题,但是对于当前读(SELECTFORUPDATE、UPDATE、DELETE等)还是会出现幻读。UPDATE语句也是一个当前读,所以它可以读取其他事务的提交结果。为什么事务1最后一次查询的结果和倒数第二次查询的结果不一样?因为根据快照读取的定义,在RR中,如果本次事务发生数据修改,快照会更新,上次查询的结果也会发生变化。如何避免幻读在了解了幻读的解决场景和无法解决的几种情况后,我们来总结一下如何解决幻读问题?首先,要想彻底解决幻读问题,只能使用InnoDB中Serializable的隔离级别。来源:MySQL8.0参考手册所以,如果想在一定程度上解决或避免幻读,可以使用RR,但是RC和RU是肯定不行的。在RR层面,可以使用快照读(无锁查询),这样既可以减少锁冲突,提高并发性,又可以避免幻读。那么,如果您必须在并发场景中锁定怎么办?那么在事务开始的时候就必须马上加锁,这样才会有间隙锁,可以有效的避免幻读的发生。但是需要注意的是,间隙锁是一个重要的死锁来源~所以,在使用的时候需要格外小心。总结在RC层面,没有办法解决幻读,因为在RC中读snapshot每次都会重新生成snapshot,RC中不会有gaplock。在RR层面,因为MVCC机制,对于普通的无锁查询,这是一次快照读,RR快照读在同一个事务中只会读一次,所以在事务处理过程中,其他事务的变化会不会影响当前事务的查询结果。所以这种幻读是可以解决的。那时MVCC只能作用于快照读,但是对于加锁的读请求,这是当前读,当前读可以查询其他事务的变化,所以会出现幻读。解决幻读可以使用Serializable的隔离级别,或者使用RR来解决大部分的幻读问题。在RR层面,为了避免幻读的发生,要么使用快照读,要么在事务开始时加锁。