mysql中的MVCC,幻讀與間隙鎖

一直搞不明白爲什麼間隙鎖爲什麼能過解決幻讀的問題,現在大致總結一下吧。

事務的隔離級別

1.Read Uncommitted(未提交讀)
2.Read Commited(提交讀)
3.Repeatable Read(可重複讀)
4.serializable(可串行化)
在這裏插入圖片描述

MVCC,mysql怎麼實現的可重複讀

mvcc對版本併發控制(Multi-Version Conncurrency Control)是mysql中基於樂觀鎖原理實現的隔離級別的方式。用於實現讀已提交和可重複讀取隔離級別。

可以參考《高性能MySQL》中MVCC的解釋如下:
InnoDB的MVCC,是通過在每行記錄後面保存兩個隱藏的列來實現的。這兩個列,一個保存了行的創建時間,一個保存行的過期時間(或刪除時間),當然存儲的並不是實際的時間值,而是系統版本號(system version number).每開始一個新的事務,系統版本號都會自動遞增,事務開始時刻的系統版本號會作爲事務的版本號,用來和查詢到的每行記錄的版本號進行比較。下面看一下在repeatable Read 隔離級別下,MVCC具體是如何操作的。
	SELECT
		(1)InnoDB只查找版本遭遇當前事務版本的數據行(行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
		(2)行的刪除版本要麼未定義,要麼大於當前事務版本號,這可以確保事務讀取到的行,在事務開始之前沒有被刪除。
		只有符合上述兩個條件的記錄,才能作爲查詢結果。
		INSERT
			innoDB爲新插入的每一行保存當前系統版本作爲行版本號。
		DELETE
			innoDB爲刪除的每一行保存當前系統版本號作爲行刪除標識。
		UPDATE
			InnoDB爲插入一行新記錄,保存當前系統版本號作爲行版本號,同時保存當前系統版本號到原來的行作爲行刪除標識

保存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖,這樣設計使得讀數據操作很簡單,性能很好,並且也能保證只讀取到符合標準的行。不足之處的每行記錄都需要額外的空間存儲。需要做更多的行檢查工作,以及一些額外的維護工作。
MVCC只在repeatable read 和read committed兩個隔離級別下工作,其他兩個隔離級別都和MVCC不兼容,因爲READ UNCOMMITTED總是讀取最新的數據行,而不是符合當前師傅版本的數據行。而serializable則會對所有讀取的行加鎖。
## 快照讀和當前讀
在RR級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,不是數據庫最新的數據,這個讀取歷史數據的方式,我們叫他快照度,而讀取數據庫最新版本數據的方式,叫做當前讀。

  1. select 快照讀
    當執行select操作,innodb默認會執行快照度,會記錄下這個select後的結果,之後select的時候就會返回這次快照的數據,即使其他事務提交了不會影響當前select的數據,這就實現了可重複讀了。快照的生成當在第一次執行select的時候,也就是說假設當A開啓了事務,然後沒有執行任何操作,這個時候B insert了一條數據然後commit,這時候A執行select,滿額返回的數據中心就會有B添加的那條數據,之後無論再有其他事務commit都沒有關係,因爲快照已經生成了,後面的select都是根據快照來的。

  2. 當前讀
    對於會對數據修改的操作(update,insert,delete)都是採用當前讀的模式。在執行這幾個操作時會讀取最新的版本號記錄,寫操作後把版本號改爲了當前事務的版本號,所以即使是被的事務提交的數據也可以查詢到。假設要update一條數據,但是在另一個事務中已經delete掉這條數據並且commit了,如果update就會產生衝突,所以在update的時候需要知道最新的數據,也正是因爲這樣才導致幻讀。

    		## 幻讀
    		在簡書上看到一篇博客吧幻讀與間隙鎖解釋的很好。
    
CREATE TABLE `test_20` (
 `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 test_20 values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
begin;
select * from t where d=5 for update;
commit;

這個語句會命中 d=5 的這一行,對應的主鍵 id=5,因此在 select 語句執行完成後,id=5 這一行會加一個寫鎖,而且由於兩階段鎖協議,這個寫鎖會在執行 commit 語句的時候釋放。

如果只在 id=5 這一行加鎖,而其他行的不加鎖的話,會怎麼樣?

在READ COMMITTED級別下可執行
在REPEATABLE READ級別下,session B會阻塞

在這裏插入圖片描述

1)Q1 只返回 id=5 這一行;
2)在 T2 時刻,session B 把 id=0 這一行的 d 值改成了 5,因此 T3 時刻 Q2 查出來的是id=0 和 id=5 這兩行;
3)在 T4 時刻,session C 又插入一行(1,1,5),因此 T5 時刻 Q3 查出來的是 id=0、id=1和 id=5 的這三行。
其中,Q3 讀到 id=1 這一行的現象,被稱爲“幻讀”。也就是說,幻讀指的是一個事務在前後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行。

說明:

1)在可重複讀隔離級別下,普通的查詢是快照讀,是不會看到別的事務插入的數據的。因此,幻讀在“當前讀”下才會出現。
2)上面 session B 的修改結果,被 session A 之後的 select 語句用“當前讀”看到,不能稱爲幻讀。幻讀僅專指“新插入的行”。

幻讀問題(更新與新增)

session A 在 T1 時刻就聲明瞭,“我要把所有 d=5 的行鎖住,不準別的事務進行讀寫操作”。而實際上,這個語義被破壞了。

數據的一致性問題

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

在這裏插入圖片描述

候 binlog 裏面的內容:

1) T2 時刻,session B 事務提交,寫入了兩條語句;
2)T4 時刻,session C 事務提交,寫入了兩條語句;
3)T6 時刻,session A 事務提交,寫入了 update t set d=100 where d=5 這條語句。
放在一起如下:

update t set d=5 where id=0; /*(0,0,5)*/
update t set c=5 where id=0; /*(0,5,5)*/
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*/

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

原因是:

這是我們假設“select * from t where d=5 for update 這條語句只給d=5 這一行,也就是 id=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)*/

這裏解決了session B的問題,但是session C新增的問題並沒有解決!

在 T3 時刻,我們給所有行加鎖的時候,id=1 這一行還不存在,不存在也就加不上鎖。也就是說,即使把所有的記錄都加上鎖,還是阻止不了新插入的記錄。

innodb怎麼解決幻讀的問題

產生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個動作,要更新的是記錄之間的“間隙”。因此,爲了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (GapLock)。

間隙鎖,鎖的就是兩個值之間的空隙。比如表 test_20,初始化插入了 6 個記錄,這就產生了 7 個間隙。

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

數據行是可以加上鎖的實體,數據行之間的間隙,也是可以加上鎖的實體。但是間隙鎖跟我們之前碰到過的鎖都不太一樣。

跟行鎖有衝突關係的是“另外一個行鎖”。

在這裏插入圖片描述
跟間隙鎖存在衝突關係的,是“往這個間隙中插入一個記錄”這個操作。間隙鎖之間都不存在衝突關係。
如下:這裏 session B 並不會被堵住。因爲表 test_20裏並沒有 c=7 這個記錄,因此 session A 加的是間隙鎖 (5,10)。而 session B 也是在這個間隙加的間隙鎖。它們有共同的目標,即:保護這個間隙,不允許插入值。但,它們之間是不衝突的。
在這裏插入圖片描述
間隙鎖和行鎖合稱 next-key lock,每個 next-key lock 是前開後閉區間。也就是說,表test_20初始化以後,如果用 select * from t for update 要把整個表所有記錄鎖起來,就形成了 7 個next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25,+suprenum]。
把間隙鎖記爲開區間,把 next-keylock 記爲前開後閉區間。

在這裏插入圖片描述

帶來的問題

在這裏插入圖片描述

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 進入互相等待狀態,形成死鎖。間隙鎖的引入,可能會導致同樣的語句鎖住更大的範圍,這其實是影響了併發度的。

解決方法:

間隙鎖是在可重複讀隔離級別下才會生效的。所以,你如果把隔離級別設置爲讀提交的話,就沒有間隙鎖了。但同時,你要解決可能出現的數據和日誌不一致問題,需要把 binlog 格式設置爲 row。這,也是現在不少公司使用的配置組合。

作者:王偵
鏈接:https://www.jianshu.com/p/03357456f1dd
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章