MVCC總結
一、MVCC機制是什么
MVCC,即Multi-Version Concurrency Control (多版本并發控制)。它是一種并發控制的方法,一般在數據庫管理系統中,實現對數據庫的并發訪問,在編程語言中實現事務內存。
個人理解
- MySQL 的 InnoDB 存儲引擎支持事務。
- 事務的四大特性:A 原子性、C 一致性、I 隔離性、D 持久性
- 原子性:事務是一組sql,這組sql要么全部執行成功,要么全都不執行。
- 而事務的并發會帶來并發問題,臟讀、不可重復讀、幻讀。
- 為了解決事務并發問題,MySQL提供了不同的隔離級別,來控制并發事務之間的隔離程度。不同的隔離級別在并發性能和數據一致性方面具有不同的權衡。例如,較低的隔離級別可以提高并發性能,但可能會導致數據一致性問題;而較高的隔離級別可以保證數據一致性,但可能會降低并發性能。
- 四種隔離級別:讀未提交 RU、讀已提交 RC、可重復讀 RR、串行化 S
- 現在回答MVCC 機制是什么,MVCC 機制是為了控制事務并發產生的并發問題的一種機制,并且根據隔離級別不同,所表現出來的機制是不同的。
二、MVCC機制解決了什么問題
MVCC 機制,
在RC 級別下,存在不可重復讀的問題,每次執行SQL都會生成ReadView。
在RR 級別下,解決了不可重復讀的問題,只會生成一次ReadView。
兩種隔離級別下的解決思路不同。
三、MVCC 的實現
MVCC 通過undolog日志和ReadView實現。
3.1、隱藏字段
InnoDB 存儲引擎的每一行都有兩個隱藏字段trx_id、roll_pointer,如果表中沒有主鍵索引或者唯一索引列,那么還會增加一個row_id字段。
| 列名 | 是否必須 | 描述 |
|---|---|---|
trx_id |
是 | 事務 id,創建事務時生成 |
roll_pointer |
是 | 回滾點,指向回滾段的undo日志 |
row_id |
否 | 不是必需的,占用6個字節. |
3.2、undolog 日志
undo log,回滾日志,用于記錄數據被修改前的信息。在表記錄修改之前,會先把數據拷貝到undo log里,如果事務回滾,即可以通過undo log來還原數據。
可以這樣認為,當delete一條記錄時,undo log 中會記錄一條對應的insert記錄,當update一條記錄時,它記錄一條對應相反的update記錄。
undolog 的作用 :
- 事務回滾:保證原子性和一致性
- 用于MVCC 的快照讀
3.3、版本鏈
同一條記錄的多次修改,會產生不同的版本,這些不同的版本通過隱藏字段roll_pointer連接。
主體是記錄,事務并發時,每一條記錄都是一個鏈表的形式。

以一個流程講解版本鏈的生成流程
- 版本鏈的頭結點是最新的值
- 最新的值存在數據庫中
- 除最新的值之外的值,存在undolog日志中
update user set name ="李四" where id=1
- 首先獲得一個事務ID=100,
trx_id = 102(事務開始時,生成事務id) - 把user表修改前的數據,拷貝到undo log(更新undolog日志)
- 修改user表中,id=1的數據,名字改為李四 (修改數據表中的數據)
- 把修改后的數據事務Id=
102改成當前事務版本號,并把roll_pointer指向undo log數據地址。

3.4、快照讀和當前讀
快照讀:讀取的是記錄數據的可見版本(有舊的版本)。不加鎖,普通的select語句都是快照讀
**select** * **from** core_user **where** id > 2;
當前讀:讀取的是記錄數據的最新版本,顯式加鎖的都是當前讀
select * from core_user where id > 2 for update; 排他鎖 / 寫鎖
select * from account where id>2 lock in share mode; 共享鎖 / 讀鎖
3.5、ReadView和數據可見性算法
ReadView 是什么?
在innodb 存儲引擎中,每個SQL語句執行前都會得到一個Read View。它就是事務執行SQL語句時,產生的讀視圖。
主要用來做可見性判斷,判斷當前事務可以看到的哪個版本的數據
Readview 的幾個重要字段
每個SQL語句執行前都會得到一個Read View。
creator_trx_id:創建當前read view的事務IDm_ids:當前系統中那些活躍(未提交)的讀寫事務ID, 它數據結構為一個List。min_limit_id:表示在生成ReadView時,當前系統中活躍的讀寫事務中最小的事務id,即m_ids中的最小值。max_limit_id:表示生成ReadView時,系統中應該分配給下一個事務的id值。
數據可見性算法:
可見性算法看到的數據就是這個SQL語句能看到的數據行的版本。(在多個版本的數據中,根據事務IDtrx_id,選出適合當前SQL語句的數據行)。
通過這四個數據和版本鏈上的數據作對比。(遍歷鏈表)
當執行SQL的時候會生成事務ID,并且同時生成ReadView。
- 當
trx_id < min_limit_id時,表明修改該數據行的事務提交發生在SQL查詢之前,所以該行數據可以被SQL查詢出來 - 當
trx_id >= max_limit_id時,表明修改該數據行的事務發生在SQL查詢之后,所以該行數據不能被SQL查詢出來 - 當
min_limit_id <= trx_id < max_limit_id時,分三種情況討論- 如果
m_ids包含trx_id,表明修改該數據行的事務還未提交,- 如果
trx_id = creator_trx_id時,表明修改該數據行的事務就是當前的SQL的事務,所以可以看到該數據行 - 如果
trx_id != creator_trx_id時,表明修改該數據行的事務不是當前的SQL的事務,所以不能看到該數據行
- 如果
- 如果
m_ids不包含trx_id,表明修改該數據行的事務已經被提交了,所以可以看到該數據行
- 如果
四、MVCC的執行流程
4.1、MVCC 查詢一條記錄(SQL)的大致流程
- 獲取事務自己的版本號,即事務ID
- 獲取Read View (在RC隔離級別下,存在)
- 查詢得到的數據,然后Read View中的事務版本號進行比較。
- 如果不符合Read View的可見性規則, 即就需要Undo log中歷史快照;
- 最后返回符合規則的數據
4.2、在RC隔離級別下,存在不可重復讀的問題分析
先準備數據
DROP TABLE IF EXISTS `student`;
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, '小明');
設置當前隔離級別為RC
# 查看隔離級別
show variables like 'transaction_isolation';
# 全局修改隔離級別
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
# SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
事務并發SQL語句
# 事務A,兩次查看同一條數據
BEGIN; # 1
select * from student; # 2 輸出 name為 aaa
select * from student; # 6 輸出 name為 bbb
COMMIT; # 7
# 事務B,修改數據
BEGIN; # 3
UPDATE student SET name = 'bbb' WHERE id = 1; # 4
COMMIT; # 5
兩個事務開兩個會話
流程分析:
- 生成事務A的id,假設為100
- 生成該SQL的ReadView,
m_ids |
min_limit_id |
max_limit_id |
creator_trx_id |
|---|---|---|---|
| [100] | 100 | 101 | 100 |
- 生成事務B的id,假設為101
- 生成該SQL的ReadView,
m_ids |
min_limit_id |
max_limit_id |
creator_trx_id |
|---|---|---|---|
| [100,101] | 100 | 102 | 101 |
- 事務B提交
- 生成該SQL的ReadView,因為是RC隔離級別,所以每一次執行SQL都重新生成ReadView
m_ids |
min_limit_id |
max_limit_id |
creator_trx_id |
|---|---|---|---|
| [100] | 100 | 103 | 100 |
查找所有記錄行的版本鏈,從鏈表頭開始遍歷,此時的版本鏈如下,

使用ReadView 和 每一行數據的版本鏈做比較,比較過程如下
min_limit_id(100) <= trx_id (101) < max_limit_id(103)并且m_ids不包含trx_id,說明在生成ReadView時,記錄的修改已經提交,所以對最新的數據行bbb對該SQL是可見的。- 返回數據庫的bbb那一行的數據。
- 事務A提交
結論:可以看到,第2步和第6步返回的數據不同,發生了不可重復的問題。
4.3、在RR隔離級別下,解決不可重復讀
修改全局隔離級別,需要新開一個會話
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
show variables like 'transaction_isolation';
之前的過程是相同的,進行到第6步時,發生了不同
因為是RR 的隔離級別,所以只會生成一次ReadView
所以,第6步的ReadView 和 第2步是相同的
1-5 同上 。。。
- 生成該SQL的ReadView,因為是RR隔離級別,所以只會生成一次ReadView,使用第2步時的ReadView
m_ids |
min_limit_id |
max_limit_id |
creator_trx_id |
|---|---|---|---|
| [100] | 100 | 101 | 100 |
版本鏈還是上面的
比較過程如下:
trx_id(101) >= max_limit_id(101),說明這個版本的數據的修改,發生在生成ReadView之后,所以對該ReadView 來說,這個版本的數據是不可見的,根據roll_pointer遍歷上一個版本trx_id(1) < min_limit_id(100),說明這個版本的數據的修改,發生在生成ReadView之前,所以對該ReadView 來說,這個版本的數據是可見的,所以返回 aaa 這一行的數據
- 同上,事務A提交
本文來自博客園,作者:永恒&,轉載請注明原文鏈接:http://www.rzrgm.cn/Sun-yuan/p/17778157.html

浙公網安備 33010602011771號