【计算基础|MySQL18】Buffer Pool 与内存管理

【计算基础|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:长期很低说明缓存紧张。

调优建议

  1. innodb_buffer_pool_size 设物理内存的 50%~70%。
  2. 内存很大(>10GB)时用 innodb_buffer_pool_instances 切成多个实例,每个实例独立 LRU、减少锁竞争,每个 instance 至少 1GB。
  3. 命中率低于 99% 要排查是不是 buffer pool 太小、或者有没有大查询污染。
  4. 脏页比例长期过高会导致刷盘跟不上、写入抖动。

总结:你真正需要记住的 7 件事

  1. Buffer Pool 是 InnoDB 的内存缓存,读写都先走它;生产建议设物理内存 50%~70%,单位是 16KB 的页。
  2. 内部三条链表分工:Free List(空闲页)、LRU List(缓存页按热度)、Flush List(脏页)。
  3. InnoDB 的 LRU 分年轻区(5/8)和老年区(3/8),新页中点插入,且 1 秒内二次访问才晋升年轻区------防全表扫描击穿热点缓存。
  4. 脏页在 redo 写满、Buffer Pool 不够、后台空闲、正常关闭时刷盘;双写缓冲防止"页写一半断电"。
  5. Change Buffer 只加速非唯一二级索引:页不在内存时先记录修改,等页被读入再 merge;唯一索引必须读页查重,缓冲不了。
  6. 预读可能带来缓存污染;AHI 自动给高频等值查询建哈希索引加速,高并发写时可能成瓶颈。
  7. 监控盯 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 架构与高可用入门》跳出单机,讲数据库怎么"成对活着":主从复制三个线程各干什么活、异步复制和半同步复制差在哪、为什么主库写得好好的从库却迟迟追不上(主从延迟)、读写分离怎么落地,以及 MGR、分库分表这些高可用与扩展方案的入门全景。所有主从状态都用 docker-compose 真实起一主一从跑出来。

相关推荐
杨云龙UP2 小时前
生产实战:Keepalived + Nginx + MySQL 双向复制高可用架构部署与原理详解
运维·数据库·mysql·nginx·架构·keepalived·高可用架构
为思念酝酿的痛2 小时前
MySQL的基本查询
数据库·mysql
花间相见2 小时前
【计算基础|Redis01】【上手】安装连接、配置与通用命令
数据库·后端·面试
geovindu2 小时前
Pivoting and Unpivoting Data in SQL using postgreSQL 18
大数据·数据库·数据仓库·sql·postgresql·数据库开发·数据库架构
我命由我123452 小时前
6-3.Python 异常处理 - 异常处理进阶(else 块与 finally 块、异常传递、自定义异常)
开发语言·前端·数据库·后端·python·学习·java-ee
深入云栈2 小时前
Netty 4.2.x 源码深度解析 (二十一):Pipeline 事件传播 —— Inbound 与 Outbound 的双向流转
java·后端·架构
卷无止境2 小时前
长篇写作 Skill 横向盘点:从单文件指令到多智能体流水线
后端·python
斯维赤3 小时前
微信步数的10点定时推送,看似简单,实则暗藏玄机
java·后端