
第一步:① 加载缓存数据(从硬盘到 BufferPool)
执行器 收到 UPDATE 语句后,首先告诉 InnoDB:"把 id=1002 这条数据所在的数据页加载到内存里"。
硬盘(.ibd文件) ──①──► BufferPool(缓存页)
数据页 缓存页
为什么要这样设计?
如果没有 BufferPool,直接读写硬盘会怎样?
-
硬盘是机械结构(或 SSD),一次随机 IO 需要 毫秒级 时间
-
内存访问是 纳秒级,相差百万倍
-
如果每次 UPDATE 都直接改硬盘,数据库性能会低到无法使用
所以设计了 BufferPool: 把硬盘上的"数据页"(默认 16KB)整体加载到内存的"缓存页"中,后续的读写都在内存里操作,大幅提升性能。
第二步:② 写入数据的旧值(到 UndoLog)
数据页加载到 BufferPool 后,InnoDB 先把这条数据的旧值写入 UndoLog。
BufferPool(旧数据) ──②──► 硬盘(UndoLog)
为什么要这样设计?
如果没有 UndoLog,会发生什么?
假设你执行了 UPDATE name='小明',然后事务还没提交,另一个事务来查询这条数据------它应该看到旧值还是新值?
-
需要看到旧值(因为事务还没提交,新数据是"脏"的)
-
也需要能回滚(如果事务最终 ROLLBACK,必须恢复到修改前的状态)
UndoLog 就是用来存"修改前的数据快照"的。 它让 InnoDB 支持:
-
事务回滚:ROLLBACK 时,用 UndoLog 把数据恢复回去
-
MVCC(多版本并发控制):其他事务读取时,可以通过 UndoLog 链找到旧版本数据
第三步:③ 更新内存数据(BufferPool 中的缓存页)
现在,执行器真正修改 BufferPool 中缓存页的数据:
执行器 ──③──► BufferPool(缓存页)
name='小明' ← 新值写入内存
注意:此时硬盘上的 .ibd 文件还没有变
BufferPool 里的缓存页变成了"脏页"(内存和硬盘不一致)。这个脏页不会立刻写回硬盘。
为什么要延迟写回硬盘?
-
UPDATE 可能只改了 100 字节,但数据页有 16KB,写回硬盘是"整页写"
-
如果每次 UPDATE 都立刻刷盘,会产生大量随机 IO,性能极差
-
脏页由后台线程异步刷盘(Checkpoint 机制),把随机 IO 合并成顺序 IO
代价: 如果此时数据库崩溃,内存里的脏页会丢失。那数据不就丢了吗?
这就是下面 RedoLog 要解决的问题。
第四步:④ 写 RedoLog Buffer
执行器把这次修改操作记录到 RedoLog Buffer(内存中的日志缓冲区):
执行器 ──④──► RedoLog Buffer(内存)
记录了:"在某某页某某偏移处,把 name 改成了 '小明'"
为什么要这样设计?
RedoLog 记录的是"物理修改"(哪个页、哪个位置、改成了什么),而不是 SQL 语句本身。
如果没有 RedoLog,会发生什么?
-
脏页还没刷盘,数据库突然崩溃 → 内存数据丢失 → 这次 UPDATE 永久丢失
-
有了 RedoLog,崩溃重启后,InnoDB 可以重放 RedoLog,把 BufferPool 恢复到崩溃前的状态
RedoLog 是"物理日志",UndoLog 是"逻辑日志":
-
RedoLog:记录"怎么把数据改回来"(用于崩溃恢复)
-
UndoLog:记录"怎么把数据改回去"(用于回滚和 MVCC)
第五步:⑤ Commit 刷 RedoLog 到硬盘
事务准备提交时,RedoLog Buffer 中的内容要通过 fsync() 强制刷到硬盘的 RedoLog 文件:
RedoLog Buffer ──⑤──► 硬盘(RedoLog文件)
fsync() 强制刷盘
这里有个关键参数:innodb_flush_log_at_trx_commit
图中标注了三种模式:
| 值 | 含义 | 安全性 | 性能 |
|---|---|---|---|
| 0 | 延迟写 | 最低(每秒才刷盘,崩溃可能丢1秒数据) | 最高 |
| 1 | 实时写,实时刷(默认) | 最高(每次事务提交都 fsync) |
最低 |
| 2 | 实时写,延迟刷 | 中等(写到 OS 的 PageCache,每秒 fsync) |
中等 |
为什么默认是 1?
因为数据库的核心承诺是 ACID 中的 D(Durability,持久性)。如果事务提交了,数据就必须永久保存。所以默认每次提交都 fsync 到硬盘。
第六步:⑥ 准备提交事务(写 binlog)
RedoLog 刷盘后,还要写 binlog:
执行器 ──⑥──► PageCache ──fsync()──► 硬盘(binlog文件)
为什么要同时有 RedoLog 和 binlog?它们不是重复了吗?
不是重复的,它们的设计目的完全不同:
| RedoLog | binlog | |
|---|---|---|
| 所属 | InnoDB 引擎层 | MySQL Server 层 |
| 用途 | 崩溃恢复 | 主从复制、数据恢复 |
| 格式 | 物理日志(页级别) | 逻辑日志(SQL 语句) |
| 写入方式 | 循环写(固定大小,会覆盖) | 追加写(不会覆盖,一直增长) |
如果没有 binlog,会发生什么?
-
主库崩溃了,RedoLog 能恢复主库数据
-
但从库怎么同步?RedoLog 是 InnoDB 私有的,MySQL Server 层看不到
-
binlog 是 Server 层的,所有存储引擎通用,主从复制必须靠 binlog
第七步:⑦ 写入 binlog 名称和 commit 标记(到 RedoLog)
最后一步,InnoDB 在 RedoLog 中写入一个特殊的标记:"本次事务对应的 binlog 文件名和位置"。
RedoLog ◄──⑦── 写入 binlog 名称 + commit 标记
为什么要这样设计?(两阶段提交)
这是整个流程中最精妙的部分,叫做 "两阶段提交"。
如果没有这个标记,会发生什么?
假设事务提交过程中,数据库崩溃了:
| 崩溃时机 | RedoLog 状态 | binlog 状态 | 后果 |
|---|---|---|---|
| RedoLog 刷盘后,binlog 刷盘前 | 有记录 | 无记录 | 主库有数据,从库没有 → 主从不一致 |
| binlog 刷盘后,RedoLog 写 commit 标记前 | 无 commit 标记 | 有记录 | 重启后 RedoLog 认为事务未完成,回滚 → 主库无数据,从库有 → 主从不一致 |
两阶段提交解决了这个问题:
-
Prepare 阶段:RedoLog 写入,状态设为 prepare
-
Commit 阶段:binlog 写入后,RedoLog 写入 commit 标记
崩溃恢复时,InnoDB 检查:
-
如果 RedoLog 有 prepare,但没有 commit 标记,且 binlog 也没有 → 回滚
-
如果 RedoLog 有 prepare,且有 commit 标记,且 binlog 也有 → 提交
-
这样就保证了 RedoLog 和 binlog 要么都有,要么都没有,永远不会不一致
完整流程总结图
客户端发送 UPDATE 语句
│
▼
┌───────────────┐
│ ① 加载数据页 │ 从 .ibd 文件加载到 BufferPool
│ 到 BufferPool│
└───────┬───────┘
▼
┌───────────────┐
│ ② 写 UndoLog │ 记录旧值,用于回滚和 MVCC
│ (硬盘) │
└───────┬───────┘
▼
┌───────────────┐
│ ③ 更新内存数据 │ 修改 BufferPool 中的缓存页(脏页)
│ (BufferPool)│
└───────┬───────┘
▼
┌───────────────┐
│ ④ 写 RedoLog │ 记录物理修改到 RedoLog Buffer
│ Buffer │
└───────┬───────┘
▼
┌───────────────┐
│ ⑤ fsync 刷 │ RedoLog Buffer → 硬盘 RedoLog 文件
│ RedoLog │ (innodb_flush_log_at_trx_commit 控制)
└───────┬───────┘
▼
┌───────────────┐
│ ⑥ 写 binlog │ 记录逻辑 SQL 到 binlog(主从复制用)
│ (fsync) │ (sync_binlog 控制)
└───────┬───────┘
▼
┌───────────────┐
│ ⑦ RedoLog 写 │ 写入 commit 标记,完成两阶段提交
│ commit 标记 │
└───────────────┘
│
▼
事务提交成功
一句话总结
BufferPool 让读写变快,UndoLog 让回滚和并发查询成为可能,RedoLog 让崩溃后不丢数据,binlog 让主从复制能工作,两阶段提交让 RedoLog 和 binlog 永远保持一致。
这四者配合,才支撑起了 InnoDB 的高性能、事务安全和数据一致性。