轉載自:https://blog.csdn.net/huyuyang6688/article/details/123028254
概述
MVCC 全稱 Mutil-Version Concurrency Control,多版本併發控制,是一種併發控制方法,旨在減少讀寫操作的衝突
我們知道,當有多個事務同時操作數據庫的相同數據時,會出現併發問題,例如,讀 + 寫事務併發可能會導致髒讀、幻讀和不可重複讀等問題,寫+寫事務併發可能會導致數據覆寫等問題
爲了解決讀 + 寫事務併發可能導致的問題,MySQL 的 innodb 引擎實現了 MVCC,做到不用加鎖也可以實現安全的非阻塞的併發讀 + 寫,而對於寫 + 寫事務併發則只能通過加鎖解決
當前讀 + 快照讀
當前讀:當前讀會對讀取的記錄加鎖,保證讀取數據是最新版本,比如:select …… lock in share mode(共享鎖)
,select …… for update | update | insert | delete(排他鎖)
快照讀:每次修改數據都會在 undo log 記錄原來的數據(保留快照),快照讀就是讀取 undo log 的某一版本的快照,讀取數據可能不是最新版本,比如:select * from t_user where id=1
MVCC 實現原理
1. 隱藏字段
MySQL 每一行記錄除了自定義字段,還有一些隱藏字段:
- row_id:當表沒定義主鍵時,InnoDB 會以 row_id 爲主鍵生成一個聚集索引
- trx_id:記錄了新增/最近修改這條記錄的事務 id,事務 id 是自增的
- roll_pointer:回滾指針指向當前記錄的上一個版本(在 undo log 中)
2. 版本鏈
在修改數據時,會向 undo log 記錄數據原來的快照,除了用於回滾事務,還用於實現 MVCC
用一個簡單的例子來畫一下MVCC 用到的 undo log 版本鏈的邏輯圖:
當事務(trx_id = 100)執行了 insert into t_user values(1,'張三',20)
當事務(trx_id=102)執行了 update t_user set name='李四' where id=1
當事務(trx_id=103)執行了 update t_user set name='王五' where id=1
3. ReadView
在上面的例子中,多個事務對 id=1 的數據修改後,這行記錄除了最新的數據,在 undo log 中還有多個版本的快照。那其他事務查詢時能查到最新版本的數據嗎?如果不能,能讀到哪個版本的快照呢?這就要由 ReadView 來決定了
在對數據進行快照讀時,會產生的一個 ReadView,ReadView 有四個比較重要的變量:
- m_ids:活躍事務 id 列表,當前系統中所有活躍的(也就是沒提交的)事務的事務 id 列表
- min_trx_id:m_ids 中最小的事務 id
- max_trx_id:生成 ReadView 時,系統應該分配給下一個事務的 id,注意不是 m_ids 中最大的事務 id,也就是 m_ids 中的最大事務 id + 1
- creator_trx_id:生成該 ReadView 的事務的事務 id
某個事務進行快照讀時可以讀到哪個版本的數據,ReadView 有一套算法:
- 當【版本鏈中記錄的 trx_id 等於當前事務 id(trx_id = creator_trx_id)】時,說明版本鏈中的這個版本是當前事務修改的,所以該快照記錄對當前事務可見
- 當【版本鏈中記錄的 trx_id 小於活躍事務的最小 id(trx_id < min_trx_id)】時,說明版本鏈中的這條記錄已經提交了,所以該快照記錄對當前事務可見
- 當【版本鏈中記錄的 trx_id 大於下一個要分配的事務 id(trx_id > max_trx_id)】時,該快照記錄對當前事務不可見
- 當【版本鏈中記錄的 trx_id 大於等於最小活躍事務 id】且【版本鏈中記錄的 trx_id 小於下一個要分配的事務 id】(min_trx_id <= trx_id < max_trx_id)時,如果版本鏈中記錄的 trx_id 在活躍事務id列表 m_ids 中,說明生成 ReadView 時,修改記錄的事務還沒提交,所以該快照記錄對當前事務不可見,否則該快照記錄對當前事務可見
當事務對 id=1 的記錄進行快照讀 select * from t_user where id=1
,在版本鏈的快照中,從最新的一條記錄開始,依次判斷這四個條件,直到某一版本的快照對當前事務可見,否則繼續比較上一個版本的記錄
MVCC 主要是用來解決 RC 隔離級別下的髒讀和 RR 隔離級別下的不可重複讀的問題,所以 MVCC 只在 RC(解決髒讀)和 RR(解決不可重複讀)隔離級別下生效,也就是 MySQL 只會在 RC 和 RR 隔離級別下的快照讀時纔會生成 ReadView。區別就是,在 RC 隔離級別下,每一次快照讀都會生成一個最新的 ReadView,在 RR 隔離級別下,只有事務中第一次快照讀會生成 ReadView,之後的快照讀都使用第一次生成的 ReadView
手動驗證 MVCC 原理
前提條件:事務(trx_id=100)向表中插入一條的數據並提交了事務:insert into t_user values(1,'張三',20)
之後又有三個事務(事務101、事務102、事務103)對這條數據進行讀寫操作:
時間順序 | 事務101 | 事務 102 | 事務 103 |
---|---|---|---|
t1 | begin | ||
t2 | select * from t_user where id=1 | ||
t3 | begin | ||
t4 | select * from t_user where id=1 | ||
t5 | begin | ||
t6 | select * from t_user where id=1 | ||
t7 | update t_user set name=‘李四’ where id=1 | ||
t8 | select * from t_user where id=1 | ||
t9 | select * from t_user where id=1 | ||
t10 | commit | ||
t11 | select * from t_user where id=1 | ||
t12 | update t_user set name=‘王五’ where id=1 | ||
t13 | commit | ||
t14 | select * from t_user where id=1 |
在時間點 t1 ~ t6,整個版本鏈中只有一個快照,trx_id 爲 100:
在時間點 t7 ~ t11,整個版本鏈中有兩個快照,trx_id 爲 102、100:
在時間點 t11 ~ t14,整個版本鏈中有三個快照,trx_id 爲 103、102、100:
1. 事務隔離級別爲 RC(讀已提交)
當前事務隔離級別爲 RC(讀已提交)時,每個事務每次查詢對應生成的 ReadView 是這樣的,跟着這張圖來梳理一下:
在時間點 t2,事務 101 查詢時生成的 ReadView 內容爲:
trx_list: 101
min_trx_id:101
max_trx_id:102
creator_trx_id:101
當前時間點,版本鏈中只有一個快照(trx_id = 100),因爲 trx_id(100) < min_trx_id(101),符合算法的第(2)條規則,所以 trx_id = 100 的這個快照對當前事務可見
在時間點 t4,事務 102 查詢時生成的 ReadView 內容爲:
trx_list: 101,102
min_trx_id:101
max_trx_id:103
creator_trx_id:102
當前時間點,版本鏈中只有一個快照(trx_id = 100),因爲 trx_id(100) < min_trx_id(101),符合算法的第(2)條規則,所以 trx_id=100 的這個快照對當前事務可見
在時間點 t6,事務 103 查詢時生成的 ReadView 內容爲:
trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103
當前時間點,版本鏈中只有一個快照(trx_id = 100),因爲 trx_id(100) < min_trx_id(101),符合算法的第(2)條規則,所以 trx_id=100 的這個快照對當前事務可見
在時間點 t8,事務 101 查詢時生成的 ReadView 內容爲:
trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:101
當前時間點,版本鏈中有兩個快照(trx_id=102 -> trx_id=100),從版本鏈中的快照中,從最新的開始,依次判斷:
對於 trx_id=102 的快照,因爲 trx_id(102) = creator_trx_id(102),符合算法的第(1)條規則,所以 trx_id=102 的這個快照對當前事務可見
在時間點 t11,事務 103 查詢時生成的 ReadView 內容爲:
trx_list: 101,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103
當前時間點,版本鏈中有兩個快照(trx_id=102 -> trx_id=100),從版本鏈中的快照中,從最新的開始,依次判斷:
對於 trx_id=102 的快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104) ,且 trx_id(102) 不在 trx_list(101,103) 中,說明當前事務生成 ReadView 時,修改該記錄的事務不是活躍事務(已經提交),根據算法的第(4)條規則,trx_id = 102 的快照對當前事務可見。這也就驗證了在 RC 隔離級別下,事務 102 修改且提交的數據對於事務 103 是可見的
在時間點 t14,事務 101 查詢時生成的 ReadView 內容爲:
trx_list: 101
min_trx_id:101
max_trx_id:104
creator_trx_id:101
當前時間點,版本鏈中有三個快照(trx_id=103 -> trx_id=102 -> trx_id=100),從版本鏈中的快照中,從最新的開始,依次判斷:
對於 trx_id = 103 的快照,min_trx_id(101) <= trx_id(103) < max_trx_id(104) ,且 trx_id(103) 不在 trx_list(101) 中,說明當前事務生成 ReadView 時,修改該記錄的事務不是活躍事務(已經提交),根據算法的第(4)條規則,trx_id = 103 的快照對當前事務可見。這也就驗證了在 RC 隔離級別下,事務 103 修改且提交的數據對於事務 101 是可見的
2. 事務隔離級別爲 RR(可重複讀)
當前事務隔離級別爲 RR(可重複讀)時,每個事務每次查詢對應生成的 ReadView 是這樣的,跟着這張圖來梳理一下:
上面說過,在 RC 隔離級別下,每一次快照讀都會生成一個最新的 ReadView;在 RR 隔離級別下,只有事務中第一次快照讀會生成 ReadView,之後的快照讀都使用第一次生成的 ReadView
所以,事務 101 在 t8、t14 時刻查詢時,使用的 ReadView 跟 t2 時刻一樣;事務 102 在 t9 時刻查詢時使用的ReadView 跟 t4 時刻一樣;事務103 在 t11 時刻查詢時使用的 ReadView 跟 t6 時刻一樣