Innodb的RR到底有沒(méi)有解決幻讀?看不懂你打我!
關(guān)于Innodb中的REPEATABLE READ這種隔離級(jí)別到底有沒(méi)有解決幻讀?好像眾說(shuō)紛紜,大家的說(shuō)法都不一致。
有的人說(shuō),RR當(dāng)然沒(méi)解決幻讀了,因?yàn)橹挥蠸erializable才能解決幻讀。
也有人說(shuō),RR解決了幻讀,因?yàn)镽R中加了間隙鎖,就能解決幻讀的問(wèn)題。
還有人說(shuō),只有間隙鎖是沒(méi)用的,還有MVCC也幫助RR解決了幻讀的問(wèn)題。
那到底真實(shí)情況是怎么樣的呢?
?我認(rèn)為,InnoDB中的REPEATABLE READ這種隔離級(jí)別通過(guò)間隙鎖+MVCC解決了大部分的幻讀問(wèn)題,只有一種特殊的幻讀情況無(wú)法解決。
為什么這么說(shuō)呢?這種特殊情況是怎么回事兒呢?本文就來(lái)把這個(gè)問(wèn)題講清楚。(本文中所有SQL的運(yùn)行環(huán)境是MySQL 5.7.9 及MySQL 8.0.30)?
什么是幻讀
?在介紹如何解決幻讀之前,有必要再明確一下什么是幻讀,確保大家理解是一致的。
幻讀就是事務(wù)在做范圍查詢(SELECT)的過(guò)程中,有另外一個(gè)事務(wù)對(duì)范圍內(nèi)新增了記錄(INSERT),導(dǎo)致范圍查詢的結(jié)果條數(shù)不一致的現(xiàn)象。?
有這樣一張表:?
接著我們進(jìn)行如下操作:
在這個(gè)例子中,在事務(wù)1中執(zhí)行了兩次相同的查詢操作。但是兩次操作中間事務(wù)2向數(shù)據(jù)庫(kù)中增加了一條符合事務(wù)1的查詢條件的數(shù)據(jù),最終事務(wù)1的兩次查詢得到的結(jié)果是不一樣的,這種現(xiàn)象就是幻讀。
MVCC與幻讀?
MVCC,是Multiversion Concurrency Control的縮寫(xiě),翻譯過(guò)來(lái)是多版本并發(fā)控制,和數(shù)據(jù)庫(kù)鎖一樣,他也是一種并發(fā)控制的解決方案。它主要用來(lái)解決讀-寫(xiě)并發(fā)的情況。關(guān)于MVCC的原理可以參考《??再有人問(wèn)你什么是MVCC,就把這篇文章發(fā)給他!???》
我們知道,在MVCC中有兩種讀,一種是快照讀、一種是當(dāng)前讀。
所謂快照讀,就是讀取的是快照數(shù)據(jù),即快照生成的那一刻的數(shù)據(jù),像我們常用的普通的SELECT語(yǔ)句在不加鎖情況下就是快照讀。
在 RC 中,每次讀取都會(huì)重新生成一個(gè)快照,總是讀取行的最新版本。
在 RR 中,快照會(huì)在事務(wù)中第一次SELECT語(yǔ)句執(zhí)行時(shí)生成,只有在本事務(wù)中對(duì)數(shù)據(jù)進(jìn)行更改才會(huì)更新快照。
那么也就是說(shuō),如果在RR下,一個(gè)事務(wù)中的多次查詢,是不會(huì)查詢到其他的事務(wù)中的變更內(nèi)容的,所以,也就是可以解決幻讀的。
所以,針對(duì)上面的例子,如果我們把事務(wù)隔離級(jí)別設(shè)置為RR,那么因?yàn)橛辛薓VCC的機(jī)制,就能解決幻讀的問(wèn)題:?
可以看到,同一個(gè)事務(wù)中的兩次查詢結(jié)果是一樣的,就是在RR級(jí)別下,因?yàn)橛锌煺兆x,所以第二次查詢其實(shí)讀取的是一個(gè)快照數(shù)據(jù)。
間隙鎖與幻讀
?上面我們講過(guò)了MVCC能解決RR級(jí)別下面的快照讀的幻讀問(wèn)題,那么當(dāng)前讀下面的幻讀問(wèn)題怎么解決呢?
當(dāng)前讀就是讀取最新數(shù)據(jù),所以,加鎖的 SELECT,或者對(duì)數(shù)據(jù)進(jìn)行增刪改都會(huì)進(jìn)行當(dāng)前讀,比如:?
舉一個(gè)下面的例子:
像上面這種情況,在RR的級(jí)別下,當(dāng)我們使用SELECT … FOR UPDATE的時(shí)候,會(huì)進(jìn)行加鎖,不僅僅會(huì)對(duì)行記錄進(jìn)行加鎖,還會(huì)對(duì)記錄之間的間隙進(jìn)行加鎖,這就叫做間隙鎖(參考:??數(shù)據(jù)庫(kù)的鎖,到底鎖的是什么????)。因?yàn)橛涗浿g的間隙被鎖住了,所以事務(wù)2的插入操作就被阻塞了,一直到事務(wù)1把鎖釋放掉他才能執(zhí)行成功。
因?yàn)槭聞?wù)2無(wú)法插入數(shù)據(jù)成功,所以也就不會(huì)存在幻讀的現(xiàn)象了。所以,在RR級(jí)別中,通過(guò)加入間隙鎖的方式,就避免了幻讀現(xiàn)象的發(fā)生。
解決不了的幻讀
前面我們介紹了快照讀(無(wú)鎖查詢)和當(dāng)前讀(有鎖查詢)下是如何解決幻讀的問(wèn)題的,但是,上面的例子就是幻讀的所有情況了嗎?顯然并不是。
我們說(shuō)MVCC只能解決快照讀的幻讀,那如果在一個(gè)事務(wù)中發(fā)生了當(dāng)前讀,并且在另一個(gè)事務(wù)插入數(shù)據(jù)前沒(méi)來(lái)得及加間隙鎖的話,會(huì)發(fā)生什么呢?
那么,我們稍加修改一下上面的SQL代碼,通過(guò)當(dāng)前讀的方式進(jìn)行查詢數(shù)據(jù):
在上面的例子中,在事務(wù)1中,我們并沒(méi)有在事務(wù)開(kāi)啟后立即加鎖,而是進(jìn)行了一次普通的查詢,然后事務(wù)2插入數(shù)據(jù)成功之后,再通過(guò)事務(wù)1進(jìn)行了2次查詢。
?我們發(fā)現(xiàn),事務(wù)1后面的兩次查詢結(jié)果完全不一樣,沒(méi)加鎖的情況下,就是快照讀,讀到的數(shù)據(jù)就和第一次查詢是一樣的,就不會(huì)發(fā)生幻讀。但是第二次查詢加了鎖,就是當(dāng)前讀,那么讀取到的數(shù)據(jù)就有其他事務(wù)提交的數(shù)據(jù)了,就發(fā)生了幻讀。
那么,如果你理解了上面的這個(gè)例子,并且你也理解了當(dāng)前讀的概念,那么你很容易就能想到,下面的這個(gè)CASE其實(shí)也是會(huì)發(fā)生幻讀的:
這里發(fā)生幻讀的原理,和上面的例子其實(shí)是一樣的,那就是MVCC只能解決快照讀中的幻讀問(wèn)題,而對(duì)于當(dāng)前讀(SELECT FOR UPDATE、UPDATE、DELETE等操作)還是會(huì)產(chǎn)生幻讀的現(xiàn)象的。
UPDATE語(yǔ)句也是一種當(dāng)前讀,所以它是可以讀到其他事務(wù)的提交結(jié)果的。
為什么事務(wù)1的最后一次查詢和倒數(shù)第二次查詢的結(jié)果也不一樣呢?
是因?yàn)楦鶕?jù)快照讀的定義,在RR中,如果本事務(wù)中發(fā)生了數(shù)據(jù)的修改,那么就會(huì)更新快照,那么最后一次查詢的結(jié)果也就發(fā)生了變化。
如何避免幻讀
?那么了解了幻讀的解決場(chǎng)景,以及不能解決的幾個(gè)CASE之后,我們來(lái)總結(jié)一下該如何解決幻讀的問(wèn)題呢?
首先,如果想要徹底解決幻讀的問(wèn)題,在InnoDB中只能使用Serializable這種隔離級(jí)別。
圖源:MySQL 8.0 Reference Manual
那么,如果想在一定程度上解決或者避免發(fā)生幻讀的話,使用RR也可以,但是RC、RU肯定是不行的。
在RR級(jí)別中,能使用快照讀(無(wú)鎖查詢)的就使用快照讀,這樣不僅可以減少鎖沖突,提升并發(fā)度,而且還能避免幻讀的發(fā)生。
那么,如果在并發(fā)場(chǎng)景中,一定要加鎖的話怎么辦呢?那就一定要在事務(wù)一開(kāi)始就立即加鎖,這樣就會(huì)有間隙鎖,也能有效的避免幻讀?的發(fā)生。
但是需要注意的是,間隙鎖是導(dǎo)致死鎖的一個(gè)重要根源~所以,用起來(lái)也需要慎重。?
總結(jié)
在RC級(jí)別中,幻讀是沒(méi)有辦法解決的,因?yàn)镽C中快照讀是每一次都會(huì)重新生成快照,并且RC中也不會(huì)有間隙鎖。
在RR級(jí)別中,因?yàn)橛蠱VCC機(jī)制,對(duì)于普通的無(wú)鎖查詢,這種是屬于快照讀的,RR的快照讀在同一個(gè)事務(wù)中只會(huì)讀一次,所以在事務(wù)過(guò)程中,其他事務(wù)的變更不會(huì)影響到當(dāng)前事務(wù)的查詢結(jié)果。所以這種幻讀是可以解決的。
當(dāng)時(shí),MVCC只能對(duì)快照讀起作用,而對(duì)于加鎖的讀請(qǐng)求,這種屬于當(dāng)前讀,當(dāng)前讀的話是可以查詢到其他事務(wù)的變更的,所以會(huì)產(chǎn)生幻讀。
?想要解決幻讀,可以使用Serializable這種隔離級(jí)別,或者使用RR也能解決大部分的幻讀問(wèn)題。?
在RR級(jí)別下,為了避免幻讀的發(fā)生,要么就是使用快照讀,要么就是在事務(wù)一開(kāi)始就加鎖。?