前言
上篇文章講到了MySQL的RR隔離級別經過MVCC+Next-key Locks解決幻讀問題,下面就給你們仔細講講這兩個機制到底是什麼。html
MVCC(多版本併發控制)
多版本併發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存儲引擎實現隔離級別的一種具體方式,用於實現提交讀和可重複讀這兩種隔離級別。而未提交讀隔離級別老是讀取最新的數據行,無需使用 MVCC。可串行化隔離級別須要對全部讀取的行都加鎖,單純使用 MVCC 沒法實現。
Mysql的大多數事務型存儲引擎實現都不是簡單的行級鎖。基於提高併發性考慮,通常都同時實現了多版本併發控制(MVCC),包括Oracle、PostgreSQL。不過實現各不相同。算法
MVCC的實現是經過保存數據在某一個時間點快照來實現的。也就是說無論實現時間多長,每一個事物看到的數據都是一致的。sql
分爲樂觀(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是行級鎖一個變種,可是他不少狀況下避免了加鎖操做,開銷更低。雖然不一樣數據庫的實現機制有所不一樣,但大都實現了非阻塞的讀操做(讀不用加鎖,且能避免出現不可重複讀和幻讀),寫操做也只鎖定必要的行(寫必須加鎖,不然不一樣事務併發寫會致使數據不一致)。ui
快照讀與當前讀
在RR級別中,經過MVCC機制,雖然讓數據變得可重複讀,但咱們讀到的數據多是歷史數據,不是數據庫最新的數據。這種讀取歷史數據的方式,咱們叫它
快照讀 (snapshot read),而讀取數據庫最新版本數據的方式,叫
當前讀 (current read)。
1. 快照讀
當執行select操做是innodb默認會執行快照讀,會記錄下此次select後的結果,以後select 的時候就會返回此次快照的數據,即便其餘事務提交了不會影響當前select的數據,這就實現了可重複讀了。快照的生成當在第一次執行select的時候,也就是說假設當A開啓了事務,而後沒有執行任何操做,這時候B insert了一條數據而後commit,這時候A執行 select,那麼返回的數據中就會有B添加的那條數據。以後不管再有其餘事務commit都沒有關係,由於快照已經生成了,後面的select都是根據快照來的。
使用 MVCC 讀取的是快照中的數據,這樣能夠減小加鎖所帶來的開銷。spa
select * from table ...;
2. 當前讀
對於會對數據修改的操做(update、insert、delete)都是採用當前讀的模式。在執行這幾個操做時會讀取最新的記錄,即便是別的事務提交的數據也能夠查詢到。假設要update一條記錄,可是在另外一個事務中已經delete掉這條數據而且commit了,若是update就會產生衝突,因此在update的時候須要知道最新的數據。
讀取的是最新的數據,須要加鎖。如下第一個語句須要加 S 鎖,其它都須要加 X 鎖。code
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:
Session A執行後會鎖住的範圍:
(5, 8], (8, 11]
除了鎖住8所在的範圍,還會鎖住下一個範圍,所謂Next-Key。
這樣,Session B執行到第六步會阻塞,跳過第六步不執行,第七步也會阻塞,可是並不阻塞第八步,第九步也不阻塞。
上面的結果彷佛並不符合預期,由於11這個值看起來就是在(8, 11]區間裏,而5這個值並不在(5, 8]這個區間裏。
看下圖就明白了:
該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的詳細實現的描述再來作出更合理的解釋。
參考自