拿捏!隔離級(jí)別、幻讀、Gap Lock、Next-Key Lock
前面我寫(xiě)了很多Mysql相關(guān)的知識(shí)點(diǎn),到這一篇稍微可以串一下了,從SQL執(zhí)行流程、MVCC到鎖,很多時(shí)候可能覺(jué)得對(duì)于間隙鎖和Next-Key Lock好像已經(jīng)理解了,但是好像又覺(jué)得理解差那么一點(diǎn)意思,這篇文章從頭來(lái)梳理一下概念,明確一下這些知識(shí)。
鎖
首先,對(duì)于Mysql來(lái)說(shuō)實(shí)現(xiàn)了兩種行級(jí)鎖:
共享鎖:允許事務(wù)讀一行數(shù)據(jù),一般記為S,也稱為讀鎖
排他鎖:允許事務(wù)刪除或者更新一行數(shù)據(jù),一般記為X,也稱為寫(xiě)鎖
關(guān)于讀寫(xiě)鎖的互斥性,應(yīng)該都很清楚,讀鎖只能和讀鎖兼容,其他場(chǎng)景都無(wú)法兼容,這里不再贅述吧。

隔離級(jí)別
繼續(xù)回顧下關(guān)于Mysql的4個(gè)隔離級(jí)別:
讀未提交Read Uncommitted:能讀到其他事務(wù)還沒(méi)有提交的數(shù)據(jù),這種現(xiàn)象叫做臟讀。
讀已提交Read Committed:只會(huì)讀取其他事務(wù)已經(jīng)提交的數(shù)據(jù),所以不會(huì)產(chǎn)生RC的臟讀問(wèn)題。所以又帶來(lái)一個(gè)問(wèn)題叫做不可重復(fù)讀,一個(gè)事務(wù)中兩次一樣的SQL查詢可能查到的結(jié)果不一樣。
可重復(fù)讀Repeatable Read:RR是Mysql的默認(rèn)隔離級(jí)別,一個(gè)事務(wù)中兩次SQL查詢總是會(huì)查到一樣的結(jié)果,不存在不可重復(fù)讀的問(wèn)題,但是還是會(huì)有幻讀的問(wèn)題。
串行Serializable:串行場(chǎng)景沒(méi)有任何問(wèn)題,完全串行化的操作,讀加讀鎖,寫(xiě)加寫(xiě)鎖。

幻讀、Next-Key Lock、MVCC
簡(jiǎn)單的回顧完了基礎(chǔ),那么我們看看RR級(jí)別下還會(huì)存在的幻讀到底是什么問(wèn)題,Mysql官方文檔這樣描述的:
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a
SELECTis executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
翻譯過(guò)來(lái)就是,幻讀指的是同一事務(wù)下,不同的時(shí)間點(diǎn),同樣的查詢,得到不同的行記錄的集合。
如果說(shuō)一個(gè)select執(zhí)行了兩次,但是第二次比第一次多出來(lái)行記錄,這就是幻讀。
所以,對(duì)于幻讀來(lái)說(shuō)那一定是新增插入的數(shù)據(jù)!
比如說(shuō)在一個(gè)事務(wù)內(nèi),先查詢select * from user where age=10 for update,得到的結(jié)果是id為[1,2,3]的記錄,再次執(zhí)行查詢,得到了結(jié)果為[1,2,3,4]的記錄,這是幻讀。
那怎么解決幻讀的問(wèn)題?以前我在文章里說(shuō)解決幻讀的原理是MVCC(MVCC原理看這里)很多網(wǎng)上的文章也有這么寫(xiě)的,其實(shí)不能說(shuō)錯(cuò),但是肯定也是不太對(duì)的,準(zhǔn)確地來(lái)說(shuō)應(yīng)該是通過(guò)MVCC+Next-Key Lock的方式才解決了幻讀的問(wèn)題。
對(duì)于MVCC中的讀可以分為兩種,分別叫做快照讀和當(dāng)前讀(這個(gè)當(dāng)前讀的說(shuō)法我在書(shū)里翻了半天也沒(méi)有找到,但是看網(wǎng)上一堆資料和大佬都叫當(dāng)前讀,那么我們就叫當(dāng)前讀吧,你知道的話可以告訴我哪本書(shū)有這個(gè)稱呼,Mysql我只看見(jiàn)Lock reading或者鎖定讀的叫法,有的也說(shuō)鎖定讀就是當(dāng)前讀,但是并沒(méi)有找到當(dāng)前讀這種稱呼的出處在哪兒)。
快照讀就是簡(jiǎn)單的select查詢,查詢的都是快照版本,這個(gè)場(chǎng)景下因?yàn)槎际腔贛VCC來(lái)查詢快照的某個(gè)版本,所以不會(huì)存在幻讀的問(wèn)題,也可以認(rèn)為是解決了幻讀的方案之一,對(duì)于RC級(jí)別來(lái)說(shuō),因?yàn)槊看尾樵兌贾匦律梢粋€(gè)read view,也就是查詢的都是最新的快照數(shù)據(jù),所以會(huì)可能每次查詢到不一樣的數(shù)據(jù),造成不可重復(fù)讀,而對(duì)于RR級(jí)別來(lái)說(shuō)只有第一次的時(shí)候生成read view,查詢的是事務(wù)開(kāi)始的時(shí)候的快照數(shù)據(jù),所以就不存在不可重復(fù)讀的問(wèn)題,當(dāng)然就更不可能有幻讀的問(wèn)題了。
所以,現(xiàn)在我們說(shuō)幻讀,其實(shí)不是指快照讀的場(chǎng)景,而是指的是當(dāng)前讀的場(chǎng)景。
當(dāng)前讀指的是lock in share mode、for update 、insert、update、delete這些需要加鎖的操作。對(duì)于MVCC來(lái)說(shuō)就是解決的快照讀的場(chǎng)景,而對(duì)于當(dāng)前讀那么就是Next-Key Lock要解決的事情。
那么Next-Key Lock是什么?怎么解決的幻讀?
行鎖有寫(xiě)鎖X和讀鎖S兩種,實(shí)際上行鎖有3種實(shí)現(xiàn)算法,Next-Key Lock是其中之一。
第一種叫做Record Lock,字面意思,行記錄的鎖,實(shí)際上指的是對(duì)索引記錄的鎖定。
比如執(zhí)行語(yǔ)句select * from user where age=10 for update,將會(huì)鎖住user表所有age=10的行記錄,所有對(duì)age=10的記錄的操作都會(huì)被阻塞。
第二種都比較熟悉,叫做Gap Lock,也就是間隙鎖,它用于鎖定的索引之間的間隙,但是不會(huì)包含記錄本身。
比如語(yǔ)句select * from user where age>1 and age<10 for update,將會(huì)鎖住age在(1,10)的范圍區(qū)間,此時(shí)其他事務(wù)對(duì)該區(qū)間的操作都會(huì)被阻塞。
間隙鎖是可重復(fù)讀RR隔離級(jí)別下特有的,另外還有幾種場(chǎng)景也會(huì)不使用間隙鎖。
事務(wù)隔離級(jí)別設(shè)置為讀已提交RC ,這樣肯定沒(méi)有間隙鎖了。
Innodb_locks_unsafe_for_binlog設(shè)置為1另外一種情況適用于主鍵索引或者唯一索引的等值查詢條件,比如
select * from user where id=1,id是主鍵索引,這樣只使用Record Lock就可以了,因?yàn)槟芪ㄒ绘i定一條記錄,所以沒(méi)有必要再加間隙鎖了,這是鎖降級(jí)的過(guò)程。
而第三種Next-Key Lock實(shí)際上就是相當(dāng)于Record Lock+Gap Lock的組合。比如索引有10,20,30幾個(gè)值,那么被鎖住的區(qū)間可能會(huì)是(-∞,10],(10,20],(20,30],(30,+∞)。
解決幻讀
上一篇關(guān)于更新SQL執(zhí)行過(guò)程我們已經(jīng)對(duì)這個(gè)基礎(chǔ)有了一定的了解,在這里我們?nèi)サ艉瓦@里內(nèi)容無(wú)關(guān)的一些日志的細(xì)節(jié),把給數(shù)據(jù)加鎖的流程加入進(jìn)去,這樣通過(guò)SQL執(zhí)行可以更好地理解Next-Key Lock到底是如何解決幻讀的,執(zhí)行過(guò)程如下:

首先第一步Server層會(huì)來(lái)查詢數(shù)據(jù) 存儲(chǔ)引擎根據(jù)查詢條件查到數(shù)據(jù)之后對(duì)數(shù)據(jù)進(jìn)行加鎖,Record Lock或者間隙鎖,然后返回?cái)?shù)據(jù) Server層拿到數(shù)據(jù)之后調(diào)用API去存儲(chǔ)引擎更新數(shù)據(jù) 最后存儲(chǔ)引擎返回結(jié)果,流程結(jié)束
搞一張表說(shuō)明一下,user表有4個(gè)字段,id是主鍵索引,name是唯一索引,age是普通索引,city沒(méi)有索引,然后插入一些測(cè)試數(shù)據(jù),下面區(qū)分一下幾種情況來(lái)說(shuō)明是怎么加Next-Key Lock的,然后就知道為啥會(huì)沒(méi)有幻讀的問(wèn)題了。

沒(méi)有索引
更新語(yǔ)句update user set city='nanjing' where city='wuhan'會(huì)發(fā)生什么?
因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 100, 65);">city是沒(méi)有索引的,所以存儲(chǔ)引擎只能給所有的記錄都加上鎖,然后把數(shù)據(jù)都返回給Server層,然后Server層把city改成nanjing,再更新數(shù)據(jù)。
因此,首先Record Lock會(huì)鎖住現(xiàn)有的7條記錄,間隙鎖則會(huì)對(duì)主鍵索引的間隙全部加上間隙鎖。
所以,更新的時(shí)候沒(méi)有索引是非??膳碌囊患虑椋喈?dāng)于把整個(gè)表都給鎖了,那表都給鎖了當(dāng)然不存在幻讀了。

普通索引
我們?cè)偌僭O(shè)一個(gè)語(yǔ)句select * from user where age=20 for update。
因?yàn)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(255, 100, 65);">age是一個(gè)普通索引,存儲(chǔ)引擎根據(jù)條件過(guò)濾查到所有匹配age=20的記錄,給他們加上寫(xiě)鎖,間隙鎖會(huì)加在(10,20),(20,30)的區(qū)間上,因此現(xiàn)在無(wú)論怎樣都無(wú)法插入age=20的記錄了
為什么要鎖定這兩個(gè)區(qū)間?如果不鎖定這兩個(gè)區(qū)間的話,那么還能插入比如id=11,age=20或者id=21,age=20的記錄,這樣就存在幻讀了。
(那實(shí)際上寫(xiě)鎖不光是在會(huì)加在age普通索引上,還會(huì)加在主鍵索引上,因?yàn)閿?shù)據(jù)都是在主鍵索引下對(duì)吧,這個(gè)肯定也要加鎖的,為了看起來(lái)簡(jiǎn)單點(diǎn),就不畫(huà)出來(lái)了)

唯一&主鍵索引
如果查詢的是唯一索引又會(huì)發(fā)生什么呢?比如有查詢語(yǔ)句select * from user where name='b' for update。
上面我們提到過(guò),如果是唯一索引或者主鍵索引的話,并且是等值查詢,實(shí)際上會(huì)發(fā)生鎖降級(jí),降級(jí)為Record Lock,就不會(huì)有間隙鎖了。
因?yàn)橹麈I或者唯一索引能保證值是唯一的,所以也就不需要再增加間隙鎖了。
很顯然,是無(wú)法插入name=b的的記錄的,也不存在幻讀問(wèn)題。
如果是范圍查詢比如id>1 and id<11呢,實(shí)際上也是一樣的鎖定方式,不再贅述。
相比稍微有點(diǎn)不同的是上面也說(shuō)過(guò),唯一索引不光鎖定唯一索引,還會(huì)鎖定主鍵索引,主鍵索引的話只要索引主鍵索引就行了。

總結(jié)
那最后說(shuō)了這么多,RR級(jí)別下不是都已經(jīng)解決了幻讀的問(wèn)題嗎,怎么還說(shuō)有幻讀的問(wèn)題呢?
關(guān)于這個(gè)問(wèn)題,可以看看這個(gè)報(bào)出的BUGhttps://bugs.mysql.com/bug.php?id=63870,回復(fù)說(shuō)了這不是BUG,這是符合隔離規(guī)范的設(shè)計(jì),有興趣的自己看看吧。


往期推薦
聽(tīng)說(shuō)你對(duì)explain 很懂?
面試官:你說(shuō)說(shuō)一條更新SQL的執(zhí)行過(guò)程?
面試官:你說(shuō)說(shuō)一條查詢SQL的執(zhí)行過(guò)程?| 文末送書(shū)
