【计算基础|MySQL17】三大日志:undo / redo / binlog

【计算基础|MySQL17】三大日志:undo / redo / binlog

面试问"InnoDB 怎么保证事务的 ACID",十个人里八个会蹦出 MVCC、锁、WAL,却答不清三个日志各自管哪一头。线上删错了表,你知道用 binlog 做时间点恢复;数据库突然断电重启,已提交的数据一条没少------这背后是 redo log 在干活;而一个长事务拖着不让 purge 线程清理 undo,导致 undo 表空间暴涨几十 G,又是很多人没踩过、但一定会听到的坑。

这一篇把 undo log、redo log、binlog 这"三大日志"一次讲透:各自在哪一层产生、是逻辑还是物理、什么时候写、什么时候删、崩溃恢复时谁先谁后。结尾的两阶段提交(2PC)是把 redo 和 binlog 串起来的关键,也是面试高频。所有输出都在 Docker MySQL 8.0.46 上真实跑过。

环境:Docker 起 MySQL 8.0.46(--log-bin=mysql-bin --binlog-format=ROW,innodb_flush_log_at_trx_commit=1,sync_binlog=1),mysqlbinlog 客户端解析 binlog。


一、先站高一层:三大日志分别是谁、管什么

结论前置:undo log 管"回滚"和"MVCC",redo log 管"崩溃不丢已提交数据",binlog 管"归档"和"主从复制"。前两个是 InnoDB 内部日志,binlog 是 Server 层日志,所有存储引擎共用。

一句话记住分工:

日志 产生层 日志类型 核心职责 对应 ACID
undo log InnoDB 逻辑日志 事务回滚、MVCC 版本链 原子性 A + 隔离性 I
redo log InnoDB 物理日志 崩溃后恢复已提交但未刷盘的数据 持久性 D
binlog Server 层 逻辑日志 时间点恢复(归档)、主从复制 不直接对应 ACID,是运维/复制底座

🔴 最容易混的一点:redo 和 undo 都在 InnoDB 内部,但 redo 记"对哪个页做了什么物理修改",undo 记"修改前长什么样(逻辑上怎么撤销)"。binlog 在 Server 层,记的是"这条 SQL/这些行最终变成了什么"。


二、undo log:回滚与 MVCC 的底牌

2.1 两个作用

undo log 干两件事:

  1. 事务回滚(保证原子性):事务中途出错或显式 ROLLBACK,靠 undo 把数据改回原样。
  2. MVCC 版本链(保证隔离性):一个行被更新后,旧版本不直接删,而是串在 undo 里,让读快照的事务能读到历史版本。

2.2 两种 undo log

类型 记录内容 回滚时动作 什么时候删
INSERT undo log 插入的主键 回滚时直接 DELETE 掉这条 事务提交后即可删除(INSERT 还没人看得见,没必要留)
UPDATE undo log 更新前的"旧行镜像" 回滚时把旧值写回去 要等没有任何 Read View 还引用它时,由 purge 线程清理

⚠️ 长事务为什么危险:一个事务开了快照一直不提交,它的 Read View 会"挡住" purge 线程------所有被它之后更新的行,旧版本都得留在 undo 里不能删。undo 表空间于是越撑越大,还不能自动收缩。这就是线上"一个忘了提交的事务,把磁盘吃满"的经典事故。

2.3 undo log 是逻辑日志,不是物理页镜像

很多人以为 undo 存的是"整个数据页的旧拷贝",错了。undo 是逻辑日志:它记录的是"如何反向操作"。

  • 你 INSERT 了一行 (1,'a'),undo 记的逻辑动作是"把 id=1 这行删掉";
  • 你 UPDATE 把 name 从 a 改成 x,undo 记的是"把 id=3 的 name 改回 a"。

它不是把整个 16KB 页原样存一份,而是记录反向逻辑操作。这也是它叫"逻辑日志"的原因。

2.4 UPDATE 时 undo 怎么写、版本链怎么串

当前行头里有个 roll_pointer,指向上一个版本;上个版本里又指向上上个版本,串成一条版本链。读快照的事务顺着这条链找到自己能看见的那个版本。

2.5 undo 存哪、长什么样

MySQL 5.7 之前 undo 挤在系统表空间 ibdata1 里,不能收缩,越用越大。5.7 开始支持独立 undo 表空间,MySQL 8 默认就是两个独立文件,还支持在线截断。本机实跑:

bash 复制代码
$ docker exec mysql8 bash -c "ls -la /var/lib/mysql/undo*"
-rw-r----- 1 mysql mysql 16777216 ... /var/lib/mysql/undo_001
-rw-r----- 1 mysql mysql 33554432 ... /var/lib/mysql/undo_002
sql 复制代码
mysql> SHOW VARIABLES LIKE 'innodb_undo%';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_undo_directory     | ./    |
| innodb_undo_tablespaces   | 2     |
| innodb_undo_log_truncate | ON    |
+--------------------------+-------+

✅ 成功标志 :看到 undo_001、undo_002 两个独立文件,innodb_undo_tablespaces=2,innodb_undo_log_truncate=ON(支持在线 truncate)。


三、redo log:WAL,崩溃不丢数据的功臣

3.1 为什么需要它

磁盘一次随机 IO 是 ms 级,内存是 ns 级,差了好几个数量级。如果每次提交事务都把改动的数据页刷回磁盘,性能根本扛不住。InnoDB 的做法是 WAL(Write-Ahead Logging,预写日志):

先把"我对哪个页做了什么修改"顺序写进 redo log,再慢慢把脏页刷盘。redo 是顺序写,比随机刷数据页快得多。

这样即使数据库突然断电,重启时只要重放 redo log,就能把"已经提交、但还没来得及刷盘"的数据恢复回来------这就是持久性 D。

3.2 redo 是物理日志(physiological logging)

redo 记的不是 SQL,也不是"怎么撤销",而是**"对哪个页(page no.)的哪个偏移量,把某几个字节改成什么值"**。它定位到物理页,再描述页内的逻辑修改,这种叫 physiological logging。正因为它知道要改的是哪个页、改成什么,崩溃后重放一遍就能精确还原,速度快。

3.3 固定大小、循环写

redo log 文件大小固定,写满了就回头覆盖最早的。本机实跑(注意:MySQL 8.0.30 后用 innodb_redo_log_capacity 统一控制,旧版的 innodb_log_file_size / innodb_log_files_in_group 被取代):

sql 复制代码
mysql> SHOW VARIABLES LIKE 'innodb_redo_log_capacity';
+------------------------+-----------+
| Variable_name          | Value     |
+------------------------+-----------+
| innodb_redo_log_capacity  | 104857600 |  -- 100MB
+------------------------+-----------+
bash 复制代码
$ docker exec mysql8 bash -c "ls /var/lib/mysql/#innodb_redo/ | head"
#ib_redo100_tmp
#ib_redo101_tmp
#ib_redo102_tmp
...

⚠️ 版本差异 :MySQL 5.x 看到的是数据目录下两个固定的 ib_logfile0、ib_logfile1(默认各 48MB)。MySQL 8.0.30 改成了 #innodb_redo/ 目录下一组编号文件,容量动态可调,但"固定大小、循环写、写满绕回"的原理完全一样。

循环写的关键约束:当 redo 要覆盖某段日志时,必须先确保那段日志对应的脏页已经刷盘了;如果还没刷,就被迫停下来等("log wait",线程被卡住),这就是高并发写入时偶发卡顿的原因之一。

3.4 刷盘时机:innodb_flush_log_at_trx_commit

这是面试必问,也是安全性和性能的权衡。redo 先写内存里的 log buffer(默认 16MB),什么时候真正 fsync 到磁盘由这个参数决定:

sql 复制代码
mysql> SHOW VARIABLES LIKE 'innodb_log_buffer_size';
+----------------------+----------+
| innodb_log_buffer_size | 16777216 |   -- 16MB
+----------------------+----------+
取值 commit 时动作 什么时候刷盘 MySQL 崩 操作系统/机器崩
0 不刷 每秒刷一次 丢 1 秒数据 丢 1 秒数据
1(默认,最安全) 每次 commit 都刷盘 commit 时 不丢已提交数据 不丢已提交数据
2 commit 只写到 OS page cache 每秒从 page cache 刷盘 不丢(还在 OS 缓存) 丢 1 秒数据

🔴 生产默认就是 1。2 比 1 快不少,但机器断电(不是 MySQL 挂,是整机掉电)时 OS page cache 里没刷的那 1 秒就没了。对多数业务,老老实实保持 1;只有能容忍极端情况下丢 1 秒的日志类、计数类业务才考虑 2。


四、binlog:归档与主从复制的日志

4.1 作用与位置

binlog 在 Server 层产生,不管你用 InnoDB 还是 MyISAM 都有。两个用途:

  1. 归档 / 时间点恢复(PITR):误操作后,用"全量备份 + 备份点之后的 binlog"把数据库恢复到任意时刻。
  2. 主从复制:主库把 binlog 发给从库重放,从库数据就跟主库一致了。

本机确认它开着:

sql 复制代码
mysql> SHOW VARIABLES LIKE 'log_bin';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| log_bin       | ON    |

mysql> SHOW BINARY LOGS;
+---------------+-----------+
| Log_name      | File_size |
+---------------+-----------+
| binlog.000003 |   2998143 |
| binlog.000004 |    947    |
+---------------+-----------+

4.2 三种格式:STATEMENT / ROW / MIXED

这是面试高频,也是实战最容易踩坑的地方。

格式 记录什么 优点 缺点 默认
STATEMENT 原始 SQL 文本 文件小 不确定函数(NOW()、UUID()、LIMIT 无 ORDER BY)主从不一致 MySQL 5.7.7 之前
ROW 每行变更的前后镜像 准确、不会不一致 文件大 MySQL 8 默认
MIXED 一般用 STATEMENT,不安全时自动切 ROW 兼顾 行为不透明 ---

我在本机用同一条含 NOW() 的 UPDATE 分别跑 STATEMENT 和 ROW,再用 mysqlbinlog 解析,差异一目了然。

STATEMENT 格式记录的是 SQL 原文:

bash 复制代码
$ mysqlbinlog binlog.000005 | grep -E "^UPDATE|^use "
use `blog17`/*!*/;
UPDATE fmt_test SET ts=NOW() WHERE id=1

注意它记的是 NOW() 这个函数调用本身 。主从两台机器时钟不同,从库重放时 NOW() 取的是从库自己的时间戳------和主库执行时的时间不一样,数据就偏了。这就是 STATEMENT 对不确定函数不安全的铁证。

ROW 格式记录的是具体行的前后镜像:

bash 复制代码
$ mysqlbinlog --base64-output=DECODE-ROWS -v binlog.000006
### UPDATE `blog17`.`fmt_test`
### WHERE
###   @1=2
###   @2='2024-01-01 00:00:00'     ← 前镜像(旧值)
### SET
###   @1=2
###   @2='2026-09-24 07:48:09'     ← 后镜像(实际写入的具体时间戳)

ROW 不关心你用了什么函数,它只记"这一行从 2024-01-01 变成了 2026-09-24 07:48:09"。从库照着实值写,绝不可能跑偏。代价是全表 UPDATE 会记录每一行的变更,binlog 体积暴涨。

binlog_row_image 控制 ROW 格式记多少列:FULL(前后镜像都记全部列,默认最安全)、MINIMAL(只记必要列)、NOBLOB(大字段没改就不记)。

4.3 binlog 怎么写、怎么刷盘

一个事务在内存里先进 binlog cache ,commit 时才一次性写进 binlog 文件,再按 sync_binlog 刷盘:

sql 复制代码
mysql> SHOW VARIABLES LIKE 'sync_binlog';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sync_binlog   | 1     |   -- 每次 commit 都刷盘,最安全
+---------------+-------+

sync_binlog=1 表示每次事务提交都 fsync binlog,和 innodb_flush_log_at_trx_commit=1 并称"双 1",是金融级业务的标配。设成 0 则交给 OS 决定何时刷,性能高但宕机可能丢 binlog。

4.4 怎么查看 binlog

sql 复制代码
mysql> SHOW BINARY LOGS;                       -- 列出所有 binlog 文件
mysql> SHOW MASTER STATUS;                     -- 当前正在写的文件和位点
mysql> SHOW BINLOG EVENTS IN 'binlog.000003';  -- 看某个文件里的事件

本机真实事件流(ROW 格式下一个 DELETE 事务长这样):

sql 复制代码
Log_name        Pos    Event_type     End_log_pos  Info
binlog.000003   236    Query          313          BEGIN
binlog.000003   313    Table_map      370          table_id: 135 (blog17.t)
binlog.000003   370    Delete_rows    419          table_id: 135 flags: STMT_END_F
binlog.000003   419    Xid            450          COMMIT /* xid=851 */

用 mysqlbinlog 命令行工具能把文件解析成可读 SQL/行镜像,是做时间点恢复的核心工具(实战篇 MySQL20 会完整演示删表后怎么救回来)。


五、两阶段提交(2PC):把 redo 和 binlog 串起来

5.1 为什么需要它

问题来了:redo log 是 InnoDB 自己的,binlog 是 Server 层的,两个独立日志。如果只写了其中一个就崩了,会发生什么?

  • 假设 redo 写了、事务提交了,但 binlog 还没写就崩了 → 从库(靠 binlog 复制)永远不知道这个事务,主从数据不一致。
  • 假设 binlog 写了,但 redo 还没 commit 就崩了 → 主库恢复后这个事务被回滚,从库却已经重放了 binlog,主库少了一行、从库多了一行。

所以必须保证 redo 和 binlog 要么都写成功、要么都不写。MySQL 用内部两阶段提交 解决,注意这是数据库内部的 XA,不是跨库分布式事务。

5.2 流程

  1. 执行器调 InnoDB 更新数据,先写 redo log,状态置为 prepare;
  2. 写 binlog 并刷盘;
  3. InnoDB 再把 redo log 改成 commit 状态。

5.3 崩溃恢复时怎么判断

重启恢复时,扫描到 prepare 状态的 redo,就去查对应的 binlog:

redo 状态 binlog 状态 恢复动作
prepare binlog 完整存在 认为事务提交成功,commit
prepare binlog 不完整/丢失 认为事务没完成,回滚
已经是 commit --- 什么都不做

这个逻辑保证了:binlog 写成功的事务一定在主库提交、一定能被从库收到;binlog 没写完的事务一定被回滚、从库也没有。两边就对齐了。


六、三大日志一表总结 + 面试怎么考

维度 undo log redo log binlog
产生层 InnoDB InnoDB Server 层
日志类型 逻辑 物理(physiological) 逻辑
作用 回滚 / MVCC 崩溃恢复(WAL) 归档 / 主从复制
写入时机 修改前记旧值 修改后、commit 时 commit 时一次性写
文件特点 动态增长、可被 purge 清理 固定大小、循环写 滚动追加、可手动删
对应 ACID A + I D 运维/复制

🔴 面试高频问题清单:

  1. 三大日志分别是什么、在哪层、什么类型、干什么用?
  2. 什么是 WAL?为什么 redo 是顺序写还能比随机刷页快?
  3. innodb_flush_log_at_trx_commit 三个值的区别?生产为什么默认 1?
  4. binlog 三种格式,为什么 MySQL 8 默认 ROW?STATEMENT 对哪些函数不安全?
  5. 为什么需要两阶段提交?过程是什么?崩溃恢复时 prepare 状态怎么处理?
  6. undo log 什么时候能删?长事务为什么会撑爆 undo?
  7. redo 写满了会怎样?(被迫刷脏页、业务卡顿)

总结:你真正需要记住的 7 件事

  1. undo 管回滚+MVCC(逻辑日志,InnoDB),redo 管崩溃恢复(物理日志,InnoDB),binlog 管归档+主从(逻辑日志,Server 层)。
  2. undo 是逻辑日志,记"怎么反向操作";UPDATE undo 要等没有 Read View 引用才由 purge 清理,所以长事务会撑爆 undo。
  3. redo 靠 WAL:先顺序写日志、后台慢慢刷脏页,崩溃后重放恢复已提交数据;文件固定大小循环写。
  4. innodb_flush_log_at_trx_commit=1 是默认最安全档;配合 sync_binlog=1 即"双 1"。
  5. binlog 三格式里 MySQL 8 默认 ROW------用真实行前后镜像,不靠重放 SQL,杜绝 NOW() 这类不确定函数导致的主从不一致。
  6. redo(InnoDB)和 binlog(Server 层)是两个独立日志,靠内部两阶段提交对齐:redo prepare → 写 binlog → redo commit。
  7. 崩溃恢复时,prepare 状态的 redo 看 binlog 完不完整:完整就提交,不完整就回滚。

验证清单

  • 能说出三大日志各自的产生层、类型、作用,并举出一个各自解决的问题
  • 能解释为什么长事务会让 undo 表空间暴涨,以及 purge 线程的作用
  • 能默写 innodb_flush_log_at_trx_commit 三档的安全性差异,并说出"双 1"是哪两个参数
  • 能用 mysqlbinlog 分别解析 STATEMENT 和 ROW 格式,指出 NOW() 在两种格式下的不同记录方式
  • 能画出两阶段提交流程,并说清 prepare 状态下崩溃时的恢复决策
  • 能解释 redo log 为什么是固定大小循环写,写满会发生什么
  • 知道 MySQL 8 的 redo 文件在 #innodb_redo/ 目录、用 innodb_redo_log_capacity 控制(而非旧版 ib_logfile)

参考资源


下一篇《Buffer Pool 与内存管理》接着讲:为什么 InnoDB 读写都要先经过一块 128MB(生产动辄几十 G)的内存?普通 LRU 为什么会被一次全表扫描击穿,InnoDB 又是怎么用"年轻区/老年区 + 中点插入"防缓存污染的?Change Buffer 凭什么只给非唯一二级索引加速?脏页、双写缓冲、自适应哈希索引一次讲透。

相关推荐
暧暧内含光1 小时前
Redis Cluster 原理与跨槽(CROSSSLOT)踩坑总结
后端
严同学正在努力1 小时前
PostgreSQL 数据库安装部署教程(Linux 多发行版 + Windows + Docker 全覆盖)
linux·数据库·postgresql
众链网络1 小时前
年卡跨淡旺季的有效期模型:不可用日期、次数与顺延规则
java·前端·数据库
用户5947614425951 小时前
Gitea Push Mirror 镜像同步卡住:磁盘写满导致 LevelDB 队列异常的排查与恢复
后端
shark_chili1 小时前
从需求到架构:我如何用 AI 写好design.md
后端
夕颜1111 小时前
一次跨 Mac 的单站点登录态迁移记录:从「能打开」到「重启后仍可用」
后端
shunwahma2 小时前
MySQL 替代选型,为什么首选 OceanBase 集中式?
数据库·mysql·oceanbase
n 55!w !1082 小时前
MySQL数据库笔记整理1
数据库·笔记·mysql
李兆龙的博客2 小时前
从一到无穷大 #104:面向 SSD 的数据库写入设计——从 FTL 到应用协同
数据库·ssd·存储引擎