前言
本篇文章主要讲解了 MySQL Buffer Pool,来看看在高并发下 MySQL Buffer Pool 到底是如何工作的。
什么是 Buffer Pool?
学过计算机组成原理的同学们都知道,我们想要取出硬盘(固态硬盘 / 机械硬盘)中的数据是消耗非常大的,如果是想取出内存(运行内存)中的数据消耗,相比较于硬盘那就小得多了。
在 MySQL 的 InnoDB 存储引擎中,所有数据都存储在磁盘上,但其管理的最小逻辑单位是 数据页(Data Page) ,每一页内部包含了多行数据记录。为了减少昂贵的磁盘 I/O 操作,InnoDB 引入了核心的内存组件------缓冲池(Buffer Pool)。
当客户端发起读请求时,InnoDB 会首先在 Buffer Pool 中查找所需的数据页。如果数据页已存在于缓冲池中(即缓存命中 ),则会直接从内存中读取并返回,速度极快。如果不存在(即缓存未命中),InnoDB 则会先从磁盘将该数据页加载到 Buffer Pool 中,然后再从内存中读取返回。
Free 链表
我们知道,Buffer Pool 就像一个为了加速数据读取而设置的"内存旅馆"。当 MySQL 刚启动时,这个"旅馆"里所有的"房间"(缓冲页)都是空的。那么,如何快速找到一间空房并登记入住呢?
这就是 Free 链表 的工作了。你可以把它想象成旅馆前台的一个**"空房列表"**。这个列表是一个双向链表,串联起了所有可用的空房间。
当需要为新的数据页(新客人)分配空间时,InnoDB 不需要逐个房间去检查,而是直接查看"空房列表"的第一个记录,立即就能找到一个空房间。一旦这个房间被分配,它就会从"空房列表"(Free 链表)中移除。
流程如下:
- 从 Free 链表的头部获取一个节点,该节点指向一个空闲的缓冲页。
- 将数据加载到该缓冲页中,并更新其元数据信息。
- 最后,将该节点从 Free 链表 中移除,并将其加入到 LRU 链表中,以便进行后续的访问和淘汰管理。
LRU 链表
LRU (Least Recently Used) 是一种经典的缓存淘汰算法,其核心思想是"末位淘汰"。您可以将 LRU 链表想象成一个活跃数据排行榜。
当一个数据页被访问时,它会被提升到排行榜的头部 (Head) ,代表它是最近被使用的"热点数据"。而长期没有被访问的数据页,则会逐渐沉降到链表的尾部 (Tail)。当 Buffer Pool 空间不足,需要释放空间时,就会优先从链表尾部淘汰那些最久未被使用的"冷数据"。
InnoDB 在此基础上做了优化 :新加载的数据页并不会 立即被放到链表的最头部,而是放在一个中间位置。只有当这个页在"年轻"的状态下被再次访问时,才会被真正移动到最头部,成为名副其实的热点数据。这个小小的改动,极大地提高了 Buffer Pool 在应对全表扫描等操作时的稳定性。
Flush 链表
Flush 链表与 Free 链表结构类似,也是一个双向链表。但其核心作用并非删除脏页 ,而是追踪并管理所有在内存中被修改过、但尚未同步到磁盘的缓冲页(即"脏页")。
脏页的产生源于写操作(INSERT
, UPDATE
, DELETE
)。为了提升性能,InnoDB 会先更新内存中的缓冲页,并立即将变更写入 redo log 。此时,内存页的数据就领先于磁盘页,该缓冲页的控制块便会被加入到 Flush 链表中,标记其为"脏页"。
InnoDB 的后台线程(如 Page Cleaner Threads)会定期遍历 Flush 链表,将这些脏页异步地、分批地写回磁盘。这个过程称为**"刷脏" (Flushing)**。一旦刷脏完成,该页就变为"干净页",并会从 Flush 链表中移除(但它依然存在于LRU链表中)。
这种机制实现了写操作的延迟和异步化,用户请求无需等待缓慢的磁盘写入即可返回,极大地提高了数据库的写入性能和吞吐量。