Skip to content

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 poscheckpoint 两个指针管理。write pos 写入新日志,checkpoint 推进表示该位置之前的数据页已刷盘,日志可覆盖。当 write pos 追上 checkpoint 时,说明 redo log 写满,必须强制刷脏页推进 checkpoint,此时数据库写入会变慢——这就是"redo log 满了"的典型表现。

sql
-- 查看 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 checkpoint

Redo 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。它有两个核心用途:

  1. 事务回滚:事务执行过程中,如果执行 ROLLBACK 或发生错误,通过 undo log 撤销已执行的修改。
  2. MVCC 多版本并发控制:undo log 构成版本链,每条记录的回滚指针指向上一个版本。事务读取时,通过 Read View 和版本链找到可见版本,实现"读不阻塞写、写不阻塞读"。

undo log 分为 insert undoupdate undo 两类。insert undo 在事务提交后可直接删除(因为 INSERT 的逆操作 DELETE 不会影响其他事务的可见性);update undo 需要保留到所有依赖于该版本的事务结束后才能被 purge 线程清理。

sql
-- 查看 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 模式下记录的是每行数据变更),所有存储引擎共享。它的核心用途:

  1. 主从复制:主库将 binlog 发送给从库,从库重放实现数据同步。
  2. 数据恢复:通过 mysqlbinlog 工具回放 binlog,实现任意时间点的数据恢复。

binlog 有三种格式:

  • STATEMENT:记录原始 SQL,日志量小,但某些函数(NOW()、UUID())在主从不一致
  • ROW(MySQL 5.7+ 默认):记录每行变更,安全但日志量大
  • MIXED:自动判断,对确定性 SQL 用 STATEMENT,否则用 ROW
sql
-- 查看 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,为什么还需要两阶段提交?考虑一个事务的执行过程:

  1. 修改数据页(Buffer Pool 中修改)
  2. 写 redo log(prepare 阶段)
  3. 写 binlog
  4. 提交事务(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 中是否包含该事务的完整日志。

java
// 伪代码:两阶段提交的核心逻辑
beginTransaction();
updateDataInBufferPool();           // 修改 Buffer Pool 中的内存页
writeRedoLogPrepare();              // 写 redo log 到 prepare 状态
writeBinlog();                      // 写 binlog(刷盘)
writeRedoLogCommit();               // 将 redo log 标记为 commit
commitTransaction();

两阶段提交的代价是需要额外一次磁盘 I/O(写 prepare 状态),但 MySQL 通过**组提交(Group Commit)**优化:多个事务的 prepare 阶段可以合并一次 fsync,大幅提升吞吐量。

总结

三大日志的分工与协同:

对比维度redo logundo logbinlog
所属层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 章

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。