MySQL MVCC機制深入

MySQL MVCC機制

什麼是MVCC

MVCC (Multiversion Concurrency Control) 中文全程叫多版本併發控制,是現代數據庫(包括 MySQLOraclePostgreSQL 等)引擎實現中常用的處理讀寫衝突的手段,目的在於提高數據庫高併發場景下的吞吐性能

如此一來不同的事務在併發過程中,SELECT 操作可以不加鎖而是通過 MVCC 機制讀取指定的版本歷史記錄,並通過一些手段保證保證讀取的記錄值符合事務所處的隔離級別,從而解決併發場景下的讀寫衝突。

InnoDB 中的 MVCC

  1. MySQLInnoDB 引擎支持 MVCC
  2. 應對高併發事務, MVCC 比單純的加行鎖更有效, 開銷更小
  3. MVCC 在讀已提交(Read Committed)和可重複讀(Repeatable Read)隔離級別下起作用
  4. MVCC 既可以基於樂觀鎖又可以基於悲觀鎖來實現

InnoDB MVCC 實現原理

begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第 一個操作InnoDB表的語句,事務才真正啓動,纔會向mysql申請事務ID,mysql內部是嚴格照事務的啓動順序來分配事務ID的事務ID依次遞增

InnoDBMVCC 的實現方式爲:每一行記錄都有兩個隱藏列:DATA_TRX_IDDATA_ROLL_PTR(如果沒有主鍵,則還會多一個隱藏的主鍵列)。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-51oF0K3l-1588519526486)(https://i.loli.net/2020/05/03/zj6l4avrf9VCNsI.png)]

  • DATA_TRX_ID

    記錄最近更新這條行記錄的事務 ID,大小爲 6 個字節

  • DATA_ROLL_PTR

    表示指向該行回滾段(rollback segment)的指針,大小爲 7 個字節,InnoDB 便是通過這個指針找到之前版本的數據。該行記錄上所有舊版本,在 undo 中都通過鏈表的形式組織。

  • DB_ROW_ID

    行標識(隱藏單調自增 ID),大小爲 6 字節,如果表沒有主鍵,InnoDB 會自動生成一個隱藏主鍵,因此會出現這個列。另外,每條記錄的頭信息(record header)裏都有一個專門的bitdeleted_flag)來表示當前記錄是否已經被刪除。

舉例說明

有一張表:user字段有id,name

事務執行表:

#Transaction 100 #Transaction 200 #Transaction 300 #select RR-ReadView RC-ReadView
begin; begin; begin; begin;
update user set name=‘zxl300’ where id=1;
commit;
select name from user where id=1; [100,200],300 rs:zxl300 [100,200],300 rs:zxl300
update user set name=‘zxl1’ where id=1;
update user set name=‘zxl2’ where id=1;
select name from user where id=1; [100,200],300 rs:zxl300 [100,200],300 rs:zxl300
commit; update user set name=‘zxl3’ where id=1;
update user set name=‘zxl4’ where id=1;
select name from user where id=1; [100,200],300 rs:zxl300 [200],300 rs:zxl2
commit;

如何組織 Undo Log 鏈

  1. userid=1 的這行記錄加排他鎖。
  2. 把該行原本的值拷貝到 undo log 中,DB_TRX_IDDB_ROLL_PTR 都不動
  3. 修改該行的值這時產生一個新版本,更新 DATA_TRX_ID 爲修改記錄的事務 ID,將 DATA_ROLL_PTR 指向剛剛拷貝到 undo log 鏈中的舊版本記錄,這樣就能通過 DB_ROLL_PTR 找到這條記錄的歷史版本。如果對同一行記錄執行連續的 UPDATEUndo Log 會組成一個鏈表,遍歷這個鏈表可以看到這條記錄的變遷
  4. 記錄 redo log,包括 undo log 中的修改

那麼 INSERTDELETE 會怎麼做呢?其實相比 UPDATE 這二者很簡單,INSERT 會產生一條新紀錄,它的 DATA_TRX_ID 爲當前插入記錄的事務 IDDELETE 某條記錄時可看成是一種特殊的 UPDATE,其實是軟刪,真正執行刪除操作會在 commit 時,DATA_TRX_ID 則記錄下刪除該記錄的事務 ID

DB_TRX_ID 只有insert/update/delete時創建

ReadView 生成規則

當執行查詢SQL時會生成一致性視圖ReadView,它由執行查詢時所有未提交的事務ID數組(數組中最小的id爲mid_id)和已創建的最大事務ID(max_id)組成,查詢的數據結果需要跟ReadView做比對從而得到快照結果。

RU 下的 ReadView 生成

RU 隔離級別下,直接讀取版本的最新記錄就 OK,對於 SERIALIZABLE 隔離級別,則是通過加鎖互斥來訪問數據,因此不需要 MVCC 的幫助。因此 MVCC 運行在 RCRR 這兩個隔離級別下,當 InnoDB 隔離級別設置爲二者其一時,在 SELECT 數據時就會用到版本鏈

核心問題是版本鏈中哪些版本對當前事務可見?

InnoDB 爲了解決這個問題,設計了 ReadView(可讀視圖)的概念。

RR 下的 ReadView 生成

RR 隔離級別下,每個事務 touch first read 時(本質上就是執行第一個 SELECT 語句時,後續所有的 SELECT 都是複用這個 ReadView,其它 update, delete, insert 語句和一致性讀 snapshot 的建立沒有關係),會將當前系統中的所有的活躍事務拷貝到一個列表生成ReadView

RC 下的 ReadView 生成

RC 隔離級別下,每個 SELECT 語句開始時,都會重新將當前系統中的所有的活躍事務拷貝到一個列表生成 ReadView。二者的區別就在於生成 ReadView 的時間點不同,一個是事務之後第一個 SELECT 語句開始、一個是事務中每條 SELECT 語句開始。

MVCC 判斷流程

根據ReadView生成規則可以將數據分爲三個區域,小於最小未提交事務Id的稱爲已提交事務大於未提交事務最小ID小於已創建最大事務ID稱爲未提交與已提交事務,大於已創建最大事務ID稱爲未開始事務

https://c01.gaitubao.net/gaitubao_Fla_tpH0xuEv1zaf9Ktn958KTlUs.png?watermark/1/image/aHR0cHM6Ly9jMDEuZ2FpdHViYW8ubmV0L2dhaXR1YmFvX0ZvNlZaakFzb3lCN1ZOVU5zYXQ3cVJMNEt4QzAucG5nP2ltYWdlTW9ncjIvcm90YXRlLzA=/gravity/Center/dx/0/dy/0
  1. 如果ROW的ID(DB_TRX_ID)落在綠色部分(DB_TRX_ID<mid_id),表示這個版本是已提交的事務生成的,這個數據是可見的。
  2. 如果ROW的ID(DB_TRX_ID)落在紅色部分(DB_TRX_ID>max_id),表示這個版本是由將來啓動事務生成的,是肯定不可見的。
  3. 如果ROW的ID(DB_TRX_ID)落在黃色部分(mid_id<=DB_TRX_ID<=max_id),則分爲兩種情況:
    • DB_TRX_ID未提交的事務ID數組,表示這個版本是由還沒有提交的事務生成的,不可見,當前自己的事物是可見的。
    • DB_TRX_ID不在未提交的事務ID數組,表示這個版本是已提交的事務生成的,可見。

對於刪除的情況可以認爲是update的特殊情況,會將版本鏈是最新的數據複製一份,然後將DB_TRX_ID修改成刪除操作的DB_TRX_ID,同時在該條記錄的頭信息(record header)裏(deleted flag)標記位寫上ture,來表示當前記錄已被刪除,在查詢時按照上而的規則查詢到對應的記錄如果deleted flag標記位爲ture,意味着記錄已被刪除,則不返回數據。

例RR MVCC 判斷流程

  1. user 表中存在一條用戶信息,id:1 name:zxl.

  2. 首先#Transaction 100,#Transaction 200, #Transaction 300及#select同時開啓事務。

  3. #Transaction 300更新name爲zxl300並提交。

  4. #select進行查詢操作,首次查詢會創建ReadView,創建ReadView爲[100,200],300,根據MVCC判斷流程進行判斷,當前記錄鏈路DB_TRX_ID爲300,300的區間應該在未提交及已提交範圍,300不在未提交數組範圍,所以本條記錄是可見的,即返回zxl300.

  5. 而後#Transaction 100,進行了兩次更新update user set name='zxl1' where id=1;update user set name='zxl2' where id=1;

  6. #select再次進行查詢select name from user where id=1;,因爲事務隔離級別爲RR所以ReadView依然爲[100,200],300,當前記錄鏈路DB_TRX_ID爲 100 ,100的區間應該在未提交及已提交範圍,100在未提交數組範圍,因此本條記錄不可見,根據鏈路繼續查詢,發現還是100,則繼續鏈路查詢發現爲300發現不在未提交範圍內則返回zxl300

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