MySQL中幻讀和幻讀存在的問題

 

 

 

 

一、概念

幻讀指的是一個事務在先後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行。數據庫

能夠看到,session A裏執行了三次查詢,分別是Q一、Q2和Q3。它們的SQL語句相同,都是select * from t where d=5 for update。這個語句的意思你應該很清楚了,查全部d=5的行,並且使用的是當前讀,而且加上寫鎖。如今,咱們來看一下這三條SQL語句,分別會返回什麼結果。session

 

Q1只返回id=5這一行;併發

 

在T2時刻,session B把id=0這一行的d值改爲了5,所以T3時刻Q2查出來的是id=0和id=5這兩行;設計

 

在T4時刻,session C又插入一行(1,1,5),所以T5時刻Q3查出來的是id=0、id=1和id=5的這三行。3d

 

其中,Q3讀到id=1這一行的現象,被稱爲「幻讀」。日誌

 

幻讀的說明:blog

 

在可重複讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的數據的。所以,幻讀在「當前讀」下才會出現。索引

 

上面session B的修改結果,被session A以後的select語句用「當前讀」看到,不能稱爲幻讀。幻讀僅專指「新插入的行」。事務

二、幻讀的問題

(1)首先是語義上面的。io

 

session A在T1時刻就聲明瞭,「我要把全部d=5的行鎖住,不許別的事務進行讀寫操做」。而實際上,這個語義被破壞了。

 

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時刻,是能夠執行這兩條update語句的。這樣,就破壞了 session A 裏Q1語句要鎖住全部d=5的行的加鎖聲明。

 

session C也是同樣的道理,對id=1這一行的修改,也是破壞了Q1的加鎖聲明。

 

(2)其次,是數據一致性的問題

 

鎖的設計是爲了保證數據的一致性。而這個一致性,不止是數據庫內部數據狀態在此刻的一致性,還包含了數據和日誌在邏輯上的一致性。

 

爲了說明這個問題,我給session A在T1時刻再加一個更新語句,即:update t set d=100 where d=5。

 

 

update的加鎖語義和select ...for update 是一致的,因此這時候加上這條update語句也很合理。session A聲明說「要給d=5的語句加上鎖」,就是爲了要更新數據,新加的這條update語句就是把它認爲加上了鎖的這一行的d值修改爲了100。

 

如今,咱們來分析一下上圖執行完成後,數據庫裏會是什麼結果。

 

通過T1時刻,id=5這一行變成 (5,5,100),固然這個結果最終是在T6時刻正式提交的;

 

通過T2時刻,id=0這一行變成(0,5,5);

 

通過T4時刻,表裏面多了一行(1,5,5);

 

其餘行跟這個執行序列無關,保持不變。

 

這樣看,這些數據也沒啥問題,可是咱們再來看看這時候binlog裏面的內容。

 

T2時刻,session B事務提交,寫入了兩條語句;

 

T4時刻,session C事務提交,寫入了兩條語句;

 

T6時刻,session A事務提交,寫入了update t set d=100 where d=5 這條語句。

 

這個語句序列,不管是拿到備庫去執行,仍是之後用binlog來克隆一個庫,這三行的結果,都變成了 (0,5,100)、(1,5,100)和(5,5,100)。

圖 4 假設掃描到的行都被加上了行鎖

因爲session A把全部的行都加了寫鎖,因此session B在執行第一個update語句的時候就被鎖住了。須要等到T6時刻session A提交之後,session B才能繼續執行。

 

 

這樣對於id=0這一行,在數據庫裏的最終結果仍是 (0,5,5)。在binlog裏面,執行序列是這樣的:

 

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)*/

 

能夠看到,按照日誌順序執行,id=0這一行的最終結果也是(0,5,5)。因此,id=0這一行的問題解決了。

 

但同時你也能夠看到,id=1這一行,在數據庫裏面的結果是(1,5,5),而根據binlog的執行結果是(1,5,100),也就是說幻讀的問題仍是沒有解決。爲何咱們已經這麼「兇殘」地,把全部的記錄都上了鎖,仍是阻止不了id=1這一行的插入和更新呢?

 

緣由很簡單。在T3時刻,咱們給全部行加鎖的時候,id=1這一行還不存在,不存在也就加不上鎖。

 

 

也就是說,即便把全部的記錄都加上鎖,仍是阻止不了新插入的記錄,這也是爲何「幻讀」會被單獨拿出來解決的緣由。

 

 

三、如何解決幻讀的問題

產生幻讀的緣由是,行鎖只能鎖住行,可是新插入記錄這個動做,要更新的是記錄之間的「間隙」。所以,爲了解決幻讀問題,InnoDB只好引入新的鎖,也就是間隙鎖(Gap Lock)。

 

 

顧名思義,間隙鎖,鎖的就是兩個值之間的空隙。好比文章開頭的表t,初始化插入了6個記錄,這就產生了7個間隙。

這樣,當你執行 select * from t where d=5 for update的時候,就不止是給數據庫中已有的6個記錄加上了行鎖,還同時加了7個間隙鎖。這樣就確保了沒法再插入新的記錄。

 

也就是說這時候,在一行行掃描的過程當中,不只將給行加上了行鎖,還給行兩邊的空隙,也加上了間隙鎖。

 

如今你知道了,數據行是能夠加上鎖的實體,數據行之間的間隙,也是能夠加上鎖的實體。可是間隙鎖跟咱們以前碰到過的鎖都不太同樣。

 

好比行鎖,分紅讀鎖和寫鎖。下圖就是這兩種類型行鎖的衝突關係。

也就是說,跟行鎖有衝突關係的是「另一個行鎖」。

 

可是間隙鎖不同,跟間隙鎖存在衝突關係的,是「往這個間隙中插入一個記錄」這個操做。間隙鎖之間都不存在衝突關係。

 

間隙鎖和行鎖合稱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, +suprenum]。

 

這是由於+∞是開區間。實現上,InnoDB給每一個索引加了一個不存在的最大值suprenum,這樣才符合咱們前面說的「都是前開後閉區間」。

 

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

 

間隙鎖的引入,可能會致使一樣的語句鎖住更大的範圍,這實際上是影響了併發度的

 

 

四、小結

 

即便給全部的行都加上行鎖,仍然沒法解決幻讀問題,所以引入了間隙鎖的概念。

 

行鎖確實比較直觀,判斷規則也相對簡單,間隙鎖的引入會影響系統的併發度,也增長了鎖分析的複雜度,但也有章可循。

相關文章
相關標籤/搜索