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