"MySQL 有三个日志"这句话经常被记住,但真正决定故障后果的是另一件事:哪一个日志归 InnoDB 引擎管,哪一个归 Server 层管。redo 与 undo 在引擎内部,负责崩溃恢复与事务回滚/MVCC;binlog 在 Server 层,负责复制与按时间点恢复。搞混层次,就会出现"我明明开了 sync_binlog=1 为什么还丢数据"这类争论。以下按 MySQL 8.0 + InnoDB 核对。
1. 三者的层次与职责
| 日志 | 所属层 | 记录什么 | 保证什么 | 生命周期 |
|---|---|---|---|---|
| redo log | InnoDB 引擎层 | 物理页修改(页号 + 偏移 + 字节) | 崩溃后已提交事务不丢(持久性) | 循环写,检查点推进后覆盖 |
| undo log | InnoDB 引擎层 | 逻辑反向操作(旧值) | 回滚、MVCC 读一致性 | purge 线程清理,长事务会拖住 |
| binlog | Server 层 | 逻辑变更(SQL 或行镜像) | 复制、按时间点恢复 | 按保留期删除 |
SHOW VARIABLES LIKE 'innodb_log_file_size';
SHOW VARIABLES LIKE 'innodb_redo_log_capacity'; -- 8.0.30+ 取代上面两项
SHOW VARIABLES LIKE 'sync_binlog';
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';
8.0.30 起 redo 容量统一由 innodb_redo_log_capacity 控制(默认 100 MB 量级),底层是 #innodb_redo 目录下的一组文件;innodb_log_file_size / innodb_log_files_in_group 退为兼容项。老文章里的调参公式要按版本核对。
2. redo log:循环写、检查点与被迫刷脏页
redo 的写指针和检查点指针在一个环形空间里追跑:
SHOW ENGINE INNODB STATUS\G
-- SECTION: LOG
-- Log sequence number (当前写入到哪)
-- Log flushed up to (已落盘到哪)
-- Pages flushed up to (脏页刷到哪)
-- Last checkpoint at (检查点位置)
SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';
SHOW GLOBAL STATUS LIKE 'Innodb_os_log_written';
| 字段 | 含义 | 异常信号 |
|---|---|---|
| Log sequence number | 最新 LSN | 与 checkpoint 的差接近容量上限 → 空间吃紧 |
| Log flushed up to | 已 fsync 的 LSN | 长期落后说明刷盘跟不上 |
| Pages flushed up to | 脏页已刷到的 LSN | 落后太多意味着 checkpoint 推不动 |
| Last checkpoint at | 当前检查点 | 与 LSN 差值是"可用余量" |
关键机制:当写指针追上检查点时,InnoDB 没有新空间可用,只能强行推进检查点、把脏页刷盘,此时前台写入会被短暂阻塞。Innodb_log_waits 增长就是这件事发生的计数器(redo 空间不足等待,另外 log buffer 满也会等)。
容量偏小的典型表现不是"崩",而是周期性抖动:写入量一大就刷脏页,IO 尖峰,TPS 掉一截再回来。容量偏大则相反:崩溃恢复要重放更多 redo,恢复时间变长。这是一对权衡,不是越大越好。
3. WAL 与两阶段提交:redo 和 binlog 怎么对齐
WAL(Write-Ahead Logging)的核心是:先写日志,后写数据页。数据页在 buffer pool 里被改脏,提交时只需把 redo 顺序追加并 fsync,随机写变成了顺序写;脏页可以慢慢刷。这是 InnoDB 能扛高并发写的根本原因。
但 redo 和 binlog 是两个独立的日志系统,必须保证"要么都生效、要么都不生效",否则主从会不一致。做法是两阶段提交:
1. InnoDB 写 redo,标记事务 prepare(携带 XID)
2. Server 层写 binlog 并 fsync(受 sync_binlog 控制)
3. InnoDB 写 redo,标记事务 commit
| 崩溃时刻 | redo 状态 | binlog 状态 | 恢复动作 |
|---|---|---|---|
| 步骤 1 之前 | 无记录 | 无 | 事务不存在 |
| 步骤 1 之后、2 之前 | prepare | 不完整/无 | 回滚 |
| 步骤 2 之后、3 之前 | prepare | 完整(含 XID) | 提交(binlog 为准) |
| 步骤 3 之后 | commit | 完整 | 已提交,无需动作 |
恢复时的判据是:redo 里处于 prepare 的事务,拿它的 XID 去 binlog 里找;找到完整事务就提交,找不到就回滚。所以"binlog 写成功但 redo 没 commit"不会丢数据,而"redo commit 了 binlog 没写"才是灾难——这也是为什么必须由 binlog 决定最终状态。
组提交(group commit)是这里的性能补丁:多个并发事务的 redo fsync 与 binlog fsync 合并成一次系统调用,按 flush → sync → commit 三阶段排队。它不改变正确性,只把 fsync 次数从"每事务一次"降到"每组一次"。
4. undo log:回滚与 MVCC 的两个身份
| undo 类型 | 产生于 | 用途 | 是否进版本链 |
|---|---|---|---|
| insert undo | INSERT |
回滚时删除插入的行 | 否,提交后即可丢弃 |
| update undo | UPDATE / DELETE |
回滚旧值、支撑 MVCC 快照读 | 是,直到没有读视图需要它 |
undo 的第二重身份容易被忽略:MVCC 的版本链就是 undo 串起来的。一行被改过 5 次,就有 5 条 update undo 通过 roll_ptr 连成链,快照读按 Read View 沿链找到可见版本。所以 undo 不能提交就删——只要还有长事务持有旧的 Read View,purge 就不能清理它。
-- 长事务:trx_state=RUNNING、trx_started 很早
SELECT trx_id, trx_state, trx_started,
trx_rows_modified, trx_isolation_level
FROM information_schema.innodb_trx
ORDER BY trx_started;
SHOW ENGINE INNODB STATUS\G
-- TRANSACTIONS 段的 History list length:待 purge 的 undo 数量
History list length 持续增长,说明 purge 追不上,通常就是长事务或大事务在拖。undo 表空间会随之膨胀:
SHOW VARIABLES LIKE 'innodb_undo_tablespaces';
SHOW VARIABLES LIKE 'innodb_max_undo_log_size';
-- 8.0 支持 undo 表空间自动 truncate,但被长事务挡住时不会生效
5. binlog 的格式与持久性组合
| 格式 | 记录内容 | 日志量 | 风险 |
|---|---|---|---|
STATEMENT |
原始 SQL | 最小 | 含 NOW()、RAND()、LIMIT 无 ORDER BY 时主从不一致 |
ROW |
行的前镜像/后镜像 | 最大(可用 binlog_row_image=MINIMAL 压缩) |
安全,8.0 默认 |
MIXED |
按语句自动切换 | 中等 | 判断边界不直观 |
ROW 下一条 UPDATE 改 10 万行就写 10 万行的镜像,日志量随影响行数增长;STATEMENT 只写一条 SQL,但重放时可能算出不同结果。批量更新多的库,ROW + MINIMAL 是常见折中。
真正决定"崩溃丢多少"的是两个 fsync 开关的组合:
innodb_flush_log_at_trx_commit |
sync_binlog |
崩溃丢什么 | 典型用途 |
|---|---|---|---|
| 1 | 1 | 不丢(默认,最安全) | 交易类、默认配置 |
| 1 | 0 | 主库 binlog 可能丢最后若干事务 | 可重放业务 |
| 2 | 1 | 操作系统崩溃时丢 redo(mysqld 崩溃不丢) | 折中 |
| 0 | 0 | 最多丢约 1 秒的已提交事务 | 批量导入、可重建数据 |
注意 =0 时不仅丢"未提交",已提交的事务也可能丢,因为提交时不 fsync,靠后台每秒刷一次。把它当"性能优化"用在订单库上,等于把持久性交给运气。
6. 容量:三类日志的膨胀信号与处理
| 日志 | 控制参数 | 膨胀信号 | 处理 |
|---|---|---|---|
| redo | innodb_redo_log_capacity |
Innodb_log_waits 增长、周期性 IO 尖峰 |
适度调大,观察恢复时间 |
| undo | innodb_max_undo_log_size |
History list length 单调上升、undo 表空间变大 | 先杀长事务,再等 truncate |
| binlog | binlog_expire_logs_seconds |
磁盘增长、备份窗口不够 | 调保留期 + 定期归档 |
-- 按时间点恢复需要连续的 binlog 链
mysqlbinlog --start-datetime='2026-11-09 02:00:00' \
--stop-datetime='2026-11-09 03:00:00' \
/var/lib/mysql/binlog.000123 | mysql -uroot -p
-- 手动清理(谨慎:清早了就无法 PITR)
SHOW BINARY LOGS;
PURGE BINARY LOGS BEFORE '2026-11-01 00:00:00';
保留期的定法不是"默认 30 天就行",而是最坏情况下你需要的恢复窗口:如果一次误删要回溯到 3 天前的全量备份,保留期至少覆盖"备份周期 + 发现延迟"。
7. 排错:五种日志相关的故障
| 现象 | 原因 | 处理 |
|---|---|---|
| 崩溃后丢了已提交事务 | innodb_flush_log_at_trx_commit=0 或 sync_binlog=0 |
改回 1/1,先确认业务可接受 |
周期性写入抖动、Innodb_log_waits 涨 |
redo 容量太小,被迫刷脏页 | 调大 innodb_redo_log_capacity |
| 磁盘被 binlog 撑满 | 保留期过长或大事务刷日志 | 调 binlog_expire_logs_seconds,查大事务 |
| 无法按时间点恢复 | binlog 保留期短于备份周期,链断了 | 延长保留期 + 归档 binlog 到对象存储 |
| undo 表空间持续变大 | 长事务持有旧 Read View,purge 被挡 | innodb_trx 找长事务并 kill |
| 主从数据不一致 | STATEMENT 格式 + 非确定性函数 |
切 ROW,核对 binlog_row_image |
8. 边界:什么时候该跳出这三个日志
| 场景 | 更合适的机制 | 理由 |
|---|---|---|
| 跨库/跨异构系统的数据同步 | CDC(Canal、Debezium)读 binlog | binlog 是事实上的变更流 |
| 精确审计"谁改了什么" | 业务审计表 / 触发器之外的应用层记录 | binlog 不记录业务语义与操作人 |
| 秒级 RPO 的容灾 | 半同步复制 / 组复制 + 异地 binlog 归档 | 单机日志抗不住整机故障 |
| 海量写入的持久化 | 批量提交 + 适度放宽 fsync | 每事务 fsync 的代价无法摊薄 |
| 逻辑备份 | mysqldump --single-transaction / XtraBackup |
依赖一致性快照,不是日志本身 |
一句话:redo 保"不丢已提交",undo 保"能回滚能快照读",binlog 保"能复制能回溯"。三者缺一,故障时的可恢复性就断一截。
9. 自测
- redo 与 binlog 分别属于哪一层?各自保证的是哪一件事?
- 写指针追上检查点时会发生什么?
Innodb_log_waits说明什么? - 崩溃恢复时,处于 prepare 状态的事务凭什么决定提交还是回滚?
insert undo与update undo的区别是什么?哪一个会被 MVCC 版本链使用?innodb_flush_log_at_trx_commit=2与=0在"操作系统崩溃"和"mysqld 崩溃"两种场景下分别丢什么?- binlog 保留期应该按什么来定?为什么不能只看默认值?