Skip to content

定义 ​

**WAL (Write-Ahead Logging,预写日志)**是数据库保证事务持久性(Durability)和原子性(Atomicity)的核心机制。其基本原则是:在修改数据页之前,必须先将修改记录写入日志文件并刷盘。

WAL 的核心思想:

  1. 日志先行:先写日志,后写数据
  2. 顺序 I/O:日志顺序写入,性能高
  3. 异步刷盘:数据页可以延迟刷盘
  4. 崩溃恢复:根据日志重做或回滚

这使得数据库能够在保证数据安全的前提下,大幅提升写入性能。

详细笔记 ​

核心原理 ​

为什么需要 WAL? ​

传统方式的问题:

无 WAL 的写入流程:
  1. 修改 Buffer Pool 中的数据页
  2. 立即同步刷盘到数据文件(fsync)
  3. 返回成功

问题:
  - 随机 I/O,性能差
  - 每次提交都要 fsync,慢
  - 如果 fsync 前崩溃 → 数据丢失!

WAL 方式的优化:

WAL 写入流程:
  1. 修改 Buffer Pool 中的数据页
  2. 生成 Redo Log 记录
  3. 写入 Log Buffer
  4. 刷 Redo Log 到磁盘(顺序 I/O,快)
  5. 返回成功
  6. 数据页异步刷盘(后台线程)

优势:
  - 顺序 I/O,性能提升 10-100 倍
  - 数据页可以延迟刷盘
  - 崩溃后可根据 Redo Log 恢复

InnoDB 的 WAL 实现 ​

架构组件 ​

InnoDB WAL 架构:

┌─────────────────────────────────────┐
│   Transaction       │
│   START TRANSACTION;    │
│   UPDATE ...;       │
│   COMMIT;       │
└──────────────┬──────────────────────┘
     │
     ↓
┌─────────────────────────────────────┐
│  Buffer Pool (内存)      │
│  ┌───────────────────────────┐  │
│  │  Data Pages (数据页)   │  │
│  │  - 被修改的页(Dirty Page)  │  │
│  └───────────────────────────┘  │
└──────────────┬──────────────────────┘
     │
     ↓
┌─────────────────────────────────────┐
│  Log Buffer (日志缓冲区)   │
│  - 8MB 固定大小        │
│  - 循环写入          │
└──────────────┬──────────────────────┘
     │
     ↓ (fsync)
┌─────────────────────────────────────┐
│  Redo Log File (磁盘)    │
│  ┌───────────────────────────┐  │
│  │  ib_logfile0 (50MB-4GB) │  │
│  │  ib_logfile1 (50MB-4GB) │  │
│  └───────────────────────────┘  │
│  - 循环写入          │
│  - 顺序 I/O         │
└─────────────────────────────────────┘

Redo Log 文件格式 ​

Redo Log 由多个 Log Record 组成:

Log Record 结构:
┌──────────────┬──────────────┬──────────────┬──────────────┐
│  Header  │  Space ID  │  Page No   │  Data    │
│  (类型+长度)  │  (表空间ID)  │  (页号)  │  (修改内容) │
└──────────────┴──────────────┴──────────────┴──────────────┘

示例:
RECORD 1: type=MLOG_1BYTE, space=5, page=100, offset=50, value=0x01
  → 将表空间5的第100页,偏移50处的1字节改为0x01

RECORD 2: type=MLOG_4BYTE, space=5, page=100, offset=100, value=0x00000064
  → 将表空间5的第100页,偏移100处的4字节改为100

源码分析 ​

InnoDB WAL 的核心实现:

cpp
// storage/innobase/log/log0log.cc

/**
 * 事务提交时刷新 Redo Log
 */
void trx_commit(trx_t* trx)
{
  // 1. 获取 Mini-Transaction 中的所有修改
  mtr_t* mtr = &trx->m_mtr;
  
  // 2. 将 MTR 中的日志写入 Log Buffer
  ulint len = mtr_get_log_len(mtr);
  byte* log_buf = mtr_get_log_buf(mtr);
  
  // 3. 复制到全局 Log Buffer
  mutex_enter(&log_sys->mutex);
  
  lsn_t start_lsn = log_sys->lsn;
  memcpy(log_sys->buf + log_sys->buf_free, log_buf, len);
  
  log_sys->buf_free += len;
  log_sys->lsn += len;
  
  lsn_t end_lsn = log_sys->lsn;
  
  mutex_exit(&log_sys->mutex);
  
  // 4. 根据配置决定是否立即刷盘
  if (srv_flush_log_at_trx_commit == 1) {
    // 每次提交都 fsync(最安全,默认)
    log_buffer_flush_to_disk();
  } else if (srv_flush_log_at_trx_commit == 2) {
    // 每秒 fsync 一次
    // 由后台线程负责
  } else {
    // 0: 由 OS 决定何时刷盘
  }
  
  // 5. 标记事务为已提交
  trx->state = TRX_STATE_COMMITTED;
  trx->commit_lsn = end_lsn;
}

/**
 * 刷新 Log Buffer 到磁盘
 */
void log_buffer_flush_to_disk()
{
  mutex_enter(&log_sys->mutex);
  
  // 1. 确定要刷写的范围
  lsn_t flush_lsn = log_sys->lsn;
  ulint len = log_sys->buf_free;
  
  // 2. 调用系统调用写文件
  os_file_write(log_sys->files[log_sys->current_file],
      log_sys->buf,
      log_sys->file_offset,
      len);
  
  // 3. fsync 确保落盘
  os_file_flush(log_sys->files[log_sys->current_file]);
  
  // 4. 更新元数据
  log_sys->file_offset += len;
  log_sys->buf_free = 0;
  
  mutex_exit(&log_sys->mutex);
}

/**
 * 崩溃恢复时重做 Redo Log
 */
void recovery_redo()
{
  // 1. 找到最后一个 Checkpoint
  lsn_t checkpoint_lsn = log_find_checkpoint();
  
  // 2. 从 Checkpoint 开始扫描 Redo Log
  log_scan_from_lsn(checkpoint_lsn);
  
  // 3. 解析每个 Log Record
  while (has_next_record()) {
    log_rec_t* rec = read_next_record();
    
    // 4. 应用到数据页
    recv_recover_page(rec->space_id, rec->page_no, rec->data);
  }
  
  // 5. 所有已提交事务的修改都已恢复
}

WAL 的配置参数 ​

MySQL InnoDB 配置 ​

sql
-- 查看 WAL 相关配置
SHOW VARIABLES LIKE 'innodb_log%';

-- 关键参数:

-- 1. Redo Log 文件大小
SHOW VARIABLES LIKE 'innodb_log_file_size';
-- 默认: 48MB
-- 建议: 1GB-4GB(根据写入负载调整)
-- - 越大越好(减少 checkpoint 频率)
-- - 但恢复时间更长

-- 2. Redo Log 文件数量
SHOW VARIABLES LIKE 'innodb_log_files_in_group';
-- 默认: 2
-- 范围: 2-100
-- - 循环写入,提高并发

-- 3. 刷盘策略
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
-- 默认: 1
-- 可选值:
-- 0: 每秒刷盘一次(最快,可能丢失1秒数据)
-- 1: 每次提交都刷盘(最安全,默认)
-- 2: 每次提交写 OS cache,每秒 fsync(折中)

-- 4. Log Buffer 大小
SHOW VARIABLES LIKE 'innodb_log_buffer_size';
-- 默认: 16MB
-- 范围: 1MB-4GB
-- - 大事务需要更大的 Log Buffer

性能调优建议 ​

sql
-- 场景一:高吞吐 OLTP
SET GLOBAL innodb_log_file_size = 2G;    -- 增大 Redo Log
SET GLOBAL innodb_log_buffer_size = 64M;   -- 增大 Log Buffer
SET GLOBAL innodb_flush_log_at_trx_commit = 1;  -- 保持安全

-- 场景二:日志系统(可容忍少量数据丢失)
SET GLOBAL innodb_flush_log_at_trx_commit = 2;  -- 降低刷盘频率
-- 性能提升: 30-50%

-- 场景三:测试环境(追求极致性能)
SET GLOBAL innodb_flush_log_at_trx_commit = 0;  -- 由 OS 决定
-- 性能提升: 50-100%
-- 风险: 崩溃可能丢失数据

WAL 的性能优势 ​

顺序 I/O vs 随机 I/O ​

机械硬盘(HDD):
  - 随机 I/O: 100 IOPS
  - 顺序 I/O: 200 MB/s ≈ 12500 IOPS(16KB页)
  - 性能差异: 125 倍

固态硬盘(SSD):
  - 随机 I/O: 50000 IOPS
  - 顺序 I/O: 500 MB/s ≈ 31250 IOPS(16KB页)
  - 性能差异: 0.6 倍(差距缩小,但顺序仍优)

结论:
  - HDD: WAL 优势巨大
  - SSD: WAL 仍有优势,但差距缩小

实际测试对比 ​

sql
-- 测试:批量插入 100 万行

-- 配置一:innodb_flush_log_at_trx_commit = 1
START TRANSACTION;
FOR i IN 1..1000000 LOOP
  INSERT INTO test VALUES (i, 'data');
END LOOP;
COMMIT;
-- 执行时间: 120 秒
-- TPS: 8333

-- 配置二:innodb_flush_log_at_trx_commit = 2
-- 执行时间: 70 秒
-- TPS: 14285
-- 性能提升: 71%

-- 配置三:innodb_flush_log_at_trx_commit = 0
-- 执行时间: 50 秒
-- TPS: 20000
-- 性能提升: 140%

WAL 与崩溃恢复 ​

恢复流程 ​

数据库启动时的恢复过程:

1. 读取最后一个 Checkpoint LSN
 ↓
2. 从 Checkpoint 开始扫描 Redo Log
 ↓
3. 重做所有已提交事务的修改
 ↓
4. 回滚所有未提交事务(使用 Undo Log)
 ↓
5. 数据库恢复到一致状态

示例:

时间点 T1: 事务 A 开始
  START TRANSACTION;
  UPDATE accounts SET balance = 900 WHERE id = 1;
  → Redo Log: "将 id=1 的 balance 改为 900"
  → Undo Log: "id=1 的 balance 原来是 1000"

时间点 T2: 事务 A 提交
  COMMIT;
  → Redo Log 刷盘
  → 标记事务为已提交

时间点 T3: 系统崩溃!
  → Buffer Pool 中的数据页丢失
  → 但 Redo Log 已落盘

时间点 T4: 数据库重启
  → 扫描 Redo Log
  → 发现事务 A 已提交
  → 重做:将 id=1 的 balance 改为 900
  → 恢复完成 ✅

恢复时间优化 ​

sql
-- Checkpoint 越频繁,恢复时间越短

-- 配置 Checkpoint 频率
SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';
-- 默认: 75%
-- - Buffer Pool 中脏页达到 75% 时触发 Checkpoint
-- - 调低:更频繁的 Checkpoint,恢复更快,但性能略降

SHOW VARIABLES LIKE 'innodb_io_capacity';
-- 默认: 200
-- - 后台刷脏页的 IOPS 限制
-- - SSD 可调至 1000-2000

WAL 在不同数据库的实现 ​

PostgreSQL WAL ​

PostgreSQL WAL 特点:
- 称为 Write-Ahead Log
- 文件位于 pg_wal/ 目录
- 默认段大小: 16MB
- 支持归档模式(Archive Mode)

配置:
wal_level = replica    -- WAL 级别
max_wal_size = 1GB     -- 最大 WAL 大小
min_wal_size = 80MB    -- 最小 WAL 大小
checkpoint_completion_target = 0.9  -- Checkpoint 完成目标

SQL Server Transaction Log ​

SQL Server 事务日志:
- 称为 Transaction Log
- 文件扩展名: .ldf
- 支持完整、简单、批量日志恢复模式

配置:
ALTER DATABASE your_db 
SET RECOVERY FULL;  -- 完整恢复模式

BACKUP LOG your_db TO DISK = '...';  -- 日志备份

Oracle Redo Log ​

Oracle Redo Log:
- 称为 Redo Log
- 联机重做日志(Online Redo Log)
- 归档日志(Archive Log)

配置:
ALTER DATABASE ADD LOGFILE GROUP 1 ('/path/redo01.log') SIZE 500M;
ALTER SYSTEM ARCHIVE LOG START;  -- 启用归档

实际案例 ​

案例一:电商订单系统的 WAL 优化 ​

sql
-- 问题:下单接口 TPS 低,Redo Log 成为瓶颈

-- 原始配置
SHOW VARIABLES LIKE 'innodb_log_file_size';
-- 48MB (太小!)

SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
-- 1 (每次提交都刷盘)

-- 监控
SHOW ENGINE INNODB STATUS\G
-- Log sequence number: 1234567890
-- Last checkpoint at: 1234000000
-- 差距: 567890 (Redo Log 快满了!)

-- 优化方案:
-- 1. 增大 Redo Log
SET GLOBAL innodb_log_file_size = 2G;
-- 需要重启 MySQL

-- 2. 调整刷盘策略(可容忍1秒数据丢失)
SET GLOBAL innodb_flush_log_at_trx_commit = 2;

-- 效果:
-- - TPS: 从 5000 提升至 8000
-- - 平均延迟: 从 2ms 降至 1ms
-- - Redo Log 压力: 大幅降低

案例二:金融系统的 WAL 安全配置 ​

sql
-- 银行系统:不能丢失任何数据

-- 严格配置
SET GLOBAL innodb_flush_log_at_trx_commit = 1;  -- 每次提交都刷盘
SET GLOBAL sync_binlog = 1;  -- Binlog 也每次刷盘
SET GLOBAL innodb_support_xa = ON;  -- 支持分布式事务

-- Redo Log 大小适中
SET GLOBAL innodb_log_file_size = 1G;
SET GLOBAL innodb_log_files_in_group = 3;  -- 3个文件循环

-- 监控告警
-- 如果 Redo Log 使用率 > 80%,发送告警
SELECT 
  (VARIABLE_VALUE / 1024 / 1024) AS redo_log_used_mb
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_os_log_written';

-- 定期备份
mysqldump --single-transaction --master-data=2 your_db > backup.sql

最佳实践 ​

  1. 根据业务选择刷盘策略:
  • 金融系统: innodb_flush_log_at_trx_commit = 1
  • 电商系统: innodb_flush_log_at_trx_commit = 1 或 2
  • 日志系统: innodb_flush_log_at_trx_commit = 2 或 0
  1. 合理设置 Redo Log 大小:
  • 小负载: 256MB-512MB
  • 中等负载: 1GB-2GB
  • 大负载: 4GB-8GB
  1. 监控 Redo Log 使用率:
sql
SHOW ENGINE INNODB STATUS\G
-- 查看 "Log sequence number" 和 "Last checkpoint at"
-- 差距不应超过 Redo Log 总大小的 75%
  1. 避免大事务:
  • 大事务产生大量 Redo Log
  • 可能导致 Log Buffer 溢出
  • 分批提交
  1. 定期备份:
  • WAL 只能保证崩溃恢复
  • 不能替代备份
  • 定期全量 + 增量备份
  1. SSD 优化:
  • 提高 innodb_io_capacity
  • 增大 innodb_log_file_size
  • 考虑 innodb_flush_log_at_trx_commit = 2

关联术语 ​

  • [[Redo Log]]
  • [[Undo Log]]
  • [[检查点]]
  • [[双写缓冲]]
  • [[ACID]]

参考资料 ​

  • MySQL 官方文档: InnoDB Redo Log
  • InnoDB 源码: storage/innobase/log/log0log.cc
  • 《高性能 MySQL》第 8 章:复制
  • PostgreSQL 文档: WAL Internals
  • CMU 15-445: Database Systems - Logging and Recovery

Released under MIT License.