MySQL 三大日志:Redo Log、Undo Log 和 Binlog 完全解析
无论在工作还是在面试中,MySQL 的Redo Log、Undo Log、Binlog的三大日志体系都是一个绕不开的硬核技术点。本文将从零开始,详细介绍每一类日志的作用、工作原理、写入机制,以及三者如何配合支撑事务的 ACID 特性。
一、为什么需要了解三大日志?
数据库操作面临一个经典矛盾:既要保证数据不丢失(持久性),又要追求高性能。现代数据库给出的答案是WAL(Write-Ahead Logging,预写日志)技术——先写日志,后写磁盘。而 MySQL 的日志体系比这更复杂,它把功能拆解到了三种不同的日志中。
MySQL 的日志有很多种(错误日志、慢查询日志等),其中最重要、最核心的三种分别是:
Binlog(归档日志,Server 层)
Redo Log(重做日志,InnoDB 引擎层)
Undo Log(回滚日志,InnoDB 引擎层)
用一句话概括三者分工:
Redo Log 保“持久”——宕机不丢数据
Undo Log 保“原子”——做错了能撤回
Binlog 保“可追溯、可同步”——主从复制、数据恢复
二、Redo Log:崩溃恢复的守护者
2.1 为什么需要 Redo Log?
MySQL 的 InnoDB 引擎使用 Buffer Pool 作为数据页的内存缓存,所有数据的修改会先在 Buffer Pool 中完成,再将“脏页”异步刷回磁盘。
这样设计的最大好处是避免频繁的磁盘随机 I/O(直接写磁盘太慢),但也带来了一个致命隐患:如果在事务提交之后、脏页落盘之前发生宕机,内存中的数据就会全部丢失。
Redo Log 就是专门解决这个问题的。
2.2 Redo Log 的核心特性
物理日志:Redo Log 记录的是“在某个数据页的某个偏移量上做了什么修改”,而不是 SQL 语句本身。
WAL(Write-Ahead Logging):修改数据之前,先把 Redo Log 写入磁盘,数据页可以稍后异步写入。先写日志再写数据,这是 InnoDB 实现 crash-safe 的核心机制。
循环写 + 固定大小:Redo Log 不是无限增长的,而是用一组固定大小的文件循环写入。MySQL 8.0.30 之前默认两个文件(ib_logfile0 和 ib_logfile1),之后支持动态配置容量。
LSN(Log Sequence Number,日志序列号):每条 Redo Log 都有一个全局唯一的 LSN,用于标识日志记录顺序,也用于判断哪些数据已经落盘、哪些还需要恢复。
2.3 Redo Log 的结构与写入流程
Redo Log 在磁盘上的组织结构由两个指针控制:
write pos:当前日志写入位置,边写边后移
checkpoint:当前已落盘数据对应的日志位置,之后的日志还需要用于恢复
当 write pos 追上 checkpoint 时,表示日志文件写满了,InnoDB 会暂停写入,先将一些脏页强制刷盘,把 checkpoint 向前推进,腾出空间后继续写入。
完整的写入流程分为三步:
事务执行时:修改操作先写入内存中的Redo Log Buffer。
事务提交时:根据
innodb_flush_log_at_trx_commit参数决定刷盘策略:1(默认):每次提交都将 Buffer 刷到 OS cache 并调用
fsync()落盘,最安全,性能稍低0:每秒刷盘一次,事务提交时不触发刷盘,性能最好,但可能丢失最近 1 秒的事务
2:每次提交刷到 OS cache(只保证写入操作系统缓存,不保证物理落盘),每秒
fsync()一次
后台异步:后台线程在系统空闲时,将 Buffer Pool 中的脏页异步刷入磁盘。
2.4 MySQL 8.0 的 Redo Log 优化
MySQL 8.0 对 Redo Log 做了重要改进:
无锁化设计:用户线程可以并行写入 Log Buffer,不再串行等待,大幅提升高并发下的写入性能
独立后台线程:Log Writer 负责将 Buffer 写入 OS cache,Log Flusher 负责调用
fsync()真正落盘,分工明确动态容量调整:MySQL 8.0.30 引入
innodb_redo_log_capacity参数,可以在线调整 Redo Log 总容量,不再依赖ib_logfile*文件的固定大小和数量
三、Undo Log:事务回滚与 MVCC 的基石
3.1 Undo Log 的核心作用
Undo Log 的主要职责有两个:
事务回滚:当事务执行失败或主动回滚时,将数据恢复到修改前的状态,保证事务的原子性
MVCC(多版本并发控制):为其他事务提供一致性读视图,让读操作不加锁,读写互不阻塞,极大提升并发性能
3.2 逻辑日志与存储结构
Undo Log 是一种逻辑日志,记录的是反向操作:
执行
INSERT→ 记录对应的主键信息(回滚时DELETE)执行
DELETE→ 记录完整的行数据(回滚时INSERT回去)执行
UPDATE→ 记录修改前的旧值(回滚时反向UPDATE)
Undo Log 存储在 InnoDB 的回滚段(rollback segment)中,每个回滚段包含 1024 个 undo slot。
3.3 隐藏字段与版本链
InnoDB 的行记录中隐藏着三个重要字段:
DB_TRX_ID(6 字节):最后一次修改该行的事务 ID,全局递增DB_ROLL_PTR(7 字节):回滚指针,指向 Undo Log 中的上一个版本DB_ROW_ID(6 字节):行 ID,仅在表没有主键时使用
当事务 A 修改某行数据时,步骤如下:
将修改前的完整数据写入 Undo Log
更新当前行的
DB_TRX_ID = 事务A的ID更新当前行的
DB_ROLL_PTR指向刚写入的 Undo Log 记录修改数据本身
第二次被事务 B 修改时,同样的流程会继续追加新的 Undo Log。事务 B 的DB_ROLL_PTR指向事务 A 的版本,以此类推,形成一条单向版本链,最靠近当前行的是最新版本,链表尾部是最古老的版本。
3.4 MVCC 与 Read View
MVCC 的全称是多版本并发控制(Multi-Version Concurrency Control)。当一个事务需要读取某行数据时,它不会直接读取当前行,而是沿着版本链往回找,找到第一个“可见”的版本。
Read View决定了可见性规则,它包含四个核心部分:
m_ids:生成 Read View 时,系统中所有活跃的未提交事务 ID 列表min_trx_id:m_ids中的最小值(最早的未提交事务)max_trx_id:系统下一个要分配的事务 IDcreator_trx_id:生成这个 Read View 的事务自己的 ID
可见性判断逻辑:对于版本链中的某个版本(trx_id为 X):
若
X == creator_trx_id→ 当前事务自己修改的 →可见若
X < min_trx_id→ 该版本在 Read View 创建前就已提交 →可见若
X >= max_trx_id→ 该版本在 Read View 创建之后才生成 →不可见若
min_trx_id <= X < max_trx_id:在
m_ids列表中(未提交)→不可见,继续沿版本链往前找不在列表中(已提交)→可见
这也就是为什么Read Committed(RC)和Repeatable Read(RR)两个隔离级别表现不同的根本原因:
RC 级别:每次查询都会生成一个新的 Read View,所以能立刻看到其他事务已提交的修改
RR 级别:只在事务启动时的第一次查询生成一个 Read View,并一直复用,从而保证了可重复读
3.5 Undo Log 的生命周期与清理
事务提交后,Undo Log 不会立即删除,因为可能还有其他事务依赖这些历史版本进行 MVCC 读取。
InnoDB 使用后台的purge 线程定期扫描,当一个 Undo Log 版本不再被任何活动的 Read View 所需要时,才会被真正清理。
Undo Log 分为两种类型:
Insert Undo Log:插入操作产生,事务提交后可直接删除(其他事务不需要看到未插入的状态)
Update Undo Log:更新和删除操作产生,需要保留到没有事务依赖这些版本时
四、Binlog:主从复制与数据恢复的利器
4.1 Binlog 是什么?
Binlog(Binary Log,二进制日志)是MySQL Server 层的日志,所有存储引擎(InnoDB、MyISAM 等)的更新操作都会被记录下来。
它的核心用途有三个:
主从复制:主库发送 binlog 给从库,从库重放实现数据同步
基于时间点的数据恢复:利用全量备份 + binlog 恢复到任意时间点
数据审计:追溯所有数据变更记录
4.2 Binlog 的三种记录格式
Binlog 的记录内容由binlog_format参数控制,共有三种格式。
| 格式 | 记录内容 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| STATEMENT | 原始 SQL 语句 | 日志量极小,一条 SQL 更新百万行只占几十字节 | 极易主从不一致:NOW()、UUID()、RAND()等函数在从库重放结果不同 | 极少用于生产 |
| ROW | 每行数据的前后镜像 | 绝对一致,支持闪回,并行复制友好 | 日志量巨大:更新 10 万行产生 10 万条记录 | 8.0 默认,生产推荐 |
| MIXED | 自动混合:确定性 SQL 用 STATEMENT,其他用 ROW | 平衡性能和一致性 | 存在隐藏风险,不推荐主动使用 | 历史项目过渡 |
MySQL 5.7.7 之后,ROW 已是默认模式。ROW 格式还有一个重要优势:它支持基于 Binlog 的闪回(flashback)。通过解析 ROW 格式的 Binlog,可以把DELETE转成INSERT、把UPDATE转成反向UPDATE,对误操作进行精确恢复。
4.3 Binlog 的写入机制与文件轮转
Binlog 采用追加写模式,不会覆盖已有数据。一个事务的 Binlog 会先写入线程私有的binlog cache,事务提交时一次性写入 binlog 文件。
刷盘时机由sync_binlog参数控制:
1(默认):每次事务提交都执行
fsync()落盘,最安全,性能折中0:不主动
fsync(),由操作系统决定何时落盘,性能最好但风险最高N:每 N 个事务执行一次
fsync()(N>1),折中方案
文件轮转规则:
使用
mysql-bin.000001、mysql-bin.000002…… 递增命名通过
max_binlog_size控制单文件大小(默认 1GB)执行
FLUSH LOGS可手动切换文件通过
expire_logs_days或PURGE BINARY LOGS自动清理旧文件
4.4 Binlog 数据恢复实战
如果开启了 Binlog(且格式为 ROW),误操作一般都能恢复。典型的恢复流程是:
步骤一:确认 Binlog 已开启
sql
SHOW VARIABLES LIKE 'log_bin%';
步骤二:找到误操作的位置
bash
# 解析 binlog 找到误删的 position 范围 mysqlbinlog --no-defaults -vv mysql-bin.000011 > binlog_content.txt # 在输出中搜索 DELETE/UPDATE 的关键词定位 start-pos 和 stop-pos
步骤三:用 mysqlbinlog 生成恢复 SQL
bash
# 提取误操作前后的日志区间 mysqlbinlog --start-position=1969 --stop-position=902120 mysql-bin.000011 | mysql -uroot -p
如果是误删且无备份的紧急场景,可以基于 ROW 格式生成反向 SQL:
bash
mysqlbinlog --base64-output=decode-rows -v mysql-bin.000011 \ --start-datetime="2025-12-27 09:00:00" \ --stop-datetime="2025-12-27 09:30:00" | \ sed -n '/### DELETE /{s/### DELETE/### INSERT/;p}' > rollback.sql恢复的基本原则:先全量备份,再应用增量 binlog;恢复前必须在测试环境验证;大事务闪回可能影响性能,高并发场景需谨慎。
五、三大日志协同与两阶段提交(2PC)
5.1 为什么需要两阶段提交?
单个事务在 MySQL 中最终会涉及两个独立的日志系统:InnoDB 引擎层的 Redo Log 和 Server 层的 Binlog。如果先写 Redo Log 后写 Binlog,或反过来,都可能出现日志不一致的“半成功状态”。
先写 Redo Log,后写 Binlog:Redo Log 已持久化、Binlog 未写入时宕机 → 主库通过 Redo Log 恢复数据,但 Binlog 没有这个事务,从库丢失更新,主从不一致
先写 Binlog,后写 Redo Log:Binlog 已写入、Redo Log 未提交时宕机 → 主库回滚该事务,但 Binlog 记录了这个变更,从库多了这个事务,同样主从不一致
为了解决这个问题,MySQL 引入了两阶段提交(2PC)协议。
5.2 两阶段提交的完整流程
两阶段提交将事务提交拆分为Prepare 阶段和Commit 阶段:
Prepare 阶段:
事务执行过程中,修改操作不断写入 Undo Log 和 Redo Log Buffer
事务提交时,InnoDB 将 Redo Log Buffer 刷盘
将事务状态标记为
PREPARE,同时将内部 XID(XA 事务 ID)写入 Redo Log
Commit 阶段:
将 XID 写入 Binlog
将 Binlog 刷盘(受
sync_binlog参数控制)调用 InnoDB 的提交接口,将 Redo Log 的事务状态从
PREPARE改为COMMIT向客户端返回“提交成功”
这里有一个精妙的设计:Commit 阶段的最后一步(Redo Log 改为
COMMIT)只需要写入操作系统缓存(OS page cache),不需要立即fsync()。因为只要 Binlog 成功落盘,即使此时崩溃重启,Redo Log 中的PREPARE状态也会被认可——Binlog 是最终判断依据。
5.3 崩溃恢复的判断逻辑
MySQL 崩溃重启后,扫描 Redo Log 中所有处于PREPARE状态的事务。对于每个这样的未决事务:
根据 Redo Log 中记录的 XID,去 Binlog 中查找对应的事务记录
如果 Binlog 中存在完整记录→ 该事务在提交阶段实际上已经成功 →提交事务
如果 Binlog 中不存在记录→ 该事务在 Commit 阶段之前就已崩溃 →回滚事务
核心原则:以 Binlog 的完整性作为事务是否提交的最终判断标准。这个机制保证了无论何时崩溃,主库的数据一定等于“从 Binlog 中恢复从库时得到的数据”,主从数据最终一致。
5.4 一个完整的 update 语句流程
以执行UPDATE t SET c = c + 1 WHERE id = 2为例,完整流程如下:
事务开始,记录 Undo Log(保存修改前的旧值)
执行更新:
从磁盘/缓存读取数据页到 Buffer Pool
在 Buffer Pool 中修改数据
将修改动作写入 Redo Log Buffer(内存)
Prepare 阶段:
Redo Log Buffer 刷盘,事务状态置为
PREPARE
Commit 阶段:
写入 Binlog 并刷盘
Redo Log 状态改为
COMMIT
后台线程择机将 Buffer Pool 中的脏页刷回磁盘(异步)
这个设计的精妙之处在于:事务提交前只写了顺序的 Redo Log 和 Binlog(写日志很快),真正的随机写磁盘操作被延迟到了异步阶段,保证了高性能;而两阶段提交又保证了分布式一致性,实现了性能与可靠性的平衡。
六、三大日志核心对比速查表
| 对比维度 | Binlog | Redo Log | Undo Log |
|---|---|---|---|
| 所属层级 | Server 层 | InnoDB 引擎层 | InnoDB 引擎层 |
| 日志类型 | 逻辑日志(SQL/行变化) | 物理日志(页修改) | 逻辑日志(反向操作) |
| 写入方式 | 追加写,生成新文件 | 循环写,大小固定 | 追加写,定期清理 |
| 核心作用 | 主从复制、数据恢复 | 崩溃恢复(crash-safe) | 事务回滚、MVCC |
| 能否保证持久性 | ❌ 不能 | ✅ 能 | ❌ 不能 |
| 能否保证原子性 | ❌ 不能 | ❌ 不能 | ✅ 能 |
| 刷盘参数 | sync_binlog | innodb_flush_log_at_trx_commit | 无独立参数 |
| 生命周期 | 手动/自动清理 | 循环覆盖 | purge 线程清理 |
七、总结
Redo Log、Undo Log 与 Binlog 三者各司其职,共同构成了 MySQL 事务和高可用能力的基石:
Undo Log(原子性基石):记录修改前的旧数据,支持事务回滚,也是 MVCC 多版本并发控制的关键
Redo Log(持久性基石):基于 WAL 机制保证崩溃恢复能力,通过两阶段提交与 Binlog 协同,确保主从数据一致
Binlog(可复制性基石):记录所有变更操作,支撑主从复制和时间点数据恢复
每一类日志单独拿出来都不复杂,但三者协同的机制(特别是 MVCC + 版本链 + Read View、两阶段提交 + 崩溃恢复)才是 MySQL 真正硬核的地方。掌握了这个体系,无论是面试还是线上问题排查,都能更加从容。
下一步学习建议:
实际验证
innodb_flush_log_at_trx_commit和sync_binlog不同取值对性能的影响搭建一主一从环境,观察 binlog 的传输与重放过程
模拟一次误删操作,用 mysqlbinlog 完整走一遍数据恢复流程(⚠️ 务必在测试环境进行)
