日志系统
问题
MySQL 的 redo log、undo log、binlog 分别是什么?WAL 机制是什么?两阶段提交如何保证数据一致性?
答案
一、三大日志概览
| 日志 | 所属层 | 作用 | 写入时机 |
|---|---|---|---|
| redo log | InnoDB 引擎层 | 保证 持久性(崩溃恢复) | 事务执行中,先写 Buffer |
| undo log | InnoDB 引擎层 | 保证 原子性(回滚)+ MVCC | 修改前写入 |
| binlog | Server 层 | 归档日志,主从复制、备份 | 事务提交时写入 |
二、redo log(重做日志)
为什么需要 redo log?
InnoDB 修改数据时先修改 Buffer Pool 中的内存页(脏页),如果每次修改都立即写磁盘,随机 IO 开销巨大。
先写日志,再写磁盘。修改数据前先将变更写入 redo log(顺序写,速度快),然后修改内存。即使宕机,也能通过 redo log 恢复数据。
redo log 的写入流程
更新操作 → 写 redo log buffer → (flush) → 写 OS page cache → (fsync) → 写磁盘文件
刷盘策略(innodb_flush_log_at_trx_commit):
| 值 | 行为 | 性能 | 安全性 |
|---|---|---|---|
| 0 | 每秒批量写入和 fsync | 最高 | 可能丢 1 秒数据 |
| 1 | 每次提交都 fsync | 最低 | 不丢数据 |
| 2 | 每次提交写 page cache,每秒 fsync | 中间 | OS 崩溃可能丢 1 秒 |
innodb_flush_log_at_trx_commit = 1 是唯一能保证不丢数据的配置。只有在能容忍丢数据的场景(如导入数据)才临时设为 0 或 2。
redo log 的循环写入
redo log 是固定大小的循环文件组(默认两个文件,每个 48MB):
write pos →
┌──────────────────────────────┐
│ ib_logfile0 │ ib_logfile1 │
└──────────────────────────────┘
← checkpoint
- write pos:当前写入位置,向前推进
- checkpoint:已刷到磁盘的位置
- write pos 追上 checkpoint 时,必须先推进 checkpoint(将脏页刷盘)
三、undo log(回滚日志)
两大用途
- 事务回滚:记录修改前的数据(逻辑日志),回滚时执行反向操作
- MVCC 版本链:多个版本通过 undo log 串成版本链,支持快照读
不同操作的 undo log
| 操作 | undo log 内容 | 类型 |
|---|---|---|
| INSERT | 记录主键,回滚时 DELETE | insert undo log |
| DELETE | 记录整行数据,回滚时 INSERT | update undo log |
| UPDATE | 记录旧值,回滚时恢复 | update undo log |
- insert undo log:事务提交后可立即删除(新插入的行对其他事务不可见)
- update undo log:需要保留到没有活跃 Read View 引用时才能被 purge 线程回收
四、binlog(归档日志)
binlog vs redo log
| 对比项 | redo log | binlog |
|---|---|---|
| 所属层 | InnoDB 引擎 | Server 层 |
| 日志类型 | 物理日志(页级修改) | 逻辑日志(SQL 语句) |
| 写入方式 | 循环写,空间固定 | 追加写,文件切换 |
| 用途 | 崩溃恢复 | 主从复制、数据备份 |
| 所有引擎 | 仅 InnoDB | 所有引擎都有 |
binlog 的三种格式
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | SQL 语句本身 | 日志量小 | 不确定函数(NOW()、UUID())主从不一致 |
| ROW | 每行数据的变更 | 精确,不会不一致 | 日志量大 |
| MIXED | 自动选择 | 折中 | 仍有边界问题 |
MySQL 8.0 默认 ROW 格式,主从一致性最好,是推荐选择。
binlog 的写入流程
事务中 → 写 binlog cache(线程内存) → 事务提交 → write 到 page cache → fsync 到磁盘
sync_binlog 参数:
| 值 | 行为 |
|---|---|
| 0 | 不主动 fsync,由 OS 决定 |
| 1 | 每次提交都 fsync(最安全) |
| N | 每 N 次提交 fsync |
五、两阶段提交
为什么需要两阶段提交?
redo log 和 binlog 是两个独立的日志系统。如果写入不是原子的:
- 先写 redo log 后写 binlog:宕机时 redo log 恢复了数据,但 binlog 缺少这条记录,从库数据丢失
- 先写 binlog 后写 redo log:宕机时 binlog 有记录但 redo log 没有,主库数据丢失,从库多了数据
两阶段提交流程
崩溃恢复规则:
| 崩溃时间点 | redo log 状态 | binlog 状态 | 恢复策略 |
|---|---|---|---|
| prepare 写完,binlog 未写 | prepare | 无 | 回滚事务 |
| binlog 写完,commit 未标记 | prepare | 完整 | 提交事务 |
| commit 写完 | commit | 完整 | 提交事务 |
关键判断:如果 redo log 是 prepare 状态,检查 binlog 是否完整(通过 XID 匹配),完整则提交,否则回滚。
六、一条 UPDATE 的完整日志流程
UPDATE users SET name = '李四' WHERE id = 1;
1. 执行器找 InnoDB 读取 id=1 的行
2. InnoDB 从 Buffer Pool 或磁盘读取数据页
3. 写 undo log(记录旧值 name='张三')
4. 更新 Buffer Pool 中的数据页为 name='李四'
5. 写 redo log(prepare 状态)
6. 写 binlog
7. redo log 标记为 commit
8. 返回成功
MySQL 5.7+ 支持 binlog 组提交,将多个事务的 fsync 合并为一次,大幅提升高并发场景的写入性能。
常见面试问题
Q1: 为什么 binlog 不能替代 redo log 做崩溃恢复?
答案:
- binlog 是逻辑日志,没有记录数据页的物理状态,无法恢复内存中的脏页
- binlog 是追加写,不知道哪些数据页是脏的
- binlog 是 Server 层的,不了解 InnoDB 的 Buffer Pool 状态
- redo log 是物理日志,精确到页级修改,可以直接应用到数据页
Q2: redo log 满了怎么办?
答案:
redo log 是循环写的。当 write pos 追上 checkpoint 时:
- 停止接收新的更新操作
- 将 checkpoint 向前推进(将脏页刷到磁盘)
- 刷完后释放空间,继续写入
这就是为什么 redo log 文件不能设太小——太小会频繁触发刷盘,影响性能。推荐配置:innodb_log_file_size = 1G(或根据写入量调整)。
Q3: 如何通过 binlog 恢复数据?
答案:
# 1. 找到误操作前的 binlog 位置
mysqlbinlog --start-datetime="2024-01-01 10:00:00" \
--stop-datetime="2024-01-01 10:05:00" \
mysql-bin.000001
# 2. 通过 position 恢复
mysqlbinlog --start-position=154 --stop-position=1024 \
mysql-bin.000001 | mysql -u root -p
配合全量备份 + binlog 可以实现任意时间点恢复(Point-in-Time Recovery)。
Q4: innodb_flush_log_at_trx_commit 和 sync_binlog 怎么配?
答案:
- 双 1 配置(最安全):
innodb_flush_log_at_trx_commit=1, sync_binlog=1 - 适用于对数据安全要求高的业务(金融、交易等)
- 代价是 IO 性能较低
- 非关键业务可考虑
2, 1或1, 100的折中方案