MVCC多版本併發控制

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第5天,點擊查看活動詳情

MVCC定義

1、MVCC簡介

MVCC,全稱Multi-Version Concurrency Control,即多版本井發控制,MVCC是一種併發控制的方法,一般在數據庫管理系統中,實現對數據庫的併發訪問,在編程語言中實現事務內存。 MVCC在MysQL InnoDB中的實現主要是為了提高數據庫併發性能,用更好的方式去處理讀寫衝突。做到即使有讀寫衝突時,也能做到不加鎖,非阻塞併發讀。

2、當前讀

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

3、快照讀

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

4、當前讀、快照讀、MVCC關係

MVCC多版本併發控制指的是維持一個數據的多個版本,使得讀寫操作沒有衝突,快照讀是MySQL為實現MVCC的一個非阻塞讀功能。 MVCC模塊在MySQL中的具體實現是由 三個隱式字段、undo日誌、read view三個組件來實現的。

5、MVCC解決的問題

前提數據庫併發場景有三種,分別為∶ 1)、讀讀∶ 不存在任何問題,也不需要併發控制 2)、讀寫∶有線程安全問題,可能會造成事務隔離性問題,可能遇到髒讀、幻讀、不可重複讀 3)、寫寫∶ 有線程安全問題,可能存在更新丟失問題 MVCC是一種用來解決讀寫衝突的無鎖併發控制,也就是為事務分配單項增長的時間戳,為每個修改保存一個版本,版本與事務時間戳關聯,讀操作只讀該事務開始前的數據庫的快照。 因此,MVCC可以為數據庫解決以下問題∶ 1)、在併發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫併發讀寫的性能。 2)、解決髒讀、幻讀、不可重複讀等事務隔離問題,但是不能解決更新丟失問題。

MVCC實現原理

mvcc的實現原理主要依賴於記錄中的三個隱藏字段,undo log,read view來實現的。

1、隱藏字段:每行記錄除了我們自定義的字段外,還有數據庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

1),DB_TRX_ID:6字節,最近修改事務id,記錄創建這條記錄或者最後一次修改該記錄的事務id。

2),DB_ROLl_PTR:7字節,回滾指針,指向這條記錄的上一個版本,用於配合undolog,指向上一個舊版本 。

3),DB_ROWjD:6字節,隱藏的主鍵,如果數據表沒有主鍵,那麼innodb會自動生成一個6字節的row_id。

隱藏字段的記錄如同所示:

img

在上圖中,DB_ROW_ID是數據庫默認為該行記錄生成的唯一隱式主鍵,DB_TRX_ID是當前操作該記錄的事務ID,DB_ROLL_PTR是一個回滾指針,用於配合undo日誌,指向上一個舊版本。 2、undolog:undolog被稱之為回滾日誌,表示在進行insert,delete,update操作的時候產生的,方便回滾的日誌。

當進行insert操作的時候,產生的undolog只在事務回滾的時候需要,並且在事務提交之後可以被立刻丟棄; 當進行update和delete操作的時候,產生的undolog不僅僅在事務回滾的時候需要,在快照讀的時候也需要,所以不能隨便刪除,只有在快照讀或事務回滾不涉及該日誌時,對應的日誌才會被purge線程統一清除(當數據發生更新和刪除操作的時候都只是設置一下老記錄的deleted bit,並不是真正的將過時的記錄刪除,因為為了節省磁盤空間,innodb有專門的purge線程來清除deleted_bit為true的記錄,如果某個記錄的deletedid為true,並且DB_TRXID相對於purge線程的read view可見,那麼這條記錄一定時可以被清除的)。

下面我們來看-下undolog生成的記錄鏈: 1)、假設有一個事務編號為1的事務向表中插入一條記錄,那麼此時行數據的狀態為;

img

2)、假設有第二個事務編號為2,對該記錄的name做出修改,改為masi在事務2修改該行記錄數據時,數據庫會對該行加排他鎖。 然後把該行數據拷貝到undolog中,作為 舊記錄,即在undolog中有當前行的拷貝副本。 拷貝完畢後,修改該行name為masi,並且修改隱藏字段的事務id為當前事務2的d,回滾指針指向拷貝到undolog的副本記錄地址,事務提交後,釋放鎖。

img

3)、假設有第三個事務編號為3對該記錄的age做了修改,改為32在事務3修改該行數據的時,數據庫會對該行加排他鎖。 然後把該行數據拷貝到undolog中,作為舊紀錄,發現該行記錄已經有undolog了,那麼最新的舊數據作為鏈表的表頭,插在該行記錄的undolog最前面。 修改該行age為32歲,並且修改隱藏字段的事務id為3,回滾指針指向剛剛拷貝的undolog的副本記錄地址,事務提交,釋放鎖。

img

從上述的一系列圖中,大家可以發現,不同事務或者相同事務的對同一記錄的修改,會導致該記錄的undolog生成一條記錄版本線性表, 即鏈表,undolog的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄。

3、Read View 上面的流程如果看明白了,那麼大家需要再深入理解下read view的概念了: Read View是事務進行快照讀操作的時候生產的讀視圖,在該事務執行快照讀的那一刻,會生成一個數據系統當前的快照,記錄並維護系統當前活躍事務的id,事務的id值是遞增的。 其實Read View的最大作用是用來做可見性判斷的,也就是説當某個事務在執行快照讀的時候,對該記錄創建一個Read View的視圖,把它當作條件去判斷當前事務能夠看到哪個版本的數據,有可能讀取到的是最新的數據,也有可能讀取的是當前行記錄的undolog中某個版本的數據。 Read View遵循的可見性算法主要是將要被修改的數據的最新記錄中的DB_TRX_ID(當前事務id)取出來,與系統當前其他活躍事務的id去對比,如果DB_TRX_ID跟Read Vew的屬性做了比較,不符合可見性。那麼就通過DB_ROLL_PTR回滾指針去取出undolog中的 DB_TRX_ID做比較,即遍歷鏈表中的DB_TRX_ID, 直到找到滿足條件的DB_TRX_ID。這個DB_TR_ID所在的舊記錄就是當前事務能看到的新老版本數據

Read View的可見性規則如下所示。首先要知道Read View中的三個全局屬性∶ - trx_list∶ 一個數值列表,用來維護Read View生成時刻系統正活躍的事務ID - up_limt_id∶ 記錄trx_list列表中事務ID最小的ID - low_limit_id∶ Read View生成時刻系統尚未分配的下一個事務ID。

具體的比較規則如下∶

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

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

3)、判斷DB_TRX_ID是否在活躍事務中,如果在,則代表在Read View生成時刻,這個事務還是活躍狀態,還沒有commit,修改的數據,當前事務也是看不到,如果不在,則説明這個事務在Read View生成之前就已經開始commit,那麼修改的結果是能夠看見的。

MVCC的整體處理流程

1、假設有四個事務同時在執行,如下圖所示∶

img

從上述表格中,我們可以看到,當事務2對某行數據執行了快照讀,數據庫為該行數據生成一個Read View視圖,可以看到事務1和事務3還在活躍狀態,事務4在事務2快照讀的前一刻提交了更新,所以,在Read Vew中記錄了系統當前活躍事務1,2,3,維護在一個列表中;同時可以看到up_limit_id的值1,而low _limit_id為5。如下圖所示:

img

在上述的例子中,只有事務4修改過該行記錄,並在事務2進行快照讀前,就提交了事務,所以該行當前數據的undolog如下所示∶

img

當事務2在快照讀該行記錄的是,會拿着該行記錄的DB_TRXID去跟up_limit_d,lower_llimit_id和活躍事務列表進行比較,判讀事務2能看到該行記錄的版本是哪個。

2、具體流程如下∶

先拿該行記錄的事務ID(4)去跟Read Vew中的up_limt_id相比較,判斷是否小於,通過對比發現不小於,所以不符合條件;繼續判斷4是否大於等於low_Imit id,通過比較發現也不大於,所以不符合條件,判斷事務4是否處理trx list列表中,發現不再次列表中,那麼符合可見性條件,所以事務4修改後提交的最新結果對事務2的快照是可見的,因此,事務2讀取到的最新數據記錄是事務 4所提交的版本,而事務4提交的版本也是全局角度的最新版本。如下圖所示∶

img

當上述的內容都看明白了的話,那麼大家就應該能夠搞清楚這幾個核心概念之間的關係了,下面我們講一個不同的隔離級別下的快照的不同。

RC、RR級別下的InnoDB快照讀有什麼不同:

1、首先mysql四種隔離級別:

1)未提交讀(READ UNCOMMITED)髒讀

2) 已提交讀 (READ COMMITED)簡稱(RC) 不可重複讀

3)可重複讀(REPEATABLE READ)簡稱(RR )

4)可串行化(SERIALIZABLE)

2、因為Read View生成時機的不同,從而造成RC、RR級別下快照讀的結果的不同:

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

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

樂觀鎖和MVCC區別

在數據庫中,併發控制是指在多個用户/進程/線程同時對數據庫進行操作時,如何保證事務的一致性和隔離性的,同時最大程度地併發。

當多個用户/進程/線程同時對數據庫進行操作時,會出現3種衝突情形:

  1. 讀-讀,不存在任何問題
  2. 讀-寫,有隔離性問題,可能遇到髒讀(會讀到未提交的數據) ,幻影讀等。
  3. 寫-寫,可能丟失更新

要解決衝突,一種辦法是是鎖,即基於鎖的併發控制,比如2PL,這種方式開銷比較高,而且無法避免死鎖。

多版本併發控制(MVCC)是一種用來解決讀-寫衝突的無鎖併發控制,也就是為事務分配單向增長的時間戳,為每個修改保存一個版本,版本與事務時間戳關聯,讀操作只讀該事務開始前的數據庫的快照。 這樣在讀操作不用阻塞寫操作,寫操作不用阻塞讀操作的同時,避免了髒讀和不可重複讀

樂觀併發控制(OCC)是一種用來解決寫-寫衝突的無鎖併發控制,認為事務間爭用沒有那麼多,所以先進行修改,在提交事務前,檢查一下事務開始後,有沒有新提交改變,如果沒有就提交,如果有就放棄並重試。樂觀併發控制類似自選鎖。樂觀併發控制適用於低數據爭用,寫衝突比較少的環境。

多版本併發控制可以結合基於鎖的併發控制來解決寫-寫衝突,即MVCC+2PL,也可以結合樂觀併發控制來解決寫-寫衝突。