幻讀指的是一個事務在先後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行。數據庫
能夠看到,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的引入,幫咱們解決了幻讀的問題,但同時也帶來了一些「困擾」。
間隙鎖的引入,可能會致使一樣的語句鎖住更大的範圍,這實際上是影響了併發度的
即便給全部的行都加上行鎖,仍然沒法解決幻讀問題,所以引入了間隙鎖的概念。
行鎖確實比較直觀,判斷規則也相對簡單,間隙鎖的引入會影響系統的併發度,也增長了鎖分析的複雜度,但也有章可循。