MySQL是怎麼解決幻讀問題的?
前言
我們知道MySQL在可重複讀隔離級別下別的事物提交的內容,是看不到的。而可提交隔離級別下是可以看到別的事務提交的。而如果我們的業務場景是在事物內同樣的兩個查詢我們需要看到的資料都是一致的,不能被別的事物影響,就使用可重複讀隔離級別。這種情況下RR級別下的普通查詢(快照讀)依靠MVCC解決“幻讀”問題,如果是“當前讀”的情況需要依靠什麼解決“幻讀”問題呢?這就是本博文需要探討的。
在探討前可以看下之前的博文(MySQL是如何實現事務隔離?),主要介紹隔離級別的具體技術細節,讀過以後看此篇文章可能更有幫助。
注:本博文討論的“幻讀”都是指在“可重複讀”隔離級別下進行。
一、什麼是幻讀?
假設我們有表t結構如下,裡面的初始資料行為:(0,0,0),(1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5)
CREATE TABLE `t` ( `id` INT(11) NOT NULL, `key` INT(11) DEFAULT NULL, `value` INT(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `value` (`value`) ) ENGINE = InnoDB; INSERT INTO t VALUES (0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4), (5, 5, 5)
假設select * from where value=1 for update,只在這一行加鎖(注意這只是假設),其它行不加鎖,那麼就會出現如下場景:
Session A的三次查詢Q1-Q3都是select * from where value=1 for update,查詢的value=1的所有row。
- T1:Q1只返回一行(1,1,1);
- T2:session B更新id=0的value為1,此時表t中value=1
- T3:Q3返回兩行(0,0,1),(1,1,1)
- T4:session C插入一行(6,6,1),此時表t中value=1的資料有三行
- T5:Q3返回三行(0,0,1),(1,1,1),(6,6,1)
- T6:session A事物commit。
其中Q3讀到value=1這一樣的現象,就稱之為幻讀,幻讀指的是一個事務在前後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行。
先對“幻讀”做出如下解釋:
- 在可重複讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的資料的。因此, 幻讀在“當前讀”下才會出現(三個查詢都是for update表示當前讀);
- 上面session B的修改update結果,被session A之後的select語句用“當前讀”看到,不能稱為幻讀,幻讀僅專指“新插入的行”。
二、幻讀有什麼問題?
(1)需要單獨解決
眾所周知,select ...for update語句就是將相應的資料行鎖住,比如session A在T1時刻的Q1查詢語句:select * from where value=1 for update就是將value=1的資料行鎖住,但顯然如果是上述的場景發生,此時的for update語義被破壞了(並沒有鎖住value=1的資料行)。
即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄,所以“幻讀”問題要單獨拿出來解決。沒法依靠MVCC或者行鎖機制來解決。這就引出“間隙鎖”,是另外一種加鎖機制。
(2)間隙鎖引發的併發度
間隙鎖引入以後,可能會導致同樣語句鎖住更大的範圍,這可能就會影響了併發度。具體請看下面介紹
三、如何解決幻讀?
產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問題,InnoDB只好引入新的鎖,也就是間隙鎖(Gap Lock)。
間隙:比如表中加入6個記錄,0,5,10,15,20,25。則產生7個間隙:
在一行行掃描的過程中,不僅將給行加上了行鎖,還給行兩邊的空隙也加上了間隙鎖。這樣就確保了無法再插入新的記錄。
間隙鎖和行鎖合稱next-key lock,每個next-key lock是前開後閉區間(間隙鎖開區間,next-key lock前開後閉區間):
間隙鎖與間隙鎖之間是不存在衝突的,衝突的是往間隙裡插入一條記錄。
表t中是沒有value=7這個資料的,所以Q1加的間隙鎖(1,5),而Q2也是加的這個間隙鎖,兩者不衝突都是為了保護這個間隙不允許插入值。
在表t初始化後,假設表的資料如下:
如果用select * from for update執行,則會把整個表所有記錄鎖起來,就形成了7個next-key lock,分別是(-∞,0]、(0,2]、(2,4]、(4,6]、(6,8]、(8, 10]、(10, +supremum]
間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,是會影響了併發度
假設發生如下場景:
則明顯發生了死鎖,分析如下:
- Q1:執行select …for update語句,由於id=9這一行並不存在,因此會加上間隙鎖 (8,10);
- Q2:執行select …for update語句,同樣會加上間隙鎖(8,10),間隙鎖之間不會衝突,因 此這個語句可以執行成功;
- session B 試圖插入一行(9,9,9),被session A的間隙鎖擋住了,只好進入等待;
- session A試圖插入一行(9,9,9),被session B的間隙鎖擋住了。
有上述可知間隙鎖的引入,可能會導致同樣語句鎖住更大的範圍,這其實是影響了併發度。
為了解決幻讀問題可以採用讀可提交隔離級別,間隙鎖是在可重複讀隔離級別下才會生效的。所以如果把隔離級別設定為讀提交的話, 就沒有間隙鎖了。但同時,你要解決可能出現的資料和日誌不一致問題,需要把binlog格式設定為row,也就是說採用“RC隔離級別+日誌格式binlog_format=row”組合。
三、總結
- RR隔離級別下間隙鎖才有效,RC隔離級別下沒有間隙鎖;
- RR隔離級別下為了解決“幻讀”問題:“快照讀”依靠MVCC控制,“當前讀”通過間隙鎖解決;
- 間隙鎖和行鎖合稱next-key lock,每個next-key lock是前開後閉區間;
- 間隙鎖的引入,可能會導致同樣語句鎖住更大的範圍,影響併發度。