MVCC 機制的原理及實現
什么是 MVCC
MVCC(Multiversion Concurrency Control)翻譯過來是多版本并發控制,和數據庫鎖一樣,也是一種并發控制的解決方案。
在InnoDB中的實現主要是為了提高數據庫并發性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞并發讀,而這個讀指的就是快照讀,而非當前讀。當前讀實際上是一種加鎖的操作,是悲觀鎖的實現。而MVCC本質是采用樂觀鎖思想的一種方式。
快照讀
所謂快照讀,就是讀取的是快照數據,即快照生成的那一刻的數據,像我們常用的普通的SELECT語句在不加鎖情況下就是快照讀:
SELECT * FROM xx_table WHERE ...
注意:快照讀的前提是隔離級別不是串行級別,串行級別下的快照讀會退化成當前讀。
當前讀
當前讀讀取的是記錄的最新版本(最新數據,而不是歷史版本的數據),讀取時還要保證其他并發事務不能修改當前記錄,會對讀取的記錄進行加鎖。加鎖的SELECT,或者對數據進行增刪改都會進行當前讀:
SELECT * FROM xx_table LOCK IN SHARE MODE; #共享鎖
SELECT * FROM xx_table FOR UPDATE; #排他鎖
INSERT INTO xx_table values ... #排他鎖
DELETE FROM xx_table WHERE ... #排他鎖
UPDATE xx_table SET ... #排他鎖
解決什么問題
我們知道,在數據庫中,對數據的操作主要有2種,分別是讀和寫,而在并發場景下,就可能出現以下三種情況:
- 讀-讀并發:不存在任何問題,也不需要并發控制
- 讀-寫并發:有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀,幻讀,不可重復讀
- 寫-寫并發:有線程安全問題,可能會存在更新丟失問題
在沒有寫的情況下讀-讀并發是不會出現問題的,而寫-寫并發這種情況比較常用的就是通過加鎖的方式實現。那么,讀-寫并發則可以通過MVCC的機制解決。
實現原理
Undo Log
undo log是Mysql中比較重要的事務日志之一,是一種用于回退的日志,在事務沒提交之前,MySQL會先記錄更新前的數據到undo log日志文件里面,當事務回滾時或者數據庫崩潰時,可以利用undo log來進行回退。
- insert undo只在事務回滾時起作用,當事務提交后,該類型的undo日志就沒用了,它占用的Undo Log Segment也會被系統回收
- update或delete時產生的undo log,不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日志時,對應的日志才會被purge線程統一清除
一條記錄在同一時刻可能有多個事務在執行,那么undo log會有一條記錄的多個快照,那么在這一時刻發生SELECT要進行快照讀的時候,要讀哪個快照呢?
行記錄的隱式字段
其實,數據庫中的每行記錄中,除了保存了我們自己定義的一些字段以外,還有一些重要的隱式字段的:
- db_row_id:隱藏主鍵,如果我們沒有給這個表創建主鍵,那么會以這個字段來創建聚簇索引
- db_trx_id:對這條記錄做了最新一次修改的事務的ID
- db_roll_ptr:回滾指針,指向這條記錄的上一個版本,其實他指向的就是Undo Log中的上一個版本的快照的地址
注意:以上字段只有在聚簇索引的行記錄中才會有,而在普通二級索引中是沒有這些值的。
每一次記錄變更之前都會先存儲一份快照到undo log中,那么這幾個隱式字段也會跟著記錄一起保存在undo log中,就這樣,每一個快照中都有一個db_trx_id字段表示了對這個記錄做了最新一次修改的事務的ID ,以及一個db_roll_ptr字段指向了上一個快照的地址。(db_trx_id和db_roll_ptr是重點,后面還會用到)
這樣就形成了一個快照鏈表:
圖片
有了undo log,又有了幾個隱式字段,我們好像還是不知道具體應該讀取哪個快照,那怎么辦呢?
Read View
Read View 是InnoDB中一個至關重要的概念,是實現MVCC的基礎,同時也是支持不同的事務隔離級別的基礎,同時提高系統的并發能力和性能。
Read View主要來幫我們解決可見性的問題的, 即他會來告訴我們本次事務應該看到哪個快照,不應該看到哪個快照。
- 在可重復讀(Repeatable Read)級別下,快照(Read View)在事務開始后第一次查詢時創建一次,并在整個事務期間保持不變。
- 在讀已提交(Read Committed)級別下,快照(Read View)會在每次查詢時重新創建,以反映數據庫中的最新提交更改。
在Read View中有幾個重要的屬性:
- trx_ids,表示在生成Read View時當前系統中活躍的讀寫事務的事務id列表。
- low_limit_id,應該分配給下一個事務的id值。
- up_limit_id,未提交的事務中最小的事務ID。
- creator_trx_id,創建這個Read View的事務ID。
Read View遵循一個可見性算法,主要是將要被修改的數據的最新記錄中的DB_TRX_ID(即當前事務ID )取出來,與系統當前其他活躍事務的ID去對比(由Read View 維護),如果DB_TRX_ID跟Read View的屬性做了某些比較,不符合可見性,那就通過DB_ROLL_PTR回滾指針去取出Undo Log中的DB_TRX_ID再比較,即遍歷鏈表的DB_TRX_ID(從鏈首到鏈尾,即從最近的一次修改查起),直到找到滿足特定條件的DB_TRX_ID,那么這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新老版本。
案例
假如一個ReadView的內容為:
trx_ids = [5,6,8)
low_limit_id = 8
up_limit_id = 5
creator_trx_id = 7
假設當前事務要讀取某一個記錄行,該記錄行的db_trx_id(即最新修改該行的事務ID)為 trx_id,那么,就有以下幾種情況了:
1、trx_id<up_limit_id,即小于5的事務,說明這些事務在生成ReadView之前就已經提交了,那么該事務的結果就是可見的。
2、trx_id>=low_limit_id,即大于8的事務,說明該事務在生成ReadView后才生成,所以該事務的結果就是不可見的。
3、up_limit_id<trx_id<low_limit_id,即大于等于5,小于8,這種情況下會再拿事務ID和Read View中的trx_ids進行逐一比較。
如果,事務ID在trx_ids列表中,如6,那么表示在當前事務開啟時,這個事務還是活躍的,那么這個記錄對于當前事務來說應該是不可見的。
如果,事務id不在trx_ids列表中,如7,那么表示的是在當前事務開啟之前,其他事務對數據進行修改并提交了,所以,這條記錄對當前事務就應該是可見的。
當然這里有個例外情況,那就是這個trx_id=creator_trx_id,那么就肯定是可見的
總結一下就是,一個事務能看到的是在他開始之前就已經提交的事務的結果,而未提交的結果都是不可見的。
當數據的事務ID不符合Read View規則時候,那就需要從undo log里面獲取數據的歷史快照,然后數據快照的事務ID再來和Read View進行可見性比較,如果找到一條快照,則返回,找不到則返回空。
總結
圖片
在InnoDB中MVCC就是通過Read View + Undo Log來實現的,undo log中保存了歷史快照,而Read View用來判斷具體哪一個快照是可見的。