文章目录
- MySQL 三大日志讲透:redo log、undo log、binlog 与两阶段提交
- 一、先搞清楚"为什么需要三份日志"
- 二、redo log:用顺序写换掉随机写
- 2.1 WAL 思想
- 2.2 redo log 是怎么写的
- 2.3 redo log 文件:环形复用 + checkpoint
- 2.4 redo log 该设多大(附测量方法)
- 2.5 崩溃恢复怎么跑
- 三、undo log:回滚和 MVCC 的共同载体
- 3.1 它是"反向操作"记录
- 3.2 两类 undo:insert undo 与 update undo
- 3.3 版本链与 MVCC(串起隔离级别)
- 3.4 DELETE 是"两步慢动作"
- 3.5 undo 的存放与运维
- 四、binlog:Server 层的逻辑日志
- 4.1 它是干什么的
- 4.2 三种格式
- 4.3 binlog 的刷盘策略
- 五、三者对比总表
- 六、关键难点:两阶段提交
- 七、"双一"的性能代价与优化手段
- 八、常见误区
- 九、小结
MySQL 三大日志讲透:redo log、undo log、binlog 与两阶段提交
为什么断电重启后,已提交的事务一条不少、没提交的一条不留?
为什么主从同步要靠 binlog 而不是 redo log?答案全在这三份日志里。本篇把它们各自的职责、写入时机、刷盘策略讲清楚,
最后落到一个必须搞懂的问题:两阶段提交。
一、先搞清楚"为什么需要三份日志"
设想这条 UPDATE 执行到一半,机房断电:
UPDATEaccountSETbalance=balance-100WHEREid=1;脏页还在 Buffer Pool 里,没落盘。重启后怎么保证数据正确?
三种情况分别要处理:
| 情况 | 需求 | 谁来负责 |
|---|---|---|
| 事务已提交,脏页还没刷盘 | 必须把修改补回来(持久性 D) | redo log |
| 事务没提交,但已经改了数据页 | 必须回滚(原子性 A) | undo log |
| 要恢复历史数据 / 做主从复制 / 同步给下游 | 需要一份"做过什么"的记录 | binlog |
不同的诉求,注定了三份日志的性质完全不同:
| 日志 | 所属层 | 日志类型 | 主要作用 |
|---|---|---|---|
| redo log | InnoDB 存储引擎层 | 物理日志(哪个页改了哪个字节) | 崩溃恢复,保证持久性 |
| undo log | InnoDB 存储引擎层 | 逻辑日志(反向操作) | 回滚 + MVCC,保证原子性 |
| binlog | MySQL Server 层 | 逻辑日志(SQL 或行前后镜像) | 主从复制、数据恢复、CDC |
注意 binlog 在Server 层,这就是它和 redo log 分工的根本原因:
Redo log 是 InnoDB 私有的,换 MyISAM 就没有;而 binlog 面向所有引擎、面向"外部世界"。
二、redo log:用顺序写换掉随机写
2.1 WAL 思想
如果每次提交都要求把修改的数据页刷到磁盘,那一次 UPDATE 可能触发多个随机 IO
(数据页在表空间文件里散落各处),性能没法看。
InnoDB 的解法是WAL(Write-Ahead Logging,预写日志):
先把"改了什么"顺序写到 redo log 里,数据页留在内存慢慢刷。
顺序写 vs 随机写在 HDD 上差两个数量级,在 SSD 上依然差好几倍。
只要 redo log 落盘了,即使数据页没刷,崩溃后也能重放出来——持久性就有了保障。
2.2 redo log 是怎么写的
事务修改数据 │ ▼ ① 改内存中的 Buffer Pool 页(变成脏页),同时写 redo log buffer │ ▼ ② redo log buffer → write() 到 page cache(OS 缓存) │ ▼ ③ fsync() 刷到磁盘上的 redo log 文件 ← 只有这一步完成,事务才算真正安全第 ②③ 步的时机由参数innodb_flush_log_at_trx_commit控制:
| 值 | commit 时做什么 | 崩溃风险 | 适用 |
|---|---|---|---|
| 1(默认) | write + fsync,每次提交都刷盘 | 不丢已提交事务 | 金融 / 交易等不能丢数据的场景 |
| 2 | 只 write 到 OS 缓存,每秒 fsync 一次 | 宕机丢最后 1 秒;OS 崩溃也丢 | 高并发、可容忍少量丢失 |
| 0 | 什么都不做,由后台线程每秒 write + fsync | 宕机丢最后 1 秒 | 测试环境,生产不建议 |
SELECT@@innodb_flush_log_at_trx_commit;-- 生产请确认是 12.3 redo log 文件:环形复用 + checkpoint
redo log 不是无限增长的,它在磁盘上是一组固定大小的文件,循环覆盖写:
┌───────────────────────────────────────────┐ │ ib_logfile0 │ ib_logfile1 │ ib_logfile2 │ ... │ └───────────────────────────────────────────┘ ▲ write pos(写指针,不停往后追加) │ ▼ checkpoint(已刷脏页的位置,之前的日志可被覆盖)这里有个关键关系:redo log 满了就必须强制刷脏页,
把 checkpoint 往前推,腾出空间。所以 redo log 太小会导致:
- checkpoint 频繁触发,脏页被迫同步刷盘
- 写入出现卡顿时快时慢的锯齿(每次追上 checkpoint 就抖一下)
2.4 redo log 该设多大(附测量方法)
MySQL 5.7用这两个参数:
SHOWVARIABLESLIKE'innodb_log_file%';-- innodb_log_file_size 单个文件大小-- innodb_log_files_in_group 文件个数(默认 2)-- 总容量 = size × files_in_groupMySQL 8.0.30 之后改成了单一参数innodb_redo_log_capacity(默认约 100MB),
旧的innodb_log_file_size/innodb_log_files_in_group已被标记废弃:
SELECT@@innodb_redo_log_capacity;-- 单位字节,8.0.30+SETGLOBALinnodb_redo_log_capacity=2147483648;-- 动态调整为 2G大小怎么定?别拍脑袋,测一下:
-- 记录 60 秒内 redo log 的写入量SHOWGLOBALSTATUSLIKE'Innodb_os_log_written';-- 等 60 秒(业务正常运行时)SHOWGLOBALSTATUSLIKE'Innodb_os_log_written';-- 计算:差值 / 60 = 每秒写入字节数-- 经验值:让 redo log 能容纳 **1 小时** 的写入量(或不低于 30 分钟)举个数:60 秒写了 240MB,即 4MB/s,那么 1 小时是 14.4GB——
这说明你的库写压力很大,redo log 至少给到几个 G;
反之如果 1 小时才 200MB,给 2GB 已经绰绰有余。生产常见区间是 1G ~ 4G。
2.5 崩溃恢复怎么跑
重启时 InnoDB 做的事:
- 找到 redo log 里的 checkpoint 位置
- 从 checkpoint 开始重放 redo(包括未提交事务的 redo 也重放,先全量前滚)
- 扫描 undo 的回滚段,找出所有处于
ACTIVE状态的事务 - 用 undo log 把这些未提交事务回滚掉
也就是说 InnoDB 走的是"先重做所有,再回滚未提交的"这种账要两步算完的路线。
这也是为什么即使你从没 ROLLBACK 过,undo log 依然是崩溃恢复的必需品。
三、undo log:回滚和 MVCC 的共同载体
3.1 它是"反向操作"记录
undo log 记的是逻辑反向操作,非常朴素:
| 操作 | undo log 记什么 | 回滚时做什么 |
|---|---|---|
| INSERT | 记下这条记录的主键值 | 按主键 DELETE 掉 |
| DELETE | 记下整行内容 | 重新 INSERT 回去 |
| UPDATE(不更新主键) | 记下被改列的旧值 | 把旧值 UPDATE 回去 |
| UPDATE(更新主键) | 拆成 delete mark + insert 两条 | 反向的两个操作 |
注意查询(SELECT)不产生 undo log——只有改数据的操作才需要后悔药。
3.2 两类 undo:insert undo 与 update undo
这是理解 undo 生命周期的关键分界:
| 类型 | 产生自 | 事务提交后 |
|---|---|---|
| insert undo | INSERT(以及因更新主键产生的 insert) | 可以直接丢弃,没人需要看到它 |
| update undo | DELETE / UPDATE | 不能丢,MVCC 的快照读要用它读历史版本 |
所以 update undo 会被挂到一个叫history list的链表上,等到「没有任何事务需要读这个旧版本」时,
才由后台的purge 线程真正清理掉。
3.3 版本链与 MVCC(串起隔离级别)
每行数据有两个隐藏列:
DB_TRX_ID:最后修改这行的事务 IDDB_ROLL_PTR:回滚指针,指向 undo log 里的上一个版本
多次修改会串成一条链,快照读顺着DB_ROLL_PTR往回找,直到找到一个自己看得见的版本。
这一部分在事务隔离级别那篇里展开过,这里只需要记住一个因果:
因为 MVCC 依赖 update undo,所以只要有一个长事务一直不走,它需要的旧版本就必须保留,
history list 就会持续变长,undo 表空间就会膨胀。这是长事务危害的物理根源(下一篇详聊)。
监控它:
SHOWENGINEINNODBSTATUS\G-- 关注这一段:-- History list length 8 ← 越大说明待 purge 的版本越多3.4 DELETE 是"两步慢动作"
InnoDB 里的 DELETE 不是立刻抹掉数据:
- delete mark:把记录的删除标记置 1,逻辑上不可见,但物理上还在页里
- purge:事务提交后,由后台 purge 线程把它从记录链表移走,并挂到页内的空闲链表上,空间可被后续插入复用
这个"延迟删除"正是为了服务 MVCC:别的会话可能还在读这个旧版本,不能马上抹掉。
推论:频繁 DELETE 之后表文件大小不会立刻下降,这是 InnoDB 的正常行为,
要回收空间得OPTIMIZE TABLE(重建表)。
3.5 undo 的存放与运维
- undo 保存在undo tablespace(MySQL 8.0 默认是 2 个独立 undo 表空间文件
undo_001、undo_002,
老版本在系统表空间ibdata1里) - 独立 undo 表空间的优点:可以自动 truncate 回收,而 ibdata1 只增不减
- 相关参数:
innodb_undo_tablespaces、innodb_undo_log_truncate=ON、innodb_max_undo_log_size(超限才触发回收) - purge 线程数:
innodb_purge_threads(默认 4),写压力大的库可以加到 8/16
SHOWVARIABLESLIKE'innodb_undo%';SHOWVARIABLESLIKE'innodb_purge%';⚠️ 8.0 里 undo 表空间一旦创建基本不可削减配置,初始化时别给太小的磁盘目录。
四、binlog:Server 层的逻辑日志
4.1 它是干什么的
redo log 只管"让本机崩溃后能恢复",它做不到这三件事:
- 主从复制(redo log 是引擎私有格式、且循环覆盖,会丢历史)
- 按时间点恢复 PITR(需要一份持续追加的完整变更历史)
- 把变更同步给 Kafka / 数仓 / 订阅系统(CDC)
binlog 是追加写、不覆盖的,因此才适合做这些事。
4.2 三种格式
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| ROW(5.7.7 起默认) | 每一行变更前后的行镜像 | 复制绝对安全,随机函数、触发器、非确定性语句都不怕 | 日志体积大(改 10 万行记 10 万条) |
| STATEMENT | 原始 SQL 语句 | 体积小 | 有不安全语句问题:UUID()、NOW()、LIMIT无 ORDER BY 等可能导致主从不一致 |
| MIXED | 多数用 STATEMENT,不安全时自动切 ROW | 折中 | 判断逻辑复杂,出问题不好排查 |
SELECT@@binlog_format,@@binlog_row_image;-- binlog_row_image = FULL(默认)/ MINIMAL / NOBLOB生产建议:ROW + FULL最安全。如果磁盘吃紧可以改成MINIMAL(只记录必要的列),
但要知道这会让某些下游工具的语义变差。
4.3 binlog 的刷盘策略
SELECT@@sync_binlog;| 值 | 行为 | 风险 |
|---|---|---|
| 1(推荐) | 每提交一次事务就 fsync binlog | 不丢 |
| N > 1 | 攒够 N 次提交才 fsync | 宕机丢最后 N 个事务的 binlog(主从可能不一致) |
| 0 | 交给 OS 调度 | 丢失不可控 |
SELECT@@binlog_expire_logs_seconds;-- 8.0 用这个(单位秒)-- 5.7 用 expire_logs_days,8.0 中已废弃保留期别设太短:至少覆盖「一次全量备份 + 追binlog」的窗口,常见 7 天。
五、三者对比总表
| 对比项 | redo log | undo log | binlog |
|---|---|---|---|
| 所属层 | InnoDB | InnoDB | MySQL Server |
| 日志类型 | 物理(页级) | 逻辑(反向操作) | 逻辑(SQL / 行镜像) |
| 解决什么 | 持久性 D | 原子性 A + MVCC | 复制 / 恢复 / 归档 |
| 写入方式 | 循环覆盖写,大小固定 | 追加,purge 后回收 | 追加写,文件轮转 |
| 是否可被替代 | 否 | 否 | 可以关掉(但等于放弃复制和恢复) |
| 能否直接读懂 | 不能(物理格式) | 不能 | 能(mysqlbinlog解析) |
| 相关线程 | log writer / flusher | purge thread | dump thread(主从) |
顺带提一句常被排进同一张表的relay log(中继日志):
它是从库上的东西,IO 线程把主库 binlog 拉回来先落地成 relay log,SQL 线程再回放。
它本质上就是主从链路的中间缓冲,不参与本机事务。
六、关键难点:两阶段提交
现在的问题是:redo log 和 binlog 是两份独立的日志,
如果它们不一致,就会出现"主库有这条数据,从库没有"——最严重的事故之一。
考虑这个顺序:
① 写 redo log,标记事务已提交 ② 写 binlog ↑ 如果在这里崩溃 → redo 有、binlog 没有 → 本机恢复后数据存在,但从库永远少了这条 → 主从不一致反之先写 binlog 再写 redo,崩溃时会出现"从库有、主库没有"。两种都不能接受。
解法是把 redo log 的提交拆成两步,中间夹着 binlog:
┌──────────────────────────────┐ 第 1 步 │ redo log: prepare 状态 │ ← fsync 落盘 └──────────────────────────────┘ │ ┌──────────────────────────────┐ 第 2 步 │ 写 binlog,fsync 落盘 │ └──────────────────────────────┘ │ ┌──────────────────────────────┐ 第 3 步 │ redo log: 标记 commit │ └──────────────────────────────┘崩溃恢复时的判定规则非常干净:
| 崩溃时 redo 状态 | binlog 中该事务的 XID | 恢复动作 |
|---|---|---|
| prepare | 完整存在 | 提交(第 3 步补上) |
| prepare | 不存在 / 不完整 | 回滚 |
| commit | 必然完整 | 什么都不用做 |
也就是说:只要 binlog 完整落盘了,事务就必须算提交成功。
这就是为什么sync_binlog=1和innodb_flush_log_at_trx_commit=1(俗称"双一")
是金融级场景的标配——少任何一个,都有丢失或不一致的窗口。
⚠️ 也正是因为这套机制,在主从架构下把sync_binlog设成 0 或 N 特别危险:
主库崩溃后那些"已提交但 binlog 没刷盘"的事务,本机靠 redo 恢复了,从库却永远收不到,
业务上会表现为"用户看到下单成功,但后台/报表查不到这笔"。
七、"双一"的性能代价与优化手段
每次事务提交要两次 fsync(redo 一次、binlog 一次),确实贵。三种常见缓解方式:
1. 组提交(group commit)——MySQL 原生支持,无需配置。多个并发事务会把各自的 redo/binlog 攒在一起做一次 fsync,
TPS 越高,单次 fsync 的开销摊得越薄。可以通过下面两个参数让它凑得更积极:
SELECT@@binlog_group_commit_sync_delay,-- 微秒级延迟,故意等一等凑更多事务@@binlog_group_commit_sync_no_delay_count;-- 攒够 N 个就不等了2. 把 binlog 和 redo log 放到不同的物理磁盘,让两个 fsync 并行,减少磁头竞争。
3. 用更好的磁盘:NVMe SSD 的 fsync 延迟比机械盘低两个数量级,
很多时候"双一太慢"的根因是还在用机械盘或网络盘。
不建议的做法:为了性能把innodb_flush_log_at_trx_commit改成 2 或 0。
如果业务确实允许丢失,那应该做的是明确记录这个决策和它的后果,而不是悄悄改掉。
八、常见误区
误区 1:redo log 记录了没提交的事务,恢复时会把未提交的也提交
——不会。恢复分两步:先 redo 前滚,再用 undo 回滚所有ACTIVE事务。这也是 undo 存在的第二个理由。
误区 2:有了 redo log 就不需要 binlog
——redo 是物理格式、循环覆盖、引擎私有。binlog 能做复制、PITR、CDC,redo 全做不到。
误区 3:binlog 可以随便开 ROW 格式,没代价
——ROW 会让日志膨胀明显。一条UPDATE ... WHERE 条件命中 10 万行的 SQL,
在 STATEMENT 下几十字节,在 ROW 下是 10 万条行镜像,可能几十 MB。
批量更新记得拆小批次,顺便防止大事务。
误区 4:事务提交后就一定已经把数据写到磁盘的数据文件了
——不一定,也不必。数据页可能还在 Buffer Pool 里等着刷,真正落盘的只是 redo log。
这也是为什么突然 kill -9 进程不会丢数据,但直接删ibd文件会灾难。
误区 5:undo 只在 ROLLBACK 时有用
——它还支撑 MVCC 的快照读和崩溃恢复的未提交事务回滚。
很多时候 undo 暴涨不是因为有人 ROLLBACK,而是因为有长事务卡着不放。
误区 6:MySQL 8.0 调 redo log 还是改innodb_log_file_size
——8.0.30 起请用innodb_redo_log_capacity,旧参数已废弃。
九、小结
- 三份日志分工明确:redo 管持久性(物理、循环写),undo 管原子性 + MVCC(逻辑、反向操作),
binlog 管复制与恢复(逻辑、追加写) - redo log 靠WAL + 顺序写把随机 IO 变成顺序 IO;太小会导致 checkpoint 频繁刷脏页,写性能锯齿抖动
- redo 大小按实测的每小时写入量估算,生产常见 1G~4G;8.0.30 起用
innodb_redo_log_capacity - undo 分insert undo(提交即可弃)和update undo(要服务 MVCC,靠 purge 线程回收)
- DELETE 是delete mark + purge两步,所以删完表文件不会立刻变小
- binlog 推荐ROW + FULL;刷盘用
sync_binlog=1 - 两阶段提交是 redo 与 binlog 一致性的保证:prepare → 写 binlog → commit;
恢复时"binlog 完整就提交,否则回滚" - 金融级配置俗称"双一":
innodb_flush_log_at_trx_commit=1+sync_binlog=1,
性能问题优先用组提交、磁盘分离、SSD 解决,不要直接降级安全性 History list length是观察 undo 堆积最直观的指标
下一篇进入 InnoDB 的内存世界:Buffer Pool 怎么管理這块最贵的内存,
LRU 链表怎么防止全表扫描污染缓存,以及脏页是怎么被刷出去的。