MySQL 事务写入流程详解

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

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

1.1 Undo Log(回滚日志)

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

1.2 Redo Log(重做日志)

  • 作用:记录数据页的物理修改(包括对普通数据页和 Undo 页的修改)
  • 目的:崩溃恢复(即使脏页没刷盘,也能通过 Redo Log 恢复)
  • 存储位置 :磁盘上的 ib_logfile0ib_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:

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):

sql 复制代码
-- 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 页和数据页的修改):

sql 复制代码
-- 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_commit 1 Redo Log 刷盘策略 0: 每秒刷(可能丢1秒事务) 1: 每次提交调用 fsync 2: 每次提交只调用 write(写系统缓存),不调用 fsync
sync_binlog 1 Binlog 刷盘策略 0: 交给操作系统控制 1: 每次提交调用 fsync N: 每N次提交调用一次 fsync
innodb_flush_method 数据页刷盘方式 O_DIRECT: 绕过系统缓存,直接写磁盘
innodb_max_dirty_pages_pct 75 脏页比例阈值,超过后触发刷脏页

生产环境推荐

sql 复制代码
-- 最安全(性能较差,但数据零丢失)
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 Log Binlog
日志类型 物理日志(页修改) 逻辑日志(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)代替随机写(数据页)
  • 可靠性:多重重放机制,崩溃可恢复
  • 一致性:两阶段提交确保日志同步
相关推荐
若阳安好2 小时前
【提效小工具】IN SQL、UPDATE SQL、INSERT SQL
java·数据库·sql
二月十六2 小时前
SQL Server 2022 新函数:DATETRUNC 日期截断详解
数据库·sqlserver·datetrunc
乐之者v2 小时前
20多个表,每个都有userId 和其他几个属性,要根据userId把他们全部汇总,如何处理?
java·mysql
qq_380619163 小时前
SQL中如何实现特定范围内数据的批量删除_范围分区与分区删除
jvm·数据库·python
qq_380619163 小时前
HTML函数开发需要独立显卡吗_HTML函数与显卡关系详解【说明】
jvm·数据库·python
2201_756847333 小时前
Golang如何处理JSON空值null_Golang JSON空值处理教程【精通】
jvm·数据库·python
我登哥MVP3 小时前
【Spring6笔记】 - 11 - JDBCTemplate
java·数据库·spring boot·mysql·spring
hef2883 小时前
怎么诊断MongoDB Config Server响应极慢的问题_高频Auto-split导致的元库写入压力
jvm·数据库·python