MySQL InnoDB MVCC 機(jī)制的原理及實(shí)現(xiàn)

什么是 MVCC

MVCC (Multiversion Concurrency Control) 中文全程叫多版本并發(fā)控制筹误,是現(xiàn)代數(shù)據(jù)庫(包括 MySQL桐早、OraclePostgreSQL 等)引擎實(shí)現(xiàn)中常用的處理讀寫沖突的手段厨剪,目的在于提高數(shù)據(jù)庫高并發(fā)場景下的吞吐性能哄酝。

如此一來不同的事務(wù)在并發(fā)過程中,SELECT 操作可以不加鎖而是通過 MVCC 機(jī)制讀取指定的版本歷史記錄丽惶,并通過一些手段保證保證讀取的記錄值符合事務(wù)所處的隔離級別炫七,從而解決并發(fā)場景下的讀寫沖突。

下面舉一個(gè)多版本讀的例子钾唬,例如兩個(gè)事務(wù) AB 按照如下順序進(jìn)行更新和讀取操作

image

在事務(wù) A 提交前后万哪,事務(wù) B 讀取到的 x 的值是什么呢?答案是:事務(wù) B 在不同的隔離級別下抡秆,讀取到的值不一樣奕巍。

  1. 如果事務(wù) B 的隔離級別是讀未提交(RU),那么兩次讀取均讀取到 x 的最新值儒士,即 20的止。
  2. 如果事務(wù) B 的隔離級別是讀已提交(RC),那么第一次讀取到舊值 10着撩,第二次因?yàn)槭聞?wù) A 已經(jīng)提交诅福,則讀取到新值 20。
  3. 如果事務(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ì)帶來一些問題,主要包括以下幾種情況:

  1. 更新丟失(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ù)悬赏。

  2. 臟讀(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)象被形象地叫做 “臟讀”

  3. 不可重復(fù)讀(Non-Repeatable Reads):一個(gè)事務(wù)在讀取某些數(shù)據(jù)已經(jīng)發(fā)生了改變盆顾、或某些記錄已經(jīng)被刪除了怠褐!這種現(xiàn)象叫做“不可重復(fù)讀”。

  4. 幻讀(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ī)制的方法主要有兩種:

  1. 加讀寫鎖

  2. 一致性快照讀揖铜,即 MVCC

但本質(zhì)上茴丰,隔離級別是一種在并發(fā)性能和并發(fā)產(chǎn)生的副作用間的妥協(xié),通常數(shù)據(jù)庫均傾向于采用 Weak Isolation天吓。

InnoDB 中的 MVCC

本文聚焦于 MySQL 中的 MVCC 實(shí)現(xiàn)贿肩,從 《高性能 MySQL》一書中對 MVCC 的介紹可知:

  1. MySQLInnoDB 引擎支持 MVCC
  2. 應(yīng)對高并發(fā)事務(wù), MVCC 比單純的加行鎖更有效, 開銷更小
  3. MVCC 在讀已提交(Read Committed)和可重復(fù)讀(Repeatable Read)隔離級別下起作用
  4. MVCC 既可以基于樂觀鎖又可以基于悲觀鎖來實(shí)現(xiàn)

InnoDB MVCC 實(shí)現(xiàn)原理

InnoDBMVCC 的實(shí)現(xiàn)方式為:每一行記錄都有兩個(gè)隱藏列:DATA_TRX_IDDATA_ROLL_PTR(如果沒有主鍵龄寞,則還會(huì)多一個(gè)隱藏的主鍵列)汰规。

image

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è)專門的 bitdeleted_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_IDDATA_ROLL_PTR 兩個(gè)參數(shù)在其中又起到什么樣的作用。

還是以上文 MVCC 的例子,事務(wù) A 對值 x 進(jìn)行更新之后,該行即產(chǎn)生一個(gè)新版本和舊版本。假設(shè)之前插入該行的事務(wù) ID100鉴吹,事務(wù) AID200,該行的隱藏主鍵為 1惩琉。

image

事務(wù) A 的操作過程為:

  1. DB_ROW_ID = 1 的這行記錄加排他鎖
  2. 把該行原本的值拷貝到 undo log 中豆励,DB_TRX_IDDB_ROLL_PTR 都不動(dòng)
  3. 修改該行的值這時(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è)鏈表可以看到這條記錄的變遷
  4. 記錄 redo log,包括 undo log 中的修改

那么 INSERTDELETE 會(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)行在 RCRR 這兩個(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 的修改。

image

下圖中勃教,事務(wù) A 的第一條 SELECT 語句在事務(wù) B 的修改提交之后淤击,因此可以讀到事務(wù) B 的修改。但是注意故源,如果事務(wù) A 的第一條 SELECT 語句查詢時(shí)污抬,事務(wù) B 還未提交,那么事務(wù) A 也查不到事務(wù) B 的修改绳军。

image

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)系來決定版本記錄的可見性蛔屹,具體判斷流程如下:

  1. 如果被訪問版本的 trx_id 小于 m_ids 中的最小值 up_limit_id,說明生成該版本的事務(wù)在 ReadView 生成前就已經(jīng)提交了豁生,所以該版本可以被當(dāng)前事務(wù)訪問兔毒。

  2. 如果被訪問版本的 trx_id 大于 m_ids 列表中的最大值 low_limit_id漫贞,說明生成該版本的事務(wù)在生成 ReadView 后才生成,所以該版本不可以被當(dāng)前事務(wù)訪問育叁。需要根據(jù) Undo Log 鏈找到前一個(gè)版本迅脐,然后根據(jù)該版本的 DB_TRX_ID 重新判斷可見性。

  3. 如果被訪問版本的 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)被提交,該版本可以被訪問躁倒。

  4. 此時(shí)經(jīng)過一系列判斷我們已經(jīng)得到了這條記錄相對 ReadView 來說的可見結(jié)果荞怒。此時(shí),如果這條記錄的 delete_flagtrue秧秉,說明這條記錄已被刪除褐桌,不返回。否則說明此記錄可以安全返回給客戶端象迎。

image

舉個(gè)例子

RC 下的 MVCC 判斷流程

我們現(xiàn)在回看剛剛的查詢過程荧嵌,為什么事務(wù) BRC 隔離級別下,兩次查詢的 x 值不同挖帘。RCReadView 是在語句粒度上生成的完丽。

當(dāng)事務(wù) A 未提交時(shí),事務(wù) B 進(jìn)行查詢拇舀,假設(shè)事務(wù) B 的事務(wù) ID300逻族,此時(shí)生成 ReadViewm_ids 為 [200,300]骄崩,而最新版本的 trx_id200聘鳞,處于 m_ids 中,則該版本記錄不可被訪問要拂,查詢版本鏈得到上一條記錄的 trx_id 為 100抠璃,小于 m_ids 的最小值 200,因此可以被訪問脱惰,此時(shí)事務(wù) B 就查詢到值 10 而非 20搏嗡。

待事務(wù) A 提交之后,事務(wù) B 進(jìn)行查詢,此時(shí)生成的 ReadViewm_ids 為 [300]采盒,而最新的版本記錄中 trx_id200旧乞,小于 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_id200 的記錄依舊處于 m_ids 中,不能被訪問窃祝,只能訪問版本鏈中的記錄 10屡贺。

一個(gè)爭論點(diǎn)

其實(shí)并非所有的情況都能套用 MVCC 讀的判斷流程,特別是針對在事務(wù)進(jìn)行過程中锌杀,另一個(gè)事務(wù)已經(jīng)提交修改的情況下,這時(shí)不論是 RC 還是 RR泻仙,直接套用 MVCC 判斷都會(huì)有問題糕再,例如 RC 下:

image

事務(wù) Atrx_id = 200,事務(wù) Btrx_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 = 300200 大绑嘹,照理應(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 級別下還是用之前那張圖:

image

這張圖的流程中翁潘,事務(wù) Btrx_id = 300 比事務(wù) A 200 小趁冈,且事務(wù) B 先于事務(wù) A 提交,按照 MVCC 的判斷流程拜马,事務(wù) A 生成的 ReadView 為 [200]渗勘,最新版本的行記錄 DATA_TRX_ID = 300200 大沐绒,照理不能訪問到,但是事務(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ù)操作期間不更新雀摘。

原文:https://zhuanlan.zhihu.com/p/64576887

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市八拱,隨后出現(xiàn)的幾起案子阵赠,更是在濱河造成了極大的恐慌,老刑警劉巖肌稻,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件清蚀,死亡現(xiàn)場離奇詭異,居然都是意外死亡爹谭,警方通過查閱死者的電腦和手機(jī)枷邪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旦棉,“玉大人齿风,你說我怎么就攤上這事“舐澹” “怎么了救斑?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長真屯。 經(jīng)常有香客問我脸候,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任运沦,我火速辦了婚禮泵额,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘携添。我一直安慰自己嫁盲,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布烈掠。 她就那樣靜靜地躺著羞秤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪左敌。 梳的紋絲不亂的頭發(fā)上瘾蛋,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音矫限,去河邊找鬼哺哼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛叼风,可吹牛的內(nèi)容都是我干的取董。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼无宿,長吁一口氣:“原來是場噩夢啊……” “哼甲葬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起懈贺,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎坡垫,沒想到半個(gè)月后梭灿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冰悠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年堡妒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溉卓。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡皮迟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出桑寨,到底是詐尸還是另有隱情伏尼,我是刑警寧澤,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布尉尾,位于F島的核電站爆阶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜辨图,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一班套、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧故河,春花似錦吱韭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至鸳吸,卻和暖如春熏挎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晌砾。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工坎拐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人养匈。 一個(gè)月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓哼勇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親呕乎。 傳聞我的和親對象是個(gè)殘疾皇子积担,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353