【计算基础|MySQL18】Buffer Pool 与内存管理
上一篇讲了三大日志,知道了 redo 先写内存再异步刷盘。那"内存里那块缓存数据的地方"到底是什么?就是 Buffer Pool。你以为 MySQL 每次查询都去读磁盘,其实 99% 的请求命中的是内存里的缓存页。Buffer Pool 命中率差一个百分点,QPS 可能腰斩;一次忘了带 WHERE 的全表 UPDATE,能把你辛苦缓存的热点页全挤出去------这就是缓存污染。
这篇把 Buffer Pool 的内部结构讲透:三个链表怎么分工、LRU 为什么要拆成年轻/老年区、脏页什么时候刷、Change Buffer 凭什么只给二级索引加速、双写缓冲怎么防止"页写一半断电"。所有状态在 Docker MySQL 8.0.46 上真实读取。
环境:Docker MySQL 8.0.46,
innodb_buffer_pool_size=128MB(默认),通过SHOW ENGINE INNODB STATUS读取运行时结构。
一、为什么需要 Buffer Pool
磁盘一次随机 IO 是 ms 级 (约 10⁻³ 秒),内存访问是 ns 级 (约 10⁻⁹ 秒),差了整整 6 个数量级。如果每次读数据都走磁盘,数据库根本扛不住并发。
InnoDB 的策略很直接:把刚访问过的数据页缓存在内存里,以后再要就读内存,不再碰磁盘。而且不光读,写也是先改内存里的页(变成脏页),靠 redo log 保证不丢,后台慢慢刷盘。所以------
🔴 InnoDB 的读和写,全部先经过 Buffer Pool。它是 InnoDB 性能的命门。
sql
mysql> SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name | Value |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 | -- 128MB(默认)
+-------------------------+-----------+
⚠️ 生产建议 :这个值默认才 128MB,生产库一般设成物理内存的 50%~70%( dedicated MySQL 机器)。设太小缓存命中率低,设太大可能挤压操作系统和连接内存。
二、Buffer Pool 的结构:三个链表
Buffer Pool 以**页(默认 16KB)**为单位缓存,数据页、索引页都往里放。内部用几条链表管这些页:
#mermaid-svg-vXutVDwumIcxYWGf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vXutVDwumIcxYWGf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vXutVDwumIcxYWGf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vXutVDwumIcxYWGf .error-icon{fill:#552222;}#mermaid-svg-vXutVDwumIcxYWGf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vXutVDwumIcxYWGf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vXutVDwumIcxYWGf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vXutVDwumIcxYWGf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vXutVDwumIcxYWGf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vXutVDwumIcxYWGf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vXutVDwumIcxYWGf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vXutVDwumIcxYWGf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vXutVDwumIcxYWGf .marker.cross{stroke:#333333;}#mermaid-svg-vXutVDwumIcxYWGf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vXutVDwumIcxYWGf p{margin:0;}#mermaid-svg-vXutVDwumIcxYWGf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vXutVDwumIcxYWGf .cluster-label text{fill:#333;}#mermaid-svg-vXutVDwumIcxYWGf .cluster-label span{color:#333;}#mermaid-svg-vXutVDwumIcxYWGf .cluster-label span p{background-color:transparent;}#mermaid-svg-vXutVDwumIcxYWGf .label text,#mermaid-svg-vXutVDwumIcxYWGf span{fill:#333;color:#333;}#mermaid-svg-vXutVDwumIcxYWGf .node rect,#mermaid-svg-vXutVDwumIcxYWGf .node circle,#mermaid-svg-vXutVDwumIcxYWGf .node ellipse,#mermaid-svg-vXutVDwumIcxYWGf .node polygon,#mermaid-svg-vXutVDwumIcxYWGf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vXutVDwumIcxYWGf .rough-node .label text,#mermaid-svg-vXutVDwumIcxYWGf .node .label text,#mermaid-svg-vXutVDwumIcxYWGf .image-shape .label,#mermaid-svg-vXutVDwumIcxYWGf .icon-shape .label{text-anchor:middle;}#mermaid-svg-vXutVDwumIcxYWGf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vXutVDwumIcxYWGf .rough-node .label,#mermaid-svg-vXutVDwumIcxYWGf .node .label,#mermaid-svg-vXutVDwumIcxYWGf .image-shape .label,#mermaid-svg-vXutVDwumIcxYWGf .icon-shape .label{text-align:center;}#mermaid-svg-vXutVDwumIcxYWGf .node.clickable{cursor:pointer;}#mermaid-svg-vXutVDwumIcxYWGf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vXutVDwumIcxYWGf .arrowheadPath{fill:#333333;}#mermaid-svg-vXutVDwumIcxYWGf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vXutVDwumIcxYWGf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vXutVDwumIcxYWGf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vXutVDwumIcxYWGf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vXutVDwumIcxYWGf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vXutVDwumIcxYWGf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vXutVDwumIcxYWGf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vXutVDwumIcxYWGf .cluster text{fill:#333;}#mermaid-svg-vXutVDwumIcxYWGf .cluster span{color:#333;}#mermaid-svg-vXutVDwumIcxYWGf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vXutVDwumIcxYWGf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vXutVDwumIcxYWGf rect.text{fill:none;stroke-width:0;}#mermaid-svg-vXutVDwumIcxYWGf .icon-shape,#mermaid-svg-vXutVDwumIcxYWGf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vXutVDwumIcxYWGf .icon-shape p,#mermaid-svg-vXutVDwumIcxYWGf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vXutVDwumIcxYWGf .icon-shape .label rect,#mermaid-svg-vXutVDwumIcxYWGf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vXutVDwumIcxYWGf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vXutVDwumIcxYWGf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vXutVDwumIcxYWGf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Buffer Pool(128MB,共 8192 个 16KB 页)
Free List
空闲页链表
(还没用的页)
LRU List
缓存页链表
(按访问热度排序)
Flush List
脏页链表
(改了还没刷盘的页)
| 链表 | 管什么 | 什么时候动 |
|---|---|---|
| Free List | 还没被使用的空闲页 | 要加载新页时从这里拿 |
| LRU List | 已经缓存的页,按访问热度排序 | 命中/加载页时调整顺序 |
| Flush List | 被修改过、还没刷盘的脏页 | 事务改页时入队,刷盘后出队 |
本机实跑 SHOW ENGINE INNODB STATUS 的 BUFFER POOL AND MEMORY 段:
BUFFER POOL AND MEMORY
----------------------
Buffer pool size 8192 ← 总页数(×16KB ≈ 128MB)
Free buffers 1024 ← Free List 上的空闲页数
Database pages 7153 ← LRU List 上已缓存的页数
Old database pages 2620 ← LRU 老年区的页数
Modified db pages 0 ← Flush List 上的脏页数
Pages made young 9105, not young 762185 ← 被提升到年轻区/没被提升的次数
Buffer pool hit rate 1000 / 1000 ← 命中率(千分比,这里是 100%)
✅ 成功标志 :Buffer pool hit rate 应该长期维持在 990/1000 以上;如果掉到 900/1000 以下,说明缓存不够、大量读磁盘,要考虑加内存或调大 buffer pool。
三、LRU 的优化:中点插入 + 年轻/老年区
3.1 普通 LRU 为什么不行
操作系统课本里的 LRU(最近最少使用):新页插到队头,访问到的页提到队头,队列满了淘汰队尾。这套在数据库里有个致命问题------缓存污染。
设想你跑了一个全表扫描,要读 10 万个页。按普通 LRU,这些页全插到队头,把本来热门的业务页挤到队尾淘汰掉了。扫描完之后,系统又回到原来的热点查询,结果热点页全被淘汰了,又得重新从磁盘加载。一次大查询把整个缓存击穿。
3.2 InnoDB 的改进:分段 LRU
InnoDB 把 LRU 链表切成两段:
渲染错误: Mermaid 渲染失败: Parse error on line 3: ... Y2"..." --- Y3"" end subgra -----------------------^ Expecting 'TAGEND', 'STR', 'MD_STR', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQE'
- young 区(前 5/8):真正的热数据,频繁访问。
- old 区(后 3/8) :冷数据。参数
innodb_old_blocks_pct=37正好是 37%。
新页不插到队头,而是插到 old 区的头部(中点位置)。这样全表扫描读进来的页先落在冷区,就算被淘汰也只淘汰冷区的尾巴,碰不到年轻区的热点页。
还有第二道保险:一个页第一次被读到 old 区时,并不会立刻晋升到 young 区 。要等 innodb_old_blocks_time=1000(1 秒)之后再次被访问,才晋升。全表扫描的页通常只读一次、读过去就不再碰,所以即使落进 old 区也不会占用宝贵的年轻区。
sql
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_pct'; -- 37(old 区占比)
mysql> SHOW VARIABLES LIKE 'innodb_old_blocks_time'; -- 1000(毫秒)
🔴 一句话:中点插入挡住"大查询直接冲进热区",1 秒二次访问门槛挡住"只访问一次的页占热区"。两个机制配合防缓存污染。
四、脏页什么时候刷盘
页被修改后就成了脏页,待在 Flush List 里。什么时候刷回磁盘?
| 触发时机 | 说明 |
|---|---|
| redo log 写满 | 推进 checkpoint,强制刷最老的脏页(否则 redo 没法覆盖) |
| Buffer Pool 不够用 | 需要空闲页时,淘汰 LRU 尾部的脏页并刷盘 |
| 后台空闲 | page cleaner 线程在系统闲时主动刷 |
| MySQL 正常关闭 | 关机前把脏页全刷干净 |
刷盘由 page cleaner 线程负责(MySQL 5.6+ 可配多个 innodb_page_cleaners)。脏页比例有个水位线:
sql
mysql> SHOW VARIABLES LIKE 'innodb_max_dirty_pages_pct';
+-----------------------------+-------+
| innodb_max_dirty_pages_pct | 90.0 | -- 脏页超过 90% 就积极刷盘
+-----------------------------+-------+
双写缓冲(Double Write Buffer):防止"页写一半断电"
刷脏页有个危险:一个 16KB 的页刷到一半机器断电,磁盘上这个页就坏了(torn page / partial write)。redo log 重放也救不回来------因为 redo 是"对页的增量修改",前提是页本身是完整的。
解法是双写缓冲:
#mermaid-svg-RSf3T2ucmt0yN1cN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RSf3T2ucmt0yN1cN .error-icon{fill:#552222;}#mermaid-svg-RSf3T2ucmt0yN1cN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RSf3T2ucmt0yN1cN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RSf3T2ucmt0yN1cN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RSf3T2ucmt0yN1cN .marker.cross{stroke:#333333;}#mermaid-svg-RSf3T2ucmt0yN1cN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RSf3T2ucmt0yN1cN p{margin:0;}#mermaid-svg-RSf3T2ucmt0yN1cN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RSf3T2ucmt0yN1cN .cluster-label text{fill:#333;}#mermaid-svg-RSf3T2ucmt0yN1cN .cluster-label span{color:#333;}#mermaid-svg-RSf3T2ucmt0yN1cN .cluster-label span p{background-color:transparent;}#mermaid-svg-RSf3T2ucmt0yN1cN .label text,#mermaid-svg-RSf3T2ucmt0yN1cN span{fill:#333;color:#333;}#mermaid-svg-RSf3T2ucmt0yN1cN .node rect,#mermaid-svg-RSf3T2ucmt0yN1cN .node circle,#mermaid-svg-RSf3T2ucmt0yN1cN .node ellipse,#mermaid-svg-RSf3T2ucmt0yN1cN .node polygon,#mermaid-svg-RSf3T2ucmt0yN1cN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RSf3T2ucmt0yN1cN .rough-node .label text,#mermaid-svg-RSf3T2ucmt0yN1cN .node .label text,#mermaid-svg-RSf3T2ucmt0yN1cN .image-shape .label,#mermaid-svg-RSf3T2ucmt0yN1cN .icon-shape .label{text-anchor:middle;}#mermaid-svg-RSf3T2ucmt0yN1cN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RSf3T2ucmt0yN1cN .rough-node .label,#mermaid-svg-RSf3T2ucmt0yN1cN .node .label,#mermaid-svg-RSf3T2ucmt0yN1cN .image-shape .label,#mermaid-svg-RSf3T2ucmt0yN1cN .icon-shape .label{text-align:center;}#mermaid-svg-RSf3T2ucmt0yN1cN .node.clickable{cursor:pointer;}#mermaid-svg-RSf3T2ucmt0yN1cN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RSf3T2ucmt0yN1cN .arrowheadPath{fill:#333333;}#mermaid-svg-RSf3T2ucmt0yN1cN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RSf3T2ucmt0yN1cN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RSf3T2ucmt0yN1cN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RSf3T2ucmt0yN1cN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RSf3T2ucmt0yN1cN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RSf3T2ucmt0yN1cN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RSf3T2ucmt0yN1cN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RSf3T2ucmt0yN1cN .cluster text{fill:#333;}#mermaid-svg-RSf3T2ucmt0yN1cN .cluster span{color:#333;}#mermaid-svg-RSf3T2ucmt0yN1cN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RSf3T2ucmt0yN1cN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RSf3T2ucmt0yN1cN rect.text{fill:none;stroke-width:0;}#mermaid-svg-RSf3T2ucmt0yN1cN .icon-shape,#mermaid-svg-RSf3T2ucmt0yN1cN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RSf3T2ucmt0yN1cN .icon-shape p,#mermaid-svg-RSf3T2ucmt0yN1cN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RSf3T2ucmt0yN1cN .icon-shape .label rect,#mermaid-svg-RSf3T2ucmt0yN1cN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RSf3T2ucmt0yN1cN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RSf3T2ucmt0yN1cN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RSf3T2ucmt0yN1cN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 崩溃恢复
脏页
先写共享表空间的
doublewrite buffer (2MB, 128页)
再写真正的数据文件
从 doublewrite 复制完整页
再重放 redo
先把页完整地写进共享表空间里一块连续的 doublewrite 区域,再写真正的数据文件。万一数据文件写一半断电,恢复时从 doublewrite 那块完整副本把页恢复成完整的,再重放 redo。
⚠️ 版本差异 :MySQL 8.0.20 之前是这种集中式 doublewrite buffer(在 ibdata 里);8.0.20 开始废弃传统方式,改成每个表空间一个 .dblwr 文件。原理不变,都是先落一份完整副本再写数据文件。
五、Change Buffer:给非唯一二级索引的写加速
5.1 它解决什么问题
改一条记录时,对应的聚簇索引页 一般已经在 Buffer Pool 里了(因为你是按主键查出来改的)。但二级索引页 呢?二级索引通常有好几个,修改可能涉及其中某些页,而这些页很可能不在内存里。
如果每次改二级索引都要从磁盘把那个页读进来(随机 IO),写性能会很差。Change Buffer 的思路是:页不在内存时,先不读盘,把"我要对这个索引页做什么修改"记在 Change Buffer 里;等以后这个页因为别的查询被读进内存时,再把记录的修改合并(merge)进去。
#mermaid-svg-J0A3uWMwbuYnY03W{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-J0A3uWMwbuYnY03W .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-J0A3uWMwbuYnY03W .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-J0A3uWMwbuYnY03W .error-icon{fill:#552222;}#mermaid-svg-J0A3uWMwbuYnY03W .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-J0A3uWMwbuYnY03W .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-J0A3uWMwbuYnY03W .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-J0A3uWMwbuYnY03W .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-J0A3uWMwbuYnY03W .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-J0A3uWMwbuYnY03W .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-J0A3uWMwbuYnY03W .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-J0A3uWMwbuYnY03W .marker{fill:#333333;stroke:#333333;}#mermaid-svg-J0A3uWMwbuYnY03W .marker.cross{stroke:#333333;}#mermaid-svg-J0A3uWMwbuYnY03W svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-J0A3uWMwbuYnY03W p{margin:0;}#mermaid-svg-J0A3uWMwbuYnY03W .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-J0A3uWMwbuYnY03W .cluster-label text{fill:#333;}#mermaid-svg-J0A3uWMwbuYnY03W .cluster-label span{color:#333;}#mermaid-svg-J0A3uWMwbuYnY03W .cluster-label span p{background-color:transparent;}#mermaid-svg-J0A3uWMwbuYnY03W .label text,#mermaid-svg-J0A3uWMwbuYnY03W span{fill:#333;color:#333;}#mermaid-svg-J0A3uWMwbuYnY03W .node rect,#mermaid-svg-J0A3uWMwbuYnY03W .node circle,#mermaid-svg-J0A3uWMwbuYnY03W .node ellipse,#mermaid-svg-J0A3uWMwbuYnY03W .node polygon,#mermaid-svg-J0A3uWMwbuYnY03W .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-J0A3uWMwbuYnY03W .rough-node .label text,#mermaid-svg-J0A3uWMwbuYnY03W .node .label text,#mermaid-svg-J0A3uWMwbuYnY03W .image-shape .label,#mermaid-svg-J0A3uWMwbuYnY03W .icon-shape .label{text-anchor:middle;}#mermaid-svg-J0A3uWMwbuYnY03W .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-J0A3uWMwbuYnY03W .rough-node .label,#mermaid-svg-J0A3uWMwbuYnY03W .node .label,#mermaid-svg-J0A3uWMwbuYnY03W .image-shape .label,#mermaid-svg-J0A3uWMwbuYnY03W .icon-shape .label{text-align:center;}#mermaid-svg-J0A3uWMwbuYnY03W .node.clickable{cursor:pointer;}#mermaid-svg-J0A3uWMwbuYnY03W .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-J0A3uWMwbuYnY03W .arrowheadPath{fill:#333333;}#mermaid-svg-J0A3uWMwbuYnY03W .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-J0A3uWMwbuYnY03W .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-J0A3uWMwbuYnY03W .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J0A3uWMwbuYnY03W .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-J0A3uWMwbuYnY03W .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J0A3uWMwbuYnY03W .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-J0A3uWMwbuYnY03W .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-J0A3uWMwbuYnY03W .cluster text{fill:#333;}#mermaid-svg-J0A3uWMwbuYnY03W .cluster span{color:#333;}#mermaid-svg-J0A3uWMwbuYnY03W div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-J0A3uWMwbuYnY03W .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-J0A3uWMwbuYnY03W rect.text{fill:none;stroke-width:0;}#mermaid-svg-J0A3uWMwbuYnY03W .icon-shape,#mermaid-svg-J0A3uWMwbuYnY03W .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-J0A3uWMwbuYnY03W .icon-shape p,#mermaid-svg-J0A3uWMwbuYnY03W .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-J0A3uWMwbuYnY03W .icon-shape .label rect,#mermaid-svg-J0A3uWMwbuYnY03W .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-J0A3uWMwbuYnY03W .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-J0A3uWMwbuYnY03W .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-J0A3uWMwbuYnY03W :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 在
不在
UPDATE/INSERT/DELETE
二级索引
目标索引页
在 Buffer Pool?
直接修改该页
记到 Change Buffer
(不读磁盘)
以后该页被读入时
merge 合并修改
5.2 为什么只对"非唯一"二级索引
- 唯一索引不行:插一条记录必须立刻检查唯一性,而唯一性检查必须读到那个索引页------躲不开随机读,没法缓冲。
- 聚簇索引不需要:聚簇索引按主键顺序写,新记录通常落在当前页附近,本身就是顺序写,没什么可缓冲的。
所以 Change Buffer 只服务于非唯一二级索引。
sql
mysql> SHOW VARIABLES LIKE 'innodb_change_buffer_max_size';
+------------------------------+-------+
| innodb_change_buffer_max_size | 25 | -- 最多占 Buffer Pool 的 25%
+------------------------------+-------+
🔴 适用场景判断 :写多读少、非唯一二级索引多的表(比如日志、订单)收益大;读多写少的表,merge 时反而要额外读页合并,可能得不偿失。innodb_change_buffering 可控制缓冲哪些操作(all/inserts/deletes/purges/none)。
六、预读与自适应哈希索引
6.1 预读(Read-Ahead)
InnoDB 预判"你接下来会读哪些页",提前异步从磁盘读进来:
| 类型 | 触发条件 | 默认 |
|---|---|---|
| 线性预读 | 顺序访问一个区(extent,1MB=64 页)里的页超过阈值(innodb_read_ahead_threshold=56),预读下一个区 |
开 |
| 随机预读 | 一个区里有较多页已在 Buffer Pool 且被访问,就把整区剩下的页读进来 | 默认关(8.0 已移除,效果不稳定) |
⚠️ 预读是把双刃剑:预读的页如果最后没用上,既白读磁盘又挤占 Buffer Pool(又是一种缓存污染)。
6.2 自适应哈希索引(AHI)
B+ 树找一条数据要走 3~4 层(3~4 次页访问)。InnoDB 发现某些页被频繁做等值查询 时,会自动在内存里给这些页建一个哈希索引,把等值查询从 O(tree height) 变成 O(1)。
sql
mysql> SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';
+----------------------------+-------+
| innodb_adaptive_hash_index | ON | -- 默认开
+----------------------------+-------+
本机 SHOW ENGINE INNODB STATUS 里能看到 AHI 已经在干活:
INSERT BUFFER AND ADAPTIVE HASH INDEX
Ibuf: size 1, free list len 0, seg size 2, 0 merges
Hash table size 34679, node heap has 1 buffer(s)
...
⚠️ AHI 是自动的、不用你建,但它在高并发写入时可能成为瓶颈(哈希表要加锁保护),这种场景下可以考虑关掉。
七、其他内存结构与监控
除了 Buffer Pool,MySQL 还有一批每个会话/全局的内存区:
| 内存区 | 作用 | 注意 |
|---|---|---|
sort_buffer_size |
ORDER BY 排序 | 每个线程一份,连接数 × 它别太大 |
join_buffer_size |
无索引 join | 同上,每线程 |
tmp_table_size / max_heap_table_size |
内存临时表 | 超了会落磁盘 |
table_open_cache |
表定义缓存 | 全局 |
监控 Buffer Pool 主要靠 SHOW ENGINE INNODB STATUS 的 BUFFER POOL AND MEMORY 段(前面贴过),重点盯:
- Buffer pool hit rate:应长期 > 990/1000;
- Modified db pages(脏页数):对应脏页比例;
- Free buffers:长期很低说明缓存紧张。
调优建议
innodb_buffer_pool_size设物理内存的 50%~70%。- 内存很大(>10GB)时用
innodb_buffer_pool_instances切成多个实例,每个实例独立 LRU、减少锁竞争,每个 instance 至少 1GB。 - 命中率低于 99% 要排查是不是 buffer pool 太小、或者有没有大查询污染。
- 脏页比例长期过高会导致刷盘跟不上、写入抖动。
总结:你真正需要记住的 7 件事
- Buffer Pool 是 InnoDB 的内存缓存,读写都先走它;生产建议设物理内存 50%~70%,单位是 16KB 的页。
- 内部三条链表分工:Free List(空闲页)、LRU List(缓存页按热度)、Flush List(脏页)。
- InnoDB 的 LRU 分年轻区(5/8)和老年区(3/8),新页中点插入,且 1 秒内二次访问才晋升年轻区------防全表扫描击穿热点缓存。
- 脏页在 redo 写满、Buffer Pool 不够、后台空闲、正常关闭时刷盘;双写缓冲防止"页写一半断电"。
- Change Buffer 只加速非唯一二级索引:页不在内存时先记录修改,等页被读入再 merge;唯一索引必须读页查重,缓冲不了。
- 预读可能带来缓存污染;AHI 自动给高频等值查询建哈希索引加速,高并发写时可能成瓶颈。
- 监控盯
SHOW ENGINE INNODB STATUS里的命中率(>99%)、脏页数、空闲页数。
验证清单
- 能说出 Buffer Pool 三条链表各自管什么,并解释脏页为什么在 Flush List 上
- 能画出分段 LRU:年轻区/老年区比例、新页插在哪、二次访问门槛是多少
- 能解释一次全表扫描为什么会击穿普通 LRU,InnoDB 两个机制怎么防
- 能说清脏页的四种刷盘时机,以及双写缓冲解决什么问题(torn page)
- 能解释 Change Buffer 原理,并回答"为什么唯一索引用不上 Change Buffer"
- 知道
innodb_buffer_pool_size生产设多大、多实例时每个 instance 至少多大 - 会用
SHOW ENGINE INNODB STATUS看 Buffer pool hit rate,并判断健康值
参考资源
- MySQL 8.0 Reference Manual --- The InnoDB Buffer Pool: https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool.html
- MySQL 8.0 Reference Manual --- Change Buffer: https://dev.mysql.com/doc/refman/8.0/en/innodb-change-buffer.html
- MySQL 8.0 Reference Manual --- Doublewrite Buffer: https://dev.mysql.com/doc/refman/8.0/en/innodb-doublewrite-buffer.html
- MySQL 8.0 Reference Manual --- Adaptive Hash Index: https://dev.mysql.com/doc/refman/8.0/en/innodb-adaptive-hash.html
- MySQL 8.0 Reference Manual --- Buffer Pool Configuration: https://dev.mysql.com/doc/refman/8.0/en/innodb-buffer-pool-resizing.html
下一篇《MySQL 架构与高可用入门》跳出单机,讲数据库怎么"成对活着":主从复制三个线程各干什么活、异步复制和半同步复制差在哪、为什么主库写得好好的从库却迟迟追不上(主从延迟)、读写分离怎么落地,以及 MGR、分库分表这些高可用与扩展方案的入门全景。所有主从状态都用 docker-compose 真实起一主一从跑出来。