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