redo/undo/binlog 三大日志协同
提出问题
日常开发中,MySQL 突然断电,重启后数据为什么还在?事务回滚为什么能撤销已执行的修改?主从复制为什么能保证数据一致?这三个问题的答案分别对应着 InnoDB 的三种日志:redo log、undo log 和 binlog。更关键的是,redo log 和 binlog 之间通过**两阶段提交(Two-Phase Commit)**保证一致性,这直接决定了 MySQL 的 crash-safe 能力。面试中,三大日志的职责、类型、写入时机和协同机制是高频考点,追到两阶段提交的细节才能真正过关。
分析问题
redo log:物理日志,崩溃恢复的基石
redo log 是 InnoDB 存储引擎特有的物理日志,记录的是"在某个数据页上做了什么修改",比如"在页号 100 的偏移量 200 处写入 0xABCD"。它的核心作用是 WAL(Write-Ahead Logging):事务提交时,先写 redo log(顺序写磁盘,性能高),再异步刷脏页到磁盘。这样即使数据库宕机,重启后通过 redo log 重放就能恢复已提交的事务。
redo log 采用循环写的固定大小文件(默认 innodb_log_file_size × innodb_log_files_in_group),通过 write pos 和 checkpoint 两个指针管理。write pos 写入新日志,checkpoint 推进表示该位置之前的数据页已刷盘,日志可覆盖。当 write pos 追上 checkpoint 时,说明 redo log 写满,必须强制刷脏页推进 checkpoint,此时数据库写入会变慢——这就是"redo log 满了"的典型表现。
-- 查看 redo log 相关配置
SHOW VARIABLES LIKE 'innodb_log_file_size';
SHOW VARIABLES LIKE 'innodb_log_files_in_group';
SHOW ENGINE INNODB STATUS\G
-- 查看 Log 段中的 sequence number 和 last checkpointRedo log 的刷盘策略由 innodb_flush_log_at_trx_commit 控制:
- 1(默认):每次事务提交都刷盘,保证 crash-safe,性能最慢
- 2:每次事务提交只写入 OS cache,每秒刷盘,OS 宕机可能丢 1s 数据
- 0:每秒写入 OS cache 并刷盘,MySQL 宕机可能丢 1s 数据
生产环境通常设为 1,用 SSD 和组提交优化性能。
undo log:逻辑日志,MVCC 与回滚的保障
undo log 是 InnoDB 存储引擎特有的逻辑日志,记录的是"操作的逆操作":INSERT 对应 DELETE,DELETE 对应 INSERT,UPDATE 对应反向 UPDATE。它有两个核心用途:
- 事务回滚:事务执行过程中,如果执行 ROLLBACK 或发生错误,通过 undo log 撤销已执行的修改。
- MVCC 多版本并发控制:undo log 构成版本链,每条记录的回滚指针指向上一个版本。事务读取时,通过 Read View 和版本链找到可见版本,实现"读不阻塞写、写不阻塞读"。
undo log 分为 insert undo 和 update undo 两类。insert undo 在事务提交后可直接删除(因为 INSERT 的逆操作 DELETE 不会影响其他事务的可见性);update undo 需要保留到所有依赖于该版本的事务结束后才能被 purge 线程清理。
-- 查看 undo log 配置
SHOW VARIABLES LIKE 'innodb_undo_tablespaces';
SHOW VARIABLES LIKE 'innodb_max_undo_log_size';
SHOW VARIABLES LIKE 'innodb_purge_threads';binlog:逻辑日志,主从复制与归档的桥梁
binlog 是 MySQL Server 层的逻辑日志,记录的是 SQL 语句的逻辑变更(Row 模式下记录的是每行数据变更),所有存储引擎共享。它的核心用途:
- 主从复制:主库将 binlog 发送给从库,从库重放实现数据同步。
- 数据恢复:通过
mysqlbinlog工具回放 binlog,实现任意时间点的数据恢复。
binlog 有三种格式:
- STATEMENT:记录原始 SQL,日志量小,但某些函数(NOW()、UUID())在主从不一致
- ROW(MySQL 5.7+ 默认):记录每行变更,安全但日志量大
- MIXED:自动判断,对确定性 SQL 用 STATEMENT,否则用 ROW
-- 查看 binlog 配置
SHOW VARIABLES LIKE 'log_bin';
SHOW VARIABLES LIKE 'binlog_format';
SHOW BINARY LOGS;
-- 查看 binlog 内容
SHOW BINLOG EVENTS IN 'mysql-bin.000001';两阶段提交:redo log 与 binlog 的一致性保证
有了 redo log 和 binlog,为什么还需要两阶段提交?考虑一个事务的执行过程:
- 修改数据页(Buffer Pool 中修改)
- 写 redo log(prepare 阶段)
- 写 binlog
- 提交事务(commit 阶段,redo log 标记为 commit)
如果第 2 步和第 3 步之间宕机,redo log 里有修改但 binlog 没有,重启后 redo log 重放会恢复数据,但主从复制时 binlog 没有这条记录,导致主库有而从库没有,数据不一致。
两阶段提交的解决方式:
事务开始
→ 修改 Buffer Pool
→ 写 redo log(prepare 状态)
→ 写 binlog(写入并刷盘)
→ 写 redo log(commit 状态)
事务提交崩溃恢复时,MySQL 扫描所有 prepare 状态的 redo log:
- 如果对应事务的 binlog 已完整写入:说明两阶段提交已经完成,事务可以提交
- 如果对应事务的 binlog 不存在或不完整:说明写入 binlog 之前就宕机了,事务回滚
这个判断依据是:redo log 的 prepare 阶段记录了一个 XID(事务 ID),MySQL 用这个 XID 去检查 binlog 中是否包含该事务的完整日志。
// 伪代码:两阶段提交的核心逻辑
beginTransaction();
updateDataInBufferPool(); // 修改 Buffer Pool 中的内存页
writeRedoLogPrepare(); // 写 redo log 到 prepare 状态
writeBinlog(); // 写 binlog(刷盘)
writeRedoLogCommit(); // 将 redo log 标记为 commit
commitTransaction();两阶段提交的代价是需要额外一次磁盘 I/O(写 prepare 状态),但 MySQL 通过**组提交(Group Commit)**优化:多个事务的 prepare 阶段可以合并一次 fsync,大幅提升吞吐量。
总结
三大日志的分工与协同:
| 对比维度 | redo log | undo log | binlog |
|---|---|---|---|
| 所属层 | InnoDB 引擎层 | InnoDB 引擎层 | MySQL Server 层 |
| 日志类型 | 物理日志(页修改) | 逻辑日志(逆操作) | 逻辑日志(SQL/行变更) |
| 用途 | 崩溃恢复 crash-safe | 回滚 + MVCC | 主从复制 + 数据恢复 |
| 写入方式 | 循环写,固定大小 | 回滚段,需 purge 清理 | 追加写,可配置保留时间 |
| 刷盘策略 | innodb_flush_log_at_trx_commit | 自动管理 | sync_binlog |
生产避坑要点:
- 生产环境
innodb_flush_log_at_trx_commit=1配合sync_binlog=1,保证 crash-safe - 两阶段提交是 MySQL crash-safe 的核心保证,不要为了性能把
sync_binlog设为 0 - redo log 太小会导致频繁刷脏页,写入抖动;建议在 4GB-16GB 之间
- undo log 太大考虑独立表空间(
innodb_undo_tablespaces拆分)
参考
参考:MySQL 官方文档 - InnoDB Redo Log / Undo Log / Binary Log;《MySQL 实战 45 讲》第 2、3 讲;《高性能 MySQL(第 4 版)》第 8 章