預寫日誌 WAL(Write-Ahead Logging)完全指南

WAL 的兩條核心規則、redo 與 undo、LSN 與髒頁追蹤、ARIES 崩潰復原三階段、WAL vs rollback journal,以及各資料庫的實作對照


目錄


什麼是 WAL

預寫日誌(Write-Ahead Logging, WAL) 的核心規則只有一句:先寫日誌,再改資料。任何對資料頁的修改,都必須把「這次改了什麼」寫成一筆 log record 並確保它落到穩定儲存(磁碟),資料頁本身才可以(稍後)寫回。

它要同時達成兩個看似衝突的目標:

  • 持久性(Durability):交易一旦 commit,即使立刻斷電也不能丟。
  • 效能:不能為了持久性,讓每次 commit 都去做大量緩慢的隨機寫。

WAL 的解法是把「貴的事」和「必須做的事」拆開:commit 時只需要把該交易的 log(循序寫、便宜)確保落地;被改動的資料頁可以留在記憶體當髒頁,稍後由 checkpoint 批次寫回。

名詞歧義提醒:嚴格說「WAL」只指上面那條「先寫日誌」的規則;但各家常把整套機制或特定模式也叫 WAL——PostgreSQL 直接把它的 redo 日誌叫 WAL,SQLite 有一個叫「WAL 模式」的 journaling 模式。看到「WAL」時要分清指的是規則還是某產品的具體實作


為什麼需要 WAL

考慮「交易 commit 當下,資料到底要不要立刻寫回磁碟」這個問題,沒有 WAL 時會陷入兩難:

做法 問題
commit 時強制把所有被改的資料頁寫回磁碟 資料頁散落各處 → 每次 commit 一堆隨機寫,慢;且寫到一半崩潰,資料頁處於半新半舊,破壞原子性
commit 時什麼都不特別做(靠 OS 慢慢刷) commit 說成功了,但變更還在記憶體,斷電就丟資料(不持久);未提交的變更若已被刷回也無法撤銷(不原子)

WAL 用一條循序寫入、成本低的日誌當「事實來源」化解:

交易改資料
  → ① 先把改動寫成 log record(循序 append 到日誌)
  → ② commit:只要該交易的 log 確實落地,就算持久了
  → ③ 資料頁留在記憶體當髒頁,稍後 checkpoint 批次寫回(隨機寫被延後 + 攤提)

崩潰後靠日誌重建:已 commit 但資料頁沒寫回的 → 用日誌重做(redo);未 commit 卻已被寫回的 → 用日誌撤銷(undo)。於是資料頁怎麼刷、何時刷都不影響正確性,才能安心延後、批次、循序化。


兩條核心規則

WAL 其實是兩條不變式(invariant)合起來,各自保證「能 undo」與「能 redo」:

  1. Undo 規則(狹義的 write-ahead rule):某個髒頁被寫回磁碟之前,描述該修改的 log record(含 undo 資訊)必須先落地。
    • 保證:就算未提交的髒頁已經被寫進磁碟,崩潰後也還有 log 可以把它撤銷回去。
  2. Redo 規則(force-log-at-commit):交易 commit 之前,它所有的 log record 必須先落地。
    • 保證:就算已提交交易的資料頁還沒寫回,崩潰後也能用 log 把它重做補上。

這兩條規則對應緩衝區管理的兩個策略,也是為什麼 redo/undo 都需要:

策略 意思 需要哪種 log
Steal(允許偷取) 允許把「未提交」交易的髒頁寫回磁碟(好騰出記憶體) 需要 undo(才能事後撤銷)
No-Force(提交不強制) commit 時強制把資料頁全部寫回 需要 redo(才能事後補做)

主流資料庫(含 SQL Server、ARIES 系統)都採 steal + no-force——這是效能最好的組合,代價就是 redo 與 undo 兩者都要。


redo log 與 undo log

redo log undo log
記什麼 修改的新值 / 如何重做 修改的舊值 / 如何撤銷
用在 崩潰後把「已提交但沒落地」的變更補回 回滾(ROLLBACK)、崩潰後撤銷未提交交易;也常供 MVCC 讀取舊版本
對應規則 Redo 規則(no-force 需要它) Undo 規則(steal 需要它)

有些系統把兩者放在同一條 log 流(如 SQL Server 的 .ldf、Oracle 的 redo 內含變更向量),有些分開(如 MySQL InnoDB 的 redo log 與 undo log 各自獨立)。PostgreSQL 則特別:它有 redo(WAL),但沒有獨立的 undo log——舊版本直接留在資料表裡(MVCC),回滾只是標記、之後由 VACUUM 清理(見對照表)。


LSN 與髒頁追蹤

WAL 靠幾個編號把「日誌」與「資料頁」對起來,才知道崩潰後哪些要重做:

  • LSN(Log Sequence Number):每筆 log record 一個單調遞增的序號,等於它在日誌流中的位置。
  • pageLSN:每個資料頁上記著「最後一次修改它的那筆 log record 的 LSN」。
  • recLSN:某髒頁「第一次被弄髒(還沒刷回)」的那筆 log 的 LSN。

判斷某筆 log 要不要對某頁 redo,就是比大小:

若  log record 的 LSN  >  該頁磁碟上的 pageLSN
   → 這次修改還沒反映到磁碟頁上 → 需要 redo
否則
   → 磁碟頁已經比這筆 log 新 → 這筆已套用過,跳過(redo 的冪等性)

這個比對讓 redo 可以重放同一段 log 多次都安全(例如 redo 到一半又崩潰再來一次),是崩潰復原能可靠運作的關鍵。


崩潰復原:ARIES 三階段

ARIES 是最經典的 WAL 崩潰復原演算法(SQL Server、DB2 等都屬這一系)。重啟後分三個階段,從最後一個 checkpoint 出發:

崩潰 → 重啟復原
  ① Analysis(分析):從最後 checkpoint 往後掃 log,重建兩張表
       - Dirty Page Table:崩潰當下有哪些髒頁、各自的 recLSN
       - Transaction Table:崩潰當下有哪些「還沒結束」的交易
       → 由髒頁表中最小的 recLSN 算出 redo 的起點 RedoLSN
  ② Redo(重做):從 RedoLSN 起「重演歷史」(repeating history),
       把所有記錄過的變更(不管已提交或未提交)依 LSN 重放,
       用 pageLSN 比對跳過已套用的 → 資料庫回到「崩潰當下」的狀態
  ③ Undo(復原):把崩潰時仍未提交的交易,依 LSN 反向逐一回滾;
       回滾動作本身也寫成 CLR(補償記錄),使 undo 可重入、崩潰可續做

三個要點常被考:

  • 先 redo 再 undo:ARIES 刻意「先把歷史完整重演一遍(含未提交的),再回滾未提交的」。這比「只 redo 已提交的」更單純可靠,能一致處理各種交錯情況。
  • CLR(Compensation Log Record,補償記錄):undo 時把「我撤銷了什麼」也寫進 log。這樣即使在 undo 過程中又崩潰,重來時不會把同一筆重複撤銷。
  • 冪等:靠 LSN 比對,redo/undo 重放幾次都得到相同結果。

checkpoint 的角色

如果每次崩潰都得從日誌最開頭開始 redo,復原會慢到不可用。checkpoint 定期把髒頁刷回磁碟、並在日誌記下一個檢查點,作用是縮短崩潰復原的起點

  • 把髒頁寫回 → 抬高「最舊未落地變更」的位置 → redo 起點(RedoLSN)往後移。
  • 復原時只需從最後一個 checkpoint 掃起,而不是掃整個歷史。

代價是 checkpoint 本身要做 I/O,所以是「復原速度 vs 平時 I/O 負擔」的取捨。現代做法多是持續小量刷髒頁的間接 checkpoint(如 SQL Server 的 TARGET_RECOVERY_TIME、PostgreSQL 的 checkpoint_completion_target),把尖峰打平。


WAL vs rollback journal

「崩潰時如何保證原子+持久」有兩條主流路線,WAL(redo 式)只是其一:

Rollback journal(undo 式) WAL(redo 式)
做法 改資料先把舊頁備份到 journal,直接改原資料檔 改動先寫進 log,原資料檔稍後才由 checkpoint 更新
commit 刪掉 journal 即完成(但改動已在原檔,需 fsync 原檔) 只需把 log 落地即完成
崩潰復原 用 journal 的舊頁還原原檔 用 log 重做未落地的變更
併發 寫時通常需獨占(讀寫互斥) 讀可與寫並行(讀舊檔、寫進 log)
代表 SQLite 傳統 rollback journal 模式、早期做法 PostgreSQL、MySQL InnoDB、SQL Server、SQLite WAL 模式

WAL 之所以成為主流,關鍵在commit 只碰循序的 log、且讀寫併發度好;rollback journal 的 commit 要動到散落的原資料檔、併發也差。(SQLite 官方文件對這兩種模式的對比是很好的實例。)


WAL 的額外紅利:複寫 / PITR / CDC

WAL 一旦存在,它就成了「資料庫發生過什麼」的完整、有序事實流。於是很多功能都直接讀這條 log 來實現,而不必另建機制:

  • 時間點還原(PITR):完整備份 + 一連串 log 備份接力,就能還原到任意時刻。
  • 複寫 / 高可用:把 log 送到另一台重放,就得到一份副本(PostgreSQL streaming replication、MySQL 的 redo/ binlog、SQL Server Always On / 交易式複寫都圍繞 log)。
  • CDC(變更資料擷取):讀 log 把每筆變更抽出來送給下游。

反過來,這也解釋了一個維運現象:只要有下游還沒讀完 log,這段 log 就不能被回收,記錄檔因此變大。(SQL Server 的表現就是 log_reuse_wait_desc = REPLICATION,詳見 SQL Server 交易記錄檔管理。)


各資料庫的實作對照

資料庫 redo(重做 / 持久性) undo(撤銷 / 回滾) 備註
PostgreSQL WAL(pg_wal 無獨立 undo log——舊版本留在表中(MVCC),回滾靠標記、VACUUM 清理 用 full-page writes 防 torn page
MySQL InnoDB redo log(redo log files) undo log(undo tablespace),兼供 MVCC 讀視圖 redo 與 undo 分離
SQL Server 交易記錄檔 .ldf(redo 與 undo 同一條流) 同左 復原模式控制 log 保留
Oracle redo log + archive log undo tablespace(undo segments) 概念近似 InnoDB
SQLite WAL 模式(-wal 檔,redo 式) rollback journal 模式(-journal,undo 式) 兩種日誌模式擇一

觀念相通、名詞各異:.ldf / pg_wal / redo log / -wal 檔本質都是「預寫日誌」。差別主要在 redo 與 undo 是否同流undo 是走獨立日誌還是靠 MVCC、以及保留與回收策略


常見問題

問題 1:WAL 就是 redo log 嗎?

不完全等價。WAL 是「先寫日誌再改資料」的規則;redo log 是為了滿足其中「Redo 規則」而記錄的日誌內容。口語上大家常互稱,但嚴格講 WAL 這套協定同時需要 redo undo 才完整(PostgreSQL 是把 undo 用 MVCC 代替的特例)。

問題 2:為什麼 redo 要連「未提交的交易」也重做?

這是 ARIES 的「重演歷史」原則。先把資料庫還原到崩潰當下的完整狀態(包含未提交的變更),再統一把未提交的回滾,邏輯比「只挑已提交的 redo」單純、且能正確處理交錯與巢狀情況。

問題 3:WAL 會不會讓寫入變兩倍(log 一次、資料一次)?

資料是寫了兩次沒錯(先 log 後資料頁),但 log 是循序、可合併(group commit),資料頁是延後、批次、可多次修改合併成一次寫回。整體 I/O 成本遠低於「每次 commit 都隨機刷所有資料頁」,對吞吐是淨賺。(吞吐面的詳細推導見 SQL Server 交易記錄檔管理 的循序寫小節。)

問題 4:WAL 和 undo log 為什麼都要?

因為主流採 steal + no-force:允許未提交髒頁被寫回(→ 要 undo 才能撤銷)、且 commit 不強制刷資料頁(→ 要 redo 才能補做)。少了任一種,就得放棄對應的緩衝策略、犧牲效能。


總結與速查

核心要點

  • WAL 一句話:先寫日誌,再改資料;commit 只需 log 落地,資料頁延後批次刷。
  • 兩條規則:Undo 規則(髒頁寫回前 log 先落地)+ Redo 規則(commit 前 log 先落地),分別支撐 steal 與 no-force。
  • redo 補做已提交沒落地的;undo 撤銷未提交卻已落地的。
  • 崩潰復原走 ARIES 三階段:Analysis → Redo(重演歷史)→ Undo(寫 CLR);靠 LSN / pageLSN 比對達成冪等。
  • checkpoint 縮短 redo 起點;WAL 同時是複寫 / PITR / CDC 的資料來源。

快速記憶

方面 說明
是什麼 「先寫日誌再改資料」的持久化協定
為什麼 用便宜的循序 log 換掉昂貴的隨機資料頁寫,同時保證崩潰不丟/可回滾
怎麼復原 Analysis → Redo → Undo(ARIES),用 LSN 保證冪等
副產品 複寫、時間點還原、CDC 全靠這條 log

延伸


建立日期:2026-07-02

🔗相關文章