WAL是什么、有什么用,这里就不赘述了。直接看伪代码,对整个流程就有数了:
关键结构体
- WAL Log Record (日志记录)
- WAL Segment (WAL 分段)
- Checkpoint Record (检查点记录)
// --------------------------------------------------------
// 描述数据库操作的基本单元,用于 Redo 和 Undo 恢复
// --------------------------------------------------------
struct WAL_Log_Record {
// 日志序列号 (Log Sequence Number, LSN):全局唯一且严格递增。
UInt64 LSN;
// 记录类型:BEGIN, COMMIT, ABORT, UPDATE, INSERT, CLR (Compensation Log Record,见下文) 等
Enum Type;
// 关联的事务 ID (Transaction ID)
Int32 TransactionID;
// 同一事务中上一条日志记录的 LSN。用于逆向扫描 (Backward Scan) 和回滚 (Undo)
UInt64 PrevLSN;
// 被修改的数据页 ID
Int32 PageID;
// 撤销(Undo)所需的数据:记录修改前的旧值
Byte[] UndoData;
// 重做(Redo)所需的数据:记录修改后的新值
Byte[] RedoData;
// 对于 CLR 记录,可能包含一个指向下一条需要 Undo 的记录的 LSN (UndoNextLSN)
UInt64 UndoNextLSN;
};
// --------------------------------------------------------
// WAL 文件在文件系统中的物理组织形式,便于管理和归档。
// --------------------------------------------------------
struct WAL_Segment {
// 分段文件的 ID,通常按时间递增
UInt32 SegmentID;
// 存储在文件系统中的路径 (e.g., "wal_00000001.log")
String FilePath;
// 此 Segment 包含的第一条日志记录的 LSN
UInt64 StartLSN;
// 此 Segment 包含的最后一条日志记录的 LSN
// 在 Segment ACTIVE 时,此值可能为 0 或指向当前写入的 LSN
UInt64 EndLSN;
// 状态:ACTIVE (正在写入), ARCHIVED (已归档), REMOVABLE (可移除/可删除)
Enum Status;
};
// --------------------------------------------------------
// checkpoint会产生一种特殊的 WAL 记录,用于标记一个恢复点
// --------------------------------------------------------
struct Checkpoint_Record {
// 此 Checkpoint 记录自身的 LSN
UInt64 CheckpointLSN;
// Redo 起始 LSN (Redo Start LSN):恢复操作应从这个 LSN 开始进行 Redo
// 通常是 Checkpoint 发生时,最早的活动的事务写入的第一条日志的 LSN
UInt64 RedoStartLSN;
// 活动事务表 (Active Transaction Table, ATT):
// Checkpoint 发生时,所有正在运行的事务(Transaction)及其状态的快照
Map<TxID, Status> ActiveTransactionTable;
// 脏页表 (Dirty Page Table, DPT):
// Checkpoint 发生时,所有脏页(Dirty Page)及其对应的 LSN (即需要 Redo 的 LSN)
Map<PageID, LSN> DirtyPageTable;
};
核心操作:WAL写、Checkpoint
1. 写入 WAL 记录 (AppendWALRecord)
FUNCTION AppendWALRecord(Record rec):
// 1. 获取当前 Segment
current_segment = WALManager.GetCurrentSegment()
// 2. 检查 Segment 是否已满
IF current_segment.IsFull():
// a. 关闭当前 Segment
WALManager.FinalizeSegment(current_segment)
// b. 创建新的 Segment (触发 Segment 分段)
new_segment = WALManager.CreateNewSegment()
current_segment = new_segment
// 3. 赋值 LSN
rec.LSN = WALManager.GetNextLSN()
// 4. 将记录写入 Segment
current_segment.Write(rec)
// 5. 将记录强制刷新到磁盘 (Force Flush)
// 根据配置,可能是每条记录都 Flush,或周期性 Flush,或 COMMIT 时 Flush。
IF rec.Type == COMMIT OR SystemConfig.WALFlushMode == IMMEDIATE:
current_segment.FlushToDisk()
RETURN rec.LSN
2. 执行 Checkpoint
Checkpoint 会周期性或按需执行,以减少崩溃恢复时间。
FUNCTION PerformCheckpoint():
// 1. 暂停所有新的数据库操作(或使用 Latch/Lock 机制)
System.PauseNewOperations()
// 2. 收集恢复所需的状态信息
checkpoint_rec = New CheckpointRecord()
checkpoint_rec.RedoStartLSN = WALManager.FindMinLSNOfActiveTransactions()
checkpoint_rec.ActiveTransactionTable = TransactionManager.GetActiveTransactions()
checkpoint_rec.DirtyPageTable = BufferManager.GetDirtyPageTable()
// 3. 将 Checkpoint 记录写入 WAL
checkpoint_lsn = AppendWALRecord(checkpoint_rec)
// 4. 将所有脏页刷新到数据文件
BufferManager.FlushAllDirtyPages()
// 5. 再次强制刷新 WAL,确保 Checkpoint 记录和之前的日志都已持久化
WALManager.FlushToDisk(checkpoint_lsn)
// 6. 写入 Checkpoint Pointer 文件
// 在一个单独的文件 (e.g., `checkpoint_pointer`) 中记录最后成功的 Checkpoint 的 LSN?
System.UpdateCheckpointPointer(checkpoint_lsn)
// 7. 恢复数据库操作
System.ResumeOperations()
// 8. 清理旧的 Segment
SignalWALCleaner(checkpoint_rec.RedoStartLSN)
RETURN checkpoint_lsn
3. 清理旧 Segment
FUNCTION CleanupOldSegments(RequiredMinLSN):
// RequiredMinLSN: 恢复操作所需的最小 LSN (即 Checkpoint 中的 RedoStartLSN)
FOREACH Segment seg IN WALManager.GetAllSegments():
IF seg.EndLSN < RequiredMinLSN:
// 确保 Segment 状态为 ARCHIVED 或类似的非 ACTIVE 状态
IF seg.Status == ARCHIVED OR seg.Status == REMOVABLE:
WALManager.DeleteSegmentFile(seg)
ELSE:
LOG_ERROR("Attempted to delete non-removable segment.")
RETURN SUCCESS
核心操作:从系统崩溃中恢复
崩溃后,还未成功刷盘的事务变更需要redo,这个很好理解。那什么时候会用到undo呢?
-
保证原子性Atomicity:如果一个事务在执行过程中因为用户取消、系统错误、死锁或其他原因abort ,那么该事务做出的一切变更都需要回滚
-
保证隔离性Isolation:在MVCC系统中,undo日志常用于存储旧版本数据。一个事务读取数据时,如果需要看到该数据在它开始时的版本,可通过undo日志日志来重构旧版本数据,从而实现隔离性
如同checkpoint操作,每个undo操作也会产生一条WAL记录,叫CLR (Compensation Log Record)。CLR记录undo的是哪条LSN、下一步应undo哪条LSN。 如此一来,即使系统在执行 Undo 过程中再次崩溃,也能知道从哪里继续回滚。
数据库崩溃恢复的三阶段流程
当数据库系统从崩溃中重启时,它会执行以下三个阶段:
阶段一:分析 (Analysis)
系统从最近的Checkpoint record开始向前扫描 WAL,确定从哪里开始 Redo,以及哪些事务需要 Undo
建立两个表:
- Active Transaction Table (ATT): 记录崩溃时所有正在运行(没有commit或abort记录)的事务。这些事务都需要被undo
- Dirty Page Table (DPT): 记录崩溃时内存中所有脏页及其对应的最小LSN (即 LSN of the Redo start point)
阶段二:Redo - 保证持久性 Durability
将所有已写入 WAL 的修改都应用到数据页上,无论事务是否commit
判断条件:
- 何时 Redo? 如果某条日志记录 的 大于 数据页上的 (该页最后一次被修改时的 ),则说明这条修改在崩溃前没有被写入磁盘,必须使用 重新应用。
- 关键点: 不仅针对已 commit 的事务,也针对未 commit 的事务。这是因为 的目的是让数据库恢复到崩溃前最新的状态(即 末尾的状态),确保所有已写入 的内容都已持久化。
阶段三:Undo - 保证原子性 Atomicity
系统从 中所有事务的最新日志记录开始,逆序扫描 WAL。对于阶段一中确定的,每一个需要回滚的事务的修改记录,使用 进行回滚。