Skip to content

SQL Server 事务日志与 Write-Ahead Logging

你知道为什么 SQL Server 能保证「事务提交后数据不丢失」吗?

很多人会说:「因为有 redo log 嘛。」

但 SQL Server 的日志机制和 MySQL 的 redo log 有何不同?什么是 Write-Ahead Logging?为什么日志文件满了系统会卡死?

今天,我们来深入理解 SQL Server 的事务日志体系。


WAL 机制:日志优先原则

Write-Ahead Logging 的定义

Write-Ahead Logging(WAL) = 在数据页写入磁盘之前,对应的日志记录必须先写入磁盘

这是数据库保证事务 ACID 中「持久性(Durability)」的核心机制。

SQL Server 的 WAL 实现

┌─────────────────────────────────────────────────────────────┐
│                 SQL Server WAL 执行流程                      │
│                                                              │
│  事务 UPDATE Orders SET status='shipped' WHERE order_id=1001 │
│                                                              │
│  步骤 1:修改数据页                                          │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 缓冲池中的数据页被修改                                  │ │
│  │ 状态:pending → shipped                                 │ │
│  │ 标记为脏页(Dirty Page)                               │ │
│  └────────────────────────────────────────────────────────┘ │
│                          ↓                                   │
│  步骤 2:写入日志(WAL 核心!)                             │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 日志记录写入日志缓冲区:                               │ │
│  │ - Before Image:status = 'pending'                    │ │
│  │ - After Image:status = 'shipped'                    │ │
│  │ - 日志缓冲区刷写到磁盘(LDF 文件)                     │ │
│  │ ✓ 日志落盘完成                                        │ │
│  └────────────────────────────────────────────────────────┘ │
│                          ↓                                   │
│  步骤 3:提交事务                                          │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ COMMIT 日志写入并落盘                                 │ │
│  │ ✓ COMMIT 落盘 = 事务完成                              │ │
│  └────────────────────────────────────────────────────────┘ │
│                          ↓                                   │
│  步骤 4:异步刷写数据页                                     │
│  ┌────────────────────────────────────────────────────────┐ │
│  │ 后台检查点(Checkpoint)异步将脏页写入磁盘             │ │
│  │ 数据页落盘可以延迟,不影响事务持久性                    │ │
│  └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

为什么 WAL 能保证持久性?

场景:事务提交后立即断电

  1. COMMIT 日志已落盘 ✓
  2. 数据页还在内存,没落盘 ✓
  3. 数据库重启 → 从日志恢复
    • 发现 LSN 有 COMMIT 记录 → 重做(REDO)该事务

场景:事务执行中断电

  1. 只有部分日志,没有 COMMIT
  2. 数据库重启 → 从日志恢复
    • 发现 LSN 是未提交事务 → 回滚(UNDO)该事务

结论:只要日志在,数据就在。


日志序列号(LSN)

LSN 的结构

LSN(Log Sequence Number)是日志记录的「身份证」,6 字节整数,单调递增。

sql
-- 查看当前 LSN
SELECT 
    current_lsn,
    first_lsn,
    last_lsn
FROM sys.dm_tran_database_transactions
WHERE database_id = DB_ID();

LSN 的作用:

  • 定位:通过 LSN 可以精确定位到某条日志记录
  • 连续性:日志在逻辑上连续,用于恢复
  • 顺序:保证 WAL 的「日志优先」原则

LSN 与恢复

日志序列:
LSN 0001: BEGIN TRAN TX_1001
LSN 0002: UPDATE Orders SET status='shipped' WHERE order_id=1001
LSN 0003: COMMIT TRAN TX_1001
LSN 0004: BEGIN TRAN TX_1002
LSN 0005: UPDATE Products SET stock=stock-1 WHERE product_id=200
(断电!)

恢复过程:
1. 检查点 LSN = 0001(假设)
2. 从 0001 开始 REDO:找到所有已提交事务,重做
   - TX_1001 已提交 → 重做 UPDATE
   - TX_1002 未提交 → 回滚 UPDATE
3. 恢复完成,数据库一致

检查点(Checkpoint)

检查点的作用

检查点 = 将脏页刷写到磁盘的标记点,用于缩短崩溃恢复时间。

┌─────────────────────────────────────────────────────────────┐
│                    检查点机制                               │
│                                                              │
│  磁盘日志文件:                                             │
│  ┌─────────────────────────────────────────────────────┐ │
│  │ LSN 001: BEGIN TRAN                                  │ │
│  │ LSN 002: UPDATE ...                                  │ │
│  │ LSN 003: COMMIT                                     │ │
│  │ LSN 004: UPDATE ...                                 │ │
│  │ LSN 005: UPDATE ...                                 │ │
│  │ ↑                                                    │ │
│  │ 检查点 LSN = 005                                     │ │
│  │ 表示:LSN 005 之前的脏页 已写入磁盘                    │ │
│  └─────────────────────────────────────────────────────┘ │
│                                                              │
│  恢复时,只需从检查点 LSN 开始恢复                           │
│  而不需要从日志开头开始                                     │
└─────────────────────────────────────────────────────────────┘

检查点触发时机

触发条件说明
间隔时间默认约 1 分钟
日志空间阈值日志 70% 满时
手动触发执行 CHECKPOINT
备份前数据库备份前自动触发
实例关闭正常关闭时触发
sql
-- 手动触发检查点
CHECKPOINT;

-- 查看检查点信息
SELECT 
    database_name,
    last_checkpoint_recovery_lsn,
    last_checkpoint_lsn,
    checkpoint_lsn
FROM sys.dm_db_recovery_status;

恢复模式与日志管理

三种恢复模式

恢复模式日志备份适用场景日志截断
FULL需要重要业务,需要 PITR备份后
BULK_LOGGED需要大批量导入备份后
SIMPLE不需要开发/测试检查点时

SIMPLE 恢复模式

日志序列:LSN 001 → 002 → 003 → 004 → 005 → 006

                                          检查点

                                          日志可截断
  • 检查点后,已提交事务的日志可以被覆盖
  • 日志不会无限增长
  • 代价:只能恢复到上次备份,无法 Point-in-Time 恢复

FULL 恢复模式

日志序列:LSN 001 → 002 → 003 → 004 → 005 → 006 → 007 → 008 → 009

                                              ├─ 日志备份 1

                                              └── 010 → 011 → 012 → 013

                                                          ├─ 日志备份 2

                                                          └── 日志继续增长...

关键点

  • 日志必须通过备份才能截断
  • 不备份日志 → 日志无限增长 → 磁盘满
  • 可以恢复到任意时间点(PITR)

日志备份与截断

sql
-- 完整备份
BACKUP DATABASE SalesDB TO DISK = 'D:\Backup\SalesDB_Full.bak'
WITH COMPRESSION, CHECKSUM;

-- 日志备份(FULL 模式下必须)
BACKUP LOG SalesDB TO DISK = 'D:\Backup\SalesDB_Log.trn'
WITH COMPRESSION, CHECKSUM;

-- 恢复到某个时间点
RESTORE LOG SalesDB FROM DISK = 'D:\Backup\SalesDB_Log.trn'
WITH STOPAT = '2024-03-15 14:30:00';

日志文件管理

日志文件结构

SQL Server 日志文件由多个 VLF(Virtual Log File) 组成:

┌─────────────────────────────────────────────────────────────┐
│                 LDF 日志文件                                │
│                                                              │
│  ┌─────────┬─────────┬─────────┬─────────┬─────────┐    │
│  │  VLF 1  │  VLF 2  │  VLF 3  │  VLF 4  │  VLF 5  │    │
│  │ (活动)  │ (活动)  │ (活动)  │ (可用)  │ (可用)  │    │
│  └─────────┴─────────┴─────────┴─────────┴─────────┘    │
│       ↑           ↑           ↑                               │
│    活动日志      活动日志      活动日志                        │
│       └───────────┴───────────┘                             │
│                    VLF 1-3:不能截断                          │
│                              VLF 4-5:可以重用                │
└─────────────────────────────────────────────────────────────┘

查看 VLF 状态

sql
-- 查看 VLF 信息
DBCC LOGINFO('SalesDB');
-- Status = 0:可重用
-- Status = 2:活动(已使用且不能截断)

-- 查看日志使用情况
DBCC SQLPERF(LOGSPACE);
-- Database Name  Log Size (MB)  Log Space Used (%)  Status
-- SalesDB        500.00         45.23              0

日志增长问题

常见原因

  1. 长时间运行的事务(日志不能截断)
  2. 日志未备份(FULL 模式)
  3. 复制未同步
  4. 镜像配置问题

解决方案

sql
-- 1. 检查长时间运行的事务
SELECT 
    session_id,
    transaction_id,
    transaction_begin_time,
    database_id,
    database_transaction_log_record_count
FROM sys.dm_tran_database_transactions
WHERE database_id = DB_ID('SalesDB')
ORDER BY transaction_begin_time;

-- 2. 备份日志(FULL 模式)
BACKUP LOG SalesDB TO DISK = 'D:\Backup\SalesDB_Log.trn' WITH TRUNCATE_ONLY;
-- TRUNCATE_ONLY 只截断,不备份

-- 3. 收缩日志文件
DBCC SHRINKFILE('SalesDB_Log', 100);  -- 收缩到 100MB

日志与崩溃恢复

崩溃恢复过程

数据库重启 → 崩溃恢复

阶段 1:分析阶段
┌─────────────────────────────────────────────────────────────┐
│ 读取最后一个检查点的信息                                     │
│ 确定需要恢复的日志范围                                       │
└─────────────────────────────────────────────────────────────┘

阶段 2:重做阶段(REDO)
┌─────────────────────────────────────────────────────────────┐
│ 从检查点开始,扫描所有日志记录                               │
│ 对每个已提交事务,执行重做                                   │
│ 将数据页恢复到最新状态                                      │
└─────────────────────────────────────────────────────────────┘

阶段 3:回滚阶段(UNDO)
┌─────────────────────────────────────────────────────────────┐
│ 对未提交事务,执行回滚                                       │
│ 使用 Before Image 恢复到修改前状态                           │
└─────────────────────────────────────────────────────────────┘


                    数据库恢复完成,在线

快速恢复(Fast Recovery)

SQL Server 2005+ 支持快速恢复:

传统恢复:
分析 → 重做 → 回滚 → 在线

      需要等待所有回滚完成

快速恢复:
分析 → 重做 → 在线 → 回滚(后台继续)

         用户可以立即连接访问

      未提交事务在后台继续回滚

与 MySQL 的对比

特性SQL ServerMySQL (InnoDB)
日志类型统一日志(LDF)redo log + undo log
WAL 实现日志先写日志先写
崩溃恢复分析→重做→回滚启动时恢复
恢复模式FULL/SIMPLE/BULK_LOGGEDinnodb_flush_log_at_trx_commit
日志备份支持需要配合 binlog

面试追问方向

  • 什么是 Write-Ahead Logging?SQL Server 如何实现 WAL?
  • 检查点和崩溃恢复有什么关系?
  • FULL 恢复模式下,日志无限增长的原因是什么?
  • 什么是 VLF?如何优化 VLF 数量?
  • 快速恢复(Fast Recovery)是什么?有什么好处?
  • SQL Server 的日志机制和 MySQL 的 redo log 有什么不同?

下一步

理解了 WAL 和事务日志,我们来看 SQL Server 隔离级别:脏读、不可重复读、幻读、快照隔离,学习并发控制的核心机制。

基于 VitePress 构建