幻讀是啥,會有什么問題?如何解決?
微信公眾號:歡少的成長之路,來個 點贊,轉(zhuǎn)發(fā),在看!
大家好,我是Leo,上篇文章大概介紹了為什么查詢一條記錄性能慢的原因。今天我們介紹一下幻讀的一些相關(guān)知識,以及幻讀相關(guān)的間隙鎖,間隙鎖死鎖的解決方案。

概念
可重復讀
兩個事務(wù)進行數(shù)據(jù)操作他們是互不干擾的 ,事務(wù)先A進行數(shù)據(jù)查詢,事務(wù)B進行一次事務(wù)修改并進行數(shù)據(jù)提交,事務(wù)A再進行一次查詢,數(shù)據(jù)是不改變的
提交讀
兩個事務(wù)進行數(shù)據(jù)操作,事務(wù)先A進行數(shù)據(jù)查詢,事務(wù)B進行一次事務(wù)修改并進行數(shù)據(jù)提交,事務(wù)A再進行一次查詢,數(shù)據(jù)是B修改后的數(shù)據(jù)。
案例
幻讀是什么
如下圖所示,我們一起分析一下。
sessionA首先開啟了一個事務(wù)并且在T1時刻給d為5的數(shù)據(jù)加上了寫鎖
sessionB沒有開啟事務(wù)。修改了id為0的數(shù)據(jù),把d改成了5
sessionA繼續(xù)執(zhí)行了d=5的數(shù)據(jù)加上了寫鎖
sessionC插入了一條數(shù)據(jù)115
sessionA再次查詢數(shù)據(jù)就發(fā)現(xiàn)數(shù)據(jù)一直在變,一直在多
這種從事務(wù)開啟到事務(wù)結(jié)束,如果同一個數(shù)據(jù)看到不同的結(jié)果。我們就稱為幻讀。
for update 加了寫鎖都是 當前讀。而當前讀的規(guī)則就是看到所有已經(jīng)提交過的數(shù)據(jù)。
幻讀有什么問題
如下圖所示,我們繼續(xù)分析一下
session B 的第二條語句 update t set c=5 where id=0,語義是“我把 id=0、d=5 這一行的 c 值,改成了 5”。
由于在 T1 時刻,session A 還只是給 id=5 這一行加了行鎖, 并沒有給 id=0 這行加上鎖。因此,session B 在 T2 時刻,是可以執(zhí)行這兩條 update 語句的。這樣,就破壞了 session A 里 Q1 語句要鎖住所有 d=5 的行的加鎖聲明。
session C 也是一樣的道理,對 id=1 這一行的修改,也是破壞了 Q1 的加鎖聲明。
以上是語義上的問題。下面還有數(shù)據(jù)一致性上的問題
我們知道,鎖的設(shè)計是為了保證數(shù)據(jù)的一致性。而這個一致性,不止是數(shù)據(jù)庫內(nèi)部數(shù)據(jù)狀態(tài)在此刻的一致性,還包含了數(shù)據(jù)和日志在邏輯上的一致性。
如下圖,我們繼續(xù)分析會有什么問題。
為了說明這個問題,我給 session A 在 T1 時刻再加一個更新語句,即:update t set d=100 where d=5。
update跟for update的含義是一樣的。都是給d為5的數(shù)據(jù)加鎖。然后修改成d為100
sessionA在T1時刻,會給d為5的數(shù)據(jù)加鎖。并且修改d為100(不提交)
sessionB在T2時刻,會修改id為0的數(shù)據(jù)改成d,c為5。(提交)
回到了sessionA的T3時刻,再次查詢加寫鎖
sessionC在T4時刻,執(zhí)行了插入語句,修改id為1的數(shù)據(jù)c為5.(提交)
這樣看好像也沒啥邏輯和一致性問題。再來看一下binlog日志
update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/
update t set d=100 where d=5;/*所有d=5的行,d改成100*/
你會發(fā)現(xiàn)在執(zhí)行這三行結(jié)果都變成了(0,5,100)、(1,5,100) 和 (5,5,100)。也就說有兩條數(shù)據(jù)被改了。
那么我們應(yīng)該怎么改?如下圖,加了鎖的
由于 session A 把所有的行都加了寫鎖,所以 session B 在執(zhí)行第一個 update 語句的時候就被鎖住了。需要等到 T6 時刻 session A 提交以后,session B 才能繼續(xù)執(zhí)行。
這樣對于 id=0 這一行,在數(shù)據(jù)庫里的最終結(jié)果還是 (0,5,5)。在 binlog 里面,執(zhí)行序列是這樣的:
insert into t values(1,1,5); /*(1,1,5)*/
update t set c=5 where id=1; /*(1,5,5)*/
update t set d=100 where d=5;/*所有d=5的行,d改成100*/
update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
上圖的binlog數(shù)據(jù)不一致的問題算是解決了。數(shù)值也是對的了。那么還有一個問題!
全部加鎖解決了每個數(shù)據(jù)的正確性,那么新數(shù)據(jù)就無法保證正確性了?,F(xiàn)在就不是讀寫鎖可以解決的了。
如何解決幻讀?間隙鎖!
今天我們聊一下間隙鎖。簡單介紹一下。比如一個表中有6行數(shù)據(jù)。那么就會加7個間隙鎖。這7個鎖就分布在每條記錄的前后。
當你執(zhí)行 select * from t where d=5 for update 的時候。就不止是給數(shù)據(jù)庫中已有的 6 個記錄加上了行鎖,還同時加了 7 個間隙鎖。這樣就確保了無法再插入新的記錄。
數(shù)據(jù)行是可以加上鎖的實體,數(shù)據(jù)行之間的間隙,也是可以加上鎖的實體。
行鎖,間隙鎖,讀鎖,寫鎖
行鎖分為:讀鎖,寫鎖。
間隙鎖是單獨的一個鎖。
也就是說,跟行鎖有沖突關(guān)系的是“另外一個行鎖”。
跟間隙鎖存在沖突關(guān)系的,是“往這個間隙中插入一個記錄”這個操作。間隙鎖之間都不存在沖突關(guān)系。
舉例說明一下
sessionA開啟一個事務(wù) 并且給c為7的數(shù)據(jù)加了一個讀鎖。
session B 并不會被堵住。因為表 t 里并沒有 c=7 這個記錄,因此 session A 加的是間隙鎖 (5,10)。而 session B 也是在這個間隙加的間隙鎖。它們有共同的目標,即:保護這個間隙,不允許插入值。但,它們之間是不沖突的。
間隙鎖和行鎖合稱 next-key lock,每個 next-key lock 是前開后閉區(qū)間
那么我們在使用for update的時候也就是加了7 個 next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
supremum:因為 +∞是開區(qū)間。實現(xiàn)上,InnoDB 給每個索引加了一個不存在的最大值 supremum,這樣才符合我們前面說的“都是前開后閉區(qū)間”。
回到案例
間隙鎖和 next-key lock 的引入,幫我們解決了幻讀的問題,但同時也帶來了一些“困擾”。
我們先引一個邏輯出來繼續(xù)理論!
**業(yè)務(wù)邏輯 **是這樣的:任意鎖住一行,如果這一行不存在的話就插入,如果存在這一行就更新它的數(shù)據(jù)
begin;
select * from t where id=N for update;
/*如果行不存在*/
insert into t values(N,N,N);
/*如果行存在*/
update t set d=N set id=N;
commit;
這個邏輯一旦有并發(fā),就會碰到死鎖。你一定也覺得奇怪,這個邏輯每次操作前用 for update 鎖起來,已經(jīng)是最嚴格的模式了,怎么還會有死鎖呢?
如下圖,假設(shè)N=9
session A 執(zhí)行 select … for update 語句,由于 id=9 這一行并不存在,因此會加上間隙鎖 (5,10);
session B 執(zhí)行 select … for update 語句,同樣會加上間隙鎖 (5,10),間隙鎖之間不會沖突,因此這個語句可以執(zhí)行成功;
session B 試圖插入一行 (9,9,9),被 session A 的間隙鎖擋住了,只好進入等待;
session A 試圖插入一行 (9,9,9),被 session B 的間隙鎖擋住了。
至此,兩個 session 進入互相等待狀態(tài),形成死鎖。當然,InnoDB 的死鎖檢測馬上就發(fā)現(xiàn)了這對死鎖關(guān)系,讓 session A 的 insert 語句報錯返回了。
結(jié)論:間隙鎖的引入,可能會導致同樣的語句鎖住更大的范圍,這其實是影響了并發(fā)度的
業(yè)務(wù)權(quán)衡
一開始我們就提到了,幻讀只會出現(xiàn)在可重復隔離級別情況下。間隙鎖是在可重復讀隔離級別下才會生效的。
所以,你如果把隔離級別設(shè)置為讀提交的話,就沒有間隙鎖了。但同時,你要解決可能出現(xiàn)的數(shù)據(jù)和日志不一致問題,需要把 binlog 格式設(shè)置為 row。這,也是現(xiàn)在不少公司使用的配置組合。
總結(jié)
生產(chǎn)庫上會經(jīng)常出現(xiàn)由于間隙鎖導致的死鎖現(xiàn)象。行鎖確實比較直觀,判斷規(guī)則也相對簡單,間隙鎖的引入會影響系統(tǒng)的并發(fā)度,也增加了鎖分析的復雜度,但也有章可循
