InnoDB和MVCC機制,瞭解MySQL的基本架構
談到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鏈如下圖所示:
undo_log_chain.png
相比與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
在[m_up_limit_id, m_low_limit_id)區間內,則判斷DB_TRX_ID
是否等於當前事務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); -- 插入一條資料
mvcc-transaction_consts_view.png
假設在所有事務開始前當前有一個活躍事務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,必須有所深入,雖則如雲,匪我思存。加油!