MVCC多版本併發控制機制

1. 定義

MVCC(Multi-Version Concurrency Control,多版本併發控制)一種併發控制機制,在數據庫中用來控制併發執行的事務,控制事務隔離進行。

2. 核心思想

MVCC是通過保存數據在某個時間點的快照來進行控制的。使用MVCC就是允許同一個數據記錄擁有多個不同的版本。然後在查詢時通過添加相對應的約束條件,就可以獲取用戶想要的對應版本的數據。

3. 基本數據結構

3.1 redo log

重做日誌記錄。存儲事務操作的最新數據記錄,方便日後使用。

3.2 undo log

撤回日誌記錄,也稱版本鏈。當前事務未提交之前,undo log保存了當前事務的正在操作的數據記錄的所有版本的信息,undo log中的數據可作爲數據舊版本快照供其他併發事務進行快照讀。每次有其它事務提交對當前數據行的修改,都是添加到undo log中。undo log是由每個數據行的多個不同的版本鏈接在一起構成的一個記錄“鏈表”。如下圖:

3.3 read_view(快照)

3.3.1 read_view的簡單理解

會對數據在每個時刻的狀態拍成照片記錄下來。那麼之後獲取某時刻的數據時就還是原來的照片上的數據,是不會變的。其實也可以簡單理解爲是一個版本鏈的集合,只不過在這裏的版本鏈是經過篩選的。

3.3.2 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中不會顯示所有的數據行,只會顯示“可見”的記錄。篩選方式如下所述。

3.3.3 read_view的記錄篩選方式

前提:DATA_TRX_ID 表示每個數據行的最新的事務ID;up_limit_id表示當前快照中的最先開始的事務;low_limit_id表示當前快照中的最慢開始的事務,即最後一個事務。

如果記錄的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:

  1. 需要在活躍事務鏈表中查找是否存在ID爲DATA_TRX_ID的值的事務。
  2. 如果存在,那麼因爲在活躍事務鏈表中的事務是未提交的,所以該記錄是不可見的。此時需要從DB_ROLL_PTR指針所指向的回滾段中取出最新的undo-log的版本號, 然後用它繼續重新開始整套比較算法。(詳細分析爲什麼“不可見”:因爲DATA_TRX_ID只有在事務提交之後纔會更新,而此時因爲事務還存在於活躍事務鏈表中,所以說明事務是還沒有commit,所以此時不可能存在對應的數據行,只有在當前事務提交之後纔會有對應的數據行。)
  3. 如果不存在,所以是可見的。(分析:按照上一點的對“不可見”原因的分析,可明白只能是當前本事務更新了這條記錄,因爲在當前read view中,只能是當前事務和正在內存中commit的事務不在事務活躍鏈表中,對於“正在內存中commit的事務”,因爲它還沒有commit,所以肯定是不可能讀取到它的即將要commit的數據的,而所以只能是當前事務對這個數據行做了修改了,雖然未提交,但是因爲是在當前事務中,所以肯定是可以讀取到更新的數據的。

3.3.4 read_view的更新方式

注意:僅分析RC級別和RR級別,因爲MVCC不適用於其它兩個隔離級別。

對於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的內容的原因所在。

對於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的具體實現

4.1 基本數據結構的定義

在mysql中,在實現MVCC時,會爲每一個表添加如下幾個隱藏的字段:

  • 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

4.2 增刪改查

4.2.1 增加insert

  • 設置新記錄的DATA_TRX_ID爲當前事務ID,其他的採用默認的。

4.2.2 刪除delete

  • 修改DATA_TRX_ID的值爲當前的執行刪除操作的事務的ID,然後設置DELETE BIT爲True,表示被刪除

4.2.3 修改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,表示被刪除。

4.2.4 查找select

  • 如果當前數據行的DELETE BIT爲False,只查找版本早於當前事務版本的數據行(也就是數據行的DATA_TRX_ID必須小於等於當前事務的ID),這確保當前事務讀取的行都是事務之前已經存在的,或者是由當前事務創建或修改的行;
  • 如果當前數據行的DELETE BIT爲True,表示被刪除,那麼只能返回DATA_TRX_ID的值大於當前事務的行。獲取在當前事務開始之前,還沒有被刪除的行。

注意:

  1. 此時就是要去查找read_view,判斷其中是否有需要的記錄;
  2. 就算在當前事務提交的時候,也不會讀取到DATA_TRX_ID大於當前事務ID的數據記錄(而默認情況下,RR隔離級別下,當前事務一commit,就能夠讀取到其他事務的commit)。這也是MVCC能夠解決幻讀的原因。

5. 使用MVCC核心優勢

(1)在mysql中,使用MVCC本質上是爲了在進行讀操作的時候代替加鎖,減少加鎖帶來的負擔。

(2)在mysql的InnoDB引擎,並且是在RR隔離級別下,通過使用MVCC和gap鎖來解決幻讀問題(詳細解決方式參見下方的第7點)。

6. MVCC與四大隔離級別的關係的分析

分析了在MVCC的控制之下,如何實現四大隔離級別。

6.1 Read Uncommitted級別

由於存在髒讀,即能讀到未提交事務的數據行,所以不適用MVCC。原因是MVCC的DATA_TRX_ID只有在事務提交之後纔會更新,而在Read uncimmitted級別下,由於是讀取未提交的,所以說MVCC在這個級別下是不適用的。

6.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級別的語義

6.3 Repeatable Read級別

查找操作:

分析:假設當前有:事務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再次進行查找操作,那麼根據MVCC的規定,還是隻能找到數據B(DATA_TRX_ID爲A-1)(因爲B(DATA_TRX_ID爲A+1)的事務ID比當前事務A的事務ID大,所以不會被找到),不會找到更新之後的。這樣子的話就等價於只能夠讀取到事務A開始時讀取到的數據記錄。這就符合RR級別的語義

6.4 Serialization級別

串行化由於是會對所涉及到的表加鎖,並非行鎖,自然也就不存在行的版本控制問題

6.5 總結

通過上面的分析可得:MVCC只適用於MySQL隔離級別中的讀已提交(Read committed)和可重複讀(Repeatable Read)

7. MVCC、gap鎖解決幻讀問題的分析

前提:InnoDB引擎、RR隔離級別(gap鎖只存在於這個級別下)

7.1 首先了解數據記錄的讀取方式:快照讀和當前讀

7.1.1 快照讀

讀快照,可以讀取數據的所有版本信息,包括舊版本的信息。其實就是讀取MVCC中的read_view,同時結合MVCC進行相對應的控制;

select * from table where ?;

7.1.2 當前讀

讀當前,讀取當前數據的最新版本。而且讀取到這個數據之後會對這個數據加鎖,防止別的事務更改。(分析:在進行寫操作的時候就需要進行“當前讀”,讀取數據記錄的最新版本)

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 ?;

7.1.3 RC和RR隔離級別下的快照讀和當前讀

  • RC隔離級別下,快照讀和當前讀結果一樣,都是讀取已提交的最新;
  • RR隔離級別下,當前讀結果是其他事務已經提交的最新結果,快照讀是讀當前事務之前讀到的結果。RR下創建快照讀的時機決定了讀到的版本。

7.2 解決幻讀問題

  1. 對於快照讀:通過MVCC來進行控制的,不用加鎖。按照MVCC中規定的“語法”進行增刪改查等操作,以避免幻讀。(MVCC的具體內容參見上方第1點到第4點的分析)
  2. 對於當前讀:通過next-key鎖(行鎖+gap鎖)來解決問題的。(next-key鎖的分析:mysql中的鎖

7.3 特殊語句分析

“MVCC不能根本上解決幻讀的情況?”

分析:這句話的含義是指對於快照讀,那麼是可以通過MVCC來解決的;但是對於當前讀,則必須通過next-key鎖(行鎖+gap鎖)來解決。

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