一、什麼是MVCC
MVCC (Multiversion Concurrency Control) 中文全程叫多版本併發控制,是現代資料庫(如MySql)引擎實現中常用的處理讀寫衝突的手段,目的在於提高資料庫高併發場景下的吞吐效能。
MySQL的InnoDB儲存引擎預設事務隔離級別是RR(可重複讀),是通過 "行級鎖+MVCC"一起實現的,正常讀的時候不加鎖,寫的時候加鎖。而 MCVV 的實現依賴:隱藏欄位、Read View、Undo log。
另外MVCC只在 Read Committed 和 Repeatable Read兩個隔離級別下工作,其他兩個隔離級別和MVCC不相容:
- Read Uncommitted總是讀取最新的記錄行,不需要MVCC的支援;
- Serializable 則會對所有讀取的記錄行都加鎖,單靠MVCC無法完成。
二、MVCC實現的核心知識點
1、事務版本號
每次事務開啟前都會從資料庫獲得一個自增長的事務ID,可以從事務ID判斷事務的執行先後順序。
可以通過這樣的命令來檢視:select TRX_ID from INFORMATION_SCHEMA.INNODB_TRX;
2、隱藏欄位(Innodb 為每行額外新增了3個欄位,具體請參考官方文件):
DB_TRX_ID:大小為6個位元組。指插入或更新該行的最後一個事務的事務識別符號,也就是事務ID。 此外,刪除在內部被視為更新,在該更新中,該行中的特殊位被設定為將其標記為已刪除。
DB_ROLL_PTR:大小為7個位元組。表示指向該行回滾段的指標。 回滾指標指向寫入回滾段的撤消日誌記錄。 如果行已更新,則撤消日誌記錄將包含在更新行之前重建行內容所必需的資訊。
DB_ROW_ID:大小為6個位元組。包含一個行ID,該行ID隨著插入新行而單調增加。 如果InnoDB自動生成聚集索引,則該索引包含行ID值。 否則,DB_ROW_ID列不會出現在任何索引中。
3、Undo log
Undo log是InnoDB MVCC事務特性的重要組成部分。Undo log 主要用於記錄資料被修改之前的日誌,在表資訊修改之前先會把資料拷貝到undo log 裡,當事務進行回滾時可以通過undo log 裡的日誌進行資料還原。具體就不詳細介紹了,請看考這兩篇文件:https://dev.mysql.com/doc/refman/8.0/en/innodb-undo-logs.html http://mysql.taobao.org/monthly/2015/04/01/
4、read view
“InnoDB支援MVCC多版本,其中RC(Read Committed)和RR(Repeatable Read)隔離級別是利用consistent read view(一致讀檢視)方式支援的。所謂consistent read view就是在某一時刻給事務系統trx_sys打snapshot(快照),把當時trx_sys狀態(包括活躍讀寫事務陣列)記下來,之後的所有讀操作根據其事務ID(即trx_id)與snapshot中的trx_sys的狀態作比較,以此判斷read view對於事務的可見性。
RR隔離級別(除了Gap鎖之外)和RC隔離級別的差別是建立snapshot時機不同。 RR隔離級別是在事務開始時刻,確切地說是第一個讀操作建立read view的;RC隔離級別是在語句開始時刻建立read view的(詳見官方文件)。”
Read view中儲存的trx_sys狀態主要包括(以下欄位解釋來源於原始碼):
trx_ids: 為活躍事務id列表,即Read View初始化時當前未提交的事務列表。所以當進行RR讀的時候,trx_ids中的事務對於本事務是不可見的(除了自身事務,自身事務對於表的修改對於自己當然是可見的)。
low_limit_id: 當前最大的事務id + 1,事務id >= low_limit_id,對於當前Read View都是不可見的。理解起來就是在建立Read View檢視的時候,之後建立的事務對於該事務肯定是不可見的。
up_limit_id: 當前已經提交的事務id + 1,事務id < up_limit_id ,對於當前Read View都是可見的。 理解起來就是在建立Read View檢視的時候,之前已經提交的事務對於該事務肯定是可見的。
creator_trx_id: 建立當前read view的事務版本號;
一旦一個Read View被建立,這三個引數將不再發生變化,理解這點很重要,其中low_limit_id 和 up_limit_id分別是 trx_Ids陣列的上下界(注意:從單詞上來區分的話很容易弄反)。
其他事務對當前事務的可見性判斷如下:
三、案例分析
下面通過案例來分析MVCC怎麼實現一致性讀取的。前期資料準備:
- 使用預設隔離級別RR;
- 建立一個表: create table test(id int AUTO_INCREMENT, score int, primary key(id)) AUTO_INCREMENT = 0;
- 假設當前事務id已經自增長到100;
步驟
|
事務1
|
事務2
|
事務3
|
1
|
begin;
|
||
2
|
begin;
|
||
3
|
insert into test(score) select 101;
此時事務ID為101
|
||
4
|
insert into test(score) select 102;
此時事務ID為102
|
||
5
|
select * from test;
+----+-------+
| id | score |
+----+-------+
| 1 | 101 |
+----+-------+
此時就會建立read view:
up_limit_id = 101
low_limit_id = 103
trx_ids為(101,102)
而101自身可見,102在活躍事務列表中不可見
|
||
6
|
insert into test(score) select 103;
此時事務ID為103
|
||
7
|
insert into test(score) select 104;
此時事務ID為104
|
||
8
|
nsert into test(score) select 105;
此時事務ID為105
|
||
9
|
select * from test;
+----+-------+
| id | score |
+----+-------+
| 3 | 103 |
| 4 | 104 |
| 5 | 105 |
+----+-------+
此時的up_limit_id=101,
low_limit_id=106,
trx_ids為(101, 102),
而101和102在trx_ds列表中不可見
|
||
10
|
select * from test;
+----+-------+
| id | score |
+----+-------+
| 2 | 102 |
| 3 | 103 |
| 4 | 104 |
| 5 | 105 |
+----+-------+
此時就會建立read view:
up_limit_id=101,
low_limit_id=106,
trx_ids為(101, 102),
102自身可見,101在活躍事務列表中不可見
而103、104、105不在trx_ids列表中所有可見
|
||
11
|
select * from test;
+----+-------+
| id | score |
+----+-------+
| 1 | 101 |
+----+-------+
由於事務內read view不變
(與RC的區別就在這),
此時的up_limit_id=101,low_limit_id=103,
trx_ids為(101, 102),
101自身可見,102在活躍事務列表中不可見
而>=103的都不可見
|
四、總結
1、MVCC主要靠Read view來實現一致性讀,也就是快照讀;底層是主要基於其中兩個隱藏欄位來實現(DB_TRX_ID、DB_ROLL_PTR)。這樣可以使不同事務的讀-寫、寫-讀操作併發執行,從而提升系統效能。
2、Read view其中幾個重要組成屬性(trx_ids、low_limit_id、up_limit_id、creator_trx_id),一旦一個Read View被建立,這三個引數將不再發生變化;
3、MVCC只在 RC 和 RR兩個隔離級別下工作, 它們的不同之處在於:
RR:read view是在first touch read時建立的,也就是執行事務中的第一條SELECT語句的瞬間,後續所有的SELECT都是複用這個read view,所以能保證每次讀取的一致性(可重複讀的語義)
RC:每次讀取,都會建立一個新的read view。這樣就能讀取到其他事務已經COMMIT的內容。
所以對於InnoDB來說,RR雖然比RC隔離級別高,但是開銷反而相對少。
補充:RU的實現就簡單多了,不使用read view,也不需要管什麼DB_TRX_ID和DB_ROLL_PTR,直接讀取最新的record即可。