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

MVCC多版本併發控制

      MySQL的測試環境是5.7

前提概要

  •        什麼是MVCC
  •        什麼是當前讀和快照讀?
  •        當前讀,快照讀和MVCC的關係

MVCC實現原理

  •       隱式字段
  •       undo日誌
  •       Read View(讀視圖)
  •       整體流程

MVCC相關問題

  •      RR是如何在RC級的基礎上解決不可重複讀的?
  •      RC,RR級別下的InnoDB快照讀有什麼不同?

前提概要



什麼是MVCC?

 

MVCC
MVCC,全稱Multi-Version Concurrency Control,即多版本併發控制。MVCC是一種併發控制的方法,一般在數據庫管理系統中,實現對數據庫的併發訪問,在編程語言中實現事務內存。

MVCCMySQL InnoDB中的實現主要是爲了提高數據庫併發性能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖,非阻塞併發讀


什麼是當前讀和快照讀?


在學習MVCC多版本併發控制之前,我們必須先了解一下,什麼是MySQL InnoDB下的當前讀快照讀?

  • 當前讀

像select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操作都是一種當前讀,爲什麼叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖

  • 快照讀

不加鎖的select操作就是快照讀即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是串行級別,串行級別下的快照讀會退化成當前讀;之所以出現快照讀的情況,是基於提高併發性能的考慮,快照讀的實現是基於多版本併發控制,即MVCC,可以認爲MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷;既然是基於多版本,即快照讀可能讀到的並不一定是數據的最新版本,而有可能是之前的歷史版本

說白了MVCC就是爲了實現讀-寫衝突不加鎖,而這個讀指的就是快照讀, 而非當前讀,當前讀實際上是一種加鎖的操作,是悲觀鎖的實現


當前讀,快照讀和MVCC的關係

 

  • 準確的說,MVCC多版本併發控制指的是 “維持一個數據的多個版本,使得讀寫操作沒有衝突” 這麼一個概念。僅僅是一個理想概念
  • 而在MySQL中,實現這麼一個MVCC理想概念,我們就需要MySQL提供具體的功能去實現它,而快照讀就是MySQL爲我們實現MVCC理想模型的其中一個具體非阻塞讀功能。而相對而言,當前讀就是悲觀鎖的具體功能實現
  • 要說的再細緻一些,快照讀本身也是一個抽象概念,再深入研究。MVCC模型在MySQL中的具體實現則是由 3個隱式字段,undo日誌 ,Read View 等去完成的,具體可以看下面的MVCC實現原理

MVCC能解決什麼問題,好處是?


數據庫併發場景有三種,分別爲:

  • 讀-讀:不存在任何問題,也不需要併發控制
  • 讀-寫:有線程安全問題,可能會造成事務隔離性問題,可能遇到髒讀,幻讀,不可重複讀
  • 寫-寫:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失

MVCC帶來的好處是?
多版本併發控制(MVCC)是一種用來解決讀-寫衝突無鎖併發控制,也就是爲事務分配單向增長的時間戳,爲每個修改保存一個版本,版本與事務時間戳關聯,讀操作只讀該事務開始前的數據庫的快照。 所以MVCC可以爲數據庫解決以下問題

  • 在併發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫併發讀寫的性能
  • 同時還可以解決髒讀,幻讀,不可重複讀等事務隔離問題,但不能解決更新丟失問題

小結一下咯
總之,MVCC就是因爲大牛們,不滿意只讓數據庫採用悲觀鎖這樣性能不佳的形式去解決讀-寫衝突問題,而提出的解決方案,所以在數據庫中,因爲有了MVCC,所以我們可以形成兩個組合:

 

 

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

     MVCC解決讀寫衝突,樂觀鎖解決寫寫衝突

這種組合的方式就可以最大程度的提高數據庫併發性能,並解決讀寫衝突,和寫寫衝突導致的問題


MVCC的實現原理



MVCC的目的就是多版本併發控制,在數據庫中的實現,就是爲了解決讀寫衝突,它的實現原理主要是依賴記錄中的 3個隱式字段,undo日誌 ,Read View 來實現的。所以我們先來看看這個三個point的概念

隱式字段


每行記錄除了我們自定義的字段外,還有數據庫隱式定義的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產生一個聚簇索引

  • 實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除並不代表真的刪除,而是刪除flag變了

在這裏插入圖片描述

如上圖,DB_ROW_ID是數據庫默認爲該行記錄生成的唯一隱式主鍵,DB_TRX_ID是當前操作該記錄的事務ID,而DB_ROLL_PTR是一個回滾指針,用於配合undo日誌,指向上一個舊版本


undo日誌


undo log主要分爲兩種:

  • insert undo log

     代表事務在insert新記錄時產生的undo log, 只在事務回滾時需要,並且在事務提交後可以被立即丟棄

  • update undo log

    事務在進行updatedelete時產生的undo log; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日誌時,對應的日誌纔會被purge線程統一清除

purge

  • 從前面的分析可以看出,爲了實現InnoDB的MVCC機制,更新或者刪除操作都只是設置一下老記錄的deleted_bit,並不真正將過時的記錄刪除。
  • 爲了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit爲true的記錄。爲了不影響MVCC的正常工作,purge線程自己也維護了一個read view(這個read view相當於系統中最老活躍事務的read view);如果某個記錄的deleted_bit爲true,並且DB_TRX_ID相對於purge線程的read view可見,那麼這條記錄一定是可以被安全清除的。

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

一、 比如一個有個事務插入persion表插入了一條新記錄,記錄如下,nameJerry, age爲24歲,隱式主鍵是1,事務ID回滾指針,我們假設爲NULL

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

  • 事務1修改該行(記錄)數據時,數據庫會先對該行加排他鎖
  • 然後把該行數據拷貝到undo log中,作爲舊記錄,既在undo log中有當前行的拷貝副本
  • 拷貝完畢後,修改該行nameTom,並且修改隱藏字段的事務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,其實在事務提交之後可能就被刪除丟失了,不過這裏爲了演示,所以還放在這裏


Read View(讀視圖)
什麼是Read View?

什麼是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所在的舊記錄就是當前事務能看見的最新老版本

那麼這個判斷條件是什麼呢?

在這裏插入圖片描述

如上,它是一段MySQL判斷可見性的一段源碼,即changes_visible方法(不完全哈,但能看出大致邏輯),該方法展示了我們拿DB_TRX_ID去跟Read View某些屬性進行怎麼樣的比較

在展示之前,我先簡化一下Read View,我們可以把Read View簡單的理解成有三個全局屬性

  • trx_list(名字我隨便取的)

   一個數值列表,用來維護Read View生成時刻系統正活躍的事務ID

  • up_limit_id

    記錄trx_list列表中事務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 是否在活躍事務之中,trx_list.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,維護在一個列表上,假設我們稱爲trx_list

  • Read View不僅僅會通過一個列表trx_list來維護事務2執行快照讀那刻系統正活躍的事務ID,還會有兩個屬性up_limit_id記錄trx_list列表中事務ID最小的ID),low_limit_id(記錄trx_list列表中事務ID最大的ID,也有人說快照讀那刻系統尚未分配的下一個事務ID也就是目前已出現過的事務ID的最大值+1我更傾向於後者 ) ;所以在這裏例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View如下圖

  • 我們的例子中,只有事務4修改過該行記錄,並在事務2執行快照讀前,就提交了事務,所以當前該行當前數據的undo log如下圖所示;我們的事務2在快照讀該行記錄的時候,就會拿該行記錄的DB_TRX_ID去跟up_limit_id,low_limit_id活躍事務ID列表(trx_list)進行比較,判斷當前事務2能看到該記錄的版本是哪個。

  • 所以先拿該記錄DB_TRX_ID字段記錄的事務ID 4去跟Read View的的up_limit_id比較,看4是否小於up_limit_id(1),所以不符合條件,繼續判斷 4 是否大於等於 low_limit_id(5),也不符合條件,最後判斷4是否處於trx_list中的活躍事務, 最後發現事務ID爲4的事務不在當前活躍事務列表中, 符合可見性條件,所以事務4修改後提交的最新結果對事務2快照讀時是可見的,所以事務2能讀到的最新數據記錄是事務4所提交的版本,而事務4提交的版本也是全局角度上最新的版本

在這裏插入圖片描述

  • 也正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同

MVCC相關問題



RR是如何在RC級的基礎上解決不可重複讀的?
當前讀和快照讀在RR級別下的區別:
表1:


在上表的順序下,事務B的在事務A提交修改後的快照讀是舊版本數據,而當前讀是實時新數據400

表2:


而在表2這裏的順序中,事務B事務A提交後的快照讀和當前讀都是實時的新數據400,這是爲什麼呢?

  • 這裏與上表的唯一區別僅僅是表1的事務B在事務A修改金額前快照讀過一次金額數據,而表2的事務B在事務A修改金額前沒有進行過快照讀。

所以我們知道事務中快照讀的結果是非常依賴該事務首次出現快照讀的地方,即某個事務中首次出現快照讀的地方非常關鍵,它有決定該事務後續快照讀結果的能力

我們這裏測試的是更新,同時刪除更新也是一樣的,如果事務B的快照讀是在事務A操作之後進行的,事務B的快照讀也是能讀取到最新的數據的


RC,RR級別下的InnoDB快照讀有什麼不同?


正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同

  • 在RR級別下的某個事務的對某條記錄的第一次快照讀會創建一個快照及Read View, 將當前系統活躍的其他事務記錄起來,此後在調用快照讀的時候,還是使用的是同一個Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那麼之後的快照讀使用的都是同一個Read View,所以對之後的修改不可見;
  • 即RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動事務的快照,這些事務的修改對於當前事務都是不可見的。而早於Read View創建的事務所做的修改均是可見
  • 而在RC級別下的,事務中,每次快照讀都會新生成一個快照和Read View, 這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因

總之在RC隔離級別下,是每個快照讀都會生成並獲取最新的Read View;而在RR隔離級別下,則是同一個事務中的第一個快照讀纔會創建Read View, 之後的快照讀獲取的都是同一個Read View。
 

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