聊聊MVCC和NextkeyLocks

NO IMAGE

前言

上篇文章講到了MySQL的RR隔離級別通過MVCC+Next-key Locks解決幻讀問題,下面就給大家仔細講講這兩個機制究竟是什麼。

MVCC(多版本併發控制)

多版本併發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存儲引擎實現隔離級別的一種具體方式,用於實現提交讀和可重複讀這兩種隔離級別。而未提交讀隔離級別總是讀取最新的數據行,無需使用 MVCC。可串行化隔離級別需要對所有讀取的行都加鎖,單純使用 MVCC 無法實現。

Mysql的大多數事務型存儲引擎實現都不是簡單的行級鎖。基於提升併發性考慮,一般都同時實現了多版本併發控制(MVCC),包括Oracle、PostgreSQL。不過實現各不相同。

MVCC的實現是通過保存數據在某一個時間點快照來實現的。也就是說不管實現時間多長,每個事物看到的數據都是一致的。

分為樂觀(optimistic)併發控制和悲觀(pressimistic)併發控制。

MVCC是如何工作的:

InnoDB的MVCC是通過在每行記錄後面保存兩個隱藏的列來實現。這兩個列一個保存了行的創建時間,一個保存行的過期時間(刪除時間)。當然存儲的並不是真實的時間而是系統版本號(system version number)。每開始一個新的事務,系統版本號都會自動新增。事務開始時刻的系統版本號會作為事務的版本號,用來查詢到每行記錄的版本號進行比較。

版本號

系統版本號:是一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
事務版本號:事務開始時的系統版本號。

隱藏的列

MVCC 在每行記錄後面都保存著兩個隱藏的列,用來存儲兩個版本號:
  • 創建版本號:創建一行數據時,將當前系統版本號作為創建版本號賦值。
  • 刪除版本號:刪除一行數據時,將當前系統版本號作為刪除版本號賦值。如果該快照的刪除版本號大於當前事務版本號表示該快照有效,否則表示該快照已經被刪除了。

REPEATABLE READ(可重複讀)隔離級別下MVCC如何工作:

當開始新一個事務時,該事務的版本號肯定會大於當前所有數據行快照的創建版本號,理解這一點很關鍵。

1. SELECT

InnoDB會根據以下條件檢查每一行記錄:

1. InnoDB只查找版本早於當前事務版本的數據行,這樣可以確保事務讀取的行要麼是在開始事務之前已經存在要麼是事務自身插入或者修改過的,在事務開始之後才插入的行,事務不會看到。

2. 行的刪除版本號要麼未定義,要麼大於當前事務版本號,這樣可以確保事務讀取到的行在事務開始之前未被刪除,在事務開始之前就已經過期的數據行,該事務也不會看到。
只有符合上述兩個條件的才會被查詢出來

2. INSERT

將當前系統版本號作為數據行快照的創建版本號。

3. DELETE

將當前系統版本號作為數據行快照的刪除版本號。

4. UPDATE

將當前系統版本號作為更新前的數據行快照的刪除版本號,並將當前系統版本號作為更新後的數據行快照的創建版本號。可以理解為先執行 DELETE 後執行 INSERT

保存這兩個版本號,使大多數操作都不用加鎖。使數據操作簡單,性能很好,並且能保證只會讀取到複合要求的行。不足之處是每行記錄都需要額外的存儲空間,需要做更多的行檢查工作和一些額外的維護工作。

MVCC只在COMMITTED READ(讀提交)和REPEATABLE READ(可重複讀)兩種隔離級別下工作。

可以認為MVCC是行級鎖一個變種,但是他很多情況下避免了加鎖操作,開銷更低。雖然不同數據庫的實現機制有所不同,但大都實現了非阻塞的讀操作(讀不用加鎖,且能避免出現不可重複讀和幻讀),寫操作也只鎖定必要的行(寫必須加鎖,否則不同事務併發寫會導致數據不一致)。

快照讀與當前讀

在RR級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,不是數據庫最新的數據。這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取數據庫最新版本數據的方式,叫當前讀 (current read)

1. 快照讀

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

使用 MVCC 讀取的是快照中的數據,這樣可以減少加鎖所帶來的開銷。

select * from table …;

2. 當前讀

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

讀取的是最新的數據,需要加鎖。以下第一個語句需要加 S 鎖,其它都需要加 X 鎖。

select * from table where ? lock in share mode; 
select * from table where ? for update; 
insert; 
update; 
delete;

如何解決幻讀

很明顯可重複讀的隔離級別沒有辦法徹底的解決幻讀的問題,如果需要解決幻讀的話也有兩個辦法:
  • 使用串行化讀的隔離級別
  • MVCC+next-key locks:next-key locks由record locks(索引加鎖) 和 gap locks(間隙鎖,每次鎖住的不光是需要使用的數據,還會鎖住這些數據附近的數據)

InnoDB有三種行鎖的算法:

1,Record Lock:單個行記錄上的鎖。
2,Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身。GAP鎖的目的,是為了防止同一事務的兩次當前讀,出現幻讀的情況。
3,Next-Key Lock:1+2,鎖定一個範圍,並且鎖定記錄本身。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。

Record Locks

鎖定一個記錄上的索引,而不是記錄本身。
如果表沒有設置索引,InnoDB 會自動在主鍵上創建隱藏的聚簇索引,因此 Record Locks 依然可以使用。

Gap Locks

鎖定索引之間的間隙,但是不包含索引本身。例如當一個事務執行以下語句,其它事務就不能在 t.c 中插入 15。

SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;

Next-Key Locks

Next-Key Locks 是 MySQL 的 InnoDB 存儲引擎的一種鎖實現。

MVCC 不能解決幻讀的問題,Next-Key Locks 就是為了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Locks 可以解決幻讀問題。

當查詢的索引含有唯一屬性的時候,Next-Key Lock 會進行優化,將其降級為Record Lock,即僅鎖住索引本身,不是範圍。

它是 Record Locks 和 Gap Locks 的結合,不僅鎖定一個記錄上的索引,也鎖定索引之間的間隙。

新建一張表:

CREATE TABLE `test` ( 
`id` int(11) primary key auto_increment, 
`xid` int, KEY `xid` (`xid`) ) 
ENGINE=InnoDB DEFAULT CHARSET=utf8; 
insert into test(xid) values (1), (3), (5), (8), (11);
注意,這裡xid上是有索引的,因為該算法總是會去鎖住索引記錄。
現在,該索引可能被鎖住的範圍如下:
(-∞, 1], (1, 3], (3, 5], (5, 8], (8, 11], (11, +∞)
根據下面的方式開啟事務執行SQL:

聊聊MVCC和NextkeyLocks

Session A執行後會鎖住的範圍:
(5, 8], (8, 11]
除了鎖住8所在的範圍,還會鎖住下一個範圍,所謂Next-Key。
這樣,Session B執行到第六步會阻塞,跳過第六步不執行,第七步也會阻塞,但是並不阻塞第八步,第九步也不阻塞。
上面的結果似乎並不符合預期,因為11這個值看起來就是在(8, 11]區間裡,而5這個值並不在(5, 8]這個區間裡。

看下圖就明白了:

聊聊MVCC和NextkeyLocks

該SQL語句鎖定的範圍是(5,8],下個鍵值範圍是(8,11],所以插入5~11之間的值的時候都會被鎖定,要求等待。即:插入5,6,7,8,9,10 會被鎖住。插入非這個範圍內的值都正常。

小結

本篇文章總結了MVCC和InnoDB下的三種行鎖的算法,這些知識屬於MySQL的原理層面,有了這方面的認識後,在以後對MySQL的使用也能更加得心應手,不過我個人而言對於上面最後一個問題為什麼xid為11時並不會被阻塞那裡還有一點點不理解,參考的別人博客給出的解釋是id是自增的,innodb的B+樹是有序的,所以並不會阻塞後面的插入。此解釋還有待我回去翻看一下《mysq技術內幕》中對於next-key locks的詳細實現的描述再來做出更合理的解釋。

參考自
zhuanlan.zhihu.com/p/35477890
www.cnblogs.com/zhoujinyi/p…
www.jianshu.com/p/bf862c37c…

相關文章

SpringBean的生命週期分析

MySQL併發更新數據時的處理方法

說一下聚簇索引&非聚簇索引

MySQL索引詳解