Multiversion (version) concurrency control (MCC or MVCC) 多版本併發控制 ,它是數據庫管理系統一種常見的併發控制。算法
咱們知道併發控制經常使用的是鎖,當線程要對一個共享資源進行操做的時候,加鎖是一種很是簡單粗暴的方法(事務開始時給 DQL 加讀鎖,給 DML 加寫鎖),這種鎖是一種 悲觀 的實現方式,也就是說這會給其餘事務形成堵塞,從而影響數據庫性能。數據庫
我來解釋一下 樂觀鎖 和 悲觀鎖 的概念。我以爲它倆主要是概念的理解。數組
不少人認爲 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 使用了「三個隱藏字段」來實現版本併發控制,我查了不少資料,看到有不少博客上寫的是經過 一個建立事務id字段和一個刪除事務id字段 來控制實現的。但後來發現並非很正確,咱們先來看一看 MySQL 在建表的時候 innoDB 建立的真正的三個隱藏列吧。3d
RowID | DB_TRX_ID | DB_ROLL_PTR | id | name | password |
---|---|---|---|---|---|
自動建立的id | 事務id | 回滾指針 | id | name | password |
其實還有一個刪除的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: 事務的回滾日誌,是 可見性算法 的很是重要的部分,分爲兩類。
read-view: 讀視圖,是MySQL秒級建立視圖的必要條件,好比一個事務在進行 select 操做(快照讀)的時候會建立一個 read-view ,這個read-view 其實只是三個字段。
這時候,萬事俱備,只欠東風了。下面我來介紹一下,最重要的 可見性算法。
其實主要思路就是:當生成read-view的時候如何去拿獲取的 DB_TRX_ID 去和 read-view 中的三個屬性(上面講了)去做比較。我來講一下三個步驟,若是不是很理解能夠參考着我後面的實踐結合着去理解。
若是此條記錄對於該事務不可見且 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
複製代碼
因此最終返回的就是
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和可見性算法來實現的版本併發控制。
爲了你再次深刻理解這個算法,我再把這張圖掛上來