2644 字
约 8 分钟
16
S11-X16 · 日志体系:redo、undo 与 binlog 的分工
2024-12-31
2026-11-08

"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. 自测

  1. redo 与 binlog 分别属于哪一层?各自保证的是哪一件事?
  2. 写指针追上检查点时会发生什么?Innodb_log_waits 说明什么?
  3. 崩溃恢复时,处于 prepare 状态的事务凭什么决定提交还是回滚?
  4. insert undo 与 update undo 的区别是什么?哪一个会被 MVCC 版本链使用?
  5. innodb_flush_log_at_trx_commit=2 与 =0 在"操作系统崩溃"和"mysqld 崩溃"两种场景下分别丢什么?
  6. binlog 保留期应该按什么来定?为什么不能只看默认值?
S11-X16 · 日志体系:redo、undo 与 binlog 的分工
https://www.clxhxhhr.top/posts/5935/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。