CMU15-445 數據庫系統播客:日志與恢復機制解析
在數據庫的世界里,我們理所當然地認為,當我們按下“提交”按鈕時,數據就安全了。即使下一秒機房斷電、系統崩潰,我們相信重啟后數據依然完好無損。這份“信任”的背后,是一套精密而強大的機制在默默守護——這就是數據庫的日志與恢復系統。
本文將基于 CMU 頂尖的數據庫課程 15-445/645 的核心內容,帶你深入探索數據庫如何確保其 ACID 特性中的 原子性 (Atomicity) 和 持久性 (Durability) 。我們將從最基本的問題開始:為什么需要日志?然后逐步揭開緩沖池管理、影子分頁 (Shadow Paging) 和現代數據庫的基石——預寫日志 (Write-Ahead Logging, WAL) 的神秘面紗。
核心矛盾:性能與安全的博弈
現代數據庫系統為了追求極致的性能,不會讓每一次數據讀寫都直接操作緩慢的磁盤。相反,它們在內存中開辟了一塊高速緩存區,稱為 緩沖池 (Buffer Pool) 。所有的數據頁 (Page) 在被修改前,都必須先從磁盤讀入緩沖池。事務的所有操作都在內存中飛速進行。
然而,這也帶來了顯而易見的問題: 內存是易失的 。如果系統在事務修改了緩沖池中的數據頁(我們稱之為“臟頁”,Dirty Page)但尚未將其寫回磁盤時崩潰,那么所有內存中的修改都會丟失。
為了解決這個矛盾,恢復算法應運而生。它包含兩個階段:
- 正常運行時 :持續記錄足夠的信息,為可能發生的崩潰做準備。
- 崩潰恢復時 :利用記錄的信息,將數據庫恢復到一個一致的、正確的狀態。
恢復的兩大基石:Undo 與 Redo
所有恢復算法都建立在兩個基本操作之上:Undo 和 Redo。
- Undo (撤銷) :確保事務的 原子性 。如果一個事務在完成(提交)之前中止或系統崩潰,Undo 負責將其所有已做的修改“抹去”,就像這個事務從未發生過一樣。為了實現這一點,日志需要記錄每個修改的 “前鏡” (Before Value) 。
- Redo (重做) :確保事務的 持久性 。如果一個事務已經成功提交,但其修改的某些臟頁還沒來得及寫回磁盤就發生了崩潰,Redo 負責在系統重啟后,根據日志重新執行這些修改,確保其效果不會丟失。為了實現這一點,日志需要記錄每個修改的 “后鏡” (After Value) 。
關鍵決策:緩沖池管理策略
數據庫系統如何處理緩沖池中的“臟頁”,直接決定了其需要的恢復操作和整體性能。這里有兩個關鍵的策略維度,它們的組合構成了不同數據庫的設計哲學。
策略一:STEAL vs. NO-STEAL (是否允許“竊取”)
這個策略決定了 未提交事務 修改的臟頁,是否允許被寫回磁盤。
STEAL (允許竊取) :系統允許將一個尚未提交的事務所產生的臟頁寫回磁盤。這通常是由于緩沖池空間不足,需要騰出空間給其他數據頁。
- 優點 :內存管理更靈活,可以支持遠超內存大小的大型事務。
- 缺點 :恢復時變得復雜。如果該事務最終中止或在寫入磁盤后崩潰,我們就必須執行
Undo操作,將磁盤上已經被“污染”的數據恢復原狀。
NO-STEAL (禁止竊取) :系統嚴格禁止將未提交事務所產生的臟頁寫回磁盤。這些臟頁必須保留在內存中,直到其所屬事務提交。
- 優點 :恢復簡單。由于未提交的修改絕不會出現在磁盤上,我們永遠不需要在磁盤上執行
Undo。 - 缺點 :嚴重限制內存使用。如果一個事務修改的數據量超過了緩沖池的容量,該事務就無法執行。
策略二:FORCE vs. NO-FORCE (是否強制落盤)
這個策略決定了 事務提交時 ,是否必須將其產生的所有臟頁立即寫回磁盤。
FORCE (強制寫入) :當一個事務提交時,系統強制要求將該事務所修改的所有臟頁立即同步到磁盤。
- 優點 :恢復極快。因為所有已提交的修改都已確保在磁盤上,所以恢復時完全不需要
Redo操作。 - 缺點 : 運行時性能極差 。每次提交都需要進行大量(可能是隨機的)磁盤 I/O,這會成為系統的巨大瓶頸。
NO-FORCE (不強制寫入) :事務提交時, 不要求 其臟頁必須立即寫回磁盤。系統只需要確保相關的日志記錄已落盤即可。臟頁可以在未來的某個時刻由后臺進程批量寫回。
- 優點 :運行時性能非常高。事務提交變得極為迅速,因為它只涉及一次或幾次日志寫入,而非大量的數據頁寫入。
- 缺點 :恢復時需要
Redo。如果提交后、臟頁寫回前發生崩潰,系統必須通過Redo來恢復這些已提交的修改。
策略組合與權衡
策略組合 | 需要 Undo? | 需要 Redo? | 運行時性能 | 恢復復雜度 |
+ | 否 | 否 | 極差 | 最簡單 |
+ | 否 | 是 | 較好 | 簡單 |
+ | 是 | 否 | 差 | 復雜 |
| 是 | 是 | 最優 | 最復雜 |
可以看到,NO-STEAL + FORCE 方案雖然恢復最簡單,但其性能和內存限制使其在現代高性能場景下幾乎不可行。與之相對,STEAL + NO-FORCE 提供了最佳的運行時性能和靈活性,盡管代價是恢復過程最為復雜(需要同時處理 Undo 和 Redo)。
現代主流數據庫幾乎無一例外地選擇了 STEAL + NO-FORCE 策略。 它們的設計哲學是:系統崩潰是小概率事件,我們應當優先優化絕大多數時間都在進行的正常操作,將復雜性留給恢復階段。
早期嘗試:影子分頁 (Shadow Paging)
影子分頁是 NO-STEAL + FORCE 策略的一種經典實現。它雖然現在已不常用,但其思想非常巧妙。
工作原理:
- 數據庫的數據頁通過一個樹狀的頁表結構進行組織,有一個根指針指向這個結構的頂端。
- 當一個事務開始修改數據時,它不會直接在原始數據頁上修改,而是創建一個該頁的 副本(影子頁) 。
- 所有修改都在副本上進行。為了定位到這些副本,系統也會相應地創建頁表的副本。這個修改過程會自底向上一直傳播到根節點。
- 此時,我們同時擁有了兩個版本的數據:一個是由舊根指針指向的、未被修改的 主副本 (Master Copy) ,另一個是由新根指針指向的、包含了修改的 影子副本 (Shadow Copy) 。
- 事務提交 :系統原子性地將磁盤上的根指針從舊的頁表樹切換到新的頁表樹。一旦切換成功,所有修改瞬間生效。
Undo/Redo 如何工作?
- Undo :極其簡單。如果事務中止,只需直接丟棄所有的影子頁和對應的頁表副本,主副本毫發無損。
- Redo :完全不需要。因為
FORCE策略保證了在提交(即指針切換)時,所有修改過的數據頁和頁表都已被完整寫入磁盤。
為什么被淘汰?
- 高昂的提交開銷 :每次提交都需要將所有修改過的臟頁和頁表寫入磁盤,涉及大量隨機 I/O。
- 數據碎片化 :舊版本的數據頁散落在磁盤各處,成為垃圾,需要額外的垃圾回收機制。
- 并發性能差 :這種模型很難支持多個寫入事務高效地并發執行。
現代標準:預寫日志 (Write-Ahead Logging, WAL)
WAL 是實現 STEAL + NO-FORCE 策略的黃金標準,被當今幾乎所有高性能數據庫(如 PostgreSQL, MySQL/InnoDB, Oracle 等)所采用。
WAL 的黃金法則
在將任何數據頁的修改寫回磁盤之前,必須確保與該修改相關的日志記錄(包括 Undo 和 Redo 信息)已經先一步寫入到穩定的存儲(磁盤)上。
這個法則是 WAL 的靈魂。它意味著:
- 事務提交時,我們 只需等待它的提交日志 (
<COMMIT>) 記錄被寫入磁盤 ,就可以向客戶端確認提交成功。我們不需要等待數據頁本身落盤。 - 即使系統在臟頁寫回前崩潰,我們也能安然無恙。因為重啟后,我們可以讀取日志,利用
Redo信息重放已提交事務的修改,利用Undo信息回滾未提交事務的修改。
WAL 的巨大優勢:化隨機 I/O 為順序 I/O
WAL 最核心的性能優勢在于,它將對數據文件的 大量隨機寫 操作,巧妙地轉化為了對日志文件的 一次順序寫 操作。
- 數據頁在磁盤上是隨機分布的,更新它們需要磁頭在盤片上反復尋道,非常耗時。
- 日志文件則是 僅追加 (Append-only) 的。寫入日志永遠是在文件末尾進行,這是一個高速的順序操作,比隨機寫快上幾個數量級。
性能優化:組提交 (Group Commit)
如果每個事務提交都立即觸發一次磁盤同步 (fsync) 來刷寫日志,當并發量很高時,磁盤 I/O 依然會成為瓶頸。為此,數據庫引入了 組提交 機制。
系統會將多個并發事務的日志記錄先在內存的日志緩沖區中攢一會兒,然后將這個“批次”的日志記錄通過一次 fsync 操作,批量寫入磁盤。這大大攤薄了單次磁盤同步的開銷,顯著提升了數據庫的事務吞吐量 (TPS)。
日志的內部:記錄的粒度
日志記錄具體寫了什么內容,也存在不同的實現方式:
- 物理日志 (Physical Logging) :記錄字節級別的變化,例如:“在頁面 P 的偏移量 O 處,將字節序列 A 改為 B”。這種日志非常簡單直接,但體積可能很大。
- 邏輯日志 (Logical Logging) :記錄高層次的操作,例如:
UPDATE students SET gpa = 4.0 WHERE id = 1。這種日志非常緊湊,但恢復時需要重新執行邏輯,可能很慢,且在并發環境下恢復狀態可能與原始執行不一致。 - 生理日志 (Physiological Logging) :這是物理和邏輯日志的混合體,也是最被廣泛采用的方式。它記錄對單個數據頁的修改,但描述的是邏輯上的變化,而非具體的字節位。例如:“在頁面 P 中,將記錄 R 的某個字段從值 V1 更新為 V2”。它兼具了恢復效率和日志緊湊性的優點。
控制日志增長:檢查點 (Checkpoints)
隨著系統運行,日志文件會無限增長。如果數據庫運行一年后崩潰,難道我們要從一年前的日志開始回放嗎?這顯然是不可接受的。
檢查點 (Checkpoint) 機制就是為了解決這個問題而生的。它的核心目標有兩個:
- 限制恢復所需掃描的日志量 。
- 回收不再需要的舊日志文件 。
一個簡化的檢查點過程如下:
- 系統暫停接受新的寫入事務。
- 將緩沖池中 所有 的臟頁全部刷新到磁盤。
- 在日志文件中寫入一條特殊的
<CHECKPOINT>記錄,并確保其落盤。 - 恢復事務處理。
當系統從崩潰中恢復時,它只需要找到最后一個成功的檢查點,然后 從這個檢查點的位置開始 向后掃描日志。對于檢查點之后:
- 所有 已提交 的事務,執行
Redo。 - 所有 未完成 (或已中止)的事務,執行
Undo。
檢查點之前的日志記錄,由于其對應的數據頁已保證落盤,因此在恢復時無需再關心,可以被安全地歸檔或刪除。檢查點的頻率是一個重要的權衡:太頻繁會影響運行時性能,太稀疏則會延長恢復時間。
總結
數據庫的日志與恢復系統是其可靠性的基石。通過本文的梳理,我們可以得出結論:
現代數據庫系統普遍采用基于
STEAL+NO-FORCE策略的預寫日志(WAL)方案,并配合檢查點(Checkpoint)機制。
這種架構選擇犧牲了恢復過程的簡單性,換取了無與倫比的運行時性能。它通過將隨機寫轉化為順序寫、利用組提交等技術,將正常事務處理的性能推向極致。正是這套復雜而優雅的系統,讓我們能夠放心地將最寶貴的數據托付給數據庫。





































