一、什么是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即可。