什么是 MVCC
MVCC (Multiversion Concurrency Control)
中文全程叫多版本并發(fā)控制筹误,是現(xiàn)代數(shù)據(jù)庫(包括 MySQL
桐早、Oracle
、PostgreSQL
等)引擎實(shí)現(xiàn)中常用的處理讀寫沖突的手段厨剪,目的在于提高數(shù)據(jù)庫高并發(fā)場景下的吞吐性能哄酝。
如此一來不同的事務(wù)在并發(fā)過程中,SELECT
操作可以不加鎖而是通過 MVCC
機(jī)制讀取指定的版本歷史記錄丽惶,并通過一些手段保證保證讀取的記錄值符合事務(wù)所處的隔離級別炫七,從而解決并發(fā)場景下的讀寫沖突。
下面舉一個(gè)多版本讀的例子钾唬,例如兩個(gè)事務(wù) A
和 B
按照如下順序進(jìn)行更新和讀取操作
在事務(wù) A
提交前后万哪,事務(wù) B
讀取到的 x
的值是什么呢?答案是:事務(wù) B
在不同的隔離級別下抡秆,讀取到的值不一樣奕巍。
- 如果事務(wù)
B
的隔離級別是讀未提交(RU),那么兩次讀取均讀取到x
的最新值儒士,即20
的止。 - 如果事務(wù)
B
的隔離級別是讀已提交(RC),那么第一次讀取到舊值10
着撩,第二次因?yàn)槭聞?wù)A
已經(jīng)提交诅福,則讀取到新值 20。 - 如果事務(wù)
B
的隔離級別是可重復(fù)讀或者串行(RR拖叙,S)氓润,則兩次均讀到舊值10
,不論事務(wù)A
是否已經(jīng)提交薯鳍。
可見在不同的隔離級別下咖气,數(shù)據(jù)庫通過 MVCC
和隔離級別,讓事務(wù)之間并行操作遵循了某種規(guī)則挖滤,來保證單個(gè)事務(wù)內(nèi)前后數(shù)據(jù)的一致性崩溪。
為什么需要 MVCC
InnoDB
相比 MyISAM
有兩大特點(diǎn),一是支持事務(wù)而是支持行級鎖斩松,事務(wù)的引入帶來了一些新的挑戰(zhàn)伶唯。相對于串行處理來說,并發(fā)事務(wù)處理能大大增加數(shù)據(jù)庫資源的利用率惧盹,提高數(shù)據(jù)庫系統(tǒng)的事務(wù)吞吐量抵怎,從而可以支持可以支持更多的用戶奋救。但并發(fā)事務(wù)處理也會(huì)帶來一些問題,主要包括以下幾種情況:
更新丟失(
Lost Update
):當(dāng)兩個(gè)或多個(gè)事務(wù)選擇同一行反惕,然后基于最初選定的值更新該行時(shí),由于每個(gè)事務(wù)都不知道其他事務(wù)的存在演侯,就會(huì)發(fā)生丟失更新問題 —— 最后的更新覆蓋了其他事務(wù)所做的更新姿染。如何避免這個(gè)問題呢,最好在一個(gè)事務(wù)對數(shù)據(jù)進(jìn)行更改但還未提交時(shí)秒际,其他事務(wù)不能訪問修改同一個(gè)數(shù)據(jù)悬赏。臟讀(
Dirty Reads
):一個(gè)事務(wù)正在對一條記錄做修改,在這個(gè)事務(wù)并提交前娄徊,這條記錄的數(shù)據(jù)就處于不一致狀態(tài)闽颇;這時(shí),另一個(gè)事務(wù)也來讀取同一條記錄寄锐,如果不加控制兵多,第二個(gè)事務(wù)讀取了這些尚未提交的臟數(shù)據(jù),并據(jù)此做進(jìn)一步的處理橄仆,就會(huì)產(chǎn)生未提交的數(shù)據(jù)依賴關(guān)系剩膘。這種現(xiàn)象被形象地叫做 “臟讀”。不可重復(fù)讀(
Non-Repeatable Reads
):一個(gè)事務(wù)在讀取某些數(shù)據(jù)已經(jīng)發(fā)生了改變盆顾、或某些記錄已經(jīng)被刪除了怠褐!這種現(xiàn)象叫做“不可重復(fù)讀”。幻讀(
Phantom Reads
):一個(gè)事務(wù)按相同的查詢條件重新讀取以前檢索過的數(shù)據(jù)您宪,卻發(fā)現(xiàn)其他事務(wù)插入了滿足其查詢條件的新數(shù)據(jù)奈懒,這種現(xiàn)象就稱為 “幻讀”。
以上是并發(fā)事務(wù)過程中會(huì)存在的問題宪巨,解決更新丟失可以交給應(yīng)用磷杏,但是后三者需要數(shù)據(jù)庫提供事務(wù)間的隔離機(jī)制來解決。實(shí)現(xiàn)隔離機(jī)制的方法主要有兩種:
加讀寫鎖
一致性快照讀揖铜,即
MVCC
但本質(zhì)上茴丰,隔離級別是一種在并發(fā)性能和并發(fā)產(chǎn)生的副作用間的妥協(xié),通常數(shù)據(jù)庫均傾向于采用 Weak Isolation
天吓。
InnoDB 中的 MVCC
本文聚焦于 MySQL
中的 MVCC
實(shí)現(xiàn)贿肩,從 《高性能 MySQL》
一書中對 MVCC
的介紹可知:
-
MySQL
中InnoDB
引擎支持MVCC
- 應(yīng)對高并發(fā)事務(wù),
MVCC
比單純的加行鎖更有效, 開銷更小 -
MVCC
在讀已提交(Read Committed)
和可重復(fù)讀(Repeatable Read)
隔離級別下起作用 -
MVCC
既可以基于樂觀鎖又可以基于悲觀鎖來實(shí)現(xiàn)
InnoDB MVCC 實(shí)現(xiàn)原理
InnoDB
中 MVCC
的實(shí)現(xiàn)方式為:每一行記錄都有兩個(gè)隱藏列:DATA_TRX_ID
、DATA_ROLL_PTR
(如果沒有主鍵龄寞,則還會(huì)多一個(gè)隱藏的主鍵列)汰规。
DATA_TRX_ID
記錄最近更新這條行記錄的事務(wù) ID
,大小為 6
個(gè)字節(jié)
DATA_ROLL_PTR
表示指向該行回滾段(rollback segment)
的指針物邑,大小為 7
個(gè)字節(jié)溜哮,InnoDB
便是通過這個(gè)指針找到之前版本的數(shù)據(jù)滔金。該行記錄上所有舊版本,在 undo
中都通過鏈表的形式組織茂嗓。
DB_ROW_ID
行標(biāo)識(shí)(隱藏單調(diào)自增 ID
)餐茵,大小為 6
字節(jié),如果表沒有主鍵述吸,InnoDB
會(huì)自動(dòng)生成一個(gè)隱藏主鍵忿族,因此會(huì)出現(xiàn)這個(gè)列。另外蝌矛,每條記錄的頭信息(record header
)里都有一個(gè)專門的 bit
(deleted_flag
)來表示當(dāng)前記錄是否已經(jīng)被刪除道批。
如何組織 Undo Log 鏈
關(guān)于 Redo Log 和 Undo Log 的相關(guān)概念可見之前的文章 InnoDB 中的 redo 和 undo log
上文提到,在多個(gè)事務(wù)并行操作某行數(shù)據(jù)的情況下,不同事務(wù)對該行數(shù)據(jù)的 UPDATE 會(huì)產(chǎn)生多個(gè)版本,然后通過回滾指針組織成一條 Undo Log
鏈敛惊,這節(jié)我們通過一個(gè)簡單的例子來看一下 Undo Log
鏈?zhǔn)侨绾谓M織的缰揪,DATA_TRX_ID
和 DATA_ROLL_PTR
兩個(gè)參數(shù)在其中又起到什么樣的作用。
還是以上文 MVCC
的例子,事務(wù) A
對值 x
進(jìn)行更新之后,該行即產(chǎn)生一個(gè)新版本和舊版本。假設(shè)之前插入該行的事務(wù) ID
為 100
鉴吹,事務(wù) A
的 ID
為 200
,該行的隱藏主鍵為 1
惩琉。
事務(wù) A
的操作過程為:
- 對
DB_ROW_ID = 1
的這行記錄加排他鎖 - 把該行原本的值拷貝到
undo log
中豆励,DB_TRX_ID
和DB_ROLL_PTR
都不動(dòng) - 修改該行的值這時(shí)產(chǎn)生一個(gè)新版本,更新
DATA_TRX_ID
為修改記錄的事務(wù)ID
瞒渠,將DATA_ROLL_PTR
指向剛剛拷貝到undo log
鏈中的舊版本記錄良蒸,這樣就能通過DB_ROLL_PTR
找到這條記錄的歷史版本。如果對同一行記錄執(zhí)行連續(xù)的UPDATE
伍玖,Undo Log
會(huì)組成一個(gè)鏈表嫩痰,遍歷這個(gè)鏈表可以看到這條記錄的變遷 - 記錄
redo log
,包括undo log
中的修改
那么 INSERT
和 DELETE
會(huì)怎么做呢窍箍?其實(shí)相比 UPDATE
這二者很簡單串纺,INSERT
會(huì)產(chǎn)生一條新紀(jì)錄,它的 DATA_TRX_ID
為當(dāng)前插入記錄的事務(wù) ID
椰棘;DELETE
某條記錄時(shí)可看成是一種特殊的 UPDATE
纺棺,其實(shí)是軟刪,真正執(zhí)行刪除操作會(huì)在 commit
時(shí)邪狞,DATA_TRX_ID
則記錄下刪除該記錄的事務(wù) ID
祷蝌。
如何實(shí)現(xiàn)一致性讀 —— ReadView
在 RU
隔離級別下,直接讀取版本的最新記錄就 OK帆卓,對于 SERIALIZABLE
隔離級別巨朦,則是通過加鎖互斥來訪問數(shù)據(jù)米丘,因此不需要 MVCC
的幫助。因此 MVCC
運(yùn)行在 RC
和 RR
這兩個(gè)隔離級別下糊啡,當(dāng) InnoDB
隔離級別設(shè)置為二者其一時(shí)拄查,在 SELECT
數(shù)據(jù)時(shí)就會(huì)用到版本鏈
核心問題是版本鏈中哪些版本對當(dāng)前事務(wù)可見?
InnoDB
為了解決這個(gè)問題悔橄,設(shè)計(jì)了 ReadView
(可讀視圖)的概念靶累。
RR 下的 ReadView 生成
在 RR
隔離級別下,每個(gè)事務(wù) touch first read
時(shí)(本質(zhì)上就是執(zhí)行第一個(gè) SELECT
語句時(shí)癣疟,后續(xù)所有的 SELECT
都是復(fù)用這個(gè) ReadView
,其它 update
, delete
, insert
語句和一致性讀 snapshot
的建立沒有關(guān)系)潮酒,會(huì)將當(dāng)前系統(tǒng)中的所有的活躍事務(wù)拷貝到一個(gè)列表生成ReadView
睛挚。
下圖中事務(wù) A
第一條 SELECT
語句在事務(wù) B
更新數(shù)據(jù)前,因此生成的 ReadView
在事務(wù) A
過程中不發(fā)生變化急黎,即使事務(wù) B
在事務(wù) A
之前提交扎狱,但是事務(wù) A
第二條查詢語句依舊無法讀到事務(wù) B
的修改。
下圖中勃教,事務(wù) A
的第一條 SELECT
語句在事務(wù) B
的修改提交之后淤击,因此可以讀到事務(wù) B
的修改。但是注意故源,如果事務(wù) A
的第一條 SELECT
語句查詢時(shí)污抬,事務(wù) B
還未提交,那么事務(wù) A
也查不到事務(wù) B
的修改绳军。
RC 下的 ReadView 生成
在 RC
隔離級別下印机,每個(gè) SELECT
語句開始時(shí),都會(huì)重新將當(dāng)前系統(tǒng)中的所有的活躍事務(wù)拷貝到一個(gè)列表生成 ReadView
门驾。二者的區(qū)別就在于生成 ReadView
的時(shí)間點(diǎn)不同射赛,一個(gè)是事務(wù)之后第一個(gè) SELECT
語句開始、一個(gè)是事務(wù)中每條 SELECT
語句開始奶是。
ReadView
中是當(dāng)前活躍的事務(wù) ID
列表楣责,稱之為 m_ids
,其中最小值為 up_limit_id
聂沙,最大值為 low_limit_id
秆麸,事務(wù) ID
是事務(wù)開啟時(shí) InnoDB
分配的,其大小決定了事務(wù)開啟的先后順序逐纬,因此我們可以通過 ID
的大小關(guān)系來決定版本記錄的可見性蛔屹,具體判斷流程如下:
如果被訪問版本的
trx_id
小于m_ids
中的最小值up_limit_id
,說明生成該版本的事務(wù)在ReadView
生成前就已經(jīng)提交了豁生,所以該版本可以被當(dāng)前事務(wù)訪問兔毒。如果被訪問版本的
trx_id
大于m_ids
列表中的最大值low_limit_id
漫贞,說明生成該版本的事務(wù)在生成ReadView
后才生成,所以該版本不可以被當(dāng)前事務(wù)訪問育叁。需要根據(jù)Undo Log
鏈找到前一個(gè)版本迅脐,然后根據(jù)該版本的 DB_TRX_ID 重新判斷可見性。如果被訪問版本的
trx_id
屬性值在m_ids
列表中最大值和最小值之間(包含)豪嗽,那就需要判斷一下trx_id
的值是不是在m_ids
列表中谴蔑。如果在,說明創(chuàng)建ReadView
時(shí)生成該版本所屬事務(wù)還是活躍的龟梦,因此該版本不可以被訪問隐锭,需要查找 Undo Log 鏈得到上一個(gè)版本,然后根據(jù)該版本的DB_TRX_ID
再從頭計(jì)算一次可見性计贰;如果不在钦睡,說明創(chuàng)建ReadView
時(shí)生成該版本的事務(wù)已經(jīng)被提交,該版本可以被訪問躁倒。此時(shí)經(jīng)過一系列判斷我們已經(jīng)得到了這條記錄相對
ReadView
來說的可見結(jié)果荞怒。此時(shí),如果這條記錄的delete_flag
為true
秧秉,說明這條記錄已被刪除褐桌,不返回。否則說明此記錄可以安全返回給客戶端象迎。
舉個(gè)例子
RC 下的 MVCC 判斷流程
我們現(xiàn)在回看剛剛的查詢過程荧嵌,為什么事務(wù) B
在 RC
隔離級別下,兩次查詢的 x
值不同挖帘。RC
下 ReadView
是在語句粒度上生成的完丽。
當(dāng)事務(wù) A
未提交時(shí),事務(wù) B
進(jìn)行查詢拇舀,假設(shè)事務(wù) B
的事務(wù) ID
為 300
逻族,此時(shí)生成 ReadView
的 m_ids
為 [200,300]骄崩,而最新版本的 trx_id
為 200
聘鳞,處于 m_ids
中,則該版本記錄不可被訪問要拂,查詢版本鏈得到上一條記錄的 trx_id 為 100
抠璃,小于 m_ids
的最小值 200
,因此可以被訪問脱惰,此時(shí)事務(wù) B
就查詢到值 10
而非 20
搏嗡。
待事務(wù) A
提交之后,事務(wù) B
進(jìn)行查詢,此時(shí)生成的 ReadView
的 m_ids
為 [300]采盒,而最新的版本記錄中 trx_id
為 200
旧乞,小于 m_ids
的最小值 300
,因此可以被訪問到磅氨,此時(shí)事務(wù) B
就查詢到 20
尺栖。
RR 下的 MVCC 判斷流程
如果在 RR
隔離級別下,為什么事務(wù) B
前后兩次均查詢到 10
呢烦租?RR
下生成 ReadView
是在事務(wù)開始時(shí)延赌,m_ids 為 [200,300],后面不發(fā)生變化叉橱,因此即使事務(wù) A
提交了挫以,trx_id
為 200
的記錄依舊處于 m_ids
中,不能被訪問窃祝,只能訪問版本鏈中的記錄 10
屡贺。
一個(gè)爭論點(diǎn)
其實(shí)并非所有的情況都能套用 MVCC
讀的判斷流程,特別是針對在事務(wù)進(jìn)行過程中锌杀,另一個(gè)事務(wù)已經(jīng)提交修改的情況下,這時(shí)不論是 RC
還是 RR
泻仙,直接套用 MVCC
判斷都會(huì)有問題糕再,例如 RC
下:
事務(wù) A
的 trx_id = 200
,事務(wù) B
的 trx_id = 300
玉转,且事務(wù) B
修改了數(shù)據(jù)之后在事務(wù) A
之前提交突想,此時(shí) RC
下事務(wù) A
讀到的數(shù)據(jù)為事務(wù) B
修改后的值,這是很顯然的究抓。下面我們套用下 MVCC
的判斷流程猾担,考慮到事務(wù) A
第二次 SELECT
時(shí),m_ids
應(yīng)該為 [200]刺下,此時(shí)該行數(shù)據(jù)最新的版本 DATA_TRX_ID = 300
比 200
大绑嘹,照理應(yīng)該不能被訪問,但實(shí)際上事務(wù) A
選取了這條記錄返回橘茉。
這里其實(shí)應(yīng)該結(jié)合 RC
的本質(zhì)來看工腋,RC
的本質(zhì)就是事務(wù)中每一條 SELECT
語句均可以看到其他已提交事務(wù)對數(shù)據(jù)的修改,那么只要該事物已經(jīng)提交其結(jié)果就是可見的畅卓,與這兩個(gè)事務(wù)開始的先后順序無關(guān)擅腰,不完全適用于 MVCC 讀。
RR
級別下還是用之前那張圖:
這張圖的流程中翁潘,事務(wù) B
的 trx_id = 300
比事務(wù) A
200
小趁冈,且事務(wù) B
先于事務(wù) A
提交,按照 MVCC
的判斷流程拜马,事務(wù) A
生成的 ReadView
為 [200]渗勘,最新版本的行記錄 DATA_TRX_ID = 300
比 200
大沐绒,照理不能訪問到,但是事務(wù) A
實(shí)際上讀到了事務(wù) B
已經(jīng)提交的修改呀邢。這里還是結(jié)合 RR
本質(zhì)進(jìn)行解釋洒沦,RR
的本質(zhì)是從第一個(gè) SELECT
語句生成 ReadView
開始,任何已經(jīng)提交過的事務(wù)的修改均可見价淌。
總結(jié)
RC
申眼、RR
兩種隔離級別的事務(wù)在執(zhí)行普通的讀操作時(shí),通過訪問版本鏈的方法蝉衣,使得事務(wù)間的讀寫操作得以并發(fā)執(zhí)行括尸,從而提升系統(tǒng)性能。RC
病毡、RR
這兩個(gè)隔離級別的一個(gè)很大不同就是生成 ReadView
的時(shí)間點(diǎn)不同濒翻,RC
在每一次 SELECT
語句前都會(huì)生成一個(gè) ReadView
,事務(wù)期間會(huì)更新啦膜,因此在其他事務(wù)提交前后所得到的 m_ids
列表可能發(fā)生變化有送,使得先前不可見的版本后續(xù)又突然可見了。而 RR
只在事務(wù)的第一個(gè) SELECT
語句時(shí)生成一個(gè) ReadView
僧家,事務(wù)操作期間不更新雀摘。