MySQL多版本并發(fā)控制MVCC底層原理解析
1 事務(wù)并發(fā)中遇到的問(wèn)題
1.1 臟讀
當(dāng)一個(gè)事務(wù)讀取到了另外一個(gè)事務(wù)修改但未提交的數(shù)據(jù),被稱為臟讀。

1.2 不可重復(fù)讀
當(dāng)事務(wù)內(nèi)相同的記錄被檢索兩次,且兩次得到的結(jié)果不同時(shí),此現(xiàn)象稱為不可重復(fù)讀。

1.3 幻讀
當(dāng)一個(gè)事務(wù)同樣的查詢條件查詢兩次(多次),查出的條數(shù)不一致稱為幻讀。

2 隔離級(jí)別
我們上邊介紹了幾種并發(fā)事務(wù)執(zhí)行過(guò)程中可能遇到的一些問(wèn)題,這些問(wèn)題也有輕重緩急之分,我們給這些問(wèn)題按照嚴(yán)重性來(lái)排一下序:
臟讀 > 不可重復(fù)讀 > 幻讀
SQL 標(biāo)準(zhǔn)中規(guī)定,針對(duì)不同的隔離級(jí)別,并發(fā)事務(wù)可以發(fā)生不同嚴(yán)重程度的
問(wèn)題,具體情況如下:
- READ UNCOMMITTED:未提交讀。
- READ COMMITTED:已提交讀。
- REPEATABLE READ:可重復(fù)讀。
- SERIALIZABLE:可串行化
SQL 標(biāo)準(zhǔn)中規(guī)定,針對(duì)不同的隔離級(jí)別,并發(fā)事務(wù)可以發(fā)生不同嚴(yán)重程度的問(wèn)題,具體情況如下:

- READ UNCOMMITTED 隔離級(jí)別下,可能發(fā)生臟讀、不可重復(fù)讀和幻讀問(wèn)題。
- READ COMMITTED 隔離級(jí)別下,可能發(fā)生不可重復(fù)讀和幻讀問(wèn)題,但是不可以發(fā)生臟讀問(wèn)題。
- REPEATABLE READ 隔離級(jí)別下,可能發(fā)生幻讀問(wèn)題,但是不可以發(fā)生臟讀和不可重復(fù)讀的問(wèn)題。
- SERIALIZABLE 隔離級(jí)別下,各種問(wèn)題都不可以發(fā)生。
3 版本鏈
我們知道,對(duì)于使用 InnoDB 存儲(chǔ)引擎的表來(lái)說(shuō),它的聚簇索引記錄中都包含兩個(gè)必要的隱藏列(row_id 并不是必要的,我們創(chuàng)建的表中有主鍵或者非 NULL的 UNIQUE 鍵時(shí)都不會(huì)包含 row_id 列):
trx_id: 每次一個(gè)事務(wù)對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把該事務(wù)的事務(wù) id 賦值給 trx_id 隱藏列。
roll_pointer: 每次對(duì)某條聚簇索引記錄進(jìn)行改動(dòng)時(shí),都會(huì)把舊的版本寫入到undo 日志中,然后這個(gè)隱藏列就相當(dāng)于一個(gè)指針,可以通過(guò)它來(lái)找到該記錄修改前的信息。
假設(shè)插入該記錄的事務(wù) id 為 80的記錄,那么此刻該條記錄的示意圖如下所示:

假設(shè)之后兩個(gè)事務(wù) id 分別為 100、200 的事務(wù)對(duì)這條記錄進(jìn)行 UPDATE 操作,操作流程如下:
Trx 100:
UPDATE t_people SET name = '關(guān)羽' WHERE number = 1; UPDATE t_people SET name = '張飛' WHERE number = 1;
Trx 200:
UPDATE t_people SET name = '趙云' WHERE number = 1; UPDATE t_people SET name = '諸葛亮' WHERE number = 1;
每次對(duì)記錄進(jìn)行改動(dòng),都會(huì)記錄一條 undo 日志,每條 undo 日志也都有一個(gè) roll_pointer 屬性(INSERT 操作對(duì)應(yīng)的 undo 日志沒(méi)有該屬性,因?yàn)樵撚涗洸](méi)有更早的版本),可以將這些 undo 日志都連起來(lái),串成一個(gè)鏈表,所以現(xiàn)在的情況就像下圖一樣:

對(duì)該記錄每次更新后,都會(huì)將舊值放到一條 undo 日志中,就算是該記錄的一個(gè)舊版本,隨著更新次數(shù)的增多,所有的版本都會(huì)被 roll_pointer 屬性連接成一個(gè)鏈表,我們把這個(gè)鏈表稱之為版本鏈,版本鏈的頭節(jié)點(diǎn)就是當(dāng)前記錄最新的值。另外,每個(gè)版本中還包含生成該版本時(shí)對(duì)應(yīng)的事務(wù) id。于是可以利用這個(gè)記錄的版本鏈來(lái)控制并發(fā)事務(wù)訪問(wèn)相同記錄的行為,那么這種機(jī)制就被稱之為多版本并發(fā)控制(Mulit-Version Concurrency Control MVCC)。
4 ReadView
4.1 ReadView 定義
InnoDB 提出了一個(gè) ReadView 的概念,這個(gè) ReadView 中主要包含 4個(gè)比較重要的內(nèi)容:
- (1) m_ids:表示在生成 ReadView 時(shí)當(dāng)前系統(tǒng)中 活躍 的讀寫事務(wù)的事務(wù) id 列表。
- (2) min_trx_id: 表示在生成 ReadView 時(shí)當(dāng)前系統(tǒng)中活躍的讀寫事務(wù)中最小的事務(wù) id,也就是 m_ids 中的最小值。
- (3) max_trx_id:表示生成 ReadView 時(shí)系統(tǒng)中應(yīng)該分配給下一個(gè)事務(wù)的 id 值。max_trx_id 并不是 m_ids 中的最大值,事務(wù) id 是遞增分配的。比方說(shuō)現(xiàn)在有 id 為 1,2,3 這三個(gè)事務(wù),之后 id 為 3 的事務(wù)提交了。那么一個(gè)新的讀事務(wù)在生成 ReadView 時(shí),m_ids 就包括 1 和 2,min_trx_id 的值就是 1,max_trx_id的值就是 4。
- (4) creator_trx_id:表示生成該 ReadView 的事務(wù)的事務(wù) id。
4.2 訪問(wèn)控制
有了這個(gè) ReadView,這樣在訪問(wèn)某條記錄時(shí),只需要按照下邊的步驟判斷記錄的某個(gè)版本是否可見(jiàn):
- (1) 如果被訪問(wèn)版本的 trx_id 屬性值與 ReadView 中的 creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問(wèn)它自己修改過(guò)的記錄,所以該版本可以被當(dāng)前事務(wù)訪問(wèn)。
- (2) 如果被訪問(wèn)版本的 trx_id 屬性值小于 ReadView 中的 min_trx_id 值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 前已經(jīng)提交,所以該版本可以被當(dāng)前事務(wù)訪問(wèn)。
- (3) 如果被訪問(wèn)版本的 trx_id 屬性值大于或等于 ReadView 中的 max_trx_id值,表明生成該版本的事務(wù)在當(dāng)前事務(wù)生成 ReadView 后才開(kāi)啟,所以該版本不可以被當(dāng)前事務(wù)訪問(wèn)。
- (4) 如果被訪問(wèn)版本的 trx_id 屬性值在 ReadView 的 min_trx_id 和 max_trx_id之間(min_trx_id < trx_id < max_trx_id),那就需要判斷一下 trx_id 屬性值是不是在m_ids 列表中,如果在,說(shuō)明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問(wèn);如果不在,說(shuō)明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問(wèn)。
- (5) 如果某個(gè)版本的數(shù)據(jù)對(duì)當(dāng)前事務(wù)不可見(jiàn)的話,那就順著版本鏈找到下一個(gè)版本的數(shù)據(jù),繼續(xù)按照上邊的步驟判斷可見(jiàn)性,依此類推,直到版本鏈中的最后一個(gè)版本。如果最后一個(gè)版本也不可見(jiàn)的話,那么就意味著該條記錄對(duì)該事務(wù)完全不可見(jiàn),查詢結(jié)果就不包含該記錄。
4.3 再談隔離
對(duì)于使用 READ UNCOMMITTED 隔離級(jí)別的事務(wù)來(lái)說(shuō),由于可以讀到未提交事務(wù)修改過(guò)的記錄,所以直接讀取記錄的最新版本就好了。
對(duì)于使用 SERIALIZABLE 隔離級(jí)別的事務(wù)來(lái)說(shuō),InnoDB 使用加鎖的方式來(lái)訪問(wèn)記錄。
在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔離級(jí)別的的一個(gè)非常大的區(qū)別就是它們生成 ReadView 的時(shí)機(jī)不同。
4.3.1 READ COMMITTED(讀已提交)
讀已提交,每次讀取數(shù)據(jù)前都生成一個(gè) ReadView。

假設(shè)現(xiàn)在有一個(gè)使用 READ COMMITTED 隔離級(jí)別的事務(wù)開(kāi)始執(zhí)行:
詳解查詢:
#使用 READ COMMITTED 隔離級(jí)別的事務(wù) #Transaction 100、200未提交,得到的列 name 的值為 劉備 SELECT name FROM t_people WHERE number = 1;
這個(gè) SELECET 的執(zhí)行過(guò)程如下:
- (1) 在執(zhí)行 SELECT 語(yǔ)句時(shí)會(huì)先生成一個(gè) ReadView,ReadView 的 m_ids 列表的內(nèi)容就是[100, 200],min_trx_id 為 100,max_trx_id 為 201,creator_trx_id 為 0。
- (2) 然后從版本鏈中挑選可見(jiàn)的記錄,從圖中可以看出,最新版本的列 name 的內(nèi)容是諸葛亮,該版本的 trx_id 值為 200,在 m_ids 列表內(nèi),所以不符合可見(jiàn)性要求。(如果被訪問(wèn)版本的 trx_id 屬性值在 ReadView 的 min_trx_id 和 max_trx_id之間,就需要判斷一下 trx_id 屬性值是不是在m_ids 列表中,如果在,說(shuō)明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)還是活躍的,該版本不可以被訪問(wèn);如果不在,說(shuō)明創(chuàng)建 ReadView 時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問(wèn) ),根據(jù) roll_pointer 跳到下一個(gè)版本。
- (3) 諸葛亮下一個(gè)版本的列name的內(nèi)容是趙云,該版本的trx_id值也為200,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
- (4) 趙云下一個(gè)版本的列name的內(nèi)容是張飛,該版本的trx_id值也為100,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
- (5) 張飛下一個(gè)版本的列name的內(nèi)容是關(guān)羽,該版本的trx_id值也為100,也在m_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。
- (6) 關(guān)羽下一個(gè)版本是劉備,該版本的trx_id值為80,小于ReadView 中的 min_trx_id 值,所以這個(gè)版本是符合要求的。
不可重復(fù)讀: 100事務(wù)、200事務(wù)開(kāi)啟讀取到name都為劉備。當(dāng)100事務(wù)提交時(shí),由于是讀已提交事務(wù)隔離級(jí)別,每次讀取都會(huì)創(chuàng)建ReadView,200事務(wù)讀取時(shí),創(chuàng)建生成的ReadView m_ids 為 [200],這時(shí)根據(jù)讀取規(guī)則讀取到的name就為張飛。
# 使用 READ COMMITTED 隔離級(jí)別的事務(wù) BEGIN; # SELECE1:Transaction 100、200 均未提交,得到name值為劉備 SELECT name FROM t_people WHERE number = 1; # SELECE2:Transaction 100 提交,Transaction 200 未提交 #Transaction 200 事務(wù)查詢,得到name值為張飛,發(fā)生不可重復(fù)讀。 SELECT name FROM teacher WHERE number = 1;
4.3.2 REPEATABLE READ(可重讀)
可重讀,在第一次讀取數(shù)據(jù)時(shí)生成一個(gè) ReadView。
解決不可重復(fù)讀: 100事務(wù)、200事務(wù)開(kāi)啟,創(chuàng)建ReadView,m_ids 為[100,200],讀取到name都為劉備。當(dāng)100事務(wù)提交時(shí),由于是可重讀事務(wù)隔離級(jí)別,只創(chuàng)建一次ReadView,m_ids 仍然是[100,200],這時(shí)根據(jù)讀取規(guī)則讀取到的name仍然是劉備。
5 幻讀
當(dāng)一個(gè)事務(wù)同樣的查詢條件查詢兩次(多次),查出的條數(shù)不一致稱為幻讀。
在 REPEATABLE READ 隔離級(jí)別下的事務(wù) T1 先根據(jù)某個(gè)搜索條件讀取到多條記錄,然后事務(wù) T2 插入一條符合相應(yīng)搜索條件的記錄并提交,然后事務(wù) T1 再根據(jù)相同搜索條件執(zhí)行查詢。結(jié)果會(huì)是什么?按照 ReadView 中的比較規(guī)則,不管事務(wù) T2 比事務(wù) T1 是否先開(kāi)啟,事務(wù) T1 都是看不到 T2 的提交的。但是,在 REPEATABLE READ 隔離級(jí)別下 InnoDB 中的 MVCC 可以很大程度地避免幻讀現(xiàn)象,而不是完全禁止幻讀。
#SELECT:快照讀。update:當(dāng)前讀。 REPEATABLE READ 可以解決快照讀幻讀問(wèn)題。解決不了當(dāng)前讀幻讀的問(wèn)題。
案例:
1 執(zhí)行begin,執(zhí)行select *。

2 開(kāi)啟另一個(gè)窗口 執(zhí)行insert、select、commit。


3 回到原窗口執(zhí)行查詢

4 執(zhí)行 update 、提交


5 查找

6 總結(jié)
從上邊的描述中我們可以看出來(lái),所謂的 MVCC(Multi-Version ConcurrencyControl ,多版本并發(fā)控制)指的就是在使用 READ COMMITTD、REPEATABLE READ這兩種隔離級(jí)別的事務(wù)在執(zhí)行普通的 SELECT 操作時(shí)訪問(wèn)記錄的版本鏈的過(guò)程,這樣子可以使不同事務(wù)的讀-寫、寫-讀操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。
READ COMMITTD、REPEATABLE READ 這兩個(gè)隔離級(jí)別的一個(gè)很大不同就是,生成 ReadView 的時(shí)機(jī)不同,READ COMMITTD 在每一次進(jìn)行普通 SELECT 操作前都會(huì)生成一個(gè) ReadView,而 REPEATABLE READ 只在第一次進(jìn)行普通 SELECT 操作前生成一個(gè) ReadView,之后的查詢操作都重復(fù)使用這個(gè) ReadView 就好了,從而基本上可以避免幻讀現(xiàn)象。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Mysql InnoDB多版本并發(fā)控制MVCC詳解
- MySQL多版本并發(fā)控制MVCC詳解
- Mysql MVCC多版本并發(fā)控制詳情
- MySQL的多版本并發(fā)控制MVCC的實(shí)現(xiàn)
- MySQL多版本并發(fā)控制MVCC深入學(xué)習(xí)
- 詳解MySQL多版本并發(fā)控制機(jī)制(MVCC)源碼
- mysql的MVCC多版本并發(fā)控制的實(shí)現(xiàn)
- mysql多版本并發(fā)控制MVCC的實(shí)現(xiàn)
- 一文詳解MYSQL的多版本并發(fā)控制MVCC(Multi-Version Concurrency Control)
相關(guān)文章
MySQL數(shù)據(jù)庫(kù)的高可用方案總結(jié)
這篇文章主要針對(duì)MySQL數(shù)據(jù)庫(kù)的高可用方案進(jìn)行詳細(xì)總結(jié),高可用架構(gòu)對(duì)于互聯(lián)網(wǎng)服務(wù)基本是標(biāo),本文是對(duì)各種方案的總結(jié),感興趣的小伙伴們可以參考一下2016-05-05
MySQL操作數(shù)據(jù)庫(kù)實(shí)戰(zhàn)指南
這篇文章主要給大家介紹了關(guān)于MySQL數(shù)據(jù)庫(kù)操作庫(kù)的相關(guān)資料,MySQL數(shù)據(jù)庫(kù)是一個(gè)關(guān)系型數(shù)據(jù)庫(kù)管理系統(tǒng),所采用的SQL語(yǔ)言是用于訪問(wèn)數(shù)據(jù)庫(kù)最常用的標(biāo)準(zhǔn)會(huì)語(yǔ)言,需要的朋友可以參考下2023-07-07
關(guān)于mysql 8.x 中insert ignore的性能問(wèn)題
這篇文章主要介紹了關(guān)于mysql 8.x 中insert ignore的性能問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。2022-08-08
MySQL ClickHouse常用表引擎超詳細(xì)講解
這篇文章主要介紹了MySQL ClickHouse常用表引擎,ClickHouse表引擎中,CollapsingMergeTree和VersionedCollapsingMergeTree都能通過(guò)標(biāo)記位按規(guī)則折疊數(shù)據(jù),從而達(dá)到更新和刪除的效果2022-11-11
解析:內(nèi)聯(lián),左外聯(lián),右外聯(lián),全連接,交叉連接的區(qū)別
本篇文章是對(duì)內(nèi)聯(lián),左外聯(lián),右外聯(lián),全連接,交叉連接的區(qū)別進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-07-07
使用mysql事件調(diào)度器定時(shí)刪除binlog
MySQL5.1.6起Mysql增加了事件調(diào)度器(Event Scheduler),可以用做定時(shí)執(zhí)行某些特定任務(wù),來(lái)取代原先只能由操作系統(tǒng)的計(jì)劃任務(wù)來(lái)執(zhí)行的工作2014-03-03
Mysql系列SQL查詢語(yǔ)句書寫順序及執(zhí)行順序詳解
這篇文章主要為大家介紹了Mysql系列SQL查詢語(yǔ)句的書寫順序及執(zhí)行順序示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-10-10

