1. 程式人生 > 資料庫 >Mysql MVCC

Mysql MVCC

一、MVCC概述

MVCC,全稱Multi-Version Concurrency Control,即多版本併發控制。整個MVCC多併發控制的目的就是為了實現讀-寫衝突不加鎖,提高併發讀寫效能,而這個讀指的就是快照度, 而非當前讀,當前讀實際上是一種加鎖的操作,是悲觀鎖的實現。 

  • 當前讀

  讀取的是記錄資料的最新版本,並且當前讀返回的記錄都會加上鎖,保證其他事務不會再併發的修改這條記錄 

  • 快照讀

  讀取的是記錄資料的可見版本(可能是過期的資料),不用加鎖。

總結來說MVCC的好處:

  • 在併發讀寫資料庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了資料庫併發讀寫的效能

  • 同時還可以解決髒讀,幻讀,不可重複讀等事務隔離問題,但不能解決更新丟失問題

  • MVCC解決讀寫衝突,悲觀鎖或者樂觀鎖解決寫寫衝突

二、mvcc原理

MVCC的目的就是多版本併發控制,目的是為了解決讀寫衝突,總的來說MVCC通過儲存資料在某個時間點的快照來實現的,意味著在同一個時刻不同事務看到的相同表裡的資料可能是不同的(即多版本)。如下示例:事務1和事務3可讀到不同的資料快照。

 

時間點 事務1 事務2 事務3
T1 開始事務 開始事務 開始事務
T2 查詢A的賬戶,金額為100    
T3   修改A的賬戶,金額從100改為200  
T4   提交事務  
T5 查詢A的賬戶,金額為100   查詢A的賬戶,金額為200

MVCC最大的優點是讀不加鎖,因此讀寫不衝突,併發效能好。InnoDB實現MVCC,多個版本的資料可以共存,它的實現原理主要是依賴記錄中的 3個隱式欄位、undo日誌 和Read View 來實現的。

1、隱藏欄位

每行記錄除了我們自定義的欄位外,還有資料庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等欄位

  • DB_TRX_ID

  6byte,最近操作(修改/插入)事務ID:記錄建立這條記錄或者最後一次修改該記錄的事務ID

  • DB_ROLL_PTR

  7byte,回滾指標,指向這條記錄的上一個版本(儲存於rollback segment裡)

  • DB_ROW_ID

  6byte,隱含的自增ID(隱藏主鍵),如果資料表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引

如上圖,DB_ROW_ID是資料庫預設為該行記錄生成的唯一隱式主鍵,DB_TRX_ID是當前操作該記錄的事務ID,而DB_ROLL_PTR是一個回滾指標,用於配合undo日誌,指向上一個舊版本。

2、undo日誌

undo log主要分為兩種:

  • insert undo log
    代表事務在insert新記錄時產生的undo log, 只在事務回滾時需要,並且在事務提交後可以被立即丟棄
  • update undo log
    事務在進行updatedelete時產生的undo log; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日誌時,對應的日誌才會被purge執行緒統一清除

因此,對MVCC有幫助的實質是update undo log ,undo log實際上就是存在rollback segment中舊記錄鏈,它的執行流程如下:

比如事務0插入person表一條新記錄,name為Jerry, age為24,隱式主鍵是1,事務ID回滾指標,我們假設為NULL,如下圖

 

現在來了一個事務1對該記錄的name做出了修改,改為Tom

  • 事務1修改該行資料時,資料庫會先對該行加排他鎖
  • 然後把該行資料拷貝到undo log中,作為舊記錄,既在undo log中有當前行的拷貝副本
  • 拷貝完畢後,修改該行name為Tom,並且修改隱藏欄位的事務ID為當前事務1的ID, 我們預設從1開始,之後遞增,回滾指標指向拷貝到undo log的副本記錄,既表示我的上一個版本就是它
  • 事務提交後,釋放鎖

 

又來了個事務2修改person表的同一個記錄,將age修改為30歲

  • 事務2修改該行資料時,資料庫也先為該行加鎖
  • 然後把該行資料拷貝到undo log中,作為舊記錄,發現該行記錄已經有undo log了,那麼最新的舊資料作為連結串列的表頭,插在該行記錄的undo log最前面
  • 修改該行age為30歲,並且修改隱藏欄位的事務ID為當前事務2的ID, 那就是2,回滾指標指向剛剛拷貝到undo log的副本記錄
  • 事務提交,釋放鎖

 

從上面,我們就可以看出,不同事務或者相同事務的對同一記錄的修改,會導致該記錄的undo log成為一條記錄版本線性表,既連結串列,undo log的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄(該undo log的節點可能是會purge執行緒清除掉,向圖中的第一條insert undo log,其實在事務提交之後可能就被刪除丟失了,不過這裡為了演示,所以還放在這裡)

3、Read View(讀檢視)

Read View就是事務進行快照讀操作的時候生產的讀檢視(Read View),記錄並維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大),所以我們知道 Read View主要是用來做可見性判斷的, 即當我們某個事務執行快照讀的時候,對該記錄建立一個Read View讀檢視,把它比作條件用來判斷當前事務能夠看到哪個版本的資料,既可能是當前最新的資料,也有可能是該行記錄的undo log裡面的某個版本的資料。

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

我先簡化一下Read View,我們可以把Read View簡單的理解成有三個全域性屬性

  • rw_trx_ids
    一個數值列表,用來維護Read View生成時刻系統正活躍的事務ID
  • up_limit_id
    記錄rw_trx_ids列表中事務ID最小的ID
  • low_limit_id
    ReadView生成時刻系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的最大值+1

判斷邏輯如下:

  • 首先比較DB_TRX_ID < up_limit_id, 如果小於,則當前事務能看到DB_TRX_ID 所在的記錄,如果大於等於進入下一個判斷

  • 接下來判斷 DB_TRX_ID 大於等於 low_limit_id , 如果大於等於則代表DB_TRX_ID 所在的記錄在Read View生成後才出現的,那對當前事務肯定不可見,如果小於則進入下一個判斷

  • 判斷DB_TRX_ID 是否在活躍事務之中,rw_trx_ids.contains(DB_TRX_ID),如果在,則代表我Read View生成時刻,你這個事務還在活躍,還沒有Commit,你修改的資料,我當前事務也是看不見的;如果不在,則說明,你這個事務在Read View生成之前就已經Commit了,你修改的結果,我當前事務是能看見的

四、整體流程

我們在瞭解了隱式欄位undo log, 以及Read View的概念之後,就可以來看看MVCC實現的整體流程是怎麼樣了。

  • 事務2對某行資料執行了快照讀,資料庫為該行資料生成一個Read View讀檢視,假設當前事務ID為2,此時還有事務1事務3在活躍中,事務4事務2快照讀前一刻提交更新了,所以Read View記錄了系統當前活躍事務1,3的ID,維護在一個列表rw_trx_ids上
事務1事務2事務3事務4
事務開始 事務開始 事務開始 事務開始
修改且已提交
進行中 快照讀 進行中  
 
  • Read View不僅僅會通過一個列表rw_trx_ids來維護事務2執行快照讀那刻系統正活躍的事務ID,還會有兩個屬性up_limit_id(記錄rw_trx_ids列表中事務ID最小的ID),low_limit_id(記錄rw_trx_ids列表中事務ID最大的ID,也有人說快照讀那刻系統尚未分配的下一個事務ID也就是目前已出現過的事務ID的最大值+1,我更傾向於後者;所以在這裡例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,rw_trx_ids集合的值是1,3。
  • 我們的例子中,只有事務4修改過該行記錄,並在事務2執行快照讀前,就提交了事務,所以當前該行資料的undo log如下圖所示;我們的事務2在快照讀該行記錄的時候,就會拿該行記錄的DB_TRX_ID去跟up_limit_id,low_limit_idrw_trx_ids進行比較,判斷當前事務2能看到該記錄的版本是哪個。
  • 所以先拿該記錄DB_TRX_ID欄位記錄的事務ID4去跟Read View的的up_limit_id比較,看4是否小於up_limit_id(1),所以不符合條件,繼續判斷 4 是否大於等於 low_limit_id(5),也不符合條件,最後判斷4是否處於rw_trx_ids中的活躍事務, 最後發現事務ID為4的事務不在當前活躍事務列表中, 符合可見性條件,所以事務4修改後提交的最新結果對事務2快照讀時是可見的,所以事務2能讀到的最新資料記錄是事務4所提交的版本,而事務4提交的版本也是全域性角度上最新的版本
  • 也正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同

五、mvcc示例解析

下面以RR隔離級別為例,結合前文提到的幾個問題分別說明。

(1)髒讀

時間點 事務1 事務2
T1 開始事務 開始事務
T2   修改A的金額,將金額從100改為200
T3 查詢A的金額,為100  
T4   提交事務

當事務1在T3時刻讀取A的餘額前,會生成ReadView,由於此時事務2沒有提交仍然活躍,因此其事務id一定在ReadView的rw_trx_ids中,因此根據前面介紹的規則,事務B的修改對ReadView不可見。接下來,事務A根據指標指向的undo log查詢上一版本的資料,得到A的餘額為100。這樣事務1就避免了髒讀。

(2)不可重複讀

 

時間點 事務1 事務2
T1 開始事務 開始事務
T2 快照讀A賬戶,為100  
T3   修改A的金額,將金額從100改為200
T4   提交事務
T5 快照讀A的金額,為100


當事務1在T2時刻讀取A的金額前,會生成ReadView。此時事務2分兩種情況討論,一種是如圖中所示,事務已經開始但沒有提交,此時其事務id在ReadView的rw_trx_ids中;一種是事務2還沒有開始,此時其事務id大於等於ReadView的low_limit_id。無論是哪種情況,根據前面介紹的規則,事務2的修改對ReadView都不可見。當事務1在T5時刻再次讀取A的餘額時,會根據T2時刻生成的ReadView對資料的可見性進行判斷,從而判斷出事務2的修改不可見;因此事務1根據指標指向的undo log查詢上一版本的資料,得到A的餘額為100,從而避免了不可重複讀。

(3)幻讀

時間點 事務1 事務2
T1 開始事務 開始事務
T2 快照讀A賬戶,為100  
T3   修改A的金額,將金額從100改為200
T4   提交事務
T5 快照讀A的金額,為100

MVCC避免幻讀的機制與避免不可重複讀非常類似。

當事務A在T2時刻讀取0<id<5的使用者餘額前,會生成ReadView。此時事務B分兩種情況討論,一種是如圖中所示,事務已經開始但沒有提交,此時其事務id在ReadView的rw_trx_ids中;一種是事務B還沒有開始,此時其事務id大於等於ReadView的low_limit_id。無論是哪種情況,根據前面介紹的規則,事務B的修改對ReadView都不可見。

當事務A在T5時刻再次讀取0<id<5的使用者餘額時,會根據T2時刻生成的ReadView對資料的可見性進行判斷,從而判斷出事務B的修改不可見。因此對於新插入的資料lisi(id=2),事務A根據其指標指向的undo log查詢上一版本的資料,發現該資料並不存在,從而避免了幻讀。