innodb下的mvcc_从InnoDB了解MVCC
原標(biāo)題:從InnoDB了解MVCC
MVCC全稱是Multi-Version Concurrency Control,即多版本并發(fā)控制。
這是種很常用的技術(shù),現(xiàn)在幾乎所有的關(guān)系數(shù)據(jù)庫都支持它。平時它默默工作,像個透明人,似乎不用關(guān)心它的細節(jié)。但是當(dāng)我們偶爾在數(shù)據(jù)庫里面遇到一些奇怪問題時,卻不得不需要關(guān)注它。因為很可能這些“奇怪”的問題,不過是MVCC里的正常行為;而且,MVCC的設(shè)計思路還能在我們?nèi)粘5拈_發(fā)中起到一些借鑒作用。所以,對于大部分開發(fā)者而言,了解MVCC還是挺有意義的。
說起來,MVCC是怎么產(chǎn)生的呢?其實看名字就能猜到啦,和并發(fā)有關(guān)。
這東西的原理挺簡單,我們自己也能設(shè)計哦:
首先我們回顧一下,以前教科書里的數(shù)據(jù)庫系統(tǒng),它的并發(fā)是怎么實現(xiàn)的呢?
如圖,當(dāng)一個事務(wù)要讀loan_id=1001的這行行數(shù)據(jù)時,會對其加讀鎖(S);而另一個事務(wù)要修改這行數(shù)據(jù)時,會要求對其加寫鎖(X)。
這樣一來,并發(fā)讀是可以的,但是讀和寫互相阻塞,性能較低。
然后,有人想到了多版本的方式來提高性能,靈感就是CopyOnWrite:
如圖,修改數(shù)據(jù)時,寫事務(wù)會插入并鎖定一條新的數(shù)據(jù)(sn=2),并不會影響舊數(shù)據(jù)(sn=1)。
所以,讀事務(wù)不需要加鎖,當(dāng)寫事務(wù)沒有提交時,可以繼續(xù)讀版本1的數(shù)據(jù);而寫事務(wù)提交并釋放鎖以后,讀事務(wù)就可以讀版本2的數(shù)據(jù)了。
這里讀和寫的事務(wù)不再相互阻塞,而且寫不需要加鎖,并發(fā)性能得到了提高。
接下來再處理下一致性的問題,于是表可以變成這樣:
從上圖可以看出,對讀事務(wù)而言,只需要讀每一條loan_id的最大有效數(shù)據(jù),也就是sn<=2的數(shù)據(jù);而寫事務(wù),會創(chuàng)建一個新版本3,并在提交修改時,將新版本的status從pending修改為success使其生效。
而在讀事務(wù)的執(zhí)行過程中,如果寫事務(wù)完成了版本3的提交,讀事務(wù)能否讀到版本3的數(shù)據(jù)呢?答案當(dāng)然是不能。讀事務(wù)應(yīng)該在最開始獲得有效的最大sn,也就是版本2。之后即使寫事務(wù)提交了版本3,讀事務(wù)仍會以版本2作為最大有效版本。這樣可以保證讀數(shù)據(jù)的一致性。也就是說,即使讀到的是較老版本的數(shù)據(jù),也比讀到一半老版本一半新版本的數(shù)據(jù)好。因為失去一致性的數(shù)據(jù)其實是錯誤的。
到此為止,一個簡單的MVCC就倒騰出來了。是不是很簡單呢?這個設(shè)計簡單但實用,其實在許多數(shù)據(jù)倉庫里還這么用著呢。
不過這個設(shè)計確實太簡單,還有一些重要問題需要解決:
1. 無效的老版本數(shù)據(jù)怎么處理?對數(shù)據(jù)倉庫,可以一直保留;但對普通應(yīng)用,通常不合適。
2. 并發(fā)修改怎么處理?對數(shù)據(jù)倉庫,修改的事務(wù)只需要一個,就是ETL job;但對普通應(yīng)用,顯然不夠啊。
好啦,接下來,我們還是去看看別人家孩子吧。畢竟別人家孩子早就會打醬油了,咱們還是別光自己折騰啦。
首先,我們來看一下Mysql InnoDB的MVCC實現(xiàn)。
關(guān)于InnoDB,在《高性能MySql》里面有段簡化描述:
它通過兩個隱藏列實現(xiàn)。兩個列一個保存了行的創(chuàng)建時間,另一個保存了過期時間(實際保存的是系統(tǒng)版本號,不過可以等價看做時間)。
每開始一個新的事務(wù),這個版本號就會增加,并且將當(dāng)前版本號作為事務(wù)版本號。
在InnoDB默認repeatable-read隔離級別下,它的工作方式是:
查詢數(shù)據(jù):它會檢索創(chuàng)建時間在事務(wù)版本號之前的數(shù)據(jù),也就是select * from table where create_version<=${version},所以新事務(wù)創(chuàng)建的數(shù)據(jù)是不可見的;同時會檢查數(shù)據(jù)的刪除時間,保證新的刪除操作不可見。最終相當(dāng)于select * from table where create_version<=${version} and (delete_version is null or delete_version>${version})
刪除數(shù)據(jù):把當(dāng)前數(shù)據(jù)的刪除時間設(shè)置為當(dāng)前事務(wù)版本號。
插入數(shù)據(jù):創(chuàng)建一條新的數(shù)據(jù),創(chuàng)建時間就是當(dāng)前事務(wù)版本號。
更新數(shù)據(jù):刪除和插入的綜合,刪除原數(shù)據(jù)并且插入一條新數(shù)據(jù)。
這是一個簡單有效的MVCC模型……不過等一下,這看起來和我們的設(shè)計不是差不多嗎?我書讀得少不要騙我,這好像也沒有解決之前我們提出的問題啊?
但其實呢,這段描述只是為了方便讀者理解,InnoDB實際的實現(xiàn)……不是這樣的。這個,書讀得多也會被騙的……
所以呢,接下來我們只好稍微深入下細節(jié)了,看看InnoDB其實是怎么實現(xiàn)并解決我們提出的問題的。
首先,它的隱藏列實際不是創(chuàng)建時間和刪除時間,而是當(dāng)前事務(wù)id列和刪除標(biāo)志位。
這兩個列更像我們之前自己的設(shè)計了吧?它們也能提供在簡化模型里提到的那些功能。而且事務(wù)id還有更多的作用,這個后面會提到。
現(xiàn)在再看看我們先前提出的第一個問題,失效的數(shù)據(jù)怎么辦?是不是可以定期去各表里掃描回收呢?
是的,這是個很好的辦法,它確實要掃描回收。不過呢,它稍微聰明一些。它并不到各個表里掃描,而是去undo log里掃描。而在這之前,它已經(jīng)將老版本的數(shù)據(jù)移動到了undo log。
這里可能需要為了解數(shù)據(jù)庫較少的同學(xué)補充一點數(shù)據(jù)庫undo log和redo log的知識:
undo log:用于事務(wù)回滾,里面放著修改前的數(shù)據(jù),需要回滾事務(wù)時可以根據(jù)它把修改前的數(shù)據(jù)替換回去;
redo log:重做日志,用于恢復(fù),可以認為它是數(shù)據(jù)的保護者。數(shù)據(jù)修改時,通常不會馬上寫入磁盤,而會記錄到redo log并只修改內(nèi)存里緩存的數(shù)據(jù)。當(dāng)修改操作被寫入redo log后,就可以認為修改不會丟失了。而對數(shù)據(jù)的磁盤持久化,可以放到后面更合適的時候。這主要是性能考慮:磁盤數(shù)據(jù)的修改,容易導(dǎo)致隨機寫入,遠比redo log的順序?qū)懭肼?。這里需要提醒下:undo log同樣需要redo log的保護,對undo的修改同樣會記錄redo log。在需要恢復(fù)系統(tǒng)的時候,會根據(jù)redo log恢復(fù)到當(dāng)前狀態(tài)(此時undo log的恢復(fù)同樣依賴redo log),再根據(jù)undo log回滾沒有提交的事務(wù)。
好,回到我們關(guān)于回收失效數(shù)據(jù)的話題吧。
所以呢,其實舊版本的數(shù)據(jù),是保存在undo log里面的。當(dāng)一條數(shù)據(jù)被修改的時候,舊版本的數(shù)據(jù)會被移動到undo log,而新的修改會在原位置進行。而每條數(shù)據(jù)還有一個隱藏列,稱為回滾指針,會指向被移動到undo log里的這條舊數(shù)據(jù)。當(dāng)我們需要讀取這條舊數(shù)據(jù)的時候,就需要先找到現(xiàn)在最新版的數(shù)據(jù),然后根據(jù)這條數(shù)據(jù)的回滾指針,去查找undo log里的舊數(shù)據(jù)。如果這條舊數(shù)據(jù)的版本還是太新,不是我們想要的怎么辦呢?它也有指向更舊數(shù)據(jù)的回滾指針,而更舊的數(shù)據(jù)也在之前就被移動到了undo log里,我們繼續(xù)回溯就好了。
而失效數(shù)據(jù)的回收,是通過Purge后臺進程實現(xiàn)的。Purge進程定期掃描undo log,按照從舊到新的順序,檢查每條記錄是否應(yīng)被清理回收。在掃描前,它會先取得當(dāng)前活動事務(wù)列表,借此判斷掃描到的記錄是不是失效數(shù)據(jù),并清理回收。
這里再提出一個小問題,我們什么情況下會需要讀取undo log里面的舊數(shù)據(jù)呢?
其實常見讀取數(shù)據(jù)有兩種方式:
一致讀:也叫快照讀。當(dāng)我們根據(jù)where條件查找數(shù)據(jù)時,或者單純的select數(shù)據(jù)時,在MVCC里采用的是一致讀。此時是根據(jù)我們事務(wù)啟動時的時間點,讀取該時間點的一個數(shù)據(jù)快照(snapshot)。如果新修改發(fā)生在我們的讀事務(wù)啟動之后,或者新修改所屬的事務(wù)還沒有提交,這些新修改對我們都應(yīng)該是不可見的。所以我們不能讀最新的數(shù)據(jù),而需要根據(jù)回滾指針讀取舊數(shù)據(jù)。
當(dāng)前讀:在真正需要修改數(shù)據(jù)時,肯定不能按照快照獲取的數(shù)據(jù)進行修改,否則會丟失修改。這時就需要讀取該數(shù)據(jù)現(xiàn)在的最新值,并且在最新值基礎(chǔ)上進行修改,這就是當(dāng)前讀。如果此時最新值所屬的事務(wù)還沒有提交,就必須等待其回滾或提交。
我們之所以需要讀取undo log里面的舊數(shù)據(jù),就是因為很多時候我們都在使用一致讀。一致讀不需要對數(shù)據(jù)加鎖,有很好的性能。
接下來我們再看看先前提出的第二個問題,在MVCC下的并發(fā)修改怎么實現(xiàn)呢?
還是靠加鎖。
1. 對該數(shù)據(jù)加寫鎖(X)
2. 記錄redo log
3. 復(fù)制修改前的舊值到undo log
4. 在原來數(shù)據(jù)的位置修改數(shù)據(jù),并修改回滾指針指向undo log中的舊值。
如果是修改主鍵的話,因為InnoDB在主鍵上有聚簇索引 ,修改步驟會有點差別。但修改主鍵本身不是好的設(shè)計,所以我們不討論。
這里又有個小問題,一致性讀時InnoDB的事務(wù)隔離是怎么實現(xiàn)的呢?
對沒有提交的事務(wù),雖然它的事務(wù)id比我們的事務(wù)id更小,它的修改仍然應(yīng)該對我們不可見。
這時,就需要前面提到的,每條數(shù)據(jù)上的隱藏列——事務(wù)id列的幫助了。
當(dāng)我們的事務(wù)開啟時,會取得當(dāng)前活動事務(wù)列表。根據(jù)這份列表,就可以排除沒有提交的修改數(shù)據(jù)。這時,通過比對數(shù)據(jù)上記錄的事務(wù)id和活動事務(wù)列表,就能判斷該數(shù)據(jù)是否可見:
1. 如果是當(dāng)前事務(wù)自己的修改,可見;
2. 如果大于當(dāng)前事務(wù)id,不可見(repeatable-read);
3. 如果小于最小活動事務(wù)id,可見;
4. 其他情況,需要和活動事務(wù)列表做詳細比對。
好啦,我們現(xiàn)在總算解決了我們前面提到的幾個問題,新的方案可以投入使用了(撒花)??雌饋韯e人家孩子果然是比較強啊。其實除InnoDB外的其他數(shù)據(jù)庫,對MVCC的實現(xiàn)也有自己的一些特點,但我們這里就不研究其差異了,大家有興趣的話可以自己去看看。
另祝大家在閱讀了這篇文章后,都能在以后的設(shè)計中有更多的思路和靈感,謝謝!
本文作者:楊真(點融黑幫),入坑Java開發(fā)十余年,喜歡軟件設(shè)計和重構(gòu),現(xiàn)于點融LoanBusiness團隊從事后端開發(fā)。返回搜狐,查看更多
責(zé)任編輯:
總結(jié)
以上是生活随笔為你收集整理的innodb下的mvcc_从InnoDB了解MVCC的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【微软官方文档】应用程序错误处理
- 下一篇: C++自定义迭代器模板,实现ArrayL