☰
MySQL 的脏页什么时候写回:Buffer Pool、redo log 与 4 个刷脏时机
2026/10/11 7:57:05 网站建设 项目流程

我不会起名字322· 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • MySQL 的脏页什么时候写回:Buffer Pool、redo log 与 4 个刷脏时机
    • 一、先建立三层结构的地图
    • 二、Buffer Pool 的冷热分离:不是普通 LRU
    • 三、redo log 的环形结构与 LSN
    • 四、4 个刷脏时机
    • 五、控制刷脏速度的 3 个参数
      • 5.1 `innodb_io_capacity`:告诉 InnoDB 磁盘有多快
      • 5.2 `innodb_max_dirty_pages_pct`:脏页比例上限
      • 5.3 `innodb_flush_neighbors`:机械盘与 SSD 的分水岭
    • 六、一份可以直接抄的配置清单
    • 七、结论分三档写
    • 八、小结

MySQL 的脏页什么时候写回:Buffer Pool、redo log 与 4 个刷脏时机

一条UPDATE明明只改一行、走的是主键索引,为什么监控上会突然出现几百毫秒的尖刺?更奇怪的是,尖刺过后业务完全恢复正常,数据也没丢。

答案通常在脏页写回上。InnoDB 的设计是"先在内存改,再找机会刷盘",这个"找机会"的时机选择,直接决定了你的写入延迟是平滑的还是有毛刺的。把 Buffer Pool、redo log、checkpoint 三者串成一条链,这几个抖动就都能解释了。

一、先建立三层结构的地图

一次更新,数据库里的数据要经过三层:

层位置单位特点
Buffer Pool内存页,默认16KB读写都在这里发生,默认大小128MB
redo log磁盘,顺序写字节流 + LSN固定大小,循环写,用完会覆盖
表空间磁盘,随机写页真正的数据落点,由刷脏推进

更新的实际顺序是:

UPDATE 到达 → 数据页不在 Buffer Pool 就从磁盘读进来 → 在内存里直接改这一页(此时内存与磁盘不一致 = 脏页) → 写 redo log(顺序 IO,很快) → 返回成功 ← 到这里客户端就已经拿到响应了 ↑ 以上全部在内存 + 顺序写里完成,所以 UPDATE 很快

注意最后一步:响应返回时,数据页根本还没落盘。真正把脏页写到表空间是后面异步完成的。所以"UPDATE 快"和"刷脏慢"是两件互不干扰的事——干扰只发生在某个时间点、刷脏来不及的时候。

二、Buffer Pool 的冷热分离:不是普通 LRU

Buffer Pool 用 LRU 管理,但不是教科书里的那个 LRU。朴素 LRU 有两个致命问题:

  • 预读失效:InnoDB 会线性预读相邻的页(innodb_read_ahead_threshold,默认56:一个 extent 里顺序读满 56 页就预读下一个 extent)。预读进来但没被访问的页,会白白挤掉尾部的热数据。
  • 缓冲池污染:一条select * from user where name like '%John%'全表扫描,会把大量冷页灌进池子,把真正的热数据全换出去。

InnoDB 的解法是把 LRU 链表切成两段:

参数默认值作用
innodb_old_blocks_pct37老生代(冷区)占整条链的比例,即新生代:老生代 ≈ 63:37
innodb_old_blocks_time1000(毫秒)页进入冷区后,必须"停留超过 1 秒且被访问"才晋升到热区头部

这两条规则一起把预读失效和缓冲池污染都压住了:

  • 新读入的页只放到冷区头部,不会被立刻当成热数据;
  • 全表扫描扫到的页,1 秒内不会被晋升,扫描结束就被淘汰——这正是全表扫描不再冲垮缓冲池的原因;
  • 热区的页被访问也不一定移动,只有热区后 3/4 被访问才挪到链表头,避免链表频繁调整带来的额外开销。

额外多实例的设置:Buffer Pool 默认只有 1 个实例(innodb_buffer_pool_instances = 1),大内存机器上建议按"每 GB 一个实例"切分,减少内部资源竞争。

-- 看清楚当前配置SHOWVARIABLESLIKE'innodb_buffer_pool_size';SHOWVARIABLESLIKE'innodb_buffer_pool_instances';SHOWVARIABLESLIKE'innodb_old_blocks_pct';SHOWVARIABLESLIKE'innodb_old_blocks_time';

三、redo log 的环形结构与 LSN

redo log 是固定大小的,写满就绕回头覆盖最旧的部分。判断"哪些日志已经被安全落盘"的标准,是 LSN:

指标含义
write pos当前写到哪里
checkpoint哪些日志对应的数据页已经刷盘、可以覆盖了
write pos - checkpoint还空着多少位置,剩得越少写得越急

关系很直白:checkpoint 推进得越慢(脏页刷得越慢),可复用的 redo 空间就越少。当两者追平时,InnoDB 就没有新空间写日志了——这时只能停下所有更新,集中刷脏、推进 checkpoint。这就是"redo log 满了"的后果。

这也是为什么刷脏速度不是由一个固定参数决定的,而是被redo的剩余空间动态驱动的:剩余越少,刷得越猛。

四、4 个刷脏时机

时机触发条件后果
redo log 满write pos追上checkpoint全场停更:所有更新堵住,等刷脏推进 checkpoint
Buffer Pool 满需要新页但池子没空位淘汰尾部页:干净页直接复用;脏页必须先刷盘才能复用
系统空闲没有请求后台安静刷脏,无感
正常关闭mysqladmin shutdown全部脏页落盘,必然变慢

先记住两者的区别:第一种最致命,第二种最常见。

第一种(redo log 满)的表现是"连接都正常、SHOW PROCESSLIST里全是updating、但一个都不返回",同时Innodb_buffer_pool_pages_dirty会飙高。如果你的实例内存小、写入量大,每个小时都可能撞一次。

第二种是最常见的毛刺来源。一次查询需要读入若干页,必须淘汰尾部页;淘汰到干净页只要释放复用,淘汰到脏页就得先写盘。如果一次要淘汰的脏页太多,这次查询的响应时间就会明显变长——这就是文章开头那个几百毫秒尖刺的成因。

五、控制刷脏速度的 3 个参数

5.1innodb_io_capacity:告诉 InnoDB 磁盘有多快

它表示"每秒能刷多少个页(IOPS)",默认值偏低,SSD 上必须调:

SHOWVARIABLESLIKE'innodb_io_capacity';-- 默认 200SETGLOBALinnodb_io_capacity=2000;-- SSD 上常见的取值

参考:机械盘 IOPS 只有几百,SSD 上千很轻松。在 SSD 上留默认的 200,等于让 InnoDB 以为自己只有机械盘的能力,刷脏会不必要地慢,redo 空间更容易被吃满。

5.2innodb_max_dirty_pages_pct:脏页比例上限

默认75%。这个值的含义是"脏页占 Buffer Pool 的比例接近它时,InnoDB 会全力刷脏"。说明书上的一句话要记住:不要让实际脏页比例接近 75%——一旦接近,说明刷脏已经跟不上写入,随时可能触发全场停顿。

实时监控脏页比例(这段 SQL 值得存下来):

SELECT(SELECTVARIABLE_VALUEFROMperformance_schema.global_statusWHEREVARIABLE_NAME='Innodb_buffer_pool_pages_dirty')ASdirty_pages,(SELECTVARIABLE_VALUEFROMperformance_schema.global_statusWHEREVARIABLE_NAME='Innodb_buffer_pool_pages_total')AStotal_pages,ROUND((SELECTVARIABLE_VALUEFROMperformance_schema.global_statusWHEREVARIABLE_NAME='Innodb_buffer_pool_pages_dirty')/(SELECTVARIABLE_VALUEFROMperformance_schema.global_statusWHEREVARIABLE_NAME='Innodb_buffer_pool_pages_total')*100,2)ASdirty_pct;

dirty_pct长期在 30% 以内是健康的;持续高于 60% 就该扩内存或者调innodb_io_capacity了。

5.3innodb_flush_neighbors:机械盘与 SSD 的分水岭

这个参数控制"刷一个脏页时,要不要顺便把相邻的脏页也一起刷"。相邻刷对机械盘是净收益(顺序 IO 比随机 IO 快得多),但对 SSD 是纯负担——SSD 的随机 IOPS 本来就上千,多刷相邻页只是白白增加写放大。

MySQL 8.0 里它的默认值已经是0(不刷相邻),如果你是从 5.7 升级过来的老实例,这一项一定要检查。

六、一份可以直接抄的配置清单

参数机械盘SSD说明
innodb_io_capacity2002000告诉 InnoDB 真实 IOPS
innodb_max_dirty_pages_pct7560留出更多余量
innodb_flush_neighbors10SSD 上必须关
innodb_buffer_pool_size内存的 50%~70%同左池子越大,脏页尖刺越少
innodb_buffer_pool_instances1内存 GB 数减少内部竞争

改完之后的验证方式很直接:盯dirty_pct的曲线。改之前它在白天长期在 60%~70% 之间游走,改之后应该稳定压到 30% 以下,同时应用侧的写入 P99 毛刺消失。

七、结论分三档写

  • 现象(可复现的数字):写入 P99 从 8ms 突增到 420ms,dirty_pct同期从 24% 升到 68%,innodb_io_capacity为默认值 200,SHOW ENGINE INNODB STATUS的LOG段显示 checkpoint 与 write pos 距离在收窄。
  • 已排除的解释:已排除锁竞争——同期Innodb_row_lock_time_avg无变化;已排除慢 SQL——慢日志在该时段只有 3 条,均为 1.2s 以外的其他业务;已排除磁盘故障——iostat的%util峰值 41%,未打满。
  • 尚未证实的猜测:把innodb_io_capacity从 200 提到 2000 后尖刺消失,但还需要在写入量翻倍的压测下复现一遍,才能确认是"IO 能力设置偏低"而不是"内存不够";另外同机另一个实例的刷脏是否互相抢 IO,暂未隔离验证。

八、小结

脏页写回这件事,记住三句话就够用:

  1. UPDATE 快是因为它只写内存和 redo,落盘是后面异步做的;
  2. 毛刺来自"淘汰脏页要先刷盘",而全场停顿来自"redo 满了只能等 checkpoint";
  3. 刷脏速度不是靠一个参数定死的,它由 redo 剩余空间动态驱动,你能调的是innodb_io_capacity(告诉它磁盘多快)和innodb_max_dirty_pages_pct(留多少余量)。

反过来,如果尖刺不是来自刷脏,而更可能是锁在等,那就是另一条排查链路了——那部分在《一条 UPDATE 等了 30 秒:InnoDB 锁等待的完整排查链路》和《MVCC 快照读为什么读不到刚提交的数据》里分别讲过。先分清是"写不进去"还是"等不到",再决定看 Buffer Pool 还是看锁。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询