懵了!女朋友突然問我MVCC實現原理

發表於 2021-04-06

前言

都知道事務的可重複讀級別實現原理是使用MVCC實現的,那麼你對MVCC的底層實現原理知道多少呢?面試高頻點,你值得擁有。

一、MVCC到底是什麼?

MVCC即多版本控制器,其特點就是在同一時間,不同事務可以讀取到不同版本的資料,從而去解決髒讀和不可重複讀的問題。

在這裡插入圖片描述
在這裡插入圖片描述

這樣的解釋你看了不下幾十遍了吧!但是你真的理解什麼是多版本控制器嗎?

生活案例:搬家

最近小Q跟自己的女朋友搬到新家,由於出小區的時候需要支付當月的物業費。

於是小Q跟自己的女朋友同時登入了小區提供的物業繳費系統。

悲觀併發控制

假設小Q正在查當月需要繳納的費用是多少進行支付的時候,此時小Q查詢的這條資料是已經被鎖定的。

那麼小Q女朋友是無法訪問該資料的,直至小Q支付完成或者退出系統將悲觀鎖釋放,小Q的女朋友才可以查詢到資料。

悲觀鎖保證在同一時間只能有一個執行緒訪問,預設資料在訪問的時候會產生衝突,然後在整個過程都加上了鎖。

這樣的系統對於使用者來說就是毫無體驗感,如果多個人同時需要訪問一條資訊,只能在一臺裝置上看嘍!

樂觀併發控制

在小Q檢視物業費欠費情況,並且支付的同時,小Q的女朋友也可以訪問到該資料。

樂觀鎖認為即使在併發環境下,也不會產生衝突問題,所以不會去做加鎖操作。

而是在資料提交的時候進行檢測,如果發現有衝突則返回衝突資訊。

小結

Innodb的MVCC機制就是樂觀鎖的一種體現,讀不加鎖,讀寫不衝突,在不加鎖的情況下能讓多個事務進行併發讀寫,並且解決讀寫衝突問題,極大的提高系統的併發性

二、悲觀鎖、樂觀鎖

鎖按照粒度分為表鎖、行鎖、頁鎖。

按照使用方式分為共享鎖、排它鎖。

根據思想分為樂觀鎖、悲觀鎖。

無論是樂觀鎖、悲觀鎖都只是一種思想而已,並不是實際的鎖機制,這點一定要清楚。

1. 悲觀鎖(悲觀併發控制)

悲觀鎖實際為悲觀併發控制,縮寫PCC。

悲觀鎖持消極態度,認為每一次訪問資料時,總是會發生衝突,因此,每次訪問必須先鎖住資料,完成訪問後在釋放鎖。

保證在同一時間只有單個執行緒可以訪問,實現資料的排它性。同時悲觀鎖使用資料庫自身的鎖機制實現,可以解決讀-寫,寫-寫的衝突。

那麼在什麼場景下可以使用悲觀鎖呢!

悲觀鎖適用於在寫多讀少的併發環境下使用,雖然併發效率不高,但是保證了資料的安全性。

2. 樂觀鎖(樂觀併發控制)

跟悲觀鎖一樣,樂觀鎖實際為樂觀併發控制,縮寫為OCC。

樂觀鎖相對於悲觀鎖而言,認為即使在併發環境下,外界對資料的操作不會產生衝突,所以不會去加鎖,而是會在提交更新的時候才會正式的對資料衝突與否進行檢測。

如果發現衝突,要麼再重試一次,要麼切換為悲觀的策略。

樂觀併發控制要解決的是資料庫併發場景下的寫-寫衝突,指用無鎖的方式去解決

三、MVCC解決了哪些問題

在事務併發的情況下會產生以下問題。

  • 髒讀:讀取其它事務未提交的資料。
  • 不可重複讀:一個事務在讀取一條資料時,由於另一個事務修改了這條資料並且提交事務,再次讀取時導致資料不一致
  • 幻讀:一個事務讀取了某個範圍的資料,同時另一個事務新增了這個範圍的資料,再次讀取發現倆次得到的結果不一致。

MVCC在Innodb儲存引擎的實現主要是為了提高資料庫併發能力,用更好的方式去處理讀--寫衝突,同時做到不加鎖、非阻塞併發讀寫。

mvcc可以解決髒讀,不可重複讀,mvcc使用快照讀解決了部分幻讀問題,但是在修改時還是使用當前讀,所以還是存在幻讀問題,幻讀問題最終就是使用間隙鎖解決。

四、當前讀、快照讀

在瞭解MVCC是如何解決事務併發帶來的問題之前,需要先明白倆個概念,當前讀、快照讀。

1. 當前讀

給讀操作加上共享鎖、排它鎖,DML操作加上排它鎖,這些操作就是當前讀。

共享鎖、排它鎖也被稱之為讀鎖、寫鎖。

共享鎖與共享鎖是共存的,但是要修改、新增、刪除時,必須等到共享鎖釋放才可進行操作。

因為在Innodb儲存引擎中,DML操作都會隱式新增排它鎖。

所以說當前讀所讀取的記錄就是最新的記錄,讀取資料時加上鎖,保證其它事務不能修改當前記錄。

2. 快照讀

如果你看到這裡就預設你對隔離級別有一定的瞭解哈!

快照讀的前提是隔離級別不是序列級別,序列級別的快照讀會退化成當前讀。

快照讀的出現旨在提高事務併發性,其實現基於本文的主角MVCC即多版本控制器。

MVCC可以認為是行鎖的一個變種,但是它在很多情況下避免了加鎖操作。

所以說快照讀的資料有可能不是最新的,而是之前版本的資料。

為什麼要提到快照讀呢!因為read-view就是通過快照讀生成的,為了防止後文概念模糊,所以在這裡進行說明。

3. 如何區分當前讀、快照讀

不加鎖的簡單的select都屬於快照讀。

select id name user where id = 1;

與之對應的則是當前讀,給select加上共享鎖、排它鎖。

select id name from user where id = 1 lock in share mode;

select id name from user where id = 1 for update;

五、MVCC實現三大要素

終於來到本文最重要的部分,前邊的敘述都是為了給原理這一塊做鋪墊。

在這之前需要知道MVCC只在REPEATABLE READ(可重複讀) 和 READ COMMITTED(已讀提交)這倆種隔離級別下適用。

MVCC實現原理是由倆個隱式欄位、undo日誌、Read view來實現的。

1. 隱式欄位

在Innodb儲存引擎中,在有聚簇索引的情況下每一行記錄中都會隱藏倆個欄位,如果沒有聚簇索引則還有一個6byte的隱藏主鍵。

這倆個隱藏列一個記錄的是何時被建立的,一個記錄的是什麼時候被刪除。

這裡不要理解為是記錄的是時間,儲存的是事務ID。

倆個隱式欄位為DB_TRX_ID,DB_ROLL_PTR,沒有聚簇索引還會有DB_ROW_ID這個欄位。

  • DB_TRX_ID:記錄建立這條資料上次修改它的事務 ID
  • DB_ROLL_PTR:回滾指標,指向這條記錄的上一個版本

隱式欄位實際還有一個delete flag欄位,即記錄被更新或刪除,這裡的刪除並不代表真的刪除,而是將這條記錄的delete flag改為true(這裡埋下一個伏筆,資料庫的刪除是真的刪除嗎?)

2. undo log(回滾日誌)

之前對undo log的作用只提到了回滾操作實現原子性,現在需要知道的另一個作用就是實現MVCC多版本控制器。

undo log細分為倆種,insert時產生的undo log、update,delete時產生的undo log

在Innodb中insert產生的undo log在提交事務之後就會被刪除,因為新插入的資料沒有歷史版本,所以無需維護undo log。

update和delete操作產生的undo log都屬於一種型別,在事務回滾時需要,而且在快照讀時也需要,則需要維護多個版本資訊。只有在快照讀和事務回滾不涉及該日誌時,對應的日誌才會被purge執行緒統一刪除。

purge執行緒會清理undo log的歷史版本,同樣也會清理del flag標記的記錄。

undo log在mvcc中的作用

寫到這裡關於undo log在mvcc中的作用估計還是蒙圈的。

undo log儲存的是一個版本鏈,也就是使用DB_ROLL_PTR這個欄位來連線的。

當資料庫執行一個select語句時會產生一致性檢視read view

那麼這個read view是由查詢時所有未提交事務ID組成的陣列,陣列中最小的事務ID為min_id和已建立的最大事務ID為max_id組成,查詢的資料結果需要跟read-view做比較從而得到快照結果。

所以說undo log在mvcc中的作用就是為了根據儲存的事務ID和一致性檢視做對比,從而得到快照結果。

3. undo log底層實現

假設一開始的資料為下圖

開始資料
開始資料

此時執行了一條更新的SQL語句update user set name = 'niuniu where id = 1',那麼undo log的記錄就會發生變化

也就是說當執行一條更新語句時會把之前的原有資料拷貝到undo log日誌中。

同時你可以看見最新的一條記錄在末尾處連線了一條線,也就是說DB_ROLL_PTR記錄的就是存放在undo log日誌的指標地址。

最終有可能需要通過指標來找到歷史資料。

undo log變化
undo log變化

4. read-view

當執行SQL語句查詢時會產生一致性檢視,也就是read-view,它是由查詢的那一時間所有未提交事務ID組成的陣列,和已經建立的最大事務ID組成的。

在這個陣列中最小的事務ID被稱之為min_id,最大事務ID被稱之為max_id,查詢的資料結果要根據read-view做對比從而得到快照結果。

於是就產生了以下的對比規則,這個規則就是使用當前的記錄的trx_id跟read-view進行對比,對比規則如下。

5. 版本鏈對比規則

如果落在trx_id<min_id,表示此版本是已經提交的事務生成的,由於事務已經提交所以資料是可見的

如果落在trx_id>max_id,表示此版本是由將來啟動的事務生成的,是肯定不可見的

若在min_id<=trx_id<=max_id時

  • 如果row的trx_id在陣列中,表示此版本是由還沒提交的事務生成的,不可見,但是當前自己的事務是可見的
  • 如果row的trx_id不在陣列中,表明是提交的事務生成了該版本,可見

在這裡還有一個特殊情況那就是對於已經刪除的資料,在之前的undo log日誌講述時說了update和delete是同一種型別的undo log,同樣也可以認為delete就是update的特殊情況。

當刪除一條資料時會將版本鏈上最新的資料複製一份,然後將trx_id修改為刪除時的trx_id,同時在該記錄的頭資訊中存在一個delete flag標記,將這個標記寫上true,用來表示當前記錄已經刪除。

在查詢時按照版本鏈的規則查詢到對應的記錄,如果delete flag標記位為true,意味著資料已經被刪除,則不返回資料。

如果你對這裡的read-view的生成和版本鏈對比規則不懂,不要著急,也不要在這裡浪費時間,請繼續往下看,咔咔會使用一個簡單的案例和一個複雜的案例給大家重現上述的規則。

六、MVCC底層原理

案例一

下圖是準備的素材,這裡應該都理解select 返回的結果為niuniu,即事務102修改後的結果

案例一
案例一

從上圖中可以看到有三個事務正在進行。

事務ID為100、101是修改的其它表,只有事務ID為102修改的需要查詢的這張表。

接下來看看select這一列查詢返回的結果是不是就是事務ID為102修改的結果。

此時生成的read-view為[100,101],102

那麼現在就可以返回去看一下read-view規則,在這裡事務ID100就是min_id,事務ID102就是max_id。

這個 select語句返回結果肯定是 niuniu。

那麼接下來看一下在MVCC中是如何查詢資料的。

當前版本鏈。

此時的版本鏈
此時的版本鏈

那麼就會拿著trx_id 為102進行比對,會發現這個102就是max_id

然後你再看一下版本鏈的對比規則中第三種情況

如果落在min_id<=trx_id<=max_id會存在倆種情況

此時資訊就已經非常明確了,事務ID102是沒有在陣列中的,所以表示這個版本是已經提交的事務生成的,那麼就是可見的唄!

毫無疑問查詢會返回niuniu這個值

先通過這個簡單的案例讓你對版本鏈有一個簡單的理解,接下來將使用一個比較繁瑣的案例再來跟大家演示一遍。

案例二

本例要求知道 select的第二個查詢結果。深黑色字型。

同樣是在kaka那一條記錄的基礎上。

案例二
案例二

當事務ID100兩次更新後,版本鏈也會改變,現在的版本鏈如下圖,紅色部分為最新資料,藍色資料為undo log的版本鏈資料。

此時的版本鏈
此時的版本鏈

對於此時生成的read-view你會有什麼疑問,在RR級別也就是可重複讀的隔離級別下。

當在一個事務下執行查詢時,所有的read-view都是沿用的第一條查詢語句生成的。

那此時的read-view也就是[100,101],102

看一下底層查詢步驟

  • 目前資料的事務ID為100
  • 根據規則會落在min_id<=trx_id<=max_id這個區間
  • 並且當前行的事務ID100是在read-view的陣列中的,表示此時事務還沒有提交則不可見
  • 繼續在版本鏈中往下尋找,此時找到的事務ID還是100,跟上述流程一致
  • 通過查詢版本鏈,將發現事務 ID為102
  • 102是read-view的max_id,同樣也會落在min_id<=trx_id<=max_id這個區間,但是跟之前不同的是事務102是沒有在陣列中的,表示這個版本事務已經提交了所以是可見的
  • 最後返回的是 niuniu

案例三

為了讓大家體驗一下可重複讀級別生成的read-view是根據在同一事務中第一條快照讀產生的,再來看一個案例

此時的事務ID101也再對資料更新兩次,然後在進行查詢看一下會返回什麼值

案例三
案例三

經過案例一、案例二的熟悉現在對undo log的版本鏈和對比規則已經有了一定的瞭解了吧!

案例三就不在那麼詳細的說明了。

此時的版本鏈如下

案例三的版本鏈
案例三的版本鏈

此時的read-view依然為[100,101],102。

那麼首先會根據事務101去版本鏈對比,事務101和事務100都會落在min_id<=trx_id<=max_id這個區間,並且還都在陣列中,所以資料是不可見的。

那麼繼續往版本鏈中尋找就會遇到事務102,這個是最大的事務ID並且不在陣列中,所以是可見的。

於是最終的返回結果還是niuniu

案例四

可以看到個案例三的圖不同的是新增了一個查詢語句,那麼假設這倆條語句執行的時間都是一致的,它們返回的結果會相同嗎?

案例三查詢到的值為niuniu

案例四
案例四

其實現在版本鏈跟案例三也是一致的

版本鏈
版本鏈

那麼來梳理一下尋找過程

  • 首先這裡的read-view發生了變化,此時的read-view為[101],102
  • 拿著當前的事務ID101跟版本鏈規則進行對比,落盤在min_id<=trx_id<=max_id,並且在陣列中,則資料不可見
  • 然後進入版本鏈,找到下一個資料的事務 ID,還是101,與上一個一致
  • 接下來是事務ID100
  • 事務ID100是落在trx_id<min_id,表示此版本是已經提交的事務生成的,由於事務已經提交所以資料是可見的
  • 所以最終返回結果為niuniu2

小結

在同一個事務中進行查詢,會沿用第一次查詢語句生成的read-view(前提是隔離級別是在可重複讀)

通過以上的四個案例,在版本鏈尋找過程中,可以總結出一個小技巧

技巧圖
技巧圖

根據這個小技巧你可以很快的得知此版本是否可見。

  • 如果當前的事務ID在綠色部分,是已經提交事務,則資料可見
  • 如果當前的事務ID在藍色部分,會有倆種情況,如果當前事務ID在read-view陣列內,是沒有提交的事務不可見,如果不在陣列內資料可見
  • 如果落在紅色部分,則不考慮,對於未來的事情不去想即可。

七、總結

閱讀本文後,在面試過程中極大可能會遇到的問題就是聊聊你對mvcc的認識

本文內容從淺到深,從什麼是mvcc到mvcc的底層實現,一步一步地陳述了mvcc的實現原理。

本文簡單總結

  • mvcc在不加鎖的情況下解決了髒讀、不可重複讀和快照讀下的幻讀問題,一定不要認為幻讀完全是mvcc解決的
  • 對當前讀、快照讀理解,簡單點說加鎖就是當前讀,不加鎖的就是快照讀。
  • mvcc實現的三大要素倆個隱式欄位、回滾日誌、read-view
  • 倆個隱式欄位:DB_TRX_ID:記錄建立這條記錄最後一次修改該記錄的事務ID,DB_ROLL_PTR:回滾指標,指向這條記錄的上一個版本
  • undo log在更新資料時會產生版本鏈,是read-view獲取資料的前提
  • read-view當SQL執行查詢語句時產生的,是由為提交的事務ID組成的陣列和建立的最大事務ID組成的
  • 版本鏈規則看第六節的小結即可

堅持學習、堅持寫作、堅持分享是咔咔從業以來一直所秉持的信念。希望在偌大網際網路中咔咔的文章能帶給你一絲絲幫助。我是咔咔,下期見。