InnoDB學習(五)之MVCC多版本並發控制

MVCC多版本並發控制,是一種數據庫管理系統並發控制的方法。MVCC多版本並發控制下,數據庫中的數據會有多個版本,分別對應不同的事務,從而達到事務之間並發數據的隔離。MVCC最大的優勢是讀不加鎖,讀寫不衝突,在讀多寫少場景中,讀寫不衝突可以大幅提升數據庫的並發性能。

MVCC多版本並發控制

在MYSQL中,MyISAM存儲引擎使用的是表鎖,InnoDB存儲引擎使用的是行鎖。而InnoDB的事務分為四個隔離級別,其中默認的隔離級別是可重複讀,可重複讀要求兩個並行的事務之間數據的修改互不影響,通過添加行鎖的方式雖然可以實現兩個事務之間數據據的修改互不影響,但是者兩個事務之間存在鎖等待的情況,影響數據庫效率。所以InnoDB的可重複讀沒有採用行鎖,而是使用了更為強大的MVCC。

MVCC只有在可重複讀和讀已提交的隔離級別下生效,其它兩個隔離級別和MVCC不兼容,因為讀未提交總是讀最新的數據行,和事務版本無關,串行化則是會對所有讀取的行加鎖。由於可重複讀的情況比較複雜,並且是MySQL的默認隔離級別,所以本文會用可重複讀來講解MVCC的原理。

可重複讀

數據庫有四種隔離級別:讀未提交/讀已提交/可重複讀/串行化,可重複度是MySQL的默認事務隔離級別,它確保同一事務的多個實例在並發讀取數據時,會看到一致的數據行。

數據行的一致性包含兩部分:

  • 情況1:已有數據的內容變更,在同一個事務中多次查詢,查詢結果應該相同,如果在當前事務中進行了修改,查詢結果應該和當前事務中的修改結果相同;
  • 情況2:數據行的增減,同一個事務只能查看到事務開啟之前數據庫中數據,或者由事務本身新增/刪除的結果集,無法看到開啟事務期間其它事務新增或刪除的結果集;

InnoDB默認的隔離級別是可重複讀,可以解決以上兩種情況的數據行一致性問題。其中解決情況1中的數據行一致性問題就是通過MVCC多版本並發控制實現的。

InnoDB用過Gap鎖實現情況2中的數據行一致性問題,不過本文不會對Gap鎖進行介紹。

MVCC的作用

MVCC可以確保同一個事務,在事務起始到結束讀到的某一個數據是一致的,並且多個事務之間互不阻塞。我們以一張用戶表為例,說明MVCC版本控制的作用。

首先我們需要創建用戶表,並向其中插入一條用戶數據,SQL語句如下:

create table user_info
(
    age int ,
    name  varchar(255)
);

insert into user_info(age,name) value (23,'張三');

假設有A,B,C三個事務,這三個事務中在不同時刻對讀取了插入用戶的信息,並對用戶信息進行了修改,時間線如下:

  1. T1時刻,事務A開始,事務A讀取age=23的用戶,該用戶的name張三
  2. T2時刻,事務B開始,事務B讀取age=23的用戶,該用戶的name張三
  3. T3時刻,事務A修改age=23的用戶,把name修改為李四
  4. T4時刻,事務A讀取age=23的用戶,該用戶的name李四,事務A提交事務;
  5. T5時刻,事務B讀取age=23的用戶,該用戶的name張三,事務B提交事務;
  6. T6時刻,事務C開始,事務C讀取age=23的用戶,該用戶的name李四,事務C提交事務;

MVCC示例

MVCC的作用可以在T5時刻體現出來,此時事務A已經提交,並且修改age=23的用戶的name李四,但是事務B看不到這次修改,事務B看到的age=23的用戶的name張三。這是因為在可重複度的隔離級別下,InnoDB事務讀取到的數據是快照讀,即事務B開始時為數據生成一個快照,事務B讀到的數據始終都是這個快照,與快照讀對應的是當前讀

  • 當前讀:讀取的是記錄的最新版本,讀取時還要保證其他並發事務不能修改當前記錄,會對讀取的記錄進行加鎖;
  • 快照讀:MVCC使用的就是快照讀,在事務啟動時為數據生成快照,快照讀可以避免了加鎖操作,提升數據庫性能;

MVCC原理

MVCC的目的就是多版本並發控制,在InnoDB中引入MVCC就是為了解決讀寫衝突,MVCC主要包含三部分內容:數據庫中的3個隱藏字段、UndoLog日誌 、ReadView讀視圖,這三部分在MVCC中的作用分別如下所示:

  1. 隱藏字段:為數據添加額外的版本信息,是MVCC版本控制的基石;
  2. UndoLog:存儲了多個版本的數據,不同版本數據隱藏字段的內容不同;
  3. ReadView:判斷當前事務應該讀取哪個版本的數據;

隱藏字段

隱藏字段意味着我們通過SQL語句查找不到這些字段,但是這些字段在數據庫中實際存在並佔用了存儲空間。為了實現MVCC版本控制,InnoDB為每一行數據添加了以下3個隱藏字段:

  1. DB_TRX_ID:6位元組,最後修改本記錄的事務ID;
  2. DB_ROLL_PTR:7位元組,回滾指針,指向這條記錄的上一個版本(存儲於Rollback Segment);
  3. DB_ROW_ID:6位元組,隱藏主鍵,如果數據表沒有顯式主鍵,InnoDB用DB_ROW_ID構建聚簇索引;

我們使用以下SQL創建用戶表,並向表中插入一條數據,新表會默認包含三個隱藏字段,表結構如下表所示。

create table user_info
(
    age int,
    name  varchar(255)
);
insert into user_info(age,name) value (23,'張三');

|age|name|DB_TRX_ID|DB_ROLL_PTR|DB_ROW_ID|
|–|–|–|–||
|23|張三|1|0x222333|1|

UndoLog日誌

我在另外一篇文章中介紹過UndoLog日誌,從名字也可以看出來,UndoLog日誌主要用於回滾事務。但是InnoDB中的MVCC的快照讀也使用了UndoLog。UndoLog可以分為兩大類:

  1. Insert UndoLog:事務中的Insert語句對應的UndoLog,只在事務回滾時需要,所以事務提交後可以被立即丟棄;
  2. Update UndoLog:事務在進行Update或Delete時產生的UndoLog; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快照讀或事務回滾不涉及該日誌時,對應的日誌才會被Purge線程統一清除;

Purge線程:InnoDB中,被刪除的數據不會直接刪除,而是先標記為刪除,無用的Update UndoLog也不會立即刪除。這些數據都是通過InnoDB中的後台任務Purge線程進行刪除的。

下文中我們以上文中的用戶表以及數據為例,解釋Update UndoLog的工作流程,如下為起始時user_info表空間的數據狀態:

MVCC示例

  1. T1時刻,事務A開始,事務Id為2,事務A讀取age=23的用戶,該用戶的name張三;此時沒有修改數據庫數據,沒有生成UndoLog,表空間無變化;

    MVCC示例

  2. T2時刻,事務B開始,事務Id為3,事務B讀取age=23的用戶,該用戶的name張三;此時沒有修改數據庫數據,沒有生成UndoLog,表空間無變化;

    MVCC示例

  3. T3時刻,事務A修改age=23的用戶,把name修改為李四;此時由於事務A尚未提交,所以會給事務A生成一條UndoLog,UndoLog中存儲了事務A修改前的數據,表空間中最新數據中的回滾指針指向這條日誌;

    MVCC示例

  4. T4時刻,事務A讀取age=23的用戶,由於表數據中的記錄的事務ID和事務A的事務ID一致,所以事務A會讀取到表數據中的記錄,讀取到用戶的name李四,事務A提交事務;

    MVCC示例

  5. T5時刻,事務B讀取age=23的用戶,由於表空間中數據不滿足可見性條件(下一節具體介紹),所以事務B會查找表數據的UndoLog,UndoLog中的數據滿足可見性條件,所以查詢到UndoLog中的用戶,用戶的name張三,事務B提交事務;

    MVCC示例

  6. T6時刻,事務C開始,事務ID為3,事務C讀取age=23的用戶,由於事務C開始時事務A已經提交,所以事務C可以查詢到已提交的數據,事務C讀取到用戶的name李四

    MVCC示例

  7. T7時刻,事務C開始,事務ID為3,事務C修改age=23的用戶,把name修改為王五;此時由於事務C尚未提交,所以會給事務C生成一條UndoLog,UndoLog中存儲了事務C修改前的數據;

    MVCC示例

從上面的例子可以看出,不同事務或者相同事務的對同一記錄的修改,會導致該記錄的UndoLog成為一條記錄版本線性鏈表,UndoLog的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄(UndoLog的節點可能會被Purge線程清除掉)

UndoLog是為回滾而用,具體內容就是複製事務前的數據庫記錄行到UndoBuffer,在適合的時間把UndoBuffer中的內容刷新到磁盤。UndoBuffer與RedoBuffer一樣,也是環形緩衝,但當緩衝滿的時候,UndoBuffer中的內容會也會被刷新到磁盤;與RedoLog不同的是,磁盤上不存在單獨的UndoLog文件,所有的UndoLog均存放在主ibd數據文件中(表空間),即使客戶端設置了每表一個數據文件也是如此。

ReadView讀視圖

ReadView就是事務進行快照讀操作的時候生產的讀視圖,在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)

所以我們知道ReadView主要是用來做可見性判斷的, 即當我們某個事務執行快照讀的時候,對該記錄創建一個ReadView讀視圖,把它比作條件用來判斷當前事務能夠看到哪個版本的數據,既可能是當前最新的數據,也有可能是該行記錄的UndoLog裏面的某個版本的數據。

ReadView遵循一個可見性算法,主要是將要被修改的數據的最新記錄中的DB_TRX_ID(即當前事務ID)取出來,與系統當前其他活躍事務的ID去對比(由ReadView維護),如果DB_TRX_ID跟ReadView的屬性做了某些比較,不符合可見性,那就通過DB_ROLL_PTR回滾指針去取出UndoLog中的DB_TRX_ID再比較,即遍歷鏈表的DB_TRX_ID(從鏈首到鏈尾,即從最近的一次修改查起),直到找到滿足特定條件的DB_TRX_ID, 那麼這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新老版本。

ReadView判斷可見性的原理如下,在InnoDB中,創建一個新事務之後,當新事務讀取數據時,數據庫為該事務生成一個ReadView讀視圖,InnoDB會將當前系統中的活躍事務列表創建一個副本保存到ReadView。當用戶在這個事務中要讀取某行記錄的時候,InnoDB會將該行當前的版本號與該ReadView進行比較。具體的算法如下:

  1. 設該行的當前事務ID為cur_trx_id,ReadView中最早的事務ID為min_trx_id, 最遲的事務ID為max_trx_id;
  2. 如果cur_trx_id < min_trx_id,那麼表明該行記錄所在的事務已經在本次新事務創建之前就提交了,所以該行記錄的當前值是可見的。跳到步驟6.
  3. 如果cur_trx_id > max_trx_id,那麼表明該行記錄所在的事務在本次新事務創建之後才開啟,所以該行記錄的當前值不可見.跳到步驟5;
  4. 如果min_trx_id<= cur_trx_id <= max_trx_id, 那麼表明該行記錄所在事務在本次新事務創建的時候處於活動狀態,從min_trx_id到max_trx_id進行遍歷,如果cur_trx_id等於他們之中的某個事務id的話,那麼不可見。跳到步驟5;
  5. 從該行記錄的DB_ROLL_PTR指針所指向的回滾段中取出最新的UndoLog的版本號,將它賦值該cur_trx_id,然後跳到步驟2;
  6. 將該可見行的值返回;

總結一下:MVCC版本控制中,以事務第一次快照讀為分界線,事務後續只能查找到第一次快照讀及之前提交的數據版本,之後提交的數據版本不可見。

讀已提交和可重複度

讀已提交和可重複度隔離級別下的InnoDB快照讀有什麼不同?答案是:ReadView生成時機的不同,從而造成讀已提交和可重複度級別下快照讀的結果的不同:

  • 可重複讀隔離級別下,事務第一次快照讀會生成ReadView時,ReadView會記錄此時所有其他活動事務的快照,這些事務的修改對於當前事務都是不可見的。而早於ReadView創建的事務所做的修改均是可見;
  • 讀已提交隔離級別下的,事務每次快照讀都會新生成一個快照和ReadView, 這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因;

總之在讀已提交隔離級別下,是每個快照讀都會生成並獲取最新的ReadView;而在可重複讀隔離級別下,則是同一個事務中的第一個快照讀才會創建ReadView, 之後的快照讀獲取的都是同一個ReadView。

MVCC與幻讀

幻讀是指,同一個事務裏面連續執行兩次同樣的SQL語句,可能導致不同結果的問題,第二次SQL語句可能會返回之前不存在的行。舉例說明:T1時刻事務A和事務B同時開啟,分別進行了快照讀,然後事務A向數據庫中插入一條新的記錄,如果事務B可以讀到這條記錄,就出現了”幻讀”,因為B第一次快照讀沒有讀到這條數據。

MVCC是否可以解決幻讀問題呢?答案是有的情況下可以解決,有的情況下不可以解決。如果事務B中的讀是快照讀,那麼MVCC版本控制可以解決幻讀問題;如果事務B中使用的是當前讀,那麼MVCC無法解決幻讀問題。

  • 快照讀是基於MVCC和UndoLog來實現的,適用於簡單Select語句;
  • 當前讀是基於Gap鎖來實現的,適用於Insert,Update,Delete,Select … For Update, Select … Lock In Share Mode語句,以及加鎖了的Select語句;

事實上,MVCC對於所有的當前讀都無效,比如事務A修改數據之後,事務B去Update對應的數據,Update語句篩選條件針對的是數據庫中當前的數據,而不是快照數據。

我是御狐神,歡迎大家關注我的微信公眾號:wzm2zsd

qrcode_for_gh_83670e17bbd7_344-2021-09-04-10-55-16

參考文檔

MySQL之MVCC與幻讀

正確的理解MySQL的MVCC及實現原理

MySQL數據庫事務各隔離級別加鎖情況–read committed && MVCC

本文最先發佈至微信公眾號,版權所有,禁止轉載!

Tags: