文章目录
- UPDATE 背后发生了什么?MySQL 四大日志与两阶段提交全揭秘
-
- 一、先看全景——一条 UPDATE 的完整生命周期
- 二、redo log——崩溃恢复的唯一保障
-
- 它长什么样
- 核心设计一:WAL(Write-Ahead Logging)
- 核心设计二:固定大小的循环写
- 核心设计三:刷盘策略
- 三、undo log——回滚的逆操作 + MVCC 的版本链
-
- 更重要的功能:撑起 MVCC 的版本链
- 四、binlog——主从同步的唯一载体
-
- 它长什么样
- 和 redo log 的核心差异
- 三种格式如何选
- 主从同步怎么用 binlog
- 五、relay log——从库的中转站
- 六、两阶段提交——为什么不能只靠 redo log
-
- 问题
- 两阶段提交流程
- 崩溃恢复逻辑
- 生产环境最高安全配置
- 七、DoubleWrite Buffer——redo log 也救不了的场景
-
- 问题:部分页写
- 解决方案
- 八、Buffer Pool——脏页、干净页和数据的最终归宿
- 九、四大日志一句话总结
- 十、整个 UPDATE 流程图(串联所有知识点)
- 总结
UPDATE 背后发生了什么?MySQL 四大日志与两阶段提交全揭秘
你写了一条 UPDATE,MySQL 返回"成功"。从 Buffer Pool 到磁盘,这中间经过了 undo log、redo log、binlog、两阶段提交。任何一个环节理解偏差,线上事故排查就多一分风险。
一、先看全景——一条 UPDATE 的完整生命周期
UPDATE t_user SET name = 'xiaolin' WHERE id = 1;
1. 执行器调用 InnoDB 接口,通过主键索引定位 id=1 的记录
→ 数据页在 Buffer Pool → 直接返回
→ 不在 → 从磁盘读到 Buffer Pool → 返回
2. 比对更新前后的值
→ 一样 → 跳过后续流程
→ 不一样 → 继续
3. 开启事务,写 undo log(记录旧值,用于回滚和 MVCC)
→ undo log 写入 Buffer Pool 的 Undo 页
→ Undo 页的修改也记 redo log(undo 也是数据,要持久化)
4. 修改 Buffer Pool 中的数据页(标记为脏页)
5. 将这次修改记录到 redo log buffer(内存中)
6. 写 binlog cache(内存中)
此时事务还没提交。提交的那一刻,两阶段启动:
7. PREPARE 阶段:redo log buffer 刷盘(fsync),事务状态=prepare
8. COMMIT 阶段:
→ binlog cache 刷盘到 binlog 文件
→ redo log 标记为 commit(并刷盘)
→ 返回"提交成功"给客户端
9. 脏页排队,后台线程异步刷回磁盘数据文件
每一步都有原因。下面四篇日志逐一拆。
二、redo log——崩溃恢复的唯一保障
它长什么样
ib_logfile0, ib_logfile1(磁盘文件,固定大小,循环写)
[LSN 1000: 页号=5, offset=200, 改 2 字节为 0x1A2B]
[LSN 1001: 页号=5, offset=300, 改 2 字节为 0x3C4D]
…
→ 物理日志:哪个页的哪个位置,改成什么
核心设计一:WAL(Write-Ahead Logging)
不直接改磁盘数据——太慢。先顺序写 redo log,再内存改 Buffer Pool,异步刷脏页。
随机刷脏页(没有 redo log):
页3 在偏移 500MB → 页17 在偏移 200MB → 页42 在偏移 1GB
→ 磁盘头满盘寻道 → 巨慢
顺序写 redo log(WAL):
→ 磁盘头一直往下写 → 快几十倍 → 写完就返回
核心设计二:固定大小的循环写
┌─────────────────────────────────┐
│ 已刷盘可覆盖 │ 未刷盘保护中 │ 空闲 │
└─────────────────────────────────┘
↑ ↑ ↑
checkpoint write pos
write pos 追上 checkpoint → redo log 满了 → 所有写入暂停 → 强制刷脏页推进 checkpoint
redo log 不能无限保留——它只保护"已修改但未刷盘的脏页"。脏页一刷盘,对应 redo log 就可覆盖。
核心设计三:刷盘策略
innodb_flush_log_at_trx_commit = 1 — 默认,每次提交都 fsync,最安全
= 0 — 每秒刷一次,可能丢 1 秒数据
= 2 — 写 OS 缓存,机器宕机丢数据
三、undo log——回滚的逆操作 + MVCC 的版本链
事务里改了数据,但还没提交。随时可能回滚——undo log 记录的就是逆操作:
UPDATE user SET age = 30 WHERE id = 1;
→ undo log 记录:把 id=1 的 age 改回 25(旧值)
回滚时按 undo log 反着执行一次,数据回到老状态。
更重要的功能:撑起 MVCC 的版本链
每行数据有三个隐藏列:
| DB_TRX_ID | 6 | 最近修改这行的事务 ID |
| DB_ROLL_PTR | 7 | 指向 undo log 旧版本的指针 |
| DB_ROW_ID | 6 | 没主键时的兜底行 ID |
同一行多次修改,通过 DB_ROLL_PTR 串成一条版本链:
当前版本 trx_id=100, age=35
↑ DB_ROLL_PTR
旧版本 trx_id=80, age=30
↑ DB_ROLL_PTR
旧版本 trx_id=50, age=25
ReadView(快照)在这条链上帮你挑一个能看的版本。这个过程就是 MVCC。
注意:undo log 也写 redo log——undo 页变更要先记 redo log,防止 undo 本身丢了。
四、binlog——主从同步的唯一载体
它长什么样
mysql-bin.000001(磁盘文件,追加写,不覆盖,满了切新文件)
STATEMENT 格式:
UPDATE t_user SET name = 'xiaolin' WHERE id = 1;
ROW 格式(推荐):
### UPDATE `db`.`t_user`
### WHERE id=1
### SET name='old_name' → name='xiaolin'
→ 逻辑日志:记录 SQL 语句或每行变更前后值。
和 redo log 的核心差异
| 层级 | InnoDB 引擎层 | Server 层 |
| 类型 | 物理日志 | 逻辑日志 |
| 写入方式 | 循环写(固定大小) | 追加写(不覆盖) |
| 作用 | 崩溃恢复 | 主从复制 + 数据恢复 |
| 能否关闭 | 不能 | 能(–skip-binlog) |
三种格式如何选
STATEMENT:记 SQL 语句,省空间。
坑:NOW() / UUID() 在主从执行结果不同 → 不一致
ROW:记每行变更的值,安全。
代价:改 1 万行 = 1 万个事件,binlog 巨大
MIXED:默认 STATEMENT,遇到非确定性语句自动切 ROW。
坑:切换边界有时不是你预期的那样
线上建议:直接 ROW。磁盘不值钱,数据不一致才是事故。
主从同步怎么用 binlog
主库:执行 UPDATE → 写 binlog
从库:IO 线程拉 binlog → 写 relay log → SQL 线程回放 relay log
从库同步位置靠 binlog 的位点(offset)记录。断连重连后,从上次位点继续追——因为 binlog 是追加写、不覆盖,断多久都能续上。
五、relay log——从库的中转站
从库上:
IO 线程:从主库拉 binlog → 写入 relay log
SQL 线程:读取 relay log → 执行 SQL → 删除 relay log
格式跟 binlog 完全一样,就是个临时的中继日志。回放完一条清一条。
六、两阶段提交——为什么不能只靠 redo log
问题
如果 redo log 和 binlog 各自提交:
场景1:redo 成功,binlog 失败
→ 主库恢复后数据在,从库没有 → 主从不一致
场景2:binlog 成功,redo 失败
→ 从库有数据,主库没有 → 主从不一致
两阶段提交流程
1. PREPARE:redo log 刷盘,事务状态=prepare
2. 写 binlog 并刷盘
3. COMMIT:redo log 标记=commit,刷盘
崩溃恢复逻辑
重启 → 扫描 redo log → 找所有 PREPARE 状态的事务
→ 查 binlog:
有完整记录 → 提交(补上 redo log 的 commit)
没有记录 → 回滚
核心原则:binlog 说了算。 从库同步全靠 binlog——binlog 有的事务主库必须留,binlog 没有的事务主库必须删。
生产环境最高安全配置
innodb_flush_log_at_trx_commit = 1 — redo log 每次提交 fsync
sync_binlog = 1 — binlog 每次提交 fsync
binlog_format = ROW — 行级复制最安全
七、DoubleWrite Buffer——redo log 也救不了的场景
问题:部分页写
一页 16KB,OS 刷盘最小单位 4KB。写入中途断电:
16KB 页 = 4 个 OS 块
块1 ✓ 写完
块2 ✗ 没写
块3 ✓ 写完
块4 ✗ 没写
→ 磁盘上的页 = 新旧数据混在一起 → checksum 不匹配 → 坏页
这个坏页 redo log 救不回来——redo log 记录的是"在完整页上改什么",页碎了连地基都没了,指令无处施加。
解决方案
脏页刷盘分两步:
Step 1:顺序写到 DoubleWrite Buffer(磁盘上 2MB 连续区域)
Step 2:随机写到数据文件的实际位置
崩溃恢复:
检查数据页 → checksum 通过 → 正常恢复
→ checksum 失败 → 从 DWB 拿完整副本替补
→ 再用 redo log 追到最新版本
redo log 保"数据是新是旧",DWB 保"页本身没碎"。 缺一个都不行。
八、Buffer Pool——脏页、干净页和数据的最终归宿
Buffer Pool 是内存缓存,两类页并存:
干净页:跟磁盘一致 → 崩了就崩了,重启重读磁盘
脏页: 修改过未刷盘 → 崩了靠 redo log 恢复
脏页是数据写入磁盘的唯一入口——任何数据要持久化,必须先在 Buffer Pool 里待过:
客户端 UPDATE → Buffer Pool(脏页)→ redo log(保险)→ 后台刷盘 → 持久化
↑ 改了马上能查 ↑ 时机由后台控制
脏页比例太高 → Buffer Pool 塞满脏页 → 新查询没空间加载数据页 → 必须等脏页先刷盘 → 整库卡顿。
redo log 满了也一样——必须等脏页刷盘才能推进 checkpoint,释放 redo log 空间,期间所有写入暂停。
九、四大日志一句话总结
| redo log | 磁盘 ib_logfile | 物理日志 | 崩溃恢复 | 等刷脏页释放 |
| undo log | undo 表空间 | 逻辑逆操作 | 回滚 + MVCC | purge 线程清理 |
| binlog | 磁盘 binlog 文件 | 逻辑 SQL/行变更 | 主从同步 | 切新文件 |
| relay log | 从库磁盘 | 同 binlog | 从库中转 | 回放完自动删 |
redo 管崩溃,bin 管复制,undo 管回滚,两阶段管一致性。
十、整个 UPDATE 流程图(串联所有知识点)
客户端发起 UPDATE
↓
执行器 → InnoDB 通过聚簇索引定位 id=1 的数据页
↓
Buffer Pool 有这页?──N──→ 从磁盘读取 16KB 页到 Buffer Pool
↓ Y
执行器比对更新前后值是否相同
↓ 不同
写 undo log(记录旧值,存到 Buffer Pool 的 Undo 页)
↓
undo log 变更记 redo log buffer(undo 页也是数据)
↓
更新 Buffer Pool 中的数据页(标记为脏页)
↓
将修改记录写入 redo log buffer(内存)
↓
将 binlog 记录写入 binlog cache(内存)
↓
══════ 事务 COMMIT ══════
↓
PREPARE:redo log buffer → fsync 到磁盘 ib_logfile(事务状态=prepare)
↓
写 binlog:binlog cache → fsync 到磁盘 binlog 文件
↓
COMMIT:redo log 标记=commit,fsync 到磁盘
↓
返回客户端 "提交成功"
↓
后台线程异步将脏页从 Buffer Pool 刷到磁盘数据文件
(DoubleWrite Buffer 保护,防止部分页写)
总结
redo 管崩溃,bin 管复制,undo 管回滚,DWB 管页碎,2PC 管一致。Buffer Pool 只管快,脏页刷盘后台来。
如果觉得有用,点赞收藏。


