MySQL 缓存机制深度解析

一、Buffer Pool(缓冲池)

1.1 ⭐Buffer Pool 缓存的东西

基本概念

我们都知道 InnoDB 存储引擎会把存储的数据划分为多个页,并且是以页作为磁盘与内存交互的基本单位,一个页的大小是 16KB。

由于 Buffer Pool 是 InnoDB 存储引擎内部的结构,所以它们都是用页来划分空间的。

初始化过程

在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照 16KB 划分出一个一个的页,Buffer Pool 中的页被称为缓存页。此时这些缓存页都是空闲的,还没有写入磁盘中的页,之后随着程序的运行才会进行写入操作。

Lazy 初始化

所以,在 MySQL 刚刚启动的时候,使用的虚拟内存很大,但是实际使用的物理内存其实很小,因为只有当这些虚拟内存被访问之后,操作系统才会触发缺页中断,申请物理内存,将虚拟地址和物理地址建立映射关系,这还有一个说法叫 lazy 初始化。

缓存内容

当然,Buffer Pool 除了缓存索引页和数据页,还会缓存一些其他的页数据。

1.2 ⭐预读失效

问题描述

根据局部性原理,MySQL 在加载数据页的时候,会把它相邻的数据页一同加载进来,目的就是为了减少磁盘 IO,但是可能这些被提前加载进来的数据页没有被访问,相当于预读失效了。

如果使用简单的 LRU 算法,就会把预读页放到 LRU 链表头部,而当 Buffer Pool 空间不够的时候,还要把尾部的页淘汰掉。

除此之外,如果这些预读页一直都没有访问到,却又占据了 LRU 链表前面的位置,可能会导致末尾的热点数据被淘汰,从而降低了缓存的命中率。

解决方案

怎么解决预读失效而导致缓存命中率降低的问题?

要避免预读失效带来的影响,我们可以将 LRU 链表划分为两个区域:young 区和 old 区。

区域划分

当然,old 区占整个 LRU 链表长度的比例可以通过 innodb_old_blocks_pct 参数来设置,默认的值是 37,表示整个 LRU 链表中 young 区和 old 区长度的比例是 63:37。

工作机制

划分完这两个区域之后,预读的页就只用加到 old 区的头部,当页真正被访问到的时候再将页插入 young 区的头部。

当然,如果预读的页一直没有被访问,就会从 old 区中移除,这样就不会影响到 young 区中的热点数据。

注意事项

注意一点,young 区的数据会被挤到 old 区,而不是直接淘汰。

虽然通过划分区域解决了预读失效的问题,不过还是没有解决 Buffer Pool 污染的问题。

1.3 ⭐Buffer Pool 污染

问题描述

一下子读入了很多冷数据,导致 Buffer Pool 中的页都被替换出去,大量的热点数据被淘汰了,而由于热点数据会被频繁访问到,这时会产生大量缓存未命中的情况,造成大量的磁盘 IO。

这时会导致 MySQL 的性能急剧下降,这个过程就是 Buffer Pool 污染(污染也就是中毒,就是由于数据读取异常出现性能急剧下降的情况)。

解决方案

怎么解决出现 Buffer Pool 污染而导致缓存命中率下降的问题?

其实解决方法很简单,就是提高进入 young 区的门槛就行了,具体就是要进入 young 区还要判断停留在 old 区的时间。

时间间隔控制

当对处于 old 区的缓存页进行第一次访问的时候,它的控制块会记录下来这个访问的时间。

  • 如果后续的访问时间与第一次访问时间的间隔太短,就不会移动到 young 区
  • 如果较长,就会移动到 young 区头部

这个时间的间隔由 innodb_old_blocks_time 参数控制,默认是 1000 ms。

young 区优化

另外,MySQL 对于 young 区也有一个优化,为了防止 young 区节点频繁地移动到头部,规定 young 区前 1/4 的节点被访问不会移动到链表头部,只有后面 3/4 才会。


二、Change Buffer(写缓冲)

2.1 ⭐是什么

历史演变

在 MySQL5.5 之前,叫插入缓冲(insert buffer),只针对 insert 做了优化;现在对 delete 和 update 也有效,叫做写缓冲(change buffer)。

核心机制

它是一种应用在非唯一普通索引页(non-unique secondary index page)不在缓冲池中,对页进行了写操作,并不会立刻将磁盘页加载到缓冲池,而仅仅记录缓冲变更(buffer changes),等未来数据被读取时,再将数据合并(merge)恢复到缓冲池中的技术。

设计目的

写缓冲的目的是降低写操作的磁盘 IO,提升数据库性能。

2.2 ⭐具体流程和使用场景

Merge 机制

因为 merge 的时候是真正进行数据更新的时刻,而 change buffer 的主要目的就是将记录的变更动作缓存下来,所以在一个数据页做 merge 之前,change buffer 记录的变更越多(也就是这个页面上要更新的次数越多),收益就越大。

合并到相应数据页的过程实际上是将变更应用到内存中的数据页,但在这个阶段,数据页本身并不会立即更新到磁盘,而是保持在内存中,处于脏页的状态,实际的磁盘更新会延迟进行。

适用场景

写多读少的业务

因此,对于写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。这种业务模型常见的就是账单类、日志类的系统。

不适用场景

反过来,假设一个业务的更新模式是写入之后马上会做查询,那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 merge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。所以,对于这种业务模式来说,change buffer 反而起到了副作用。

2.3 ⭐一些参数

innodb_change_buffer_max_size

change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size(配置写缓冲的大小,占整个缓冲池的比例,默认值是 25%,最大值是 50%。写多读少的业务,才需要调大这个值,读多写少的业务,25%其实也多了。) 来动态设置。

这个参数设置为 50 的时候,表示 change buffer 的大小最多只能占用 buffer pool 的 50%。

innodb_change_buffering

当然还有一个 innodb_change_buffering 参数,主要是配置哪些写操作启用写缓冲,可以设置成 all/none/inserts/deletes 等。

2.4 读取流程

(读取流程的详细内容)

2.5 更新流程

(更新流程的详细内容)