定义
**WAL (Write-Ahead Logging,预写日志)**是数据库保证事务持久性(Durability)和原子性(Atomicity)的核心机制。其基本原则是:在修改数据页之前,必须先将修改记录写入日志文件并刷盘。
WAL 的核心思想:
- 日志先行:先写日志,后写数据
- 顺序 I/O:日志顺序写入,性能高
- 异步刷盘:数据页可以延迟刷盘
- 崩溃恢复:根据日志重做或回滚
这使得数据库能够在保证数据安全的前提下,大幅提升写入性能。
详细笔记
核心原理
为什么需要 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-2000WAL 在不同数据库的实现
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最佳实践
- 根据业务选择刷盘策略:
- 金融系统:
innodb_flush_log_at_trx_commit = 1 - 电商系统:
innodb_flush_log_at_trx_commit = 1或2 - 日志系统:
innodb_flush_log_at_trx_commit = 2或0
- 合理设置 Redo Log 大小:
- 小负载: 256MB-512MB
- 中等负载: 1GB-2GB
- 大负载: 4GB-8GB
- 监控 Redo Log 使用率:
sql
SHOW ENGINE INNODB STATUS\G
-- 查看 "Log sequence number" 和 "Last checkpoint at"
-- 差距不应超过 Redo Log 总大小的 75%- 避免大事务:
- 大事务产生大量 Redo Log
- 可能导致 Log Buffer 溢出
- 分批提交
- 定期备份:
- WAL 只能保证崩溃恢复
- 不能替代备份
- 定期全量 + 增量备份
- 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