資料庫恢復 —— 關於事務原子性、持久性
作者| @韓富晟
數據的一致性
在資料庫系統作為一個單獨的軟體誕生之前,工程師需要給每一個系統開發管理數據文件的邏輯,一個業務接著一個業務都有類似的在文件中存儲數據的需求。「那個時候,開發軟體最難的部分不是完成業務的執行邏輯,而是如何保證不同數據文件里數據的一致性。」,Thomas Nies 曾這麼說過,於是他創建了 TOTAL 資料庫系統,把所有數據處理的邏輯都納入其中,開始了計算機產業內第一次獨立的軟體售賣,並且成為了前關係資料庫時代最成功的資料庫系統之一。
經典的數據一致性問題可以用一筆用戶之間的轉賬操作來解釋。
轉賬操作:
1) 借方賬戶餘額減去 100 元
2) 貸方賬戶餘額加上 100 元
3) 記一條轉賬記錄
一次轉賬操作需要修改三個數據,第一步從借方賬戶(轉出賬戶)的餘額減去 100 元;第二步從貸方賬戶(轉入賬戶)的餘額加上 100 元;第三步要在記錄表裡為此次轉賬操作記一條記錄,為了之後的對賬、查詢等操作,三步修改是三處不同的數據。如果只有第一步完成,在執行第二步之前,機器斷電,重啟之後系統需要知道斷電之前的轉賬操作是不完整的,借方餘額需要恢復原狀。否則,借方少了 100 元,貸方卻沒收到 100 元,這種即數據的不一致。
在計算機系統里,早期是磁帶,後來是磁碟,現在還有固態盤,都是可以持久化存儲數據的。所謂持久化存儲數據,主要是說機器斷電後數據還在。但是內存不行,存儲在內存中的數據斷電後就都消失了。資料庫系統就是在統籌這兩個存儲設備的特性,完成數據的存儲和查詢。在 IBM 孵化關係資料庫的原型系統 System R 時,Jim Gray 提出了要用「事務」來保證數據的原子性(Atomicity)、持久性(Durability)。資料庫內的每一組數據修改都以「事務」為介面。原子性保證了事務的多個操作或者都生效或者都不生效,不會存在中間狀態。持久性保證一旦事務的操作生效,不會因為任何原因其修改被撤銷或丟失。從事務的持久性角度看,事務寫入的數據一定要寫到硬碟等持久化存儲里才能保證持久性,只存儲在內存中的數據如果遇到系統突然終止或者系統斷電就會丟失。事務修改的數據分散在硬碟上的不同地方時,第一處修改完成時,第二處修改還沒開始,這個時候出現系統故障,那麼只做了一半的事務就殘留在了硬碟上,當系統恢復時需要能夠明確事務一半的狀態,然後把已經做的修改恢復原狀,好像這個事務沒發生過一樣,這才能保證事務的原子性。
原子性和持久性是事務密切相關的兩個屬性,資料庫系統會保障在任何異常情況下原子性和持久性都能夠被保證,諸如程序異常終止、操作系統故障、系統斷電等。事務恢復功能(Transaction Recovery)就是用來完成這件事情的。下面的文章就會介紹實現的方法。
事務恢復(Transaction Recovery)
經過了多年的發展後,事務恢復有三種成熟的技術方案,分別是 Commit Logging, Shadow Paging 和 Write Ahead Logging。了解資料庫的人應該知道 Write Ahead Logging 方案,尤其是 80 年代末 C. Mohan 發表了著名的 ARIES 論文後,Write Ahead Logging 技術得到了全面加強,ARIES 也成為了資料庫領域的經典演算法。Wrtie Ahead Logging 也是目前使用最為廣泛的一種技術,但是技術細節也很繁複。Commit Logging、Shadow Paging 也一直在被使用,只是很少被提及。下面分別進行介紹。
Commit Logging
Commit Logging 是最樸素的方法,甚至沒有一篇專門的論文來論訴它。早期,因為內存很小,Commit Logging 解決不了實際問題,所以沒有人去寫論文。後來,Write Ahead Logging 非常成熟,也沒有必要再為 Commit Logging 寫文章了。
本質上 Commit Logging 就是 Write Ahead Logging 在內存特別大時的一種特例,但是 Commit Logging 不需要考慮臟數據(事務進行到一半還未確定提交時已經修改過的數據)持久化的狀態,沒有 Write Ahead 的行為,也不需要考慮數據和日誌之間對應關係,是與 Write Ahead Logging 出發點完全不同的一種解決方法,整體結構比 Write Ahead Logging 簡單非常多。簡單有一個潛台詞就是高效。
使用 Commit Logging 的資料庫有 OceanBase、Hekaton(SQL Server 的內存存儲引擎)。OceanBase 所有新寫入和修改的增量數據都是記在內存中,內存中的增量數據定期和硬碟中的數據進行合併,相當於一個內存存儲引擎和硬碟存儲引擎的結合。Hekaton 是完全的內存存儲引擎。這兩者共同的特點是臟數據不會持久化。
日誌(Log)是硬碟上的一個數據文件並且只會順序追加寫入,可以用來高效的持久化信息。Commit Logging 就是在事務內所有操作都結束開始執行提交(Commit)時,將事務所有修改的數據(包括插入、更新、刪除)寫入日誌,如果所有的日誌寫入都成功,那麼事務最終就是成功的,即使出現系統故障,資料庫系統重啟後依然可以從日誌中恢復出事務所有修改的數據,保證了事務的持久性。那麼事務的原子性是如何保證的呢?如果日誌只寫到一半沒有完全寫完時出現系統故障,首先對於內存資料庫來說,事務對應的修改都在內存中,系統故障後這些修改也都隨著內存內容的丟失一起消失了。其次,當資料庫系統重新恢復時,會發現日誌中事務只寫了一部分並沒有完成標記,那麼這個事務就記為回滾狀態,並不會重放到內存中,那麼這個事務就好像完全沒有發生過一樣。
如上圖,如果資料庫中的兩個賬戶 A、B 的餘額分別是 800 和 400,進行 100 元的轉賬操作的事務會記錄的日誌是:
A 的餘額是 700
B 的餘額是 500
轉賬記錄:A 轉賬 100 元給 B
END
當上面的日誌內容成功寫入日誌文件後,事務就可以宣布執行成功了。
Shadow Paging
世界上實例數量最多的資料庫是 SQLite,SQLite Version 3 使用的就是 Shadow Paging 的事務恢復機制。
Shadow Paging 和日誌完全沒有關係,Shadow Paging 方案中,事務操作時新修改的數據直接寫到硬碟的數據中,但是並不是直接就地修改原先的數據,而是會把之前的數據複製出來,保留原先的數據不動,修改複製出的數據。在事務操作過程中,被修改的數據會同時存在兩份,一份修改前的數據,一份是修改後的數據,這就是影子(Shadow)這個名字的由來。
事務的修改是直接持久化在硬碟上,持久性的保證很直觀。Shadow Paging 如何保證事務的原子性呢?其原理就是在事務執行過程中修改的數據雖然以拷貝出的新的一份數據寫入到硬碟中,但是這些新的寫入在事務提交前並沒有生效,當事務提交時,才以一次原子的數據寫入讓整個事務新的修改生效。事務提交時的原子修改是因為只修改一處很少的數據,所以可以由硬碟等持久化設備來保證這次寫入的原子性。最終達成整個事務的原子性。如果在事務提交的操作執行之前,出現系統故障,資料庫恢復時是見不到剛才未完成事務的修改的,就好像這個事務沒有發生過。硬碟上的這個事務曾經修改的數據後期也會由垃圾回收模塊回收重用。
如上圖,還以轉賬事務為例。A 和 B 的餘額都是直接寫入新的位置,保證原先的數據沒有改動。系統通過兩個目錄結構分別指向修改前的數據和修改後的數據,最後 Current 指針原子切換到新的目錄上,表示事務提交成功。
Shadow Paging 方案事務的並發能力受限制,但是,Shadow Paging 相比基於日誌的方案,事務修改的數據是直接落盤的,不需要在日誌和數據里寫兩份,所以大部分資料庫系統雖然沒有直接使用 Shadow Paging 方案,但是在存儲 LOB(Large OBject)數據時一般會結合 Shadow Paging 和另外一種事務恢復方案。使用 Shadow Paging 直接寫 LOB,將指向 LOB 的指針作為數據內容記日誌,這樣可以避免大塊的 LOB 數據在日誌和數據中寫兩遍,優化 LOB 的處理能力。
Write Ahead Logging
Write Ahead Logging 也是使用日誌(Log)來支持事務的恢復,與之前描述的 Commit Logging 最大的不同是 Write Ahead Logging 不假定事務的修改在事務提交之前可以一直保存在內存中,換句話說,事務執行過程中已經修改過的數據允許寫到硬碟等持久化存儲中。這點是符合事務技術最初發展階段(70、80 年代)的計算機硬體環境,當時機器的內存很小。
使用 Write Ahead Logging 的資料庫有 Oracle, SQL Server, MySQL InnoDB 等常見的傳統資料庫系統。ARIES 也已經是基於硬碟的資料庫實現事務恢復的標準方案。
Write Ahead Logging 面對的場景是事務執行修改的數據也是先記錄在內存的緩存中,如果內存不足,修改後的數據會直接持久化在硬碟中。但是之後事務可能需要回滾,所以日誌中需要記錄兩類信息,一類是事務做了哪些修改,被稱為 REDO;另一類是事務修改之前數據是什麼樣子,被稱為 UNDO。事務的持久性還是由事務對應的 REDO 日誌部分完全持久化成功來保證。事務的原子性由日誌的 UNDO 部分保證,如果事務進展到一半有一部分修改過的數據已經持久化了,出現系統故障,資料庫重啟之後會根據 UNDO 裡面記錄的信息將事務曾經做過的修改恢復原狀,恢復到事務沒發生之前的樣子。為了保證 UNDO 的完整性,不能出現有數據對應的 UNDO 日誌還沒有,所以日誌要在數據落盤之前持久化,這也就是 Write Ahead 名字的由來。
還以上面的轉賬操作為例,Write Ahead Logging 需要記錄的日誌是:
A 原始餘額是 800,修改後的餘額是 700
B 原始餘額是 400,修改後的餘額是 500
轉賬記錄:A 轉賬 100 元給 B
END
推薦閱讀:
※手把手教您解決90%的自然語言處理問題
※KDB+的RDB實現
※OceanBase:當金融擁抱科技
※實習小記
※基於協程和非同步IO的NoSQL資料庫AsyncDB正式發布