日志系统
问题
MySQL 有哪些重要的日志?redo log、undo log 和 binlog 分别有什么作用?它们之间如何协同工作?什么是两阶段提交?
答案
MySQL 日志全景
| 日志 | 层级 | 作用 | 类型 |
|---|---|---|---|
| redo log | InnoDB 引擎层 | 崩溃恢复(crash-safe) | 物理日志 |
| undo log | InnoDB 引擎层 | 事务回滚 + MVCC | 逻辑日志 |
| binlog | Server 层 | 主从复制、数据恢复 | 逻辑日志 |
| 慢查询日志 | Server 层 | 记录慢 SQL | 文本日志 |
| 错误日志 | Server 层 | 记录错误信息 | 文本日志 |
| 通用查询日志 | Server 层 | 记录所有 SQL | 文本日志 |
面试重点是前三个:redo log、undo log、binlog。
Redo Log(重做日志)
作用
redo log 保证持久性(Durability)。它记录的是数据页的物理修改("在某个数据页的某个偏移量处写入了什么值"),用于崩溃恢复。
为什么需要 Redo Log
修改数据时如果每次都直接写磁盘(随机 I/O),性能极差。InnoDB 采用 WAL(Write-Ahead Logging) 策略:
WAL 的核心思想:先写日志(顺序 I/O,速度快),再写磁盘(随机 I/O,后台异步)。即使崩溃,也能通过 redo log 恢复未刷盘的数据。
Redo Log 的结构
redo log 采用固定大小,循环写入的方式:
- write pos:当前写入位置,边写边后移
- checkpoint:已刷盘到数据文件的位置,边擦边后移
- write pos 和 checkpoint 之间的空间是可用空间
- 当 write pos 追上 checkpoint 时,必须停下来先刷脏页
刷盘策略
由 innodb_flush_log_at_trx_commit 控制:
| 值 | 行为 | 安全性 | 性能 |
|---|---|---|---|
| 0 | 每秒将 Log Buffer 写入 OS Cache 并 fsync | 最低(崩溃丢 1 秒) | 最高 |
| 1(默认) | 每次事务提交都 fsync 到磁盘 | 最高(不丢数据) | 最低 |
| 2 | 每次提交写入 OS Cache,每秒 fsync | 中等(OS 崩溃丢 1 秒) | 中等 |
核心业务表设置 innodb_flush_log_at_trx_commit = 1,非核心表可设为 2 提升性能。
Undo Log(回滚日志)
作用
undo log 保证原子性(Atomicity) 和支持 MVCC:
- 事务回滚:记录数据修改前的值,回滚时执行"反操作"还原数据
- MVCC 版本链:通过
DB_ROLL_PTR指针串成版本链,实现快照读
Undo Log 的类型
| 类型 | 产生时机 | 说明 |
|---|---|---|
| insert undo log | INSERT 操作 | 事务提交后即可删除(其他事务不需要读取新插入的旧版本) |
| update undo log | UPDATE/DELETE 操作 | 需要保留到没有事务需要读取时才能清理(MVCC 需要) |
关于 MVCC 如何利用 undo log 版本链,参见 MVCC 多版本并发控制。
Binlog(归档日志)
作用
binlog 是 MySQL Server 层的日志,所有存储引擎共用。核心作用:
- 主从复制:从库通过读取主库的 binlog 来同步数据
- 数据恢复:通过 binlog 可将数据库恢复到任意时间点
Binlog 的三种格式
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| Statement | SQL 语句原文 | 日志量小 | 某些函数(如 NOW()、UUID())可能导致主从不一致 |
| Row | 行数据变更(修改前后的值) | 数据一致性好 | 日志量大(尤其批量操作) |
| Mixed | 自动选择 Statement 或 Row | 折中方案 | 仍可能不一致 |
MySQL 5.7.7+ 默认使用 Row 格式。虽然日志量较大,但数据一致性最好,且可以用于数据恢复(能精确看到哪一行被改成了什么值)。
刷盘策略
由 sync_binlog 控制:
| 值 | 行为 |
|---|---|
| 0 | 由操作系统决定何时 fsync |
| 1(推荐) | 每次事务提交都 fsync |
| N | 每 N 个事务 fsync 一次 |
Redo Log vs Binlog
| 对比维度 | Redo Log | Binlog |
|---|---|---|
| 层级 | InnoDB 引擎层 | MySQL Server 层 |
| 内容 | 物理日志(数据页修改) | 逻辑日志(SQL 或行变更) |
| 写入方式 | 循环写,固定大小 | 追加写,不覆盖 |
| 作用 | 崩溃恢复(crash-safe) | 主从复制、数据归档恢复 |
| 事务支持 | 有(InnoDB 独有) | 有(所有引擎) |
两阶段提交
为了保证 redo log 和 binlog 的数据一致性,MySQL 使用两阶段提交(2PC):
为什么需要两阶段提交
假设不用两阶段提交:
场景 1:先写 redo log,后写 binlog
- redo log 写完后崩溃,binlog 没写
- 主库通过 redo log 恢复了数据,但从库没有这条 binlog → 主从不一致
场景 2:先写 binlog,后写 redo log
- binlog 写完后崩溃,redo log 没写
- 主库没有恢复数据(缺少 redo log),但从库执行了 binlog → 主从不一致
两阶段提交保证了:无论在哪一步崩溃,恢复后 redo log 和 binlog 都是一致的。
崩溃恢复规则
| 崩溃时机 | redo log 状态 | binlog 状态 | 恢复操作 |
|---|---|---|---|
| 写 redo log (prepare) 之后 | prepare | 无 | 回滚事务 |
| 写 binlog 之后 | prepare | 有 | 提交事务(redo log 补 commit) |
| 两阶段完成后 | commit | 有 | 正常,无需恢复 |
MySQL 8.0.17+ 引入了 binlog 组提交 和 redo log 组提交,将多个事务的日志合并写入,减少 fsync 次数,大幅提升高并发下的性能。
一条 UPDATE 的完整日志流程
UPDATE user SET name = '李四' WHERE id = 1;
常见面试问题
Q1: redo log 和 binlog 有什么区别?
答案:
三个核心区别:
- 层级不同:redo log 是 InnoDB 引擎层的,binlog 是 MySQL Server 层的(所有引擎共用)
- 内容不同:redo log 是物理日志(记录数据页修改),binlog 是逻辑日志(记录 SQL 或行变更)
- 用途不同:redo log 用于崩溃恢复(保证 crash-safe),binlog 用于主从复制和数据归档恢复
它们需要通过两阶段提交来保证一致性。
Q2: 什么是 WAL?为什么要用 WAL?
答案:
WAL(Write-Ahead Logging) 即"日志先行":修改数据时,先将修改写入日志(redo log),再由后台线程异步将脏页写入磁盘。
为什么用 WAL:
- 写日志是顺序 I/O,速度极快(磁盘顺序写接近内存速度)
- 写数据页是随机 I/O,速度慢(磁头需要频繁寻道)
- WAL 将随机写转化为顺序写,大幅提升写入性能
- 即使崩溃,redo log 中也保存了修改信息,可用于恢复
Q3: 为什么需要两阶段提交?
答案:
两阶段提交保证了 redo log 和 binlog 的一致性。如果两者不一致:
- redo log 有但 binlog 没有:主库恢复了数据,从库缺少该变更 → 主从不一致
- binlog 有但 redo log 没有:从库执行了变更,主库没恢复 → 主从不一致
两阶段提交的流程:
- Prepare 阶段:redo log 写入并标记为 prepare
- Commit 阶段:binlog 写入并 fsync,然后 redo log 标记为 commit
崩溃恢复时:如果 redo log 是 prepare 且 binlog 完整 → 提交事务;如果 binlog 不完整 → 回滚事务。
Q4: undo log 的两个作用分别是什么?
答案:
- 事务回滚:undo log 记录了数据修改前的旧值。当事务执行 ROLLBACK 时,通过 undo log 将数据恢复到修改前的状态,保证原子性
- MVCC 版本链:undo log 通过
DB_ROLL_PTR指针串成版本链。快照读时,InnoDB 沿着版本链找到对当前事务可见的旧版本,实现隔离性
undo log 的清理:由 Purge 线程负责,当没有活跃事务需要读取某个旧版本时才清理。长事务会导致 undo log 膨胀。
Q5: binlog 的三种格式怎么选?
答案:
| 格式 | 推荐程度 | 说明 |
|---|---|---|
| Row(推荐) | ⭐⭐⭐ | 精确记录每行变更,数据一致性最好,支持精确的数据恢复和审计 |
| Statement | ⭐ | 日志量小,但 NOW()、UUID()、RAND() 等函数可能导致主从不一致 |
| Mixed | ⭐⭐ | 自动判断用 Statement 还是 Row,折中方案 |
MySQL 5.7.7+ 默认使用 Row 格式,生产环境建议使用 Row 格式,配合 sync_binlog = 1 保证数据安全。虽然日志量较大,但存储成本远低于数据不一致的风险。