本文節選自作者書籍《軟體架構設計:大型網站技術架構與業務架構融合之道》。作者微信公眾號:架構之道與術。公眾號底部菜單有書友群可以加入,與作者和其他讀者進行深入討論。也可以在京東、天貓上購買紙質書籍。 6.6 事務實現原理之2:Undo Log 6.6.1 Undo Log是否一定需要 說到Undo ...
本文節選自作者書籍《軟體架構設計:大型網站技術架構與業務架構融合之道》。
作者微信公眾號:架構之道與術。公眾號底部菜單有書友群可以加入,與作者和其他讀者進行深入討論。也可以在京東、天貓上購買紙質書籍。
6.6 事務實現原理之2:Undo Log
6.6.1 Undo Log是否一定需要
說到Undo Log,很多人想到的只是“事務回滾”。“事務回滾”有四種場景:
場景1:人為回滾。事務執行到一半時發生異常,客戶端調用回滾,通知資料庫回滾,資料庫回滾成功。
場景 2:宕機回滾。事務執行到一半時資料庫宕機,重啟,需要回滾。
場景 3:人為回滾 + 宕機回滾。客戶端調用回滾,資料庫開始回滾數據,回滾到一半時資料庫宕機,重啟,繼續回滾。
場景 4:宕機回滾 + 宕機回滾。宕機重啟,在回滾的過程中再次宕機。
對於這四種場景的解決方法,在上文的ARIES演算法已經給出了答案,其中要用到Redo和Undo Log。這裡擴展一下,除了ARIES演算法,是否還有其他的方法可以做事務回滾?或者說,Undo Log是否一定需要?
回滾,就是取消已經執行的操作。無論從物理上取消,還是從邏輯上取消,只要能達到目的即可。假設Page數據都在記憶體裡面,每個事務執行,都只在記憶體中修改數據,必須等到事務Commit之後寫完Redo Log,再把Page數據刷盤。在這種策略下,不需要Undo Log也能實現數據回滾!因為在這種數據刷盤策略下,正好利用了“記憶體斷電消失”的特性,磁碟上存儲的全部是已經提交的數據,宕機重啟,記憶體中還未完成的事務自然被一筆勾銷了!在這種策略之下,未提交的事務不會進入Redo Log;未提交的事務,也不會刷盤,全都在記憶體裡面。
把這個展開,就是Page數據刷盤的四種策略,如表6-10所示。下麵對這四種策略進行詳細分析:
表6-10 Page數據刷盤的四種策略
No Steal和Steal:指未提交的事務是否可以寫入磁碟中?No Steal是未提交的事務不能寫入磁碟,只能在記憶體中操作,等到事務提交完,再把數據一次性寫入;Steal是指未提交的事務也能寫入,如果事務需要回滾,再更改磁碟上的數據。
No Force和Force: 是指已經提交的事務是否必須寫入磁碟?No Force是指已經提交的事務可以保留在記憶體里,暫時不用寫入磁碟;Force是指已經提交的事務必須強制寫入磁碟。
策略1:Force和No Steal。已經提交的事務必須強制寫入磁碟,未提交的事務,只能保留在記憶體里,等事務提交後再寫入磁碟,這種策略不需要Redo Log和Undo Log,僅靠數據本身就能實現原子性和持久性。但很顯然不可行,未提交的事務不能寫入磁碟,這還可以接受;已提交的事務必須強制寫入磁碟,這需要多次I/O,性能會受影響,所以才有了RedoLog。
策略2:No Force和No Steal。已提交的事務可以不立即寫入磁碟,未提交的事務只能保留在記憶體里。在這個策略下,只需要Redo Log即可,因為有“記憶體斷電消失”這個天然特性。
策略3:Force/Steal。已提交的事務立即寫入磁碟,未提交的事務也立即寫入磁碟。這種只需要UndoLog回滾宕機時未提交的事務,不需要Redo Log。但和策略1一樣,顯然不可行,多次I/O的性能會受影響。
策略4:No Force/Steal。第4種策略是我們最想要的,也是InnoDB實現的策略。就是已經提交的事務可以不立即寫入磁碟;未提交的事務可以立即寫入磁碟,也可以延遲寫入磁碟!再通俗一點,無論事務是否提交,既可以立即寫入磁碟,也可以不寫,寫入磁碟時機任意,想什麼時候寫就什麼時候寫。
策略1和策略3因為性能問題不能接受,所以必須要有Redo Log。而策略4和策略2都可以接受,但策略4比策略2好的地方在於提高了I/O效率。因為事務沒有提交,就開始寫入磁碟,等到提交事務的時候,要寫入磁碟的數據量會小,不然要把所有數據都累積到事務提交時再一次性寫入磁碟。
也正是因為現代的資料庫用的都是第4種,是最靈活的一種數據刷盤策略。在這種策略下,為了實現事務的原子性和持久性,才有瞭如此複雜的Redo Log和Undo Log機制,才有了上面的ARIES演算法。
除了在宕機恢復時對未提交的事務進行回滾,Undo Log還有兩個核心作用:
(1)實現ACID中I(隔離性)。
(2)高併發。
6.6.2 Undo Log(MVCC)
在多線程編程中,讀寫的併發問題有三種策略,如表6-9所示。
表6-11 併發讀寫的三種策略
在JDK的JUC代碼中,有CopyOnWriteArrayList和CopyOnWriteArraySet 兩個類,有興趣的讀者可以閱讀源碼來理解CopyOnWrite的思想。
對比上面表格的三種併發策略可以知道,從上到下,併發度越來越高。而InnoDB用的就是CopyOnWrite思想,是在Undo Log裡面實現的。每個事務修改記錄之前,都會先把該記錄拷貝一份出來,拷貝出來的這個備份存在Undo Log里。因為事務有唯一的編號ID,ID從小到大遞增,每一次修改,就是一個版本,因此Undo Log維護了數據的從舊到新的每個版本,各個版本之間的記錄通過鏈表串聯。
也正因為每條記錄都有多版本,才很容易實現事務ACID屬性中的I(隔離性)。事務要併發,多個事務要讀寫同一條記錄,為了實現第二個、第三個隔離級別,就不能讓事務讀取到正在修改的數據,而只能讀取歷史版本。
也正因為有了MVCC這種特性,通常的select語句都是不加鎖的,讀取的全部是數據的歷史版本,從而支撐高併發的查詢。這種讀,專業術語叫作“快照讀”,與之相對應的是“當前讀”。表6-10列舉了快照讀和當前讀對應的SQL語句,快照讀就是最常用的select語句,當前讀包括了加鎖的select語句和insert/update/delete語句。
表6-12 快照讀與當前讀對應的SQL語句
6.6.3 Undo Log不是Log
瞭解Undo Log的功能後,進一步來看Undo Log的結構。其實Undo Log這個詞有很大的迷惑性,它其實不是Log,而是數據。為什麼這麼說?
(1)Undo Log並不像Redo Log一樣按照LSN的編號,從小到大依次執行append操作。Undo Log其實沒有順序,多個事務是並行地向Undo Log中隨機寫入的。
(2)一個事務一旦Commit之後,數據就“固化”了,固化之後不可能再回滾。這意味著Undo Log只在事務Commit過程中有用,一旦事務Commit了,就可以刪掉UndoLog。具體來說:
對於insert記錄,沒有歷史版本數據,因此insert的Undo Log只記錄了該記錄的主鍵ID,當事務提交之後,該Undo Log就可以刪除了;
對於update/delete記錄,因為MVCC的存在,其歷史版本數據可能還被當前未提交的其他事務所引用,一旦未提交的事務提交了,其對應的Undo Log也就可以刪除了。
所以,更應該把UndoLog叫作記錄的“備份數據”,即在事務未提交之前的時間里的“備份數據”!提交事務後,沒有其他事務引用歷史版本了,就可以刪除了。
下麵來看這個“備份數據”是怎麼操作的。如圖6-18所示,Page中的每條記錄,除了自身的主鍵ID和數據外,還有兩個隱藏欄位:一個是修改該記錄的事務ID,一個是rollback_ptr,用來串聯所有的歷史版本。假設該記錄被tx_id為68、80、90、100的四個事務修改了四次,該數據就有四個版本,通過rollback_ptr從新到舊串聯起來。
然後,三個歷史版本分別被其他不同的事務讀取。為什麼會出現不同的事務讀取到不同的版本呢?因為T1、T2最先,此時歷史版本3是最新的,還沒有歷史版本1、2;之後該記錄被修改,產生了歷史版本2,然後出現了T3;之後該記錄又被修改,產生了歷史版本1,然後出現了T4。每個事務讀取的都是這個事務執行時最新的歷史版本。
這些歷史版本什麼時候可以刪除呢?在T1、T2提交之後,歷史版本3就可以刪除了;在T3提交之後,歷史版本2就可以刪除了,依此類推。
圖6-18 Undo Log邏輯結構
註意:在這裡有一個專業名詞,叫“回滾段”,很多描述Undo Log的文章花大篇幅描述它。但本書作者不想解釋這個名詞,因為它不僅不會幫助我們對原理的理解,還會把簡單問題複雜化。說得通俗一點,就是修改記錄之前先把記錄拷貝一份出來,然後拷貝出來的這些歷史版本形成一個鏈表,僅此而已。
6.6.4 Undo Log與Redo Log的關聯
Undo Log本身也要寫入磁碟,但一個事務修改多條記錄,產生多條Undo Log,不可能同步寫入磁碟,所以遇到了開篇講Write-Ahead時的問題。如何解決Undo Log需要多次寫入磁碟的效率問題呢?
Redo Log記錄的是對數據的修改,凡是對數據的修改,都必須記入Redo Log。可以把Undo Log也當作數據!在記憶體中記錄Undo Log,非同步地刷盤,宕機重啟,用Redo Log恢復Undo Log。
拿一個事務來舉例:
start transaction
update表1某行記錄
delete表1某行記錄
insert表2某行記錄
commit
把Undo Log和Redo Log加進去,此事務類似下麵偽代碼所示:
start transaction
寫Undo Log1: 備份該行數據(update)
update表1某行記錄
寫Redo Log1
Undo Log2:備份該行數據(insert)
delete 表1某行記錄
寫Redo Log2
Undo Log3:該行的主鍵ID(delete)
insert表2某行記錄
寫Redo Log3
commit
在這裡,所有Undo Log和Redo Log的寫入都可以只在記憶體中進行,只要保證Commit之後Redo Log落盤即可,Undo Log可以一直保留在記憶體里,之後非同步刷盤。
文章太長,未完待續,接下來的1篇,會繼續分析Undo Log。