你真的懂MVCC嗎?來手動實踐一下?

MVCC 是什麼?

數據庫併發控制——鎖

Multiversion (version) concurrency control (MCC or MVCC) 多版本併發控制 ,它是數據庫管理系統一種常見的併發控制。算法

咱們知道併發控制經常使用的是鎖,當線程要對一個共享資源進行操做的時候,加鎖是一種很是簡單粗暴的方法(事務開始時給 DQL 加讀鎖,給 DML 加寫鎖),這種鎖是一種 悲觀 的實現方式,也就是說這會給其餘事務形成堵塞,從而影響數據庫性能。數據庫

我來解釋一下 樂觀鎖悲觀鎖 的概念。我以爲它倆主要是概念的理解。數組

  • 悲觀鎖: 當一個線程須要對共享資源進行操做的時候,首先對共享資源進行加鎖,當該線程持有該資源的鎖的時候,其餘線程對該資源進行操做的時候會被 阻塞。好比 Java 中的 Synchronized 關鍵字。
  • 樂觀鎖:當一個線程須要對一個共享資源進行操做的時候,不對它進行加鎖,而是在操做完成以後進行判斷。(好比樂觀鎖會經過一個版本號控制,若是操做完成後經過版本號進行判斷在該線程操做過程當中是否有其餘線程已經對該共享資源進行操做了,若是有則通知操做失敗,若是沒有則操做成功),固然除了 版本號 還有 CAS,若是不瞭解的能夠去學習一下,這裏不作過多涉及。

數據庫併發控制——MVCC

不少人認爲 MVCC 就是一種 樂觀鎖 的實現形式,而我認爲 MVCC 只是一種 樂觀 的實現形式,它是經過 一種 可見性算法 來實現數據庫併發控制。併發

MVCC 的兩種讀形式

在講 MVCC 的實現原理以前,我覺頗有必要先去了解一下 MVCC 的兩種讀形式。性能

  • 快照讀:讀取的只是當前事務的可見版本,不用加鎖。而你只要記住 簡單的 select 操做就是快照讀(select * from table where id = xxx)學習

  • 當前讀:讀取的是當前版本,好比 特殊的讀操做,更新/插入/刪除操做spa

    好比:線程

    select * from table where xxx lock in share mode,
     select * from table where xxx for update,
     update table set....
     insert into table (xxx,xxx) values (xxx,xxx)
     delete from table where id = xxx
    複製代碼

MVCC 的實現原理

MVCC 使用了「三個隱藏字段」來實現版本併發控制,我查了不少資料,看到有不少博客上寫的是經過 一個建立事務id字段和一個刪除事務id字段 來控制實現的。但後來發現並非很正確,咱們先來看一看 MySQL 在建表的時候 innoDB 建立的真正的三個隱藏列吧。3d

RowID DB_TRX_ID DB_ROLL_PTR id name password
自動建立的id 事務id 回滾指針 id name password
  • RowID:隱藏的自增ID,當建表沒有指定主鍵,InnoDB會使用該RowID建立一個聚簇索引。
  • DB_TRX_ID:最近修改(更新/刪除/插入)該記錄的事務ID。
  • DB_ROLL_PTR:回滾指針,指向這條記錄的上一個版本。

其實還有一個刪除的flag字段,用來判斷該行記錄是否已經被刪除。 指針

而 MVCC 使用的是其中的 事務字段,回滾指針字段,是否刪除字段。咱們來看一下如今的表格(isDelete是我本身取的,按照官方說法是在一行開頭的content裏面,這裏其實位置無所謂,你只要知道有就好了)。

isDelete DB_TRX_ID DB_ROLL_PTR id name password
true/false 事務id 回滾指針 id name password

那麼如何經過這三個字段來實現 MVCC 的 可見性算法 呢?

還差點東西! undoLog(回滾日誌)read-view(讀視圖)

  • undoLog: 事務的回滾日誌,是 可見性算法 的很是重要的部分,分爲兩類。

    • insert undo log:事務在插入新記錄產生的undo log,當事務提交以後能夠直接丟棄
    • update undo log:事務在進行 update 或者 delete 的時候產生的 undo log,在快照讀的時候仍是須要的,因此不能直接刪除,只有當系統沒有比這個log更早的read-view了的時候才能刪除。ps:因此長事務會產生不少老的視圖致使undo log沒法刪除 大量佔用存儲空間。
  • read-view: 讀視圖,是MySQL秒級建立視圖的必要條件,好比一個事務在進行 select 操做(快照讀)的時候會建立一個 read-view ,這個read-view 其實只是三個字段。

    • alive_trx_list(我本身取的):read-view生成時刻系統中正在活躍的事務id
    • up_limit_id:記錄上面的 alive_trx_list 中的最小事務id
    • low_limit_id:read-view生成時刻,目前已出現的事務ID的最大值 + 1

這時候,萬事俱備,只欠東風了。下面我來介紹一下,最重要的 可見性算法

其實主要思路就是:當生成read-view的時候如何去拿獲取的 DB_TRX_ID 去和 read-view 中的三個屬性(上面講了)去做比較。我來講一下三個步驟,若是不是很理解能夠參考着我後面的實踐結合着去理解。

  • 首先比較這條記錄的 DB_TRX_ID 是不是 小於 up_limit_id 或者 等於當前事務id。若是知足,那麼說明當前事務能看到這條記錄。若是大於則進入下一輪判斷
  • 而後判斷這條記錄的 DB_TRX_ID 是否 大於等於 low-limit-id。若是大於等於則說明此事務沒法看見該條記錄,否則就進入下一輪判斷。
  • 判斷該條記錄的 DB_TRX_ID 是否在活躍事務的數組中,若是在則說明這條記錄還未提交對於當前操做的事務是不可見的,若是不在則說明已經提交,那麼就是可見的。

若是此條記錄對於該事務不可見且 ROLL_PTR 不爲空那麼就會指向回滾指針的地址,經過undolog來查找可見的記錄版本。

下面我畫了一個可見性的算法的流程圖

可見性算法流程圖

實踐

準備數據

首先我建立了一個很是簡單的表,只有id和name的學生表。

id name
學生id 學生姓名

這個時候咱們將咱們須要的隱藏列也標識出來,就變成了這樣

isDelete id name DB_TRX_ID DB_ROLL_PTR
是否被刪除 學生id 學生姓名 建立刪除更新該記錄的事務id 回滾指針

這個時候插入三行數據,將表的數據變成下面這個樣子。

isDelete id name DB_TRX_ID DB_ROLL_PTR
false 1 小明 1 null
false 2 小方 1 null
false 3 小張 1 null

示例一

使用過 MySQL 的都知道,由於隔離性,事務 B 此時獲取到的數據確定是這樣的。

id name
1 小明
2 小方
3 小張

爲何事務A未提交的修改對於事務B是不可見的,MVCC 是如何作到的?咱們用剛剛的可見性算法來實驗一下。

首先事務A開啓了事務(固然這不算開啓,在RR模式下 真正獲取read-view的是在進行第一次進行快照讀的時候)。咱們假設事務A的事務id爲2,事務B的id爲3。

而後事務A進行了更新操做,如圖所示,更新操做建立了一個新的版本而且新版本的回滾指針指向了舊的版本(注意 undo log其實存放的是邏輯日誌,這裏爲了方便我直接寫成物理日誌)。

最後 事務B 進行了快照讀,注意,這是咱們分析的重點。

首先,在進行快照讀的時候咱們會建立一個 read-view (忘記回去看一下那三個字段)
 這個時候咱們的 read-view 是
 up-limit-id = 2
 alive-trx-list = [2,3]
 low-limit-id = 4
 
 而後咱們獲取那兩個沒有被修改的記錄(沒有順序,這裏爲了一塊兒解釋方便)
 咱們獲取到(2,小方)和(3,小張)這兩條記錄,發現他們兩的 DB_TRX_ID = 1
 咱們先判斷 DB_TRX_ID 是否小於 up-limit-id 或者等於當前事務id 
 發現 1<2 小於 up-limit-id ,則可見 直接返回視圖。
 
 而後咱們獲取更改了的數據行
複製代碼

其實你也發現了這是一個鏈表,此時鏈表頭的 DB_TRX_ID 爲 2
 咱們進行判斷 2 < 2 不符合,進入下一步判斷
 判斷 DB_TRX_ID >= low_limit_id 發現此時是 2 >= 4 不符合 故再進入下一步
 此時判斷 Db_TRX_ID 是否在 alive_trx_list 活躍事務列表中,發現這個 DB_TRX_ID
 在活躍列表中,因此只能說明該行記錄還未提交,不可見。
 最終判斷不可見以後經過回滾指針查看舊版本,發現此時 DB_TRX_ID 爲1
 故再次進行判斷 DB_TRX_ID < up-limit-id ,此時 1 < 2 符合 ,因此可見並返回

 因此最終返回的是
複製代碼
id name
1 小明
2 小方
3 小張

咱們再來驗證一下,這個時候咱們將事務A提交,從新建立一個事務C並select。

咱們預期的結果應該是這樣的

id name
1 小強
2 小方
3 小張

這個操做的流程圖以下

這個時候咱們再來分析一下 事務c產生的 read-view。

這個時候事務A已經提交,因此事務A不在活躍事務數組中,此時 read-view 的三個屬性應該是

up-limit-id = 3
 alive-trx-list = [3,4]
 low-limit-id = 5
複製代碼
  • 跟上面同樣,咱們首先獲取(2,小方)和(3,小張)這兩條記錄,發現他們兩的 DB_TRX_ID = 1,此時 1 < up-limit-id = 3,故符合可見性,則返回。
  • 而後咱們獲取剛剛被修改的id爲1的記錄行,發現鏈表頭部的 DB_TRX_ID 爲 2, 此時 2 < up-limit-id = 3 故也符合可見性,則返回。

因此最終返回的就是

id name
1 小強
2 小方
3 小張

示例二

爲了加深理解,咱們再使用一個相對來講比較複雜的示例來驗證 可見性算法

首先咱們在事務A中刪除一條記錄,這個時候就變成了下面的樣子。

而後事務B進行了插入,這樣就變成了下面這樣。

而後事務B進行了 select 操做,咱們能夠發現 這個時候整張表其實會變成這樣讓這個 select 操做進行選取。

此時的 read-view 爲

up-limit-id = 2
 alive-trx-list = [2,3,4]
 low-limit-id = 5
複製代碼

這個時候咱們進行 快照讀,首先對於前面兩條小明和小方的記錄是同樣的,此時 DB_TX_ID 爲 1,咱們能夠判斷此時 DB_TX_ID = 1 < up-limit-id = 2 成立故返回。而後判斷小張這條記錄,首先也是 DB_TX_ID = 2 < up-limit-id = 2 不成立故進入下一輪,DB_TX_ID = 2 >= low-limit-id 不成立再進入最後一輪判斷是否在活躍事務列表中,發現 DB_TX_ID = 2 在 alive-trx-list = [2,3,4] 中故不可見(若是可見則會知道前面的刪除標誌是已經刪除,則返回的是空),則根據回滾指針找到上一個版本記錄,此時 DB_TX_ID = 1 和上面同樣可見則返回該行。

最後一個判斷小亮這條記錄,由於 DB_TX_ID = current_tx_id(當前事務id) 因此可見並返回。

這個時候返回的表則是這樣的

id name
1 小明
2 小方
3 小張
4 小亮

而後是事務A進行了select的操做,咱們能夠得知如今的 read-view 爲

up-limit-id = 2
 alive-trx-list = [2,3,4]
 low-limit-id = 5
複製代碼

而後此時所見和上面也是同樣的

這個時候咱們進行 快照讀,首先對於前面兩條小明和小方的記錄是同樣的,此時 DB_TX_ID 爲 1,咱們能夠判斷此時 DB_TX_ID = 1 < up-limit-id = 2 成立故返回。而後判斷小張這條記錄,首先 DB_TX_ID = 2 = current_tx_id = 2 成立故返回發現前面的 isDelete 標誌爲true 則說明已被刪除則返回空,對於第四條小亮的也是同樣判斷 DB_TX_ID = 4 < up-limit-id = 2 不成立進入下一步判斷 DB_TX_ID = 4 >= low-limit-id = 5 不成立進入最後一步發如今活躍事務數組中故不可見且此條記錄回滾指針爲null因此返回空。

那麼此時返回的列表應該就是這樣了

id name
1 小明
2 小方

雖然要分析不少,但多多益善嘛,多熟悉熟悉就能更深入理解這個算法了。

以後是事務C進行 快照讀 操做。首先此時視圖仍是這個樣子

而後對於事務C的 read-view 爲

up-limit-id = 2
 alive-trx-list = [2,3,4]
 low-limit-id = 5
複製代碼

小明和小方的兩條記錄和上面同樣是可見的這裏我就不重複分析了,而後對於小張這條記錄 DB_TX_ID = 2 < up-limit-id = 2 || DB_TX_ID == curent_tx_id = 4 不成立故進入下一輪發現 DB_TX_ID >= low-limit-id = 5 更不成立故進入最後一輪發現 DB_TX_ID = 2 在活躍事務數組中故不可見,而後經過回滾指針判斷 DB_TX_ID = 1 的小張記錄發現可見並返回。最後的小亮也是如此 最後會發現 DB_TX_ID = 3 也在活躍事務數組中故不可見。

因此事務C select 的結果爲

id name
1 小明
2 小方
3 小張

後面事務A和事務B都進行了提交的動做,而且有一個事務D進行了快照讀,此時視圖仍是如此

但此時的 read-view發生了變化

up-limit-id = 4
 alive-trx-list = [4,5]
 low-limit-id = 6
複製代碼

咱們首先判斷小明和小方的記錄——可見(不解釋了),小張的記錄 DB_TX_ID = 2 < up-limit-id = 4 成立故可見,由於前面 isDelete 爲 true 則說明刪除了返回空,而後小亮的記錄 DB_TX_ID = 3 < up-limit-id = 4 成立故可見則返回。因此此次的 select 結果應該是這樣的

id name
1 小明
2 小方
4 小亮

最後(真的最後了,不容易吧!),事務C有一次進行了 select 操做。由於在 RR 模式下 read-view 是在第一次快照讀的時候肯定的,因此此時 read-view是不會更改的,而後前面視圖也沒有進行更改,因此此時即便前面事務A 事務B已經進行了提交,對於這個時候的事務C的select結果是沒有影響的。故結果應該爲

id name
1 小明
2 小方
3 小張

總結

咱們來總結一下吧。

其實 MVCC 是經過 "三個" 隱藏字段 (事務id,回滾指針,刪除標誌) 加上undo log和可見性算法來實現的版本併發控制

爲了你再次深刻理解這個算法,我再把這張圖掛上來

相關文章
相關標籤/搜索