關鍵詞:MVCC、解決幻讀、read_view、undo log、快照讀、當前讀
1、定義:
2、核心思想:
MVCC是通過保存數據在某個時間點的快照來進行控制的。使用MVCC就是允許同一個數據記錄擁有多個不同的版本。然后在查詢時通過添加相對應的約束條件,就可以獲取用戶想要的對應版本的數據。
3、基本數據結構:
(1)redo log:
(2)undo log:

(3)read_view(快照):
①read_view的簡單理解:
②read_view的基本結構:
read_view->creator_trx_id = 當前事務id; # 當前的事務id read_view->up_limit_id = 12654; # 當前活躍事務的最小id read_view->low_limit_id = 12659; # 當前活躍事務的最小id read_view->trx_ids = [12654, 12659]; # 當前活躍的事務的id列表,又稱活躍事務鏈表。表示在記錄當前快照時的所有活躍的、未提交的事務 read_view->m_trx_ids = 2; # 當前活躍的事務id列表長度
注意:
- read_view中包含了活躍事務鏈表,這個鏈表表示此時還在活躍的事務,指的是那些在當前快照中還未提交的事務。(注意:新建事務(當前事務)與正在內存中commit 的事務不在活躍事務鏈表)。
- read_view中不會顯示所有的數據行,只會顯示“可見”的記錄。篩選方式如下所述。
③read_view的記錄篩選方式:
-
如果記錄的DATA_TRX_ID < up_limit_id:在創建read_view時,修改該記錄的事務已提交,該記錄可被快照中的事務讀取到(即可見)。
-
如果DATA_TRX_ID >= low_limit_id:表示該記錄是在當前read_view創建之后被其它事務修改的,該記錄在當前快照中肯定不可見。此時需要從
DB_ROLL_PTR
指針所指向的回滾段中取出最新的undo-log的版本號, 然后用它繼續重新開始整套比較算法。 -
如果up_limit_id <= DATA_TRX_ID < low_limit_i:
-
需要在活躍事務鏈表中查找是否存在ID為DATA_TRX_ID的值的事務。
-
如果存在,那么因為在活躍事務鏈表中的事務是未提交的,所以該記錄是不可見的。此時需要從
DB_ROLL_PTR
指針所指向的回滾段中取出最新的undo-log的版本號, 然后用它繼續重新開始整套比較算法。(詳細分析為什么“不可見”:因為DATA_TRX_ID只有在事務提交之后才會更新,而此時因為事務還存在於活躍事務鏈表中,所以說明事務是還沒有commit,所以此時不可能存在對應的數據行,只有在當前事務提交之后才會有對應的數據行。) - 如果不存在,所以是可見的。(分析:按照上一點的對“不可見”原因的分析,可明白只能是當前本事務更新了這條記錄,因為在當前read view中,只能是當前事務和正在內存中commit的事務不在事務活躍鏈表中,對於“正在內存中commit的事務”,因為它還沒有commit,所以肯定是不可能讀取到它的即將要commit的數據的,而所以只能是當前事務對這個數據行做了修改了,雖然未提交,但是因為是在當前事務中,所以肯定是可以讀取到更新的數據的。
④read_view的更新方式:
注意:僅分析RC級別和RR級別,因為MVCC不適用於其它兩個隔離級別。
a、對於Read Committed級別的:
- 基本描述:每次執行select都會創建新的read_view,更新舊read_view,保證能讀取到其他事務已經COMMIT的內容(讀提交的語義);
- 詳細分析:假設當前有事務A和事務A+1並發進行。在當前級別下,事務A每次select的時候會創建新的read_view,此時可以簡單理解為事務A會提交,也就是讓事務A執行完畢,然后創建一個新的事務比如是事務A+2。這樣子的話,因為事務A+2的事務ID肯定是比事務A+1的ID大,所以就能夠讀取到事務A+1的更新了。那么便可以讀取到在創建這個新的read_view之前事務A+1所提交的所有信息。這是RC級別下能讀取到其他事務已經COMMIT的內容的原因所在。
b、對於Repeatable Read級別的:
- 第一次select時更新這個read_view,以后不會再更新,后續所有的select都是復用這個read_view。所以能保證每次讀取的一致性,即都是讀取第一次讀取到的內容(可重復讀的語義)。
注意:通過對read view的更新方式的分析可以得出:對於InnoDB下的MVCC來說,RR雖然比RC隔離級別高,但是開銷反而相對少(因為不用頻繁更新read_view)。
read_view的詳細分析:https://www.iteye.com/blog/mahl1990-2347029
4、MVCC在mysql的具體實現:
(1)基本數據結構的定義:
- 6字節的DATA_TRX_ID:標記了最新更新這條行記錄的transaction id,每處理一個事務,其值自動設置為當前事務ID(DATA_TRX_ID只有在事務提交之后才會更新);
- 7字節的DATA_ROLL_PTR:一個rollback指針,指向當前這一行數據的上一個版本,找之前版本的數據就是通過這個指針,通過這個指針將數據的多個版本連接在一起構成一個undo log版本鏈;
- 6字節的DB_ROW_ID:隱含的自增ID,如果數據表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引。這是一個用來唯一標識每一行的字段;
- DELETE BIT位:用於標識當前記錄是否被刪除,這里的不是真正的刪除數據,而是標志出來的刪除。真正意義的刪除是在commit的時候。
MVCC在二級索引結構下的分析:https://www.cnblogs.com/stevenczp/p/8018986.html
(2)增刪改查:
①增加:INSERT
- 設置新記錄的DATA_TRX_ID為當前事務ID,其他的采用默認的。
②刪除:DELETE
- 修改DATA_TRX_ID的值為當前的執行刪除操作的事務的ID,然后設置DELETE BIT為True,表示被刪除
③修改:UPDATE <==> INSERT + DELETE
- 用X鎖鎖定該行(因為是寫操作);
- 記錄redo log:將更新之后的數據記錄到redo log中,以便日后使用;
- 記錄undo log:將更新之后的數據記錄到undo log中,設置當前數據行的DATA_TRX_ID為當前事務ID,回滾指針DATA_ROLL_PTR指向undo log中的當前數據行更新之前的數據行,同時設置更新之前的數據行的DATA_TRX_ID為當前事務ID,並且設置DELETE BIT為True,表示被刪除。
④查找:SELECT
- 如果當前數據行的DELETE BIT為False,只查找版本早於當前事務版本的數據行(也就是數據行的DATA_TRX_ID必須小於等於當前事務的ID),這確保當前事務讀取的行都是事務之前已經存在的,或者是由當前事務創建或修改的行;
- 如果當前數據行的DELETE BIT為True,表示被刪除,那么只能返回DATA_TRX_ID的值大於當前事務的行。獲取在當前事務開始之前,還沒有被刪除的行。
5、使用MVCC核心優勢:
6、MVCC與四大隔離級別的關系的分析:
分析了在MVCC的控制之下,如何實現四大隔離級別。
(1)Read Uncimmitted級別:
由於存在臟讀,即能讀到未提交事務的數據行,所以不適用MVCC。原因是MVCC的DATA_TRX_ID只有在事務提交之后才會更新,而在Read uncimmitted級別下,由於是讀取未提交的,所以說MVCC在這個級別下是不適用的。
(2)Read Committed級別:
查找操作:
分析:假設當前有事務A、事務A+1、數據B(DATA_TRX_ID為A-1)。
- 事務A進行查找,此時找出事務ID小於它本身的,所以此時數據B可以被找到;
- 如果在事務A還沒有執行完畢的時候,事務A+1對數據B進行了更新操作,那么此時數據B的undo log則被更新為“數據B(DATA_TRX_ID為A+1)-> 數據B(DATA_TRX_ID為A-1)”;
- 此時如果事務A再次進行查找操作,會更新read_view。更新舊的read_view,並且開啟新的事務A+2。那么根據MVCC的規定,就能夠找到數據B(DATA_TRX_ID為A+1),可以找到更新之后的。這樣子的話就等價於能夠讀取到別的事務commit的最新的數據記錄。這就符合RC級別的語義。
(3)Repeatable Read級別:
查找操作:
- 事務A進行查找,此時找出事務ID小於它本身的,所以此時數據B可以被找到;
- 如果在事務A還沒有執行完畢的時候,事務A+1對數據B進行了更新操作,那么此時數據B的undo log則被更新為“數據B(DATA_TRX_ID為A+1)-> 數據B(DATA_TRX_ID為A-1)”;
- 此時如果事務A再次進行查找操作,那么根據MVCC的規定,還是只能找到數據B(DATA_TRX_ID為A-1)(因為B(DATA_TRX_ID為A+1)的事務ID比當前事務A的事務ID大,所以不會被找到),不會找到更新之后的。這樣子的話就等價於只能夠讀取到事務A開始時讀取到的數據記錄。這就符合RR級別的語義。
(4)Serialization級別:
串行化由於是會對所涉及到的表加鎖,並非行鎖,自然也就不存在行的版本控制問題
總結:通過上面的分析可得:MVCC只適用於MySQL隔離級別中的讀已提交(Read committed)和可重復讀(Repeatable Read)
7、MVCC、gap鎖解決幻讀問題的分析:
前提:InnoDB引擎、RR隔離級別(gap鎖只存在於這個級別下)
(1)首先了解數據記錄的讀取方式:快照讀和當前讀
①快照讀:
讀快照,可以讀取數據的所有版本信息,包括舊版本的信息。其實就是讀取MVCC中的read_view,同時結合MVCC進行相對應的控制;
select * from table where ?;
②當前讀:
讀當前,讀取當前數據的最新版本。而且讀取到這個數據之后會對這個數據加鎖,防止別的事務更改。
(分析:在進行寫操作的時候就需要進行“當前讀”,讀取數據記錄的最新版本)
select * from table where ? lock in share mode; # 讀鎖 select * from table where ? for update; # 寫鎖 insert into table values (…); update table set ? where ?; delete from table where ?;
詳見:https://www.jianshu.com/p/27352449bcc0
③RC和RR隔離級別下的快照讀和當前讀:
- RC隔離級別下,快照讀和當前讀結果一樣,都是讀取已提交的最新;
- RR隔離級別下,當前讀結果是其他事務已經提交的最新結果,快照讀是讀當前事務之前讀到的結果。RR下創建快照讀的時機決定了讀到的版本。
(2)解決幻讀問題:
①對於快照讀:通過MVCC來進行控制的,不用加鎖。按照MVCC中規定的“語法”進行增刪改查等操作,以避免幻讀。(MVCC的具體內容參見上方第1點到第4點的分析)
②對於當前讀:通過next-key鎖(行鎖+gap鎖)來解決問題的。(next-key鎖的分析:mysql中的鎖)
(3)特殊語句分析:
“MVCC不能根本上解決幻讀的情況?”
分析:這句話的含義是指對於快照讀,那么是可以通過MVCC來解決的;但是對於當前讀,則必須通過next-key鎖(行鎖+gap鎖)來解決。