如何解決幻讀?

現在你知道了,產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,爲了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)。
 
顧名思義,間隙鎖,鎖的就是兩個值之間的空隙。比如文章開頭的表 t,初始化插入了 6個記錄,這就產生了 7 個間隙。
 
這樣,當你執行 select * from t where d=5 for update 的時候,就不止是給數據庫中已有的 6 個記錄加上了行鎖,還同時加了 7 個間隙鎖。這樣就確保了無法再插入新的記錄。
 
 
也就是說這時候,在一行行掃描的過程中,不僅將給行加上了行鎖,還給行兩邊的空隙,也加上了間隙鎖。
 
現在你知道了,數據行是可以加上鎖的實體,數據行之間的間隙,也是可以加上鎖的實體。但是間隙鎖跟我們之前碰到過的鎖都不太一樣。 比如行鎖,分成讀鎖和寫鎖。下圖就是這兩種類型行鎖的衝突關係。
 
 
也就是說,跟行鎖有衝突關係的是“另外一個行鎖”。
 
但是間隙鎖不一樣,跟間隙鎖存在衝突關係的,是“往這個間隙中插入一個記錄”這個操作。間隙鎖之間都不存在衝突關係。
 
這句話不太好理解,我給你舉個例子:
 
 
這裏 session B 並不會被堵住。因爲表 t 裏並沒有 c=7 這個記錄,因此 session A 加的是間隙鎖 (5,10)。而 session B 也是在這個間隙加的間隙鎖。它們有共同的目標,即:保護這個間隙,不允許插入值。但,它們之間是不衝突的。
 
間隙鎖和行鎖合稱 next-key lock,每個 next-key lock 是前開後閉區間。也就是說,我們的表 t 初始化以後,如果用 select * from t for update 要把整個表所有記錄鎖起來,就形成了 7 個 next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20,
25]、(25, +supremum]。
 

間隙鎖和 next-key lock 的引入,幫我們解決了幻讀的問題,但同時也帶來了一些“困擾”。

 
在前面的文章中,就有同學提到了這個問題。我把他的問題轉述一下,對應到我們這個例子的表來說,業務邏輯這樣的:任意鎖住一行,如果這一行不存在的話就插入,如果存在這一行就更新它的數據,代碼如下:
 
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;
這個同學碰到的現象是,這個邏輯一旦有併發,就會碰到死鎖。你一定也覺得奇怪,這個邏輯每次操作前用 for update 鎖起來,已經是最嚴格的模式了,怎麼還會有死鎖呢?
 
這裏,我用兩個 session 來模擬併發,並假設 N=9。
 
你看到了,其實都不需要用到後面的 update 語句,就已經形成死鎖了。我們按語句執行順序來分析一下:
 
1. session A 執行 select … for update 語句,由於 id=9 這一行並不存在,因此會加上間隙鎖 (5,10);
2. session B 執行 select … for update 語句,同樣會加上間隙鎖 (5,10),間隙鎖之間不會衝突,因此這個語句可以執行成功;
3. session B 試圖插入一行 (9,9,9),被 session A 的間隙鎖擋住了,只好進入等待;
4. session A 試圖插入一行 (9,9,9),被 session B 的間隙鎖擋住了。
至此,兩個 session 進入互相等待狀態,形成死鎖。當然,InnoDB 的死鎖檢測馬上就發現了這對死鎖關係,讓 session A 的 insert 語句報錯返回了。
你現在知道了,間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,這其實是影響了併發度的
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章