日志系统
开篇:MySQL 靠什么保证数据不丢?
飞机有黑匣子,即使坠毁也能还原事故经过。MySQL 也有自己的"黑匣子"——日志系统。
每一次数据变更,MySQL 都会忠实地记录下来。即使在写入过程中突然断电、宕机,重启后也能通过日志恢复到一致的状态,不丢数据(至少在默认配置下如此)。
MySQL 的日志系统由多种日志组成,其中和事务直接相关的核心日志有三种:
| 日志 | 一句话定位 | 谁产生的 |
|---|---|---|
| redo log | 崩溃恢复的保障,保证持久性 | InnoDB 引擎层 |
| undo log | 事务的后悔药,支持回滚和 MVCC | InnoDB 引擎层 |
| binlog | 数据同步的桥梁,用于主从复制和数据恢复 | MySQL Server 层 |
记忆技巧:redo = "re" + "do",重新做一遍 → 崩溃后重做恢复。undo = 撤销 → 回滚到修改前。bin = binary,最原始最完整的记录 → 备份和复制的基础。
一、redo log:崩溃恢复的保障
1.1 为什么需要 redo log?
InnoDB 读写数据的基本单位是"页"(16KB)。数据页从磁盘加载到内存的 Buffer Pool 后才能被读写。修改完的数据页叫"脏页",需要在合适的时机刷回磁盘。
问题来了:如果事务刚提交,脏页还没来得及刷盘,数据库就宕机了怎么办?
最简单的方案是"每次修改立即刷盘",但这有两个致命缺陷:
- 写放大:你只改了页中的一个字节,却要把整个 16KB 的页写入磁盘
- 随机 IO:一条 UPDATE 可能修改多个不相邻的页,每个页都要一次随机 IO
redo log 的思路更聪明:不急着刷数据页,而是先把"做了什么修改"记到一个专用的日志文件中。 这个日志文件就是 redo log。
这样即使宕机了,重启后只要重放 redo log 中的记录就能恢复数据——这就是 WAL(Write-Ahead Logging) 策略:先写日志,再写数据。
1.2 redo log 的特点
- 体积小:只记录"哪个页的哪个偏移量改了什么值",不记录整页数据
- 顺序写:redo log 是追加写入,顺序 IO 比随机 IO 快得多
- 循环使用:redo log 的空间是固定大小的环形缓冲区,写满后从头覆盖
1.3 redo log 的写入流程
一次完整的 redo log 写入分四步:
- 读取数据页:从磁盘加载到 Buffer Pool
- 写入 redo log buffer:修改数据后,将修改记录写入内存中的 redo log buffer
- 刷入 redo log file:事务提交时,将 redo log buffer 刷入磁盘上的 redo log 文件
- 异步刷脏页:后台线程在合适时机将 Buffer Pool 中的脏页写入数据文件
1.4 redo log 的刷盘策略
redo log buffer 何时刷入磁盘,由参数 innodb_flush_log_at_trx_commit 控制:
| 值 | 行为 | 安全性 | 性能 |
|---|---|---|---|
| 0 | 每秒刷盘一次(由后台线程完成) | 最低,宕机可能丢 1 秒数据 | 最高 |
| 1(默认) | 每次事务提交都 fsync 刷盘 | 最高,不丢数据 | 最低 |
| 2 | 每次提交写入 OS page cache,由操作系统决定何时 fsync | 中等,OS 崩溃可能丢数据 | 中等 |
生产环境推荐设置为 1。如果能接受少量数据丢失换取更高性能(如非关键业务),可以设为 2。
二、undo log:事务的后悔药
2.1 undo log 的两大作用
作用一:事务回滚。 事务执行过程中,每次修改数据前都先把旧值记到 undo log。如果事务需要回滚(手动 ROLLBACK 或遇到错误),就按 undo log 逆向操作:
- INSERT 了一行 → undo log 记录 DELETE
- DELETE 了一行 → undo log 记录 INSERT(原始数据)
- UPDATE 了一行 → undo log 记录反向的 UPDATE(改回旧值)
作用二:MVCC 支持。 undo log 中存储的旧版本数据,构成了 MVCC 的版本链。当事务执行快照读时,通过版本链找到合适的历史版本。
undo log 本身的持久性也需要 redo log 来保证——undo log 的写入操作也会产生对应的 redo log。
2.2 两种 undo log
| 类型 | 触发操作 | 用途 | 何时回收 |
|---|---|---|---|
| insert undo log | INSERT | 仅用于事务回滚 | 事务提交后立即可回收 |
| update undo log | UPDATE / DELETE | 回滚 + MVCC 快照读 | 必须等到没有活跃事务依赖时才能被 purge 线程回收 |
这也是长事务的隐患之一:长事务持有的 ReadView 会"钉住"老的 update undo log,阻止 purge 回收,导致 undo log 空间持续膨胀。
2.3 undo log 的页复用
为了避免为每个事务分配独立的 undo 页(太浪费),InnoDB 设计了 undo 页复用机制:
- 事务提交后,undo 页不会立即释放
- 如果 undo 页的使用空间 < 3/4,则标记为可复用
- 后续事务的 undo log 可以追加写入到这个页中
代价是 undo log 变得分散(不连续),清理效率不如连续存储。
三、binlog:数据同步的桥梁
3.1 binlog 是什么?
binlog(binary log)是 MySQL Server 层 产生的日志,记录了所有对数据库的 DDL 和 DML 操作(不包括 SELECT 和 SHOW 等只读操作)。
binlog 的两大核心用途:
- 主从复制:从库读取主库的 binlog,回放其中的操作来保持数据同步
- 数据恢复:通过回放 binlog 可以将数据库恢复到任意时间点
3.2 binlog 的三种格式
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| statement | SQL 语句原文 | 日志量小 | 某些语句主从执行结果可能不一致(如含 NOW()、LIMIT 无 ORDER BY) |
| row | 每行数据变更前后的值 | 主从完全一致 | 日志量大,批量修改时尤其明显 |
| mixed | MySQL 自动判断用 statement 还是 row | 兼顾两者 | 某些场景仍可能不一致 |
目前生产环境推荐使用 row 格式。虽然日志量大,但主从一致性有保障。
RC 隔离级别下只能使用 row 格式。如果设为 mixed,MySQL 会自动切换到 row。
3.3 binlog 的写入机制
binlog 的写入时机和 redo log 不同:redo log 在事务执行过程中不断写入,而 binlog 在事务提交时一次性写入。
具体流程:
- 事务执行过程中,先把日志写到线程私有的 binlog cache
- 事务提交时,把 binlog cache 写入到 binlog 文件
binlog 的刷盘策略由参数 sync_binlog 控制:
| 值 | 行为 | 安全性 | 性能 |
|---|---|---|---|
| 0 | 只 write 到 page cache,由 OS 决定何时 fsync | 最低 | 最高 |
| 1(默认) | 每次提交都 fsync | 最高 | 最低 |
| N (>1) | 每 N 个事务提交后 fsync 一次 | 折中 | 折中 |
四、两阶段提交:保证 redo log 和 binlog 的一致性
redo log 和 binlog 是两个独立的日志系统,分属不同层(引擎层和 Server 层)。如果它们的写入不同步,就会导致数据不一致。
4.1 为什么需要两阶段提交?
假设不做任何协调,执行 UPDATE users SET name='Hollis' WHERE id=10:
场景一:先写 redo log,后写 binlog
redo log 写成功后宕机,binlog 没写。重启后 redo log 恢复数据,主库有 name='Hollis'。但 binlog 没记录,从库还是旧值——主从不一致。
场景二:先写 binlog,后写 redo log
binlog 写成功后宕机,redo log 没写。重启后 redo log 中没有记录,主库数据还是旧值。但 binlog 已同步到从库,从库有 name='Hollis'——主从不一致。
不管先写谁,都有可能不一致。解决方案就是 两阶段提交(2PC)。
4.2 两阶段提交的流程
关键点在于 redo log 被拆成了两个阶段:
- Prepare 阶段:redo log 写入磁盘,标记为 prepare
- Commit 阶段:binlog 写入磁盘后,redo log 标记为 commit
4.3 崩溃恢复时的处理
MySQL 重启后会检查 redo log:
| 崩溃时机 | redo log 状态 | binlog 状态 | 恢复策略 |
|---|---|---|---|
| prepare 之后、binlog 之前 | prepare | 未写入 | 回滚事务 |
| binlog 之后、commit 之前 | prepare | 已写入且完整 | 提交事务 |
| commit 之后 | commit | 已写入 | 正常,无需处理 |
MySQL 通过事务的 XID(全局唯一事务标识)来关联 redo log 和 binlog。恢复时检查 redo log 中 prepare 状态的事务,再去 binlog 中查找是否有对应的 XID 记录。有则提交,无则回滚。
4.4 一次 INSERT 操作的完整日志写入顺序
undo log → redo log (prepare) → binlog → redo log (commit)- undo log:记录变更前的数据(INSERT 的 undo 记录对应的 DELETE 信息),最先写入
- redo log prepare:记录数据变更,标记为 prepare 状态
- binlog:将 binlog cache 写入 binlog 文件并刷盘
- redo log commit:将 redo log 标记为 commit 状态
五、三种日志对比
| 对比维度 | redo log | undo log | binlog |
|---|---|---|---|
| 产生层 | InnoDB 引擎层 | InnoDB 引擎层 | MySQL Server 层 |
| 适用引擎 | 仅 InnoDB | 仅 InnoDB | 所有引擎 |
| 记录内容 | 物理日志(页级修改) | 逻辑日志(修改前的数据) | 逻辑日志(SQL 或行数据变更) |
| 核心作用 | 崩溃恢复,保证持久性 | 事务回滚 + MVCC | 主从复制 + 数据恢复 |
| 写入时机 | 事务执行过程中持续写入 | 数据修改前写入 | 事务提交时一次性写入 |
| 空间管理 | 固定大小,循环覆盖 | 动态分配,purge 回收 | 持续追加,按保留策略清理 |
六、常见面试题精选
Q1:redo log 和 binlog 的核心区别是什么?
三个关键区别:(1) redo log 是引擎层产生的,binlog 是 Server 层产生的;(2) redo log 是物理日志(记录页修改),binlog 是逻辑日志(记录 SQL 或行变更);(3) redo log 空间固定循环使用,binlog 持续追加。
Q2:为什么需要两阶段提交?
为了保证 redo log 和 binlog 的一致性。如果不做协调,无论先写哪个,中间宕机都会导致主从数据不一致。两阶段提交通过 prepare → binlog → commit 三步保证了原子性。
Q3:MySQL 能保证数据 100% 不丢吗?
不能。即使 innodb_flush_log_at_trx_commit=1 + sync_binlog=1,在以下极端情况下仍可能丢数据:
- 硬盘或 RAID 控制器的 write-back cache 欺骗了 fsync
- 没有 UPS 或 BBU 保护的突然断电
- 磁盘物理损坏
要尽可能接近"不丢",需要配合硬件保障:带 BBU 的 RAID 卡、支持掉电保护的 SSD、UPS 电源。
Q4:undo log 会一直存在吗?
不会。insert undo log 在事务提交后即可回收;update undo log 需等到没有活跃事务依赖时才能被 purge 线程清理。这也是为什么长事务会导致 undo log 空间膨胀的原因。
小结
| 概念 | 一句话总结 |
|---|---|
| redo log | 崩溃后重做恢复,保证持久性。WAL 策略的核心载体 |
| undo log | 事务的后悔药。既支持回滚,又是 MVCC 版本链的物理存储 |
| binlog | 主从复制和数据恢复的基础。推荐 row 格式 |
| 两阶段提交 | prepare → binlog → commit,保证 redo log 和 binlog 的一致性 |
| 刷盘策略 | innodb_flush_log_at_trx_commit=1 + sync_binlog=1 是最安全的配置 |
附录:MySQL 的其他日志
除了 redo log、undo log、binlog 这三种和事务直接相关的核心日志外,MySQL 还有几种常用的辅助日志:
慢查询日志(Slow Query Log)
记录所有执行时间超过 long_query_time(默认 10 秒)的 SQL 语句。是查询性能优化的重要工具。
-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query_log';
-- 查看慢查询时间阈值
SHOW VARIABLES LIKE 'long_query_time';
-- 临时开启
SET GLOBAL slow_query_log = ON;
-- 设置阈值为 1 秒
SET GLOBAL long_query_time = 1;通用查询日志(General Query Log)
记录 MySQL 收到的所有 SQL 语句,包括 SELECT、SHOW 等。日志量极大,通常只在排查问题时临时开启。
-- 查看状态
SHOW VARIABLES LIKE '%general%';
-- 临时开启
SET GLOBAL general_log = ON;
-- 关闭
SET GLOBAL general_log = OFF;错误日志(Error Log)
记录 MySQL 启动、停止和运行过程中的错误、警告信息。是排查 MySQL 服务异常的首选日志,默认开启且无法禁用。
中继日志(Relay Log)
只存在于从库上。从库从主库拉取 binlog 后,先写入本地的中继日志,再由 SQL 线程读取中继日志进行数据回放。
日志的代价
日志不是免费的:
- 性能开销:每次写入都需要 IO 操作,尤其是 fsync 刷盘
- 磁盘空间:binlog 持续增长,需要定期清理;redo log 固定大小但占用一定空间
- 网络开销:主从复制时 binlog 需要通过网络传输
因此,在配置日志策略时,需要在 数据安全性 和 系统性能 之间做权衡。
附录:一次完整的 UPDATE 操作流程
把前面讲的所有日志知识串起来,看一次 UPDATE users SET name='Hollis' WHERE id=10 的完整流程:
整个过程的关键设计思想:
- undo log 最先写:保证回滚能力
- redo log 用两阶段提交:保证和 binlog 的一致性
- binlog 在两阶段中间写入:卡在 prepare 和 commit 之间
- 数据文件最后异步写:不影响事务提交速度
这套日志机制让 MySQL 在保证数据安全性的同时,还能维持不错的写入性能。
附录:组提交优化(Group Commit)
在高并发场景下,每个事务都做一次 fsync 的开销很大。MySQL 5.7 引入了 组提交 优化:将多个事务的 fsync 操作合并成一次批量 fsync,大幅减少磁盘 IO。
-- 查看组提交配置
SHOW VARIABLES LIKE '%group_commit%';
-- binlog_group_commit_sync_delay:延迟多少微秒再 fsync
-- binlog_group_commit_sync_no_delay_count:累积多少个事务再 fsync两个参数是"或"关系——满足任一条件就触发 fsync。
组提交后,两阶段提交的流程变为:
- 多个事务的 redo log 写入(prepare 状态)
- 多个事务的 binlog 写入 page cache
- 一次批量 fsync,将所有 binlog 刷盘
- 多个事务的 redo log 标记为 commit
通过攒批 fsync,磁盘 IO 次数从"每事务一次"降为"每批一次",在高并发写入场景下性能提升非常明显。