3. 事务日志与恢复架构

SQL Server 的恢复能力并不只依赖“有没有做备份”,更依赖事务日志是否被正确管理。事务日志既是数据库的“变更流水账”,也是恢复架构的核心:它决定了数据库能恢复到哪个时间点、能否做日志备份、以及崩溃后能否自动恢复到一致状态。理解这一节,需要把四个概念串起来看:虚拟日志文件(VLF)是日志的物理组织单位,日志截断决定空间能否被复用,检查点决定恢复时需要重做多少日志,而恢复过程(分析、重做、撤销)则把这些机制在启动或还原时真正执行出来。

虚拟日志文件:日志的物理骨架

事务日志在物理上是一个或多个日志文件(.ldf),但 SQL Server 并不直接按字节逐条写入,而是把日志文件切分成若干虚拟日志文件(VLF)。VLF 是日志空间分配和截断的最小管理单位。日志文件创建时,SQL Server 会按文件大小和增长方式一次性划分出多个 VLF;之后日志按顺序写入,写满一个 VLF 再写下一个,形成逻辑上的环形结构。

VLF 的数量和大小受日志文件初始大小、自动增长增量影响。如果日志文件频繁以很小的增量增长,就会产生大量小 VLF,导致日志备份、恢复和截断时扫描开销上升,表现为日志操作变慢。可以用下面的命令观察 VLF 分布:

DBCC LOGINFO('YourDatabase');
-- 每一行代表一个 VLF,Status=2 表示该 VLF 仍处于活动状态,不能被截断

判断 VLF 是否健康,重点看两点:一是 VLF 总数是否过多(例如数千个),二是活动 VLF 是否长期占满日志空间。若日志文件总是增长到很大却无法截断,通常不是 VLF 本身的问题,而是日志截断被某些因素阻塞。

日志截断:空间复用的前提

日志截断不是“删除日志”,而是把日志中不再需要用于恢复的部分标记为可复用,使日志空间可以循环使用。一个日志记录在什么情况下不再需要?取决于数据库的恢复模式:

  • 简单恢复模式:检查点之后,日志即可被截断。因为不需要日志备份,日志只用于崩溃恢复和当前活动事务。
  • 完整恢复模式:日志必须等到日志备份完成后才能截断。日志备份把日志链中的一段复制到备份设备,之后这部分日志才可复用。
  • 大容量日志恢复模式:与完整恢复模式类似,但某些大容量操作会减少日志记录量,代价是时间点恢复能力受限。

因此,完整恢复模式下如果长期不做日志备份,日志会持续增长,直到磁盘写满。常见阻塞日志截断的原因包括:没有日志备份、存在长时间运行的活动事务、数据库镜像或可用性组中的日志发送延迟、复制未同步、以及某些延迟事务。排查时可以先查日志空间使用:

DBCC SQLPERF(LOGSPACE);
-- 查看 Log Space Used (%) 与 Status
SELECT name, log_reuse_wait_desc
FROM sys.databases
WHERE name = 'YourDatabase';
-- log_reuse_wait_desc 会直接告诉你日志为什么不能截断

其中 log_reuse_wait_desc 是最关键的判断依据:NOTHING 表示可以截断;LOG_BACKUP 表示需要日志备份;ACTIVE_TRANSACTION 表示有长事务;AVAILABILITY_REPLICA 表示可用性组副本延迟。看到具体原因,才能对症处理,而不是盲目收缩日志文件。

检查点:缩短恢复时间的机制

检查点(Checkpoint)的作用是把内存中已修改的数据页(脏页)刷写到数据文件,并在日志中记录一个已知的、所有脏页都已落盘的起点。这样在崩溃恢复时,重做阶段只需要从最近一次检查点开始扫描日志,而不必从日志开头重做全部历史。

检查点分为多种:自动检查点由 SQL Server 按恢复间隔自动触发;间接检查点用于控制目标恢复时间;手动检查点通过 CHECKPOINT 命令执行。检查点越频繁,恢复时需要重做的日志越少,但 I/O 压力越大。可以用 recovery interval 配置间接影响自动检查点频率,但现代版本更推荐使用间接检查点来设定目标恢复时间。

检查点只保证“数据页已落盘”,并不等于日志可以截断。日志能否截断仍取决于恢复模式和日志备份,这是两个独立机制。

恢复过程:分析、重做、撤销

当数据库启动、还原或崩溃后重新联机时,SQL Server 会执行恢复过程,分为三个阶段:

  1. 分析阶段(Analysis):扫描日志,确定哪些事务已提交、哪些未提交,以及需要重做和撤销的范围。它构建脏页表和活动事务表。
  2. 重做阶段(Redo):把已提交但尚未写入数据文件的操作重新应用,保证已提交事务不丢失(Durability)。
  3. 撤销阶段(Undo):把未提交的事务回滚,保证数据库回到一致状态(Atomicity)。

这三个阶段共同保证 ACID 中的原子性和持久性。恢复时间主要取决于重做和撤销的日志量,而日志量又受检查点频率和长事务影响。如果存在长时间未提交的事务,撤销阶段可能耗时很长,数据库在恢复完成前无法接受连接。

事务日志与恢复架构总览 事务日志文件 (.ldf) VLF1 VLF2 VLF3 VLF4 VLF5 VLF6 VLF7 VLF8 橙色=活动 红色=待截断 绿色=可复用 日志截断条件 简单恢复:检查点后即可截断 完整恢复:需日志备份完成 大容量日志:部分操作少记日志 阻塞:长事务 / 副本延迟 / 复制 检查点 Checkpoint 脏页刷写到数据文件 记录重做起点 缩短崩溃恢复时间 不决定日志能否截断 恢复过程:分析 → 重做 → 撤销 ① 分析 Analysis 扫描日志 识别已提交/未提交事务 构建脏页表 确定重做/撤销范围 ② 重做 Redo 应用已提交操作 保证持久性 Durability 从检查点开始扫描 已提交事务不丢失 ③ 撤销 Undo 回滚未提交事务 保证原子性 Atomicity 释放锁与资源 数据库恢复一致状态 日志链完整性

日志链完整性:恢复能力的生命线

在完整恢复模式下,事务日志备份构成一条日志链:从最近一次完整备份开始,经过一系列日志备份,直到当前日志尾部。只要日志链不断裂,就可以恢复到链上任意时间点。日志链断裂的典型原因包括:

  • 把数据库从完整恢复模式切换为简单恢复模式,再切回完整模式,此时必须重新做完整备份才能重建日志链。
  • 执行了没有日志备份的完整备份或差异备份,虽然不会直接断链,但恢复时仍需从完整备份起点开始。
  • 日志备份文件丢失或损坏,导致链中缺少一段。
  • 使用了 WITH NO_LOG 或 WITH TRUNCATE_ONLY 等旧式截断命令(已废弃),会直接破坏日志链。

验证日志链是否完整,最直接的方法是查看 msdb 中的备份历史,确认完整备份与日志备份的先后顺序和连续性:

SELECT database_name, backup_start_date, type, first_lsn, last_lsn
FROM msdb.dbo.backupset
WHERE database_name = 'YourDatabase'
ORDER BY backup_start_date DESC;
-- type: D=完整, I=差异, L=日志
-- 检查日志备份的 first_lsn 是否衔接上一段的 last_lsn

如果发现日志链断裂,唯一可靠的修复方式是立即执行一次完整备份,重新建立新的日志链起点,然后继续按计划做日志备份。不要试图用旧日志备份拼接断裂的链,那样恢复时会在断点处失败。

完整恢复模式下,日志备份不是可选项,而是日志截断和日志链维持的必要条件。只做完整备份而不做日志备份,日志文件会持续增长,且无法做时间点恢复。

把机制串起来:一次典型恢复的判断路径

假设数据库处于完整恢复模式,每天 02:00 做完整备份,每 15 分钟做日志备份。某天 10:07 发生误删表操作,需要恢复到 10:06 的状态。恢复路径是:先还原最近一次完整备份(02:00),再按顺序还原 02:00 到 10:06 之间的所有日志备份,最后用 STOPAT 指定时间点:

RESTORE DATABASE YourDatabase
FROM DISK = 'D:\Backup\Full_0200.bak'
WITH NORECOVERY;

RESTORE LOG YourDatabase
FROM DISK = 'D:\Backup\Log_1000.trn'
WITH NORECOVERY;

RESTORE LOG YourDatabase
FROM DISK = 'D:\Backup\Log_1015.trn'
WITH STOPAT = '2025-01-15 10:06:00', RECOVERY;

这里 NORECOVERY 表示还原后不恢复数据库,继续等待后续日志;STOPAT 指定恢复到的时间点;RECOVERY 表示恢复完成、数据库可访问。整个过程中,日志链必须连续,否则在断点处会报错。恢复完成后,SQL Server 会自动执行分析、重做、撤销三个阶段,把数据库带到指定时间点的一致状态。

总结来看,VLF 决定日志的物理组织效率,日志截断决定空间能否复用,检查点决定恢复时需要重做多少,恢复过程决定崩溃后如何回到一致状态,而日志链完整性决定能否做时间点恢复。五者环环相扣,任何一环出问题,都会直接影响 RTO 和 RPO。日常运维中,应定期检查 log_reuse_wait_desc、VLF 数量、日志备份连续性和恢复模式,确保恢复架构始终处于可用状态。