More  

小編的世界 優質文選 資料

面試必備,MySQL InnoDB MVCC機制


2020年9月08日 - 資料小編 Java編程技術樂園 
   

MySQL InnoDB MVCC機制吐血總結

談到MySQL事務,必然離不開InnoDB和MVCC機制,同時,MVCC也是數據庫面試中的殺手問題,寫這篇總結的目的,就是為了讓自己加深映像,這樣面試就不會忘記了。在搜索時發現關於MVCC的文章真的是參差不齊(老子真的是零零散散看了三個月都迷迷糊糊),所以這裏集合了各家所言之後進行了自我總結,苦苦研究了許久,才得到的比較清晰的認知,這可能也是我目前最有深度的一篇博客了把,希望對我和看到的人都有所幫助,哈哈。

MVCC: Multiversion Concurrency Control,翻譯為多版本並發控制,其目標就是為了提高數據庫在高並發場景下的性能。

MVCC最大的優勢:讀不加鎖,讀寫不沖突。在讀多寫少的場景下極大的增加了系統的並發性能

在講解MVCC之前我們需要先了解MySQL的基本架構,如下圖所示:

圖一MySQL事務

MySQL的事務是在存儲引擎層實現的,在MySQL中,我們最常用的就是InnoDB和MyISAM,我們都知道,MYISAM並不支持事務,所以InnoDB實現了MVCC的事務並發處理機制,也是我們這篇文章的主要研究內容。

可能我們都看到過,MVCC只在RC和RR下,為了分析這個問題,我們先回顧一下SQL標准事務隔離級別隔離性

read uncommitted 讀未提交: 一個事務還沒提交時,它做的變更就能被別的事務看到。

read committed 讀提交:一個事務提交之後,它做的變更才會被其他事務看到。

repeatable read 可重複讀:一個事務執行過程中看到的數據,總是跟這個事務在啟動時看到的數據是一致的。在可重複讀隔離級別下,未提交變更對其他事務也是不可見的。

serializable 串行化 :對於同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。

我們通過兩個事務提交流程來說明事務隔離級別的具體效果:

我們假設有一個表,僅有一個字段field:DROP TABLE IF EXISTS `mvcc_test`;CREATE TABLE `mvcc_test`( `field` INT)ENGINE=InnoDB;INSERT INTO `mvcc_test` VALUES(1); -- 插入一條數據

如下的操作流程:

圖二

根據事務隔離級別的定義,我們可以來推測,事務A提交前後,事務B的兩次讀取3和4分別讀取的值:

若事務B的隔離級別為 read uncommitted,事務B的兩次讀取都讀取到了20,即修改後的值

若事務B的隔離級別是read committed,那麼,事務B的操作3讀取到的值為1,而4讀取到的值為20,因為4時事務A已經完成了提交

若事務B的隔離級別是repeatable read或serializable,那麼操作3和4讀取的值都是1。

MVCC的必要性

MySQL中MYISAM並不支持事務,同樣的, MVCC也就和他沒有半毛錢關系了,InnoDB相比與MYISAM的提升就是對於行級鎖的支持和對事務的支持,而應對高並發事務, MVCC 比單純的加行鎖更有效, 開銷更小。

但是單純的並發也會帶來十分嚴重的問題:

Lost Update更新丟失: 多個事務對同一行數據進行讀取初值更新時,由於每個事務對其他事務都未感知,會造成最後的更新覆蓋了其他事務所做的更新。

dirty read髒讀: 事務一個正在對一條記錄進行修改,在完成並提交前事務二也來讀取該條記錄,事務二讀取了事務一修改但未提交的數據,如果事務一回滾,那麼事務二讀取到的數據就成了“髒”數據。

non-repeatable read不可重複讀: 個事務在讀取某些數據後的某個時間再次讀取之前讀取過的數據,發現讀出的數據已經發生了改變或者刪除,這種現象稱為“不可重複讀”

phantom read幻讀: 個事務按相同的查詢條件重新讀取以前檢索過的數據,發現其他事務插入了滿足查詢條件的新數據,這種現象稱為“幻讀”

不可重複讀與幻讀的現象是比較接近的,也有人直接就說幻讀就是不可重複讀,我比較傾向與他兩就是他兩個: 不可重複讀針對的是值的不同,幻讀指的是數據條數的不同。同樣的對於幻讀,單純的MVCC機制並不能解決幻讀問題,InnoDB也是通過加間隙鎖來防止幻讀。

從本質上來說,事務隔離級別就是系統並發能力和數據安全性間的妥協,我們在剛開始學習數據庫時就在說: 隔離性越高,數據庫的性能就越差,就是這個結果,只是我們當時只知其然罷了。

解決並發帶來的問題,最通常的就是加鎖,但鎖對於性能也是腰斬性的,所以MVCC就顯得十分重要了。

抄大佬的一句話: 在不同的隔離級別下,數據庫通過 MVCC 和隔離級別,讓事務之間並行操作遵循了某種規則,來保證單個事務內前後數據的一致性。InnoDB 下的 MVCC 實現原理

在InnoDB中MVCC的實現通過兩個重要的字段進行連接:DB_TRX_ID和DB_ROLL_PT,在多個事務並行操作某行數據的情況下,不同事務對該行數據的UPDATE會產生多個版本,數據庫通過DB_TRX_ID來標記版本,然後用DB_ROLL_PT回滾指針將這些版本以先後順序連接成一條 Undo Log 鏈。

對於一個沒有指定PRIMARY KEY的表,每一條記錄的組織大致如下:

DB_TRX_ID: 事務id,6byte,每處理一個事務,值自動加一。InnoDB中每個事務有一個唯一的事務ID叫做 transaction id。在事務開始時向InnoDB事務系統申請得到,是按申請順序嚴格遞增的每行數據是有多個版本的,每次事務更新數據時都會生成一個新的數據版本,並且把transaction id賦值給這個數據行的DB_TRX_ID

DB_ROLL_PT: 回滾指針,7byte,指向當前記錄的ROLLBACK SEGMENT 的undolog記錄,通過這個指針獲得之前版本的數據。該行記錄上所有舊版本在 undolog 中都通過鏈表的形式組織。

還有一個DB_ROW_ID(隱含id,6byte,由innodb自動產生),我們可能聽說過InnoDB下聚簇索引B+Tree的構造規則:如果聲明了主鍵,InnoDB以用戶指定的主鍵構建B+Tree,如果未聲明主鍵,InnoDB 會自動生成一個隱藏主鍵,說的就是DB_ROW_ID。另外,每條記錄的頭信息(record header)裏都有一個專門的bit(deleted_flag)來表示當前記錄是否已經被刪除

我們通過圖二的UPDATE(即操作2)來舉例Undo log鏈的構建(假設第一行數據DB_ROW_ID=1):

事務A對DB_ROW_ID=1這一行加排它鎖

將修改行原本的值拷貝到Undo log中

修改目標值,產生一個新版本,將DB_TRX_ID設為當前事務ID即100,將DB_ROLL_PT指向拷貝到Undo log中的舊版本記錄

記錄redo log, binlog

最終生成的Undo log鏈如下圖所示:

相比與UPDATE,INSERT和DELETE都比較簡單:

INSERT: 產生一條新的記錄,該記錄的DB_TRX_ID為當前事務ID

DELETE: 特殊的UPDATE,在DB_TRX_ID上記錄下當前事務的ID,同時將delete_flag設為true,在執行commit時才進行刪除操作

MVCC的規則大概就是以上所述,那麼它是如何實現高並發下RC和RR的隔離性呢,這就是在MVCC機制下基於生成的Undo log鏈和一致性視圖ReadView來實現的。一致性視圖的生成 ReadView

要實現read committed在另一個事務提交之後其他事務可見和repeatable read在一個事務中SELECT操作一致,就是依靠ReadView,對於read uncommitted,直接讀取最新值即可,而serializable采用加鎖的策略通過犧牲並發能力而保證數據安全,因此只有RC和RR這兩個級別需要在MVCC機制下通過ReadView來實現。

在read committed級別下,readview會在事務中的每一個SELECT語句查詢發送前生成
(也可以在聲明事務時顯式聲明START TRANSACTION WITH CONSISTENT SNAPSHOT),因此每次SELECT都可以獲取到當前已提交事務和自己修改的最新版本。而在repeatable read級別下,每個事務只會在第一個SELECT語句查詢發送前或顯式聲明處生成,其他查詢操作都會基於這個ReadView,這樣就保證了一個事務中的多次查詢結果都是相同的,因為他們都是基於同一個ReadView下進行MVCC機制的查詢操作。

InnoDB為每一個事務構造了一個數組m_ids用於保存一致性視圖生成瞬間當前所有活躍事務(開始但未提交事務)的ID,將數組中事務ID最小值記為低水位m_up_limit_id,當前系統中已創建事務ID最大值+1記為高水位m_low_limit_id,構成如圖所示:

一致性視圖下查詢操作的流程如下:

當查詢發生時根據以上條件生成ReadView,該查詢操作遍曆Undo log鏈,根據當前被訪問版本(可以理解為Undo log鏈中每一個記錄即一個版本,遍曆都是從最新版本向老版本遍曆)的DB_TRX_ID,如果DB_TRX_ID小於m_up_limit_id,則該版本在ReadView生成前就已經完成提交,該版本可以被當前事務訪問。DB_TRX_ID在綠色範圍內的可以被訪問

若被訪問版本的DB_TRX_ID大於m_up_limit_id,說明該版本在ReadView生成之後才生成,因此該版本不能被訪問,根據當前版本指向上一版本的指針DB_ROLL_PT訪問上一個版本,繼續判斷。DB_TRX_ID在藍色範圍內的都不允許被訪問

若被訪問版本的DB_TRX_ID在只有DB_TRX_ID等於當前事務ID才允許訪問橙色範圍內的版本

最後,還要確保滿足以上要求的可訪問版本的數據的delete_flag不為true,否則查詢到的就會是刪除的數據。

所以以上總結就是只有當前事務修改的未commit版本和所有已提交事務版本允許被訪問
。我想現在看文章的你應該是明白了(主要是說我自己)。一致性讀和當前讀

前面說的都是查詢相關,那麼涉及到多個事務的查詢同時還有更新操作時,MVCC機制如何保證在實現事務隔離級別的同時進行正確的數據更新操作,保證事務的正確性呢,我們可以看一個案例:DROP TABLE IF EXISTS `mvccs`;CREATE TABLE `mvccs`( `field` INT)ENGINE=InnoDB;INSERT INTO `mvccs` VALUES(1); -- 插入一條數據

假設在所有事務開始前當前有一個活躍事務10,且這三個事務期間沒有其他並發事務:

在操作1開始SELECT語句時,需要創建一致性視圖,此時當前事務的一致性視圖為<10, 100, 200,301), 事務100開始查詢Undo log鏈,第一個查詢到的版本為為事務200的操作4的更新操作, DB_TRX_ID在m_ids數組但並不等於當前事務ID, 不可被訪問;

向上查詢下一個即事務300在操作6時生成的版本,小於高水位m_up_limit_id,且不在m_ids中,處於已提交狀態,因此可被訪問;

綜上在RR和RC下得到操作1查詢的結果都是2

那麼操作5查詢到的field的值是多少呢?

在RR下,我們可以明確操作2和操作3查詢field的值都是1,在RC下操作2為1,操作3的值為2,那麼操作5的值呢?

答案在RR和RC下都是是3,我一開始以為RR下是2,因為這裏如果按照一致性讀的規則,事務300在操作2時都未提交,對於事務200來說應該時不可見狀態,你看我說的是不是好像很有道理的樣子?

上面的問題在於UPDATE操作都是讀取當前讀(current read)
數據進行更新的,而不是一致性視圖ReadView,因為如果讀取的是ReadView,那麼事務300的操作會丟失。當前讀會讀取記錄中的最新數據,從而解決以上情形下的並發更新丟失問題。參考資料

《高性能 MySQL》

MySQL InnoDB MVCC 機制的原理及實現

MySQL實戰45講尾巴

說是為了應付面試,可是簡曆都已經石沉大海,回首整個春招,真的只有三次可憐的面試機會,面試我的連MySQL事務都不問的,emm現在春招已過,已經來臨的秋招已經完美忽略了我這個2020的渣渣畢業生,雖然少,也有收獲與感動,前路坎坷,仍要欣然前往。可能自己是真的比較笨了,一個MVCC前前後後斷斷續續搞了3個月才差不多搞懂,這一年各方面都實在太難,但一直告訴自己,不能一直在表面停留,滿足與CURD,必須有所深入,雖則如雲,匪我思存。加油!

  大家在看