1. 程式人生 > >MySQL鎖(三)行鎖:幻讀是什麼?如何解決幻讀?

MySQL鎖(三)行鎖:幻讀是什麼?如何解決幻讀?

## 概述 前面兩篇文章介紹了MySQL的[全域性鎖](https://www.cnblogs.com/liang24/p/14136898.html)和[表級鎖](https://www.cnblogs.com/liang24/p/14140990.html),今天就介紹一下MySQL的行鎖。 MySQL的行鎖是各個引擎內部實現的,不是所有的引擎支援行鎖,例如MyISAM就不支援行鎖。 不支援行鎖就意味著在併發操作時,就要使用表鎖,在任意時刻都只能有一個更新操作在執行,這樣會影響業務的併發性。這也是為什麼MyISAM會被InnoDB取代的原因之一。 行鎖是鎖裡最小粒度的鎖,InnoDB引擎裡的行鎖的實現演算法有三種: - Record Lock:行鎖,鎖住記錄本身 - Gap Lock:間隙鎖,鎖住某個範圍,但不包括記錄本身 - Next-Key Lock:Record Lock + Gap Lock,既鎖範圍,又鎖記錄 InnoDB是使用Next-Key Lock來解決幻讀問題的。 ## 什麼是幻讀? 我們看一下這個例子,有一個表 t,插入部分資料。 ```mysql CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(0,0,0),(5,5,5), (10,10,10),(15,15,15),(20,20,20),(25,25,25); ```
圖1 假設只在id=5這一行加行鎖 有三個會話併發執行,Session A在T1,T3,T5時刻分別查詢同一個語句,出現不同的結果。其中Q3讀到的id=1這一行的現象,被稱為幻讀。 > 幻讀,指同一個事務中,兩次相同的查詢操作,得到的結果行數不一樣。 這裡要對“幻讀”做兩點說明: 1. 在可重複讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的資料的。因此幻讀在“當前讀”下才會出現。 2. 上面的Session B的修改結果,被Session A之後的select語句用“當前讀”看到了,不能稱為幻讀。**幻讀僅專指“新插入的行”。** 根據[資料可見性規則](https://www.cnblogs.com/liang24/p/14120122.html)分析,這三個查詢都加了for update,都是“當前讀”,符合資料可見性規則。 這麼看來,好像沒什麼問題,是不是真的沒有問題呢? 不,這裡還真就有問題。 ## 幻讀有什麼問題? ### 語義上不一致 Session A在T1時刻就聲明瞭,“我要把所有d=5的行鎖住,不準別的事務進行讀寫操作”。而實際上,這個語義被破壞了。 上面的例子可能還看不太出來,我們給Session B和Session C分別加兩個語句,再看看會出現什麼現象。

圖2 假設只在id=5這一行加行鎖--語義被破壞 Session B的第二條語句update t set c = 5 where id=0,語義是“我要把id=0、d=5的這一行的c的值改成了5”。 由於在T1時刻,Session A還只是給t=5這一行加了行鎖,並沒有給id=0這一行加鎖。因此Session B在T2時刻,是可以執行這條語句的。 同理,Session C對id=1這行的修改,一樣是破壞了Q1的加鎖宣告。 ### 資料上不一致 其次是造成資料上不一致。鎖的設計就是為了保證資料一致性的,這裡的一致性除了內部資料在此刻的一致性外,還包含資料和日誌在邏輯上的一致性。

圖 3 假設只在id=5這一行加行鎖--資料一致性問題 我們來分析一下圖3執行完成後,資料庫的資料是什麼: 1. 經過T1時刻,id=5這一行變成 (5,5,100),當然這個結果最終是在T6時刻正式提交的 2. 經過T2時刻,id=0這一行變成(0,5,5); 3. 經過T4時刻,表裡面多了一行(1,5,5); 我們再來看看binlog的內容: ```mysql // session B update t set d=5 where id=0; update t set c=5 where id=0; // session C insert into t values(1,1,5); update t set c=5 where id=1; update t set d=100 where d=5; ``` 按照這個語句序列,這三行的結果變成:(0,5,100),(1,5,100),(5,5,100)。 也就是說id=0和id=1這兩行,發生了資料不一致。這個問題很嚴重,是不行的。 那究竟**這個資料不一致是怎樣引入的呢?**

圖 4 假設掃描到的行都被加上了行鎖 假設我們對掃描到的行都加上行鎖,來看看圖4執行後會出現什麼現象。 1. 經過T1時刻,id=5這一行變成 (5,5,100),當然這個結果最終是在T6時刻正式提交的 2. 經過T2時刻,Session B被阻塞,等到T6時刻Session A釋放鎖才能執行; 3. 經過T4時刻,表裡面多了一行(1,5,5); 4. 經過T6時刻,id=1這一行變成(1,5,100); id=1這一行還是出現數據不一致的問題。**即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄。** ## 如何解決幻讀? 我們現在知道產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問題,InnoDB引入了間隙鎖(Gap Lock)。 前面介紹過,間隙鎖,鎖住某個範圍,但不包括記錄本身。比如前面說到的表t,初始化有6條記錄,這就產生了7個間隙。
圖 5 表t主鍵索引上的行鎖和間隙鎖 當你執行select * from t where d=5 for update的時候,就不止是給資料庫中6個記錄加了行鎖,還同時加了7個間隙鎖。這樣就確保了無法再插入新的記錄。 也就是說這時候,在一行行掃描的過程中,不僅給行加上行鎖,還給行兩邊的空隙也加上間隙鎖。 我們回到上面的圖4,再來看看加上間隙鎖後,執行的效果如何。 1. 經過T1時刻,id=5這一行變成 (5,5,100),當然這個結果最終是在T6時刻正式提交的。因為select * from t where d=6 for update,對6個記錄加了行鎖,同時加了7個間隙鎖。 2. 經過T2時刻,Session B被阻塞,**因為id=0這一行被鎖**; 3. 經過T4時刻,Session C被阻塞,**因為主鍵索引上加了間隙鎖(0,5)**,所以id=1這個值無法被插入; Session B和Session C都要等待Session A釋放鎖後才能繼續執行,這樣就解決了幻讀的問題。 行鎖保證更新行,間隙鎖保證插入行,而行鎖+間隙鎖=Next-Key Lock,也就是本文開頭說到的,InnoDB是通過Next-Key Lock來解決幻讀問題的。 但是間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,這會影響併發度的。比如上面的select * from t where d=5 for update,相當於加了表鎖。 ## 參考資料 - [07 | 行鎖功過:怎麼減少行鎖對效能的影響?](https://time.geekbang.org/column/article/70215) - [20 | 幻讀是什麼,幻讀有什麼問題?](https://time.geekbang.org/column/article/75173) - [MySQL技術內幕 : InnoDB儲存引擎(第2版)](https://book.douban.com/subject/24