Buffer Pool 详解
如果把 MySQL(准确来说是 InnoDB) 比作一个大型图书馆:
-
磁盘(Disk) = 仓库
-
Buffer Pool = 阅览室
-
CPU = 读书的人
数据库不会每次查询都跑到磁盘拿数据,而是会先把热点数据放到内存中,这块内存区域就是 Buffer Pool(缓冲池)。
一、什么是 Buffer Pool
Buffer Pool 是 InnoDB 存储引擎用于:
-
缓存数据页(Data Page)
-
缓存索引页(Index Page)
-
缓存 Undo Page
-
缓存 Insert Buffer
-
缓存 Adaptive Hash Index
的一块巨大的内存区域。
其核心目标:
减少磁盘 IO,提高数据库性能。
例如:
SELECT * FROM user WHERE id = 100;
第一次查询:
磁盘
↓
读取数据页
↓
Buffer Pool
↓
返回结果
第二次查询:
Buffer Pool
↓
直接返回
不需要再访问磁盘。
二、为什么需要 Buffer Pool
先看磁盘和内存的速度差异。
| 存储介质 | 延迟 |
|---|---|
| CPU缓存 | ns |
| 内存 | 100ns |
| SSD | 100μs |
| HDD | 10ms |
换算:
内存 ≈ 0.0001 ms
SSD ≈ 0.1 ms
HDD ≈ 10 ms
差距:
磁盘 > 内存
约1000~100000倍
如果每次 SQL 都访问磁盘:
select * from user where id=1;
性能会极差。
因此:
热点数据
↓
Buffer Pool
↓
减少IO
↓
提升QPS
三、Buffer Pool 缓存的是什么
很多人以为缓存的是一条记录。
实际上:
Buffer Pool 缓存的是 Page(页)。
四、InnoDB 的页(Page)
InnoDB 最小管理单位:
Page
默认大小:
16KB
例如表:
user
数据:
id name
1 Tom
2 Jack
3 Lucy
...
磁盘中存储:
Page1
Page2
Page3
...
结构:
Page1
├─ Row1
├─ Row2
├─ Row3
└─ ...
当查询:
select * from user where id=1;
并不是读取这一行。
而是:
读取整个Page
16KB
加载到 Buffer Pool。
Disk
└─ Page1
↓
Buffer Pool
└─ Page1
后续访问同页数据:
select * from user where id=2;
select * from user where id=3;
无需磁盘IO。
五、Buffer Pool 的内部结构
Buffer Pool 本质:
Buffer Pool
├─ Free List
├─ LRU List
├─ Flush List
└─ Hash Table
这是高频题。
六、Free List(空闲链表)
Buffer Pool 初始化时:
+-----+
| Page|
+-----+
| Page|
+-----+
| Page|
+-----+
这些内存块称:
Buffer Frame
结构:
Buffer Frame
└─ 存放一个16KB Page
未使用的 Frame:
Free List
Free List
Frame1
Frame2
Frame3
Frame4
当需要读取数据页:
从Free List取一个Frame
放入:
Disk Page
↓
Frame
七、LRU List(最重要)
Buffer Pool 不可能无限大。
例如:
innodb_buffer_pool_size=8G
满了怎么办?
需要淘汰页面。
普通LRU
理论上:
最近访问
↓
放头部
长时间没访问
↓
尾部淘汰
Head
A→B→C→D→E
Tail
淘汰:
E
问题
例如:
select * from order;
全表扫描:
Page1
Page2
Page3
...
Page100000
这些页都只访问一次。
如果全部放到 LRU 头部:
热点页被挤出去
缓存命中率暴跌。
八、InnoDB 改进版 LRU
MySQL 没直接使用传统 LRU。
而是:
Young Area
Old Area
结构:
Head
Young
│
│
├─────────────
│
Old
Tail
默认比例:
5 : 3
约:
63%
37%
新页进入 Old
很多人以为:
新页 → Young
实际上:
新页 → Old头部
Young
---------
Old
↑
新页
原因:
防止全表扫描污染缓存。
第二次访问
如果:
第一次访问
进入:
Old
之后再次访问:
Old → Young
说明:
是真热点数据
因此:
一次访问
↓
Old
多次访问
↓
Young
提高缓存命中率。
九、Flush List(刷盘链表)
当执行:
update user
set name='Tom'
where id=1;
修改流程:
磁盘Page
↓
Buffer Pool Page
↓
修改内存
此时:
内存 ≠ 磁盘
产生:
Dirty Page(脏页)
例如:
Page1
name=Tom
内存:
Page1
name=Jerry
磁盘:
Page1
name=Tom
不一致。
所有脏页:
Flush List
维护。
Flush List
Page3
Page8
Page10
后台线程:
Page Cleaner
负责:
脏页刷盘
十、Hash Table(页定位)
假设:
Buffer Pool
8GB
里面:
50万Page
查询一个 Page:
遍历?
显然不现实。
所以有:
Page Number
↓
Hash
↓
Buffer Frame
实现:
O(1)
快速定位。
十一、Buffer Pool 与 Redo Log 的关系
这是最核心的设计之一。
假设:
update user
set age=18
where id=1;
如果:
修改内存
↓
立刻刷盘
问题:
随机IO太多
性能差。
InnoDB 采用:
WAL(Write Ahead Logging)
先写日志。
流程:
1 修改Buffer Pool
2 记录Redo Log
3 提交事务
4 后台刷脏页
UPDATE
↓
Buffer Pool
(脏页)
↓
Redo Log
(顺序写)
↓
Commit
↓
后台刷盘
优势:
随机写
↓
顺序写日志
↓
性能暴增
十二、Buffer Pool 命中率
查看:
SHOW ENGINE INNODB STATUS;
或者:
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool%';
重点:
Innodb_buffer_pool_read_requests
逻辑读次数。
Innodb_buffer_pool_reads
真正磁盘读次数。
计算:
Hit Rate
=
1 - reads/read_requests
例如:
read_requests = 1000000
reads = 1000
命中率:
99.9%
优秀。
十三、Buffer Pool 为什么通常配置为物理内存的 60%~80%
例如:
服务器
64GB RAM
配置:
innodb_buffer_pool_size=48G
原因:
系统还需要:
-
OS Page Cache
-
连接线程
-
排序缓冲区
-
Join Buffer
-
Binlog Cache
-
Redo Log Buffer
不能全部给 Buffer Pool。
十四、Buffer Pool 完整工作流程
SQL
│
▼
查找数据页
│
▼
Buffer Pool ?
│ │
│命中 │未命中
▼ ▼
直接返回 磁盘读取
│
▼
Free List
│
▼
LRU List
│
▼
返回数据
------------------------------------------------
UPDATE
│
▼
修改Buffer Pool页
│
▼
变成Dirty Page
│
▼
加入Flush List
│
▼
写Redo Log
│
▼
事务提交
│
▼
后台线程刷盘
一句话总结:
Buffer Pool 是 InnoDB 最重要的内存组件,本质上是一个以 Page(默认16KB)为单位的缓存系统。它通过 Free List 管理空闲页、LRU 管理热点页、Flush List 管理脏页,并结合 Redo Log 的 WAL 机制实现“内存修改 + 日志持久化 + 异步刷盘”,从而将大量随机磁盘 IO 转换成内存访问和顺序日志写入,这是 MySQL 高性能的核心基础之一。