一文帶你搞懂MySQL的MVCC機(jī)制
MVCC,英文全稱Multiversion Concurrency Control,多版本并發(fā)控制。簡(jiǎn)單理解,就是相當(dāng)于給我們的MySQL數(shù)據(jù)庫(kù)拍個(gè)“快照”,定格某個(gè)時(shí)刻數(shù)據(jù)庫(kù)的狀態(tài)。
那你可能問(wèn)為什么要拍個(gè)“快照”,也就是MVCC機(jī)制?
還記得事務(wù)的一大特性就是隔離性,一共有4個(gè)隔離級(jí)別,讀未提交,讀已提交,可重復(fù)讀,串行化。
以MySQL InnoDB 引擎的默認(rèn)隔離級(jí)別可重復(fù)讀為例,可重復(fù)讀指一個(gè)事務(wù)執(zhí)行過(guò)程中看到的數(shù)據(jù),一直跟這個(gè)事務(wù)啟動(dòng)時(shí)看到的數(shù)據(jù)是一致的。
為了保證事務(wù)啟動(dòng)到結(jié)束整個(gè)生命周期看到的數(shù)據(jù)是一致的, 一般有兩種方案:
MySQL對(duì)數(shù)據(jù)“讀-寫”的時(shí)候,加鎖,其他事務(wù)寫這條數(shù)據(jù)時(shí)加上鎖,其他事務(wù)讀取的時(shí)候阻塞。MySQL可以對(duì)事務(wù)啟動(dòng)的時(shí)候,對(duì)數(shù)據(jù)庫(kù)拍個(gè)“快照”,那么事務(wù)運(yùn)行過(guò)程中讀取都從這個(gè)快照讀取,不也是保證數(shù)據(jù)一致么。第一種方案存在明顯的問(wèn)題,加鎖會(huì)引發(fā)阻塞,從而降低數(shù)據(jù)庫(kù)性能。而MySQL設(shè)計(jì)者們采用第二種,也就是大名鼎鼎的MVCC,它不僅能夠解決不可重復(fù)讀,還一定程度解決幻讀的問(wèn)題,因?yàn)槟阏麄€(gè)數(shù)據(jù)庫(kù)快照都有了,你就知道那個(gè)時(shí)刻的數(shù)據(jù)了。
雖然說(shuō)SQL標(biāo)準(zhǔn)定義中可重復(fù)讀隔離級(jí)別下會(huì)存在幻讀的現(xiàn)象,但是不同的數(shù)據(jù)庫(kù)廠商可以基于SQL標(biāo)準(zhǔn)下有不同的實(shí)現(xiàn),那么不同隔離級(jí)別下發(fā)生的現(xiàn)象也會(huì)有出入,就拿MySQL的可重復(fù)讀隔離級(jí)別就可以一定程度保證幻讀。
小結(jié)一下:
MVCC在MySQL InnoDB中的實(shí)現(xiàn)主要是為了提高數(shù)據(jù)庫(kù)并發(fā)性能,用更好的方式去處理讀-寫沖突 ,做到即使有讀寫沖突時(shí),也能做到不加鎖 , 非阻塞并發(fā)讀,而這個(gè)讀指的就是快照讀 , 而非當(dāng)前讀。
什么是快照讀和當(dāng)前讀?前面提到了快照讀和當(dāng)前讀,這又有什么不一樣呢,什么樣的sql語(yǔ)句算是快照讀,什么樣的又算是當(dāng)前讀呢?
快照讀快照讀又叫普通讀,也就是利用MVCC機(jī)制讀取快照中的數(shù)據(jù)。不加鎖的簡(jiǎn)單的SELECT 都屬于快照讀,比如這樣:
SELECT * FROM user WHERE ...快照讀是基于MVCC實(shí)現(xiàn)的,提高了并發(fā)的性能,降低開(kāi)銷大部分業(yè)務(wù)代碼中的讀取都屬于快照讀當(dāng)前讀當(dāng)前讀讀取的是記錄的最新版本,讀取時(shí)會(huì)對(duì)讀取的記錄進(jìn)行加鎖, 其他事務(wù)就有可能阻塞。加鎖的 SELECT,或者對(duì)數(shù)據(jù)進(jìn)行增刪改都會(huì)進(jìn)行當(dāng)前讀。比如:
SELECT * FROM user LOCK IN SHARE MODE; # 共享鎖SELECT * FROM user FOR UPDATE; # 排他鎖INSERT INTO user values ... # 排他鎖DELETE FROM user WHERE ... # 排他鎖UPDATE user SET ... # 排他鎖update、delete、insert語(yǔ)句雖然沒(méi)有select, 但是它們也會(huì)先進(jìn)行讀取,而且只能讀取最新版本。MVCC機(jī)制是咋工作的呢?前面打個(gè)比方說(shuō)MVCC機(jī)制相當(dāng)于是基于整個(gè)數(shù)據(jù)庫(kù)“拍了個(gè)快照”,這時(shí),你會(huì)說(shuō)這看上去不太現(xiàn)實(shí)啊。如果一個(gè)庫(kù)有 100G,那么我啟動(dòng)一個(gè)事務(wù),MySQL 就要保存 100G 的數(shù)據(jù)出來(lái),這個(gè)過(guò)程得多慢啊,而且也很占用空間啊,根本就不能支持幾個(gè)事務(wù)啊。別急,我們現(xiàn)在來(lái)講解下MVCC機(jī)制是如何工作的。
數(shù)據(jù)的多個(gè)版本首先MySQL innoDB存儲(chǔ)引擎需要支持一條數(shù)據(jù)可以保留多個(gè)歷史版本。怎么保留呢?還記得事務(wù)日志undo log嗎?
對(duì)于使用 InnoDB 存儲(chǔ)引擎的數(shù)據(jù)庫(kù)表,它的聚簇索引記錄中都包含下面兩個(gè)隱藏列:
trx_id,當(dāng)一個(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è)隱藏列是個(gè)指針,指向每一個(gè)舊版本記錄,于是就可以通過(guò)它找到修改前的記錄。InnoDB 里面每個(gè)事務(wù)有一個(gè)唯一的事務(wù) ID,叫作 transaction id。它是在事務(wù)開(kāi)始的時(shí)候向 InnoDB 的事務(wù)系統(tǒng)申請(qǐng)的,是按申請(qǐng)順序嚴(yán)格遞增的。
如上圖所示,針對(duì)id=1的這條數(shù)據(jù),都會(huì)將舊值放到一條undo日志中,就算是該記錄的一個(gè)舊版本,隨著更新次數(shù)的增多,所有的版本都會(huì)被 roll_pointer 屬性連接成一個(gè)鏈表,我們把這個(gè)鏈表稱之為版本鏈,根據(jù)版本鏈就可以找到這條數(shù)據(jù)歷史的版本。
一致性視圖ReadView利用undo log日志我們已經(jīng)保留下了數(shù)據(jù)的各個(gè)版本,那么現(xiàn)在關(guān)鍵的問(wèn)題是要讀取哪個(gè)版本的數(shù)據(jù)呢?
這時(shí)就需要用到ReadView了,ReadView就是事務(wù)在使用MVCC機(jī)制進(jìn)行快照讀操作時(shí)產(chǎn)生的一致性視圖, 比如針對(duì)可重復(fù)讀隔離級(jí)別,是在事務(wù)啟動(dòng)的時(shí)候,創(chuàng)建一個(gè)ReadView, 那ReadView種都有哪些關(guān)鍵信息呢?
trx_ids: 指的是在創(chuàng)建 ReadView 時(shí),當(dāng)前數(shù)據(jù)庫(kù)中「活躍事務(wù)」的事務(wù) id 列表,注意是一個(gè)列表, “活躍事務(wù)”指的就是,啟動(dòng)了但還沒(méi)提交的事務(wù)。min_trx_id: 指的是在創(chuàng)建 ReadView 時(shí),當(dāng)前數(shù)據(jù)庫(kù)中「活躍事務(wù)」中事務(wù) id 最小的事務(wù),也就是 m_ids 的最小值。max_trx_id:這個(gè)并不是 m_ids 的最大值,而是創(chuàng)建 ReadView 時(shí)當(dāng)前數(shù)據(jù)庫(kù)中應(yīng)該給下一個(gè)事務(wù)的 id 值,也就是全局事務(wù)中最大的事務(wù) id 值 + 1;creator_trx_id :指的是創(chuàng)建該 ReadView 的事務(wù)的事務(wù) id, 只有在對(duì)表中的記錄做改動(dòng)時(shí)(執(zhí)行INSERT、DELETE、UPDATE這些語(yǔ)句時(shí))才會(huì)為 事務(wù)分配事務(wù)id,否則在一個(gè)只讀事務(wù)中的事務(wù)id值都默認(rèn)為0。對(duì)于當(dāng)前事務(wù)的啟動(dòng)瞬間來(lái)說(shuō),讀取的一個(gè)數(shù)據(jù)版本的trx_id,有以下幾種可能:
如果被訪問(wèn)版本的trx_id屬性值與ReadView中的 creator_trx_id 值相同,意味著當(dāng)前事務(wù)在訪問(wèn)它自己修改過(guò)的記錄,所以該版本可以被當(dāng)前事務(wù)訪問(wèn)。如果落在綠色部分,表示這個(gè)版本是已提交的事務(wù)或者是當(dāng)前事務(wù)自己生成的,這個(gè)數(shù)據(jù)是可見(jiàn)的;如果落在紅色部分,表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的,是肯定不可見(jiàn)的;如果落在黃色部分,那就包括兩種情況若 數(shù)據(jù)的trx_id在trx_ids數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn), 去讀取這條數(shù)據(jù)的歷史版本,這條數(shù)據(jù)的歷史版本中都包含了事務(wù)id信息,去查找第一個(gè)不在活躍事務(wù)數(shù)組的版本記錄。若 數(shù)據(jù)的trx_id不在trx_ids數(shù)組中,表示這個(gè)版本是已經(jīng)提交了的事務(wù)生成的,可見(jiàn)。這種通過(guò)版本鏈 + 一致性視圖 來(lái)控制并發(fā)事務(wù)訪問(wèn)同一個(gè)記錄時(shí)的行為就叫 MVCC(多版本并發(fā)控制),現(xiàn)在你明白MySQL如何實(shí)現(xiàn)了“秒級(jí)創(chuàng)建快照”的能力了吧。
還是不懂?舉例說(shuō)明如果你對(duì)MVCC機(jī)制的整個(gè)流程還是比較模糊,我們現(xiàn)在舉例來(lái)說(shuō)明下。
比如student表中有一個(gè)事務(wù)id為8的插入記錄:
insert into student(id, name, class) values(1, '張三', '一班')我們現(xiàn)在在MySQL的讀已提交和可重復(fù)讀隔離級(jí)別下,MVCC機(jī)制的整個(gè)工作流程。
MySQL中的讀未提交和序列化并不需要MVCC機(jī)制,讀未提交,直接讀取別人未提交的數(shù)據(jù),而序列化全程用加鎖的方式,也用不上MVCC, 大家體會(huì)下。
可重復(fù)讀隔離級(jí)別下可重復(fù)讀REPEATABLE READ 隔離級(jí)別的事務(wù)來(lái)說(shuō),只會(huì)在第一次執(zhí)行查詢語(yǔ)句時(shí)生成一個(gè) ReadView ,之后的查詢就不會(huì)重復(fù)生成了。
begin/start transaction 命令并不是一個(gè)事務(wù)的起點(diǎn),在執(zhí)行到它們之后的第一個(gè)操作 InnoDB 表的語(yǔ)句,事務(wù)才真正啟動(dòng)。如果你想要馬上啟動(dòng)一個(gè)事務(wù),可以使用 start transaction with consistent snapshot 這個(gè)命令。
事務(wù)10事務(wù)20事務(wù)30beginUPDATE student SET name="李四" WHERE id=1;UPDATE student SET name="王五" WHERE id=1;begin更新了一些其他表的數(shù)據(jù)beginSELECT * FROM事務(wù)10和20均未提交,現(xiàn)在事務(wù)30執(zhí)行select, 那么得到的結(jié)果是什么呢?
在執(zhí)行select語(yǔ)句時(shí)會(huì)先生成一個(gè)ReadView,ReadView的trx_ids列表的內(nèi)容就是[10, 20],min_trx_id為10,max_trx_id為21,creator_trx_id為0。然后從版本鏈中挑選可見(jiàn)的記錄,從圖中看出,最新版本的列name的內(nèi)容是'王五',該版本的trx_id值為10,在trx_ids列表內(nèi),所以不符合可見(jiàn)性要求,根據(jù)roll_pointer跳到下一個(gè)版本。下一個(gè)版本的列name的內(nèi)容是'李四',該版本的trx_id值也為10,也在trx_ids列表內(nèi),所以也不符合要求,繼續(xù)跳到下一個(gè)版本。下一個(gè)版本的列name的內(nèi)容是'張三',該版本的trx_id值為8,小于ReadView中的min_trx_id值10,說(shuō)明已經(jīng)提交了,那么最終返回'張三'。讀已提交隔離級(jí)別下讀已提交READ COMMITTED是每次讀取數(shù)據(jù)前都生成一個(gè)ReadView。基本的規(guī)則和流程與可重復(fù)讀隔離級(jí)別一致,這里不做重復(fù)贅敘。
總結(jié)本問(wèn)重點(diǎn)介紹了MVCC機(jī)制,以及 MVCC 在 READ COMMITTD、 REPEATABLE READ 這兩種隔離級(jí)別的事務(wù)在執(zhí)行快照讀操作時(shí)訪問(wèn)記錄的版本鏈的過(guò)程。這樣使不同事務(wù)的 讀-寫 、 寫-讀 操作并發(fā)執(zhí)行,從而提升系統(tǒng)性能。
READ COMMITTD 在每一次進(jìn)行普通SELECT操作前都會(huì)生成一個(gè)ReadViewREPEATABLE READ 只在第一次進(jìn)行普通SELECT操作前生成一個(gè)ReadView,之后的查詢操作都重復(fù)使用這個(gè)ReadView就好了。以上就是一文帶你搞懂MySQL的MVCC機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于MySQL MVCC機(jī)制的資料請(qǐng)關(guān)注好吧啦網(wǎng)其它相關(guān)文章!
相關(guān)文章:
