【MySQL(5)| 五分鐘搞清楚 MVCC 機制】

程式猿雜貨鋪發表於2019-02-17

Multiversion concurrency control 多版本併發控制

併發訪問(讀或者寫)資料庫時,對正在事務內處理的資料做多版本的管理,用來避免由於寫操作的堵塞,而引發讀操作失敗的併發問題。

引言

先看一個案例:

1.檢視資料的事務隔離級別

對事務隔離級別不熟悉的同學可以參考文章 【MySQL (三) | 五分鐘搞清楚MySQL事務隔離級別】

SELECT @@tx_isolation;
複製程式碼

檢視資料庫的事務隔離級別

可見 資料庫隔離級別使用的是MySQL預設的RR級別。

REPEATABLE READ 意味著:

  • 同一個事務中多次執行同一個select,讀取到的資料沒有發生改變;
  • 此時:允許幻讀,但不允許不可重複讀和髒讀,所以RR隔離級別要求解決不可重複讀;

2.在不同會話中執行以下SQL

補充一下建表語句:

create table `test_zq` (
	`id` int (11),
	`test_id` int (11)
); 
insert into `test_zq` (`id`, `test_id`) values('1','18');
insert into `test_zq` (`id`, `test_id`) values('4','8');
insert into `test_zq` (`id`, `test_id`) values('7','4');
insert into `test_zq` (`id`, `test_id`) values('10','1234');
複製程式碼

使用者1:

begin;
-- 更新 id 為 1 的資料
UPDATE test_zq SET test_id = 20 WHERE id = 1;
複製程式碼

使用者2:

begin;
--查詢 id 為 1 的資料
SELECT * FROM test_zq WHERE id = 1;
複製程式碼

執行結果大致如下:

執行結果

根據事務隔離級別來看,我們理論上對獲得 X 鎖(關於鎖的概念可以參考 【MySQL (四) | 五分鐘搞清楚InnoDB鎖機制】)的資料行是不能再被獲取讀鎖而訪問的,但是事實上我們依然訪問到了這個資料!

通過結果說明:我們可以在一個事務未進行 commit/rollback操作之前,另一個事務仍然可以讀取到資料庫中的資料,只不過是讀取到的是其他事務未改變之前的資料。此處是利用了MVCC多資料做了多版本處理,讀取的資料來源於快照。

3.同理,在不同會話中執行以下SQL

使用者1:

begin;
SELECT * FROM test_zq WHERE id = 1;
複製程式碼

使用者2:

begin;
update test_zq set test_id = 22 where id = 1;
複製程式碼

執行完之後再回到使用者1進行一次資料查詢

SELECT * FROM test_zq WHERE id = 1;
複製程式碼

執行結果:

執行結果2

執行結果和上一步的執行結果一樣,只不過區別在於2步驟中是先 update 後 select , 3 步驟是先 select 後 update.

雖然兩者執行結果是一致的,但是我們要思考兩個問題:

  • 他們的底層實現是一樣的嗎?
  • 他們的實現和MVCC有什麼關係呢?

接下來我們便開始瞭解一下 MVCC 機制

什麼是MVCC

MVCC,Multi-Version Concurrency Control,多版本併發控制。MVCC 是一種併發控制的方法,一般在資料庫管理系統中,實現對資料庫的併發訪問;在程式語言中實現事務記憶體。

如果有人從資料庫中讀資料的同時,有另外的人寫入資料,有可能讀資料的人會看到『半寫』或者不一致的資料。有很多種方法來解決這個問題,叫做併發控制方法。最簡單的方法,通過加鎖,讓所有的讀者等待寫者工作完成,但是這樣效率會很差。MVCC 使用了一種不同的手段,每個連線到資料庫的讀者,在某個瞬間看到的是資料庫的一個快照,寫者寫操作造成的變化在寫操作完成之前(或者資料庫事務提交之前)對於其他的讀者來說是不可見的。

當一個 MVCC 資料庫需要更一個一條資料記錄的時候,它不會直接用新資料覆蓋舊資料,而是將舊資料標記為過時(obsolete)並在別處增加新版本的資料。這樣就會有儲存多個版本的資料,但是隻有一個是最新的。這種方式允許讀者讀取在他讀之前已經存在的資料,即使這些在讀的過程中半路被別人修改、刪除了,也對先前正在讀的使用者沒有影響。**這種多版本的方式避免了填充刪除操作在記憶體和磁碟儲存結構造成的空洞的開銷,但是需要系統週期性整理(sweep through)以真實刪除老的、過時的資料。**對於面向文件的資料庫(Document-oriented database,也即半結構化資料庫)來說,這種方式允許系統將整個文件寫到磁碟的一塊連續區域上,當需要更新的時候,直接重寫一個版本,而不是對文件的某些位元位、分片切除,或者維護一個鏈式的、非連續的資料庫結構。

MVCC 提供了時點(point in time)一致性檢視。MVCC 併發控制下的讀事務一般使用時間戳或者事務 ID去標記當前讀的資料庫的狀態(版本),讀取這個版本的資料。讀、寫事務相互隔離,不需要加鎖。讀寫並存的時候,寫操作會根據目前資料庫的狀態,建立一個新版本,併發的讀則依舊訪問舊版本的資料。

一句話總結就是:

MVCC(Multiversion concurrency control) 就是 同一份資料臨時保留多版本的一種方式,進而實現併發控制

哪麼此處需要注意的點就是:

  • 在讀寫併發的過程中如何實現多版本?
  • 在讀寫併發之後,如何實現舊版本的刪除(畢竟很多時候只需要一份最新版的資料就夠了)?

下面介紹一下MySQL中對於 MVCC 的邏輯實現

MVCC邏輯流程-插入

在MySQL中建表時,每個表都會有三列隱藏記錄,其中和MVCC有關係的有兩列

  • 資料行的版本號 (DB_TRX_ID)
  • 刪除版本號 (DB_ROLL_PT)
id test_id DB_TRX_ID DB_ROLL_PT

在插入資料的時候,假設系統的全域性事務ID從1開始,以下SQL語句執行分析參考註釋資訊:

begin;-- 獲取到全域性事務ID
insert into `test_zq` (`id`, `test_id`) values('5','68');
insert into `test_zq` (`id`, `test_id`) values('6','78');
commit;-- 提交事務
複製程式碼

當執行完以上SQL語句之後,表格中的內容會變成:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 NULL
6 78 1 NULL

可以看到,插入的過程中會把全域性事務ID記錄到列 DB_TRX_ID 中去

MVCC邏輯流程-刪除

對上述表格做刪除邏輯,執行以下SQL語句(假設獲取到的事務邏輯ID為 3)

begin--獲得全域性事務ID = 3
delete test_zq where id = 6;
commit;
複製程式碼

執行完上述SQL之後資料並沒有被真正刪除,而是對刪除版本號做改變,如下所示:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 NULL
6 78 1 3

MVCC邏輯流程-修改

修改邏輯和刪除邏輯有點相似,修改資料的時候 會先複製一條當前記錄行資料,同事標記這條資料的資料行版本號為當前是事務版本號,最後把原來的資料行的刪除版本號標記為當前是事務。

執行以下SQL語句:

begin;-- 獲取全域性系統事務ID 假設為 10
update test_zq set test_id = 22 where id = 5;
commit;
複製程式碼

執行後表格實際資料應該是:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 10
6 78 1 3
5 22 10 NULL

MVCC邏輯流程-查詢

此時,資料查詢規則如下:

  • 查詢資料行版本號早於當前事務版本號的資料行記錄

    也就是說,資料行的版本號要小於或等於當前是事務的系統版本號,這樣也就確保了讀取到的資料是當前事務開始前已經存在的資料,或者是自身事務改變過的資料

  • 查詢刪除版本號要麼為NULL,要麼大於當前事務版本號的記錄

    這樣確保查詢出來的資料行記錄在事務開啟之前沒有被刪除

根據上述規則,我們繼續以上張表格為例,對此做查詢操作

begin;-- 假設拿到的系統事務ID為 12
select * from test_zq;
commit;
複製程式碼

執行結果應該是:

id test_id DB_TRX_ID DB_ROLL_PT
6 22 10 NULL

MySQL 中 MVCC 版本控制案例

回到文章剛開始的哪個例子,我們使用 MVCC 機制分析一遍

為了方便描述,對SQL語句做如下標記:

begin;--假設當前獲取到的事務 ID 為 2				 ----1
select * from test_zq;						    ----2
commit;

begin;--假設當前獲取到的事務 ID 為 3				 ----3
UPDATE test_zq SET test_id = 20 WHERE id = 1;	----4
commit;
複製程式碼

對錶中資料做初始化:

begin;
insert into `test_zq` (`id`, `test_id`) values('1','18');
insert into `test_zq` (`id`, `test_id`) values('4','8');
commit;
複製程式碼

表中的原始資料為:

id test_id DB_TRX_ID DB_ROLL_PT
1 18 1 NULL
4 8 1 NULL

案例1

執行順序為 1 2 3 4 2

1 2 步驟執行結果為:

id test_id
1 18
4 8

3 4 步驟執行結果為:

id test_id DB_TRX_ID DB_ROLL_PT
1 18 1 3
4 8 1 NULL
1 20 3 NULL

2執行後的結果為:

id test_id
1 18
4 8

上述結果符合預期,接下來看案例2

案例2

執行順序為3 4 1 2

3 4 步驟執行後結果為:

id test_id DB_TRX_ID DB_ROLL_PT
1 18 1 3
4 8 1 NULL
1 20 3 NULL

1 2 步驟執行後結果為:

假設此時的事務ID為 txid = 4

則查詢結果是 :

id test_id
1 20
4 8

顯然,結果應該是不對的,但是我們在文章開頭也是按照這樣的順序執行的,但是MySQL的返回結果沒有任何問題,可是這裡根據MVCC機制來分析卻出現了這樣的狀況,所以問題出在哪裡?

我們大概可以猜測到:

此處問題不是出在 MVCC 機制,MySQL解決不可重複讀和髒讀並不是單純利用 MVCC 機制來實現的。

限於篇幅,這個問題留到下一篇文章,下一篇將會討論 Undo Log 和 Redo Log等。

微信公眾號

在這裡插入圖片描述

相關文章