一、先认识一下这几个"角色"

在深入流程之前,先搞清楚这几个日志各自是干什么的:

1.1 Undo Log(回滚日志)

  • 作用:记录数据修改前的旧值
  • 目的:事务回滚、MVCC(多版本并发控制)
  • 存储位置:表空间(undo tablespace)
  • 写入时机:事务修改数据时,先写 Undo 页到 Buffer Pool(对 Undo 页的修改同样会生成 Redo Log,由 Redo Log 保证持久性)
  • 特点:逻辑日志,记录的是"反向操作"

1.2 Redo Log(重做日志)

  • 作用:记录数据页的物理修改(包括对普通数据页和 Undo 页的修改)
  • 目的:崩溃恢复(即使脏页没刷盘,也能通过 Redo Log 恢复)
  • 存储位置:磁盘上的 ib_logfile0、ib_logfile1…
  • 结构:循环写,写满后覆盖
  • 特点:物理日志,记录的是"页面的修改"

1.3 Binlog(二进制日志)

  • 作用:记录所有修改数据的 SQL 语句(以事件形式)
  • 目的:主从复制、基于时间点的数据恢复
  • 存储位置:磁盘上的 mysql-bin.xxxxxx
  • 特点:逻辑日志,追加写,不会覆盖

1.4 数据页(Data Page)

  • 作用:InnoDB 存储数据的最小单位(默认 16KB)
  • 存储位置:磁盘的表空间文件(.ibd)
  • 修改特点:先在内存的 Buffer Pool 中修改(脏页),异步刷盘

二、核心概念:两阶段提交(2PC)

MySQL 为什么要搞这么复杂?因为要保证:

  1. 一致性:Binlog 和 Redo Log 要么都成功,要么都失败
  2. 持久性:事务提交后,即使 MySQL 崩溃,数据也不能丢

解决方案就是两阶段提交,通过 XID(事务标识符) 关联 Redo Log 和 Binlog:

阶段1:prepare 阶段
├─ 写入 Redo Log(标记为 prepare 状态,并带上 XID)
└─ Redo Log 持久化到磁盘(fsync)

阶段2:commit 阶段
├─ 写入 Binlog(Binlog 中也包含相同的 XID)
├─ Binlog 持久化到磁盘(fsync)
└─ 将 Redo Log 标记为 commit 状态(此标记不必须持久化,崩溃恢复时通过 XID 比对即可)

三、完整流程:从 BEGIN 到 COMMIT

假设执行这样一条 SQL:

BEGIN;
UPDATE users SET balance = balance - 100 WHERE id = 1;
COMMIT;

步骤 1:事务开始(BEGIN)

  • 创建一个事务 ID(TRX_ID)和一个 XID(用于两阶段提交)
  • 在内存中分配事务结构
  • 此时什么都没写磁盘

步骤 2:执行 UPDATE 语句

2.1 查找数据
  • 通过索引(主键/二级索引)定位到数据页
  • 先从 Buffer Pool 查找,如果不在内存,则从磁盘加载
2.2 记录 Undo Log(在 Buffer Pool 中修改 Undo 页)

在修改数据之前,先记录旧值到 Undo 页(位于 Buffer Pool):

-- Undo Log 记录的内容(示例)
TRX_ID: 1000
XID: 0x1234
TYPE: UPDATE
OLD_VALUE: {id: 1, balance: 1000}
NEW_VALUE: {id: 1, balance: 900}

关键点:Undo 页本身也是数据页,对 Undo 页的修改同样会生成 Redo Log,因此 Undo 的持久性由 Redo Log 间接保证。

Undo 页变成脏页,后续通过 Redo Log 和刷盘机制持久化。

2.3 修改数据页(Buffer Pool)
  • 在内存中修改数据页:balance 从 1000 改为 900
  • 这块数据页变成脏页(Dirty Page),还没同步到磁盘
2.4 记录 Redo Log(Redo Log Buffer)

记录对数据页的物理修改(包括对 Undo 页和数据页的修改):

-- Redo Log 记录的内容(示例)
LOG_SEQ_NO: 12345
PAGE_ID: 0x0001 (数据页)
OFFSET: 120
OLD_DATA: 0x000003E8  (1000 的十六进制)
NEW_DATA: 0x00000384  (900 的十六进制)

-- 此外还有对 Undo 页的修改记录,格式类似
  • Redo Log 写入内存的 Redo Log Buffer
  • 注意:此时 Redo Log 还没有刷到磁盘!

步骤 3:事务提交(COMMIT)- 阶段1(Prepare)

当执行 COMMIT 时,真正的重头戏开始了:

3.1 将 Redo Log 标记为 Prepare 状态
Redo Log 的最后一条记录会加上一个标记:
{LOG_TYPE: XA_PREPARE, TRX_ID: 1000, XID: 0x1234}
3.2 Redo Log 刷盘(关键!)
  • 调用 fsync() 强制将 Redo Log Buffer 写入磁盘
  • 此时 Redo Log 已经持久化了

为什么要在这里刷 Redo?

  • 如果 MySQL 在这一步之后崩溃,恢复时发现 Redo Log 是 prepare 状态,Binlog 还没写,说明事务没提交,可以回滚
  • Redo Log 虽然刷了,但状态是 prepare,不会用于恢复数据(除非 Binlog 中已有对应 XID)

步骤 4:事务提交(COMMIT)- 阶段2(Commit)

4.1 写入 Binlog
  • 将事务的 SQL 语句以事件形式写入 Binlog,并附上 XID
  • 调用 fsync() 刷盘(取决于 sync_binlog 配置)
  • 此时 Binlog 已经持久化了
4.2 将 Redo Log 标记为 Commit 状态
在 Redo Log 中追加一条记录:
{LOG_TYPE: XA_COMMIT, TRX_ID: 1000}

重要说明:这个 commit 标记只需要写入 Redo Log Buffer,不需要立即刷盘,甚至即使永远不刷盘也不影响正确性。

崩溃恢复时,如果 Redo Log 处于 prepare 状态,就去 Binlog 中查找相同 XID 的事务:

  • 找到 → 认为事务已提交,重做 Redo Log 中的修改
  • 未找到 → 回滚事务

步骤 5:事务结束

  • 释放事务持有的锁
  • 清理内存中的事务结构
  • 此时对用户来说,事务已提交完成

四、数据页什么时候刷盘?

你可能注意到了,整个流程中数据页本身并没有刷盘!

是的,数据页的刷盘是异步的,由后台线程负责:

4.1 什么时候刷盘?

触发条件说明
Checkpoint定期触发,将脏页刷盘
Buffer Pool 空间不足内存不够时,需要淘汰旧页
Redo Log 空间不够Redo Log 循环写,要覆盖旧记录前,必须先刷对应的脏页
脏页比例超过阈值innodb_max_dirty_pages_pct(默认 75%),触发刷脏页
MySQL 关闭优雅关闭时刷所有脏页
innodb_flush_method=O_DIRECT每次写数据页都直接绕过系统缓存(但依然是异步批量刷)

4.2 为什么数据页可以延迟刷盘?

因为有 Redo Log!

即使脏页没刷盘,MySQL 崩溃了:

  1. 重启时读取 Redo Log
  2. 通过 XID 比对 Binlog,找出所有已提交的事务
  3. 重新应用这些事务对数据页的修改
  4. 数据就恢复了

这就是 Redo Log 的价值:用顺序写(Redo Log)代替随机写(数据页),性能提升巨大!


五、崩溃恢复:如果 MySQL 中途挂了怎么办?

MySQL 崩溃后的恢复流程非常巧妙,核心是 XID 匹配:

情况1:Redo Log = prepare,Binlog 中没有这个 XID

判断逻辑:
- Redo Log 是 prepare,说明事务还没完全提交
- Binlog 中没有相同 XID,说明事务确实失败了

处理:回滚事务
- 根据 Undo Log 将数据改回旧值

情况2:Redo Log = prepare,Binlog 中有这个 XID

判断逻辑:
- Redo Log 是 prepare,说明事务处于中间状态
- Binlog 中有相同 XID,说明事务已经提交成功(Binlog 已刷盘)

处理:提交事务
- 将 Redo Log 标记为 commit(内存中即可)
- 根据 Redo Log 重做数据页修改

情况3:Redo Log = commit

判断逻辑:
- Redo Log 已经是 commit,说明事务完全提交

处理:正常恢复
- 根据 Redo Log 重做数据页修改
- Binlog 已经存在,无需额外处理

注意:崩溃恢复时只关心已刷盘的 Redo Log 和 Binlog。内存中未刷盘的 Redo Log commit 标记并不影响判断,因为 XID 比对已经足够。


六、写入顺序总结(重要!)

6.1 内存中的顺序(快速)

1. 写 Undo Log(在 Buffer Pool 中修改 Undo 页,同时生成对应的 Redo Log)
2. 修改数据页(Buffer Pool)
3. 写 Redo Log(Redo Log Buffer)
4. 提交时:Redo Log prepare → 刷盘(fsync)
5. 提交时:写 Binlog → 刷盘(fsync)
6. 提交时:Redo Log commit(仅内存,不强制刷盘)

6.2 磁盘持久化的顺序

1. Redo Log(prepare 阶段刷盘)
2. Binlog(commit 阶段刷盘)
3. 数据页(异步刷盘,由后台线程在 checkpoint 等时机触发)
4. Undo 页(同样通过 Redo Log 和异步刷盘持久化)
5. Redo Log commit 标记(从不强制刷盘,也不依赖它)

七、关键配置参数

参数默认值作用
innodb_flush_log_at_trx_commit1Redo Log 刷盘策略
0: 每秒刷(可能丢1秒事务)
1: 每次提交调用 fsync
2: 每次提交只调用 write(写系统缓存),不调用 fsync
sync_binlog1Binlog 刷盘策略
0: 交给操作系统控制
1: 每次提交调用 fsync
N: 每N次提交调用一次 fsync
innodb_flush_method空数据页刷盘方式
O_DIRECT: 绕过系统缓存,直接写磁盘
innodb_max_dirty_pages_pct75脏页比例阈值,超过后触发刷脏页

生产环境推荐:

-- 最安全(性能较差,但数据零丢失)
SET GLOBAL innodb_flush_log_at_trx_commit = 1;
SET GLOBAL sync_binlog = 1;

-- 性能优先(有一定风险,最多丢1秒事务或一组事务)
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
SET GLOBAL sync_binlog = 0;

八、流程图总结

BEGIN
  │
  ├─ 创建事务 TRX_ID 和 XID
  │
UPDATE 语句
  │
  ├─ [1] 在 Buffer Pool 中写 Undo 页(同时生成对 Undo 页的 Redo Log)
  ├─ [2] 修改数据页(Buffer Pool,变成脏页)
  └─ [3] 写 Redo Log(Redo Log Buffer,记录对数据页和 Undo 页的修改)
  │
COMMIT
  │
  ├─ 阶段1:Prepare
  │   ├─ Redo Log 标记为 XA_PREPARE(带上 XID)
  │   └─ Redo Log 刷盘(fsync)← 第一次持久化
  │
  ├─ 阶段2:Commit
  │   ├─ 写 Binlog(带上相同 XID)
  │   └─ Binlog 刷盘(fsync)← 第二次持久化
  │
  └─ Redo Log 标记为 XA_COMMIT(仅内存,不刷盘)
  │
事务结束(释放锁)
  │
后台异步任务:
  ├─ 数据页刷盘(Checkpoint、脏页比例阈值等触发)
  ├─ Undo 页刷盘(同样由后台线程处理)
  └─ (Redo Log commit 标记永不强制刷盘)

九、为什么是这样的设计?

9.1 为什么要先写 Undo?

  • 为了回滚和 MVCC,必须知道旧值
  • Undo 是逻辑日志,相对小,写得不亏
  • 注意:Undo 本身也通过 Redo Log 保护,并非直接写磁盘

9.2 为什么要 Redo Log?

  • 数据页是随机写(16KB 一页,改一个字节也要写整页)
  • Redo Log 是顺序写,性能高 10 倍以上
  • 用 Redo Log 代替数据页的即时刷盘

9.3 为什么要 Binlog?

  • Redo Log 是物理日志,无法用于主从复制(不同版本、不同存储引擎的物理格式不同)
  • Binlog 是逻辑日志,记录 SQL 或行变更,适合复制和恢复

9.4 为什么要两阶段提交?

  • 保证 Redo Log 和 Binlog 的一致性
  • 崩溃恢复时能够通过 XID 判断事务是否真正提交

9.5 为什么数据页可以延迟刷盘?

  • 有 Redo Log 撑着,崩溃也能恢复
  • 脏页批量刷盘,减少 I/O 次数

十、常见面试题(修正版)

Q1:为什么需要两阶段提交?

答:保证 Binlog 和 Redo Log 的一致性。如果是单阶段提交,可能出现 Redo 写了但 Binlog 没写(主从不一致),或者 Binlog 写了但 Redo 没写(数据丢失)。通过 prepare + commit 两阶段,配合 XID,崩溃恢复时总能判断事务的真实状态。

Q2:Redo Log 和 Binlog 有什么区别?

对比项Redo LogBinlog
日志类型物理日志(页修改)逻辑日志(SQL 或行事件)
存储位置MySQL 数据目录MySQL 数据目录
写入方式循环写,可覆盖追加写,不会覆盖
主要用途崩溃恢复主从复制、数据恢复
内容记录数据页的修改记录 SQL 语句或行变更
所属层InnoDB 引擎层MySQL Server 层

Q3:什么是 WAL(Write-Ahead Logging)?

答:先写日志,再写数据。Redo Log 就是 WAL 的典型应用,确保即使数据页没刷盘,也能通过日志恢复。注意:这里"先写日志"指 Redo Log 先于对应的数据页刷盘,但 Undo 的写入同样遵循 WAL。

Q4:innodb_flush_log_at_trx_commit = 2 会丢数据吗?

答:会,但只在操作系统崩溃或断电时丢数据。如果只是 MySQL 进程崩溃,由于数据已写入操作系统 page cache,操作系统会负责将其落盘,因此不丢数据。=2 表示每次提交只调用 write(写 page cache),不调用 fsync,所以操作系统崩溃会导致 page cache 中的日志丢失。

Q5:为什么 Binlog 不能像 Redo Log 一样循环写?

答:Binlog 用于主从复制和基于时间点的恢复,如果循环写覆盖了旧数据,从库就无法同步,或者无法恢复到历史时间点。Binlog 需要保留完整的历史记录。

Q6:Undo Log 是如何持久化的?也是靠 Redo Log 吗?

答:是的。Undo 页本身是 InnoDB 表空间中的普通数据页,对 Undo 页的修改同样会生成 Redo Log。因此 Undo 的持久性由 Redo Log 保证,遵循 WAL 原则。崩溃恢复时,Redo Log 会重放对 Undo 页的修改,确保 Undo 数据完整。


十一、总结

MySQL 事务的写入流程,核心就是两个原则:

  1. 先写日志,再写数据(WAL 原则)
  2. 两阶段提交保证一致性(通过 XID 关联 Redo Log 和 Binlog)

整个流程精心设计,平衡了:

  • 性能:用顺序写(Redo Log)代替随机写(数据页)
  • 可靠性:多重重放机制,崩溃可恢复
  • 一致性:两阶段提交确保日志同步
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐