存储引擎与 Buffer Pool
MySQL 的数据最终由存储引擎管理。现在商业项目最常用的是 InnoDB,因为它支持事务、行锁、MVCC、崩溃恢复和外键等能力。
一句话理解:
Server 层负责解析、优化和执行 SQL,InnoDB 负责真正读写数据页、维护索引、处理事务和恢复。
MySQL 分层
flowchart TD
A["客户端"] --> B["连接层"]
B --> C["Server 层<br/>解析器 / 优化器 / 执行器"]
C --> D["存储引擎接口"]
D --> E["InnoDB<br/>页 / B+Tree / 事务 / 锁"]
E --> F["磁盘文件<br/>数据文件 / redo / undo"]为什么要分层:
| 层 | 职责 |
|---|---|
| Server 层 | SQL 解析、权限、优化器、执行器、binlog |
| 存储引擎层 | 数据页、索引、事务、锁、崩溃恢复 |
这解释了一个常见问题:binlog 是 Server 层日志,redo log 是 InnoDB 日志,两者职责不同。
InnoDB 为什么重要
InnoDB 提供:
- 聚簇索引。
- 事务 ACID。
- 行级锁。
- MVCC。
- redo log 崩溃恢复。
- Buffer Pool 缓存。
如果没有这些能力,商业系统里的支付、库存、订单状态流转很难保证正确性。
Buffer Pool 是什么
磁盘很慢,内存很快。InnoDB 不会每次查询都直接读磁盘,而是优先从 Buffer Pool 找数据页。
flowchart TD
A["查询一行数据"] --> B{"Buffer Pool 有该页吗"}
B -- "有" --> C["直接从内存读页"]
B -- "没有" --> D["从磁盘读页到 Buffer Pool"]
D --> C
C --> E["从页中找到记录"]注意:Buffer Pool 缓存的是页,不是单独一行。默认页大小通常是 16KB。
页为什么重要
即使你只查一行:
select *
from users
where id = 1001;InnoDB 底层也通常会把这一行所在的数据页读入内存。后续如果查询同一页上的其他行,就可能直接命中 Buffer Pool。
这也是为什么:
- 行越短,一页能放越多行。
- 主键递增,热点页更集中。
- 随机 UUID 主键会让写入分散。
- 大字段放主表会降低缓存效率。
Buffer Pool 与脏页
更新数据时,InnoDB 不是每次都立刻把数据页刷到磁盘。
flowchart TD
A["执行 update"] --> B["修改 Buffer Pool 中的数据页"]
B --> C["页变成脏页"]
C --> D["写 redo log 保证崩溃可恢复"]
D --> E["事务提交"]
E --> F["后台线程择机刷脏页到磁盘"]脏页是指:内存里的页已经被修改,但磁盘上的旧页还没同步。
为什么可以这样做:
- 直接刷数据页是随机 IO,成本高。
- redo log 是顺序写,成本低。
- 宕机后可以用 redo log 恢复已提交修改。
LRU 和冷热数据
Buffer Pool 空间有限,需要淘汰不常用的页。InnoDB 使用改进版 LRU 思路管理冷热页。
flowchart TD
A["新读入的数据页"] --> B["进入 Buffer Pool"]
B --> C{"是否频繁访问"}
C -- "是" --> D["进入热数据区域"]
C -- "否" --> E["停留在冷数据区域"]
E --> F["空间不足时优先淘汰"]为什么要区分冷热:
如果一次全表扫描把大量冷数据都放进 Buffer Pool,可能把原本高频访问的订单、用户热点页挤出去,导致系统整体变慢。
Change Buffer
对于非唯一二级索引的修改,如果目标索引页不在 Buffer Pool,InnoDB 可以先把变更记录到 Change Buffer,等以后读到该页时再合并。
适合:
- 写多读少。
- 非唯一二级索引。
- 随机写入较多。
不适合:
- 唯一索引,因为必须检查唯一性。
- 写完马上读,因为很快就要合并。
商业场景:为什么大字段影响性能
用户表如果这样设计:
create table users (
id bigint primary key auto_increment,
username varchar(50) not null,
avatar_base64 longtext null,
created_at datetime not null
) engine = InnoDB default charset = utf8mb4;问题:
- 行变大。
- 一页能放的记录变少。
- Buffer Pool 能缓存的用户数量变少。
- 列表查询可能被大字段拖慢。
更合理:
create table users (
id bigint primary key auto_increment,
username varchar(50) not null,
avatar_url varchar(255) not null default '',
created_at datetime not null
) engine = InnoDB default charset = utf8mb4;图片本体放对象存储,数据库只保存 URL。
排查方向
| 现象 | 可能原因 | 排查 |
|---|---|---|
| 查询忽快忽慢 | Buffer Pool 命中率变化 | 看缓存命中、磁盘 IO |
| 写入偶尔抖动 | 脏页刷盘压力 | 看 redo、刷脏页、IO 利用率 |
| 大查询影响全站 | 冷数据挤出热点页 | 排查全表扫描、报表 SQL |
| 内存很大但还是慢 | 索引设计差,扫描页太多 | EXPLAIN、慢日志 |
面试标准回答
MySQL Server 层负责 SQL 解析、优化和执行,InnoDB 负责真正的数据页、索引、事务、锁和崩溃恢复。Buffer Pool 是 InnoDB 的核心缓存,缓存的是数据页和索引页,不是单独一行。查询时先看页是否在 Buffer Pool,命中则直接读内存,不命中才从磁盘加载。更新时先修改内存页形成脏页,并写 redo log 保证崩溃恢复,之后由后台线程刷脏页。理解 Buffer Pool 后就能解释为什么行要短、主键要递增、大字段要拆出、全表扫描会影响热点缓存。关联学习:架构分层、redo log 与 binlog、存储结构。
