type
Post
status
Published
date
Dec 13, 2025
slug
mysql1
summary
tags
技术探索
category
icon
password
MySQL 数据到底怎么读、怎么写?从 .ibd 文件、B+ 树到 Buffer Pool 和 Redo Log
我们平时写业务代码的时候,经常会把用户数据、订单数据、商品数据存到 MySQL 里。
从应用层看,事情好像很简单。
查一条用户数据:
更新一条用户数据:
但是问题来了:
MySQL 内部到底是怎么存数据、查数据、改数据的?
很多人学 MySQL 时,会背很多概念:
InnoDB 默认 16KB 页。InnoDB 使用 B+ 树。Buffer Pool 是缓存。Undo Log 用来回滚。Redo Log 保证持久性。Binlog 用来主从复制。
这些说法都对。
但如果只是孤立地背概念,很容易出现一个问题:
每个概念都听过,但不知道它们之间到底怎么配合。
所以这篇文章从一条 SQL 的读写链路出发,看看 MySQL 背后到底发生了什么。
一、先看整体:MySQL 的读路径和写路径
MySQL 执行 SQL,大体可以分成两条主线。
第一条是读路径,也就是
select:怎么找到数据?怎么减少磁盘 IO?
第二条是写路径,也就是
update、insert、delete:怎么修改数据?怎么支持事务回滚?怎么保证提交后的数据不丢?怎么保证主从复制的一致性?
先用一张图看整体:
MySQL 读数据,核心是通过 B+ 树定位数据页,再通过 Buffer Pool 减少磁盘 IO。MySQL 写数据,核心是先改内存页,再通过 Undo Log、Redo Log、Binlog 保证事务、恢复和复制。
下面我们拆开来看。
第一部分:一条 select 是怎么读数据的?
二、MySQL 的数据最终存在哪里?
MySQL 本质上是一个用来存放和管理数据的软件。
应用程序通过 SQL 对 MySQL 做增删改查,MySQL 再把这些数据持久化到磁盘文件里。
比如我们有一张
user 表:id | name | age |
1 | 小白 | 18 |
2 | 小黑 | 20 |
3 | 小红 | 22 |
从逻辑上看,它像一张 Excel 表。
但在磁盘上,它并不是直接以 Excel 表格的形式存在。
对于 InnoDB存储引擎来说,在开启
innodb_file_per_table 的情况下,每张表通常会有一个独立的 .ibd 表空间文件,用来存放这张表的数据页和索引页。比如:
这里要注意,“一张表对应一个
.ibd 文件”是常见场景下的理解方式。更严谨地说,InnoDB 还支持系统表空间、通用表空间等形式,所以不能简单理解为所有数据都一定只存在某个单独的
.ibd 文件里。但对于大多数 MySQL 8 默认场景来说,可以先这样理解:
一张 InnoDB 表的数据和索引,通常会存放在对应的.ibd表空间文件里。
三、InnoDB 不会一行一行读数据,而是按页读
如果 MySQL 每次查一行数据,都直接去磁盘文件里一行一行找,那性能会非常差。
因为磁盘 IO 很慢。
所以 InnoDB 不会以“行”作为磁盘读写的最小单位,而是以“页”作为基本单位。
默认情况下,InnoDB 一个数据页大小是 16KB。
也就是说,
user.ibd 文件可以理解成由很多个 16KB 的页组成。每个数据页里面会存放多行数据。
所以 MySQL 查询数据时,是读取“一个页”。
比如你只是查:
InnoDB 真正从磁盘加载时,可能会把包含
id = 1 的整个 16KB 数据页读到内存里。这样做的好处是:
一次磁盘 IO 可以带回多行相邻数据,后续如果又访问同一个页里的其他数据,就可以直接从内存读取。
四、数据页那么多,MySQL 怎么快速找到目标页?
现在问题来了。
如果一个
.ibd 文件里有成千上万个数据页,MySQL 怎么知道 id = 1 这行数据在哪个页里?总不能从第一个页开始,一个页一个页扫吧?
这时候就需要索引。
在 InnoDB 里,最核心的索引结构就是 B+ 树。
你可以先把它理解成一棵“多层目录树”。
最上层是根节点。
中间层是索引页。
最底层的叶子节点是真正存放数据的页。
比如执行:
InnoDB 可以沿着 B+ 树查找:
这样就不需要全表扫描。
这也是为什么索引能提升查询效率。
五、InnoDB 的主键索引为什么叫聚簇索引?
在 InnoDB 里,表数据本身就是按照主键索引组织起来的。
主键索引的叶子节点,存的不是一个简单的地址,而是完整的行数据。
这就是 聚簇索引。
可以这样理解:
InnoDB 的主键索引树,既是索引,也是数据本身。
比如
user 表的主键是 id,那么主键索引大概可以理解成这样:所以通过主键查询时,效率通常很高:
因为 InnoDB 沿着主键 B+ 树找到叶子节点后,直接就能拿到完整行数据。
六、那二级索引和回表又是什么?
除了主键索引,我们也可以给普通字段建立索引。
比如:
这个
idx_age 就是二级索引,也叫辅助索引。但是二级索引的叶子节点,通常不存完整行数据,而是存:
二级索引字段值 + 主键值。
比如
age 索引大概可以理解成这样:如果执行:
假设优化器选择走
idx_age 索引,那么过程可能是:- 先通过
age二级索引找到age = 18对应的主键id = 1;
- 再拿着
id = 1回到主键索引里查完整行数据。
第二步就叫 回表。
所以什么是回表?
可以这样回答:
InnoDB 的二级索引叶子节点通常存的是主键值。如果通过二级索引查询完整行数据,需要先在二级索引中找到主键,再根据主键回到聚簇索引中查询完整记录,这个过程就是回表。
七、为什么不用 Hash 表?不是 O(1) 更快吗?
很多人刚学索引时,都会有一个疑问:
Hash 表查找不是 O(1) 吗?B+ 树查询是 O(logN),为什么 MySQL 不直接用 Hash 表?
原因是,数据库查询不只是等值查询。
Hash 表适合这种场景:
但数据库里还有大量范围查询:
还有排序:
还有分页、范围扫描、联合索引匹配。
Hash 表虽然等值查询很快,但它不适合范围查询,也不适合顺序遍历。
B+ 树虽然不是 O(1),但它非常适合数据库场景:
- B+ 树节点可以存很多索引项,树高很低;
- 每个节点通常对应一个页,适合磁盘 IO;
- 叶子节点天然有序,适合范围查询;
- 叶子节点之间有链表,适合范围扫描;
- 主键索引叶子节点存完整行数据,适合按主键组织数据。
所以 MySQL 选择 B+ 树,是因为它更适合数据库的综合查询场景。
八、每次都读磁盘还是太慢,于是有了 Buffer Pool
到这里,我们已经知道:
InnoDB 查询数据,本质上是通过 B+ 树索引找到对应的数据页。
但问题是,磁盘还是太慢。
如果每次查询都要从磁盘读 16KB 的数据页,性能肯定扛不住。
所以 InnoDB 在内存里维护了一块非常重要的缓存区域,叫 Buffer Pool。
Buffer Pool 里缓存的主要就是:
- 数据页;
- 索引页;
- Undo 页;
- 自适应哈希索引等内部结构。
对初学者来说,可以先重点理解:
Buffer Pool 是 InnoDB 自己维护的数据页和索引页缓存。
当执行:
InnoDB 大致会这样做:
- 通过 B+ 树定位目标数据页;
- 先看这个页在不在 Buffer Pool;
- 如果在,直接从内存读取;
- 如果不在,就从磁盘
.ibd表空间文件加载到 Buffer Pool;
- 再从 Buffer Pool 中读取数据并返回。
这就是为什么 MySQL 的数据虽然最终落在磁盘上,但查询性能依然可以很高。
因为大量热点数据,其实是从 Buffer Pool 里读出来的。
九、操作系统不是也有文件缓存吗?为什么 MySQL 还要 Buffer Pool?
操作系统本身也有 Page Cache。
如果 MySQL 读取磁盘文件,操作系统可能会把文件内容缓存到内存里。
那为什么 InnoDB 还要自己维护 Buffer Pool?
原因是:
操作系统知道你在读文件,但它不知道你在执行事务。
数据库比操作系统更清楚:
- 哪些页是热点页;
- 哪些页是索引页;
- 哪些页是数据页;
- 哪些页是脏页;
- 哪些页涉及事务可见性;
- 哪些页淘汰前必须先刷盘。
所以 InnoDB 不能完全依赖操作系统缓存。
在一些场景下,InnoDB 还会通过直接 IO 绕过操作系统文件缓存,避免同一份数据在 OS Page Cache 和 Buffer Pool 中被重复缓存。
Buffer Pool 是 InnoDB 自己掌控的数据页缓存,它比操作系统更懂数据库自己的访问模式。
十、一条 select SQL 的完整读流程
现在我们把读链路串起来。
还是这条 SQL:
大致流程如下:
所以,一条 select SQL 的本质链路就是:
SQL 进入 Server 层,经过解析和优化后,由执行器调用 InnoDB。InnoDB 通过 B+ 树定位数据页,优先从 Buffer Pool 读取,未命中才从磁盘加载。
第二部分:一条 update 是怎么写数据的?
十一、update 为什么比 select 更复杂?
查询只是读数据。
但更新数据,就不能只是简单地“把新值写进去”。
比如执行:
MySQL 必须考虑几个问题:
如果事务执行一半失败,怎么回滚?如果事务提交成功后,MySQL 突然宕机,数据会不会丢?如果主库提交了事务,从库怎么同步?如果内存页改了但还没刷盘,磁盘数据不一致怎么办?
这就引出了 InnoDB 的几个核心机制:
- Undo Log;
- Buffer Pool 脏页;
- Redo Log;
- Binlog;
- 两阶段提交。
十二、Undo Log:不是只用来回滚
更新一行数据之前,InnoDB 需要先记录这行数据的旧版本。
比如原来是:
id = 1, name = chengqian
现在要改成:
id = 1, name = mochengqian
InnoDB 会先把旧值记录到 Undo Log 里。
这样,如果事务后面失败了,或者主动执行:
InnoDB 就可以根据 Undo Log 把数据恢复到修改前的状态。
所以 Undo Log 的第一个作用是:
支持事务回滚。
但 Undo Log 不只是“后悔药”。
它还有一个非常重要的作用:
支持 MVCC 一致性读。
比如一个事务正在修改某行数据,但还没有提交。
另一个事务执行普通
select 时,不能随便读到未提交的新数据。这时候 InnoDB 就可以通过 Undo Log 构造历史版本,让普通查询读到符合当前隔离级别的数据快照。
所以更完整地说:
Undo Log 有两个核心作用:第一,事务回滚时恢复旧值;第二,配合 MVCC 构造历史版本,让普通 select 可以读到一致性快照。
十三、Buffer Pool 中被修改过的页,叫脏页
InnoDB 更新数据时,通常不是直接修改磁盘上的
.ibd 文件。如果每次 update 都立刻随机写磁盘,性能会非常差。
所以 InnoDB 通常会先把目标数据页加载到 Buffer Pool,然后修改内存里的数据页。
修改之后,Buffer Pool 中的数据页和磁盘上的数据页就不一致了。
这种页叫 脏页。
那脏页什么时候刷回磁盘?
不是每次 update 后立刻刷。
InnoDB 会在一些时机把脏页刷回磁盘,比如:
- 后台线程定期刷脏页;
- Buffer Pool 空间不够,需要淘汰页;
- Checkpoint 推进;
- Redo Log 空间压力变大;
- MySQL 正常关闭。
这里先记住一句话:
Buffer Pool 让修改可以先发生在内存里,脏页后续再异步刷盘,从而减少频繁随机写磁盘。
但是这也带来了一个新问题:
如果脏页还没刷盘,MySQL 宕机了怎么办?
这就需要 Redo Log。
十四、Redo Log:用来崩溃恢复
Undo Log 解决的是:
事务想反悔怎么办?
Redo Log 解决的是:
事务已经提交了,但脏页还没刷盘,机器宕机怎么办?
比如执行:
InnoDB 修改了 Buffer Pool 中的数据页,这个页变成了脏页。
但这时候磁盘上的
.ibd 文件可能还没改。如果 MySQL 突然宕机,内存里的脏页就没了。
那提交成功的数据不就丢了吗?
为了解决这个问题,InnoDB 会写 Redo Log。
Redo Log 记录的是:
对某个数据页做了什么物理层面的修改。
只要事务提交时,Redo Log 已经安全落盘,即使脏页还没刷回
.ibd 文件,MySQL 宕机后也可以根据 Redo Log 把数据恢复回来。这就是 WAL,也就是 Write Ahead Logging。
很多人会把 WAL 简单理解成:
先写日志,再写数据。
这个方向没错,但更准确地说应该是:
在脏页真正刷回磁盘之前,必须先保证对应的 Redo Log 已经持久化。这样即使数据页还没刷盘,宕机后也能通过 Redo Log 恢复。
所以 Redo Log 的核心价值是:
让 InnoDB 不必每次 update 都立刻刷数据页,同时又能保证事务提交后数据不丢。
十五、Checkpoint:Redo Log 和脏页刷盘之间的桥
Redo Log 不是无限大的。
如果一直写 Redo Log,而脏页一直不刷盘,Redo Log 总会被写满。
所以 InnoDB 需要不断推进 Checkpoint。
你可以把 Checkpoint 理解成一个标记:
哪些修改已经安全刷进数据页了。
当某些脏页刷回磁盘后,对应的 Redo Log 就不再是崩溃恢复必须依赖的内容,Redo Log 空间就可以被循环复用。
所以 Redo Log、脏页、Checkpoint 三者关系可以这样理解:
Redo Log 让脏页可以延迟刷盘,Checkpoint 负责推进脏页刷盘进度,并释放 Redo Log 的可复用空间。
十六、Change Buffer:减少二级索引页的随机 IO
更新数据时,除了修改聚簇索引里的数据,有时候还要修改二级索引。
比如
user 表有一个普通二级索引:如果更新了
age 字段,就可能需要修改 idx_age 对应的索引页。但问题是,这个二级索引页不一定在 Buffer Pool 中。
如果每次修改二级索引,都要先从磁盘把索引页读进来,再修改,就会产生额外的随机 IO。
于是 InnoDB 做了一个优化,叫 Change Buffer。
对于一些普通二级索引的变更,如果目标索引页不在 Buffer Pool 中,InnoDB 可以先把变更记录到 Change Buffer 里。
等以后这个索引页被读取到 Buffer Pool 时,再把 Change Buffer 中的变更合并进去。
不过 Change Buffer 不是所有索引都能用。
尤其是唯一索引,InnoDB 往往需要先判断唯一性,不能简单地把变更延迟到以后再合并。
所以更准确地说:
Change Buffer 主要用于普通二级索引的插入、删除标记、更新等变更,用来减少随机磁盘 IO;对于唯一索引,通常不能简单依赖 Change Buffer 延迟合并。
十七、Binlog:Server 层的逻辑日志
Redo Log 是 InnoDB 存储引擎自己的日志。
但 MySQL 还有一个非常重要的日志,叫 Binlog。
Binlog 属于 MySQL Server 层。
它记录的是逻辑层面的变更,比如:
或者行级别变更记录。
Binlog 主要用于:
- 主从复制;
- 数据恢复;
- 数据同步;
- 审计分析。
这里要区分 Redo Log 和 Binlog。
Redo Log 更偏物理日志,记录的是数据页层面的修改,主要用于 InnoDB 崩溃恢复。
Binlog 更偏逻辑日志,记录的是 SQL 或行级别的逻辑变更,主要用于主从复制和逻辑恢复。
所以:
Redo Log 解决的是 InnoDB 自己怎么崩溃恢复。Binlog 解决的是 MySQL Server 层怎么复制和恢复逻辑变更。
十八、为什么既要 Redo Log,又要 Binlog?
有人可能会问:
Redo Log 已经能保证崩溃恢复了,为什么还需要 Binlog?
因为它们解决的问题不同。
Redo Log 是 InnoDB 专用的崩溃恢复日志。
Binlog 是 MySQL Server 层的逻辑日志,用于主从复制和数据恢复。
MySQL 的存储引擎不只有 InnoDB,Server 层不能只依赖 InnoDB 的 Redo Log。
所以 MySQL 需要同时维护这两类日志。
但这又带来一个新问题:
一次事务提交时,既要写 Redo Log,又要写 Binlog,怎么保证它们一致?
比如可能出现两种极端情况:
- Redo Log 写成功了,Binlog 没写成功;
- Binlog 写成功了,Redo Log 没提交成功。
如果这两个日志不一致,就可能导致主库崩溃恢复出来的数据,和从库通过 Binlog 复制出来的数据不一致。
所以 MySQL 使用了类似两阶段提交的机制。
十九、Redo Log 和 Binlog 的两阶段提交
一条 update SQL 提交时,大致会经历:
- InnoDB 写 Redo Log,并标记为 prepare 状态;
- Server 层写 Binlog;
- InnoDB 再把 Redo Log 标记为 commit 状态;
- 返回事务提交成功。
这个流程的关键点在于:
Redo Log 先 prepare,Binlog 写成功后,Redo Log 再 commit。
如果 Redo Log 已经 prepare,Binlog 也写成功了,但 Redo Log commit 前 MySQL 宕机,恢复时 MySQL 可以根据 Binlog 是否存在来判断这个事务是否应该提交。
这样就能保证:
主库崩溃恢复出来的结果,和从库通过 Binlog 复制出来的结果一致。
二十、一条 update SQL 的完整执行流程
现在我们把 update 链路完整串起来。
执行:
大致流程如下:
所以,一条 update SQL 的本质链路是:
Server 层负责解析和优化 SQL,InnoDB 负责找到目标数据页;修改前先写 Undo Log 保存旧版本,然后修改 Buffer Pool 中的数据页形成脏页;事务提交时,通过 Redo Log 保证崩溃恢复,通过 Binlog 支持复制和逻辑恢复,再通过 Redo Log 和 Binlog 的两阶段提交保证一致性;脏页后续再异步刷回磁盘。
二十一、最后总结:MySQL 读写数据的本质
MySQL 看起来只是执行一条 SQL,但背后其实是一整套存储、索引、缓存、事务和日志机制在协同工作。
读数据时,核心链路是:
SQL → Server 层 → InnoDB → B+ 树 → Buffer Pool → 磁盘数据页。
写数据时,核心链路是:
SQL → Server 层 → InnoDB → Undo Log → Buffer Pool 脏页 → Redo Log → Binlog → 两阶段提交 → 异步刷盘。
可以分成几层理解:
第一层,数据最终会持久化到磁盘表空间文件中,常见情况下是
.ibd 文件。第二层,InnoDB 不会一行一行读写磁盘,而是按 16KB 的页读写。
第三层,为了快速定位数据页,InnoDB 使用 B+ 树索引。
第四层,InnoDB 的主键索引是聚簇索引,叶子节点存完整行数据;二级索引叶子节点通常存主键值,查询完整行时可能需要回表。
第五层,为了避免每次都读磁盘,InnoDB 使用 Buffer Pool 缓存数据页和索引页。
第六层,为了支持事务回滚和 MVCC,InnoDB 使用 Undo Log。
第七层,为了保证提交后的数据宕机不丢,InnoDB 使用 Redo Log。
第八层,为了支持主从复制和逻辑恢复,MySQL Server 层使用 Binlog。
第九层,为了保证 Redo Log 和 Binlog 一致,MySQL 提交事务时使用两阶段提交。
一句话总结:
MySQL 的核心是通过 B+ 树解决定位问题,通过 Buffer Pool 解决性能问题,通过 Undo Log 解决回滚和 MVCC 问题,通过 Redo Log 解决崩溃恢复问题,通过 Binlog 解决复制和逻辑恢复问题。
一条 SQL 在 MySQL 里是怎么执行的?
可以这样回答:
一条
select SQL 进入 MySQL 后,先经过 Server 层做连接管理、SQL 解析和优化,然后由执行器调用 InnoDB 存储引擎。InnoDB 会根据索引定位到对应的数据页,优先从 Buffer Pool 读取;如果 Buffer Pool 未命中,才从磁盘表空间文件中加载数据页到内存,再返回查询结果。如果是一条
update SQL,InnoDB 会先通过索引找到目标数据页,必要时从磁盘加载到 Buffer Pool。修改之前,会写 Undo Log 保存旧版本,用于事务回滚和 MVCC。然后修改 Buffer Pool 中的数据页,形成脏页。事务提交时,会写 Redo Log 保证崩溃恢复,同时 Server 层写 Binlog 支持主从复制和逻辑恢复。为了保证 Redo Log 和 Binlog 一致,MySQL 会使用两阶段提交。事务提交成功后,脏页不一定立刻刷盘,而是后续由后台线程或 Checkpoint 机制异步刷回磁盘。压缩成一句话:
select 的核心是“索引定位 + Buffer Pool 缓存”;update 的核心是“先改内存页,再用 Undo 保证可回滚,用 Redo 保证崩溃恢复,用 Binlog 保证复制恢复,用两阶段提交保证日志一致”。
分享
