🧊 Buffer Pool
MySQL 的数据是存储在磁盘中的,为了提升读写性能,InnoDB 设计了一个缓存池 Buffer Pool。
本质上是 InnoDB 向操作系统申请的一段连续内存空间,可以通过 innodb_buffer_pool_size
来进行调整大小。
当读取数据时,如果数据已经在 Buffer Pool 中,会直接返回;如果不在,就会将数据完整的页信息全部加载到内存中,再返回给客户端。
当修改数据时,会修改 Buffer Pool 中的缓存页(-> 变成脏页),在合适的时机,由后台线程写入到磁盘。
🧩 结构
Buffer Pool 中的连续内存,会被分割成一个个的页,和 InnoDB 的页大小一样,都是 16KB
,称为缓存页。
每一个缓存页都会由一个控制块进行管理(位于 Buffer Pool 的最前面),每个缓存页与控制块一一对应。
🧩 内容
在 Buffer Pool 中,主要包括了六部分的内容:
- 数据页
- 索引页
- 插入缓冲
- undo 页
- 自适应哈希索引
- 锁信息
👷♂️ 管理 Buffer Pool 的链表
在 MySQL 服务运行过程中,连续的 BuuferPool 内存空间的页面有空闲的,也有非空闲的,为了能够快速的找到需要的页面,防止一个一个的遍历浪费时间, InnoDB 使用了许多🔗链表管理 Buffer Pool 的页(空闲页、干净页、脏页)。
⛓️ Free 链表 – 管理空闲页
💡 空闲页:Buffer Pool 内存中未被使用的页。
为了能快速找到空闲的缓存页,使用 Free 空闲链表管理空闲页。
Free 链表的节点是一个个空闲页所对应的控制块。
当需要从磁盘中加载一个页到 Buffer Pool 时,就从 Free 链表中找到一个空闲页,加载数据并更改控制块的信息,并从 Free 链表中移除。
⛓️ Flush 链表 – 管理脏页
💡 脏页(dirty page):修改了缓存页中的数据,与磁盘中的页数据不一致,称为脏页。
被修改过的缓存页的控制块,会被作为节点加入到 Flush 链表。
Flush 链表的结构和 Free 链表差不多。后台线程会在一定的时机,通过遍历 Flush 链表,将脏页写入磁盘(WAL)。
⛓️ LRU 链表 – 管理脏页&干净页
💡 干净页(clean page):加载了数据未作修改的缓存页。
💡 脏页(dirty page):加载了数据做了修改的缓存页。
Buffer Pool 的大小是有限的,不可能将所有的数据都放在内存中,innoDB 通过 LRU 算法(修改过的),将频繁访问的数据停留在 Buffer Pool 中。
简单的 LRU 算法在 MySQL 中可能会导致:
😵 预读失效
MySQL 会将被加载页的临近页一同加载到内存(为了减少磁盘 IO),如果这些页没有被访问,这个预读相当于浪费了。
(通过将 LRU 链表划分为 young 区和 old 区,加载的数据现放到 old 区域,被访问到之后,才被加入 young 区域)
🤢 Buffer Pool 污染
一个 SQL 扫描大量的数据(如索引失效时,进行全表扫描来判断条件),由于链表空间有限,可能会把内存中的所有页全部替换出去,导致热点数据全部失效,后续缓存全部没有命中,导致大量磁盘 IO。
(加大数据放入 young 区的条件,加载到 old 区域的数据,停留时间超过阈值时,才有资格放到 young 区域)
InnoDB 的 LRU 链表被分为两个区域:(1)热数据 young 区域;(2)冷数据 old 区域。
📃 预读的页只会加载到 old 区域的头部,当该页真正被访问的时候,才会被移动到 young 区的头部。
⏱️ 移动到 young 区域的条件除了被访问外,还需要判断停留在 old 区域的时间。(防止全表扫描时,被访问一次后就不在使用)。当访问的缓存页已经在 old 区域停留时间超过阈值
innodb_old_blocks_time=1000ms
时,才会被插入到 young 区头部。
💿 脏页刷盘的时机
当修改数据时,会记录对应的 redo log & bin log(WAL),修改的是 Buffer Pool 中的缓存页,然后将其设置为脏页并加入 Flush 链表,由后台线程刷新数据到磁盘。
以下情况会主动刷新数据到磁盘:
🎗️ 当 redo log 日志写满时
🎗️ 当 Buffer Pool 空间不足时,会淘汰一些缓存页,如果时脏页,会刷入磁盘
🎗️ 当 MySQL 认为空闲时,异步地将脏页刷新到磁盘
🎗️ 当 MySQL 主动正常关闭前