内存

缓存池的作用

缓存池主要是为了加快数据的查询效率。如果每个查询语句都到磁盘中的数据库进行查询的话,那么进行io执行效率不高。所以在内存中开辟了一块空间,用来存储高频的查询语句,目的是为了提高查询效率。

  • 当执行一条查询语句时,执行器会先到缓存池中寻找,如果命中,就直接返回。如果未命中,就到磁盘里读取数据,在将这条数据加入到缓存池中。
  • 当执行一条更新语句时,会先进行查询定位要原始语句,流程同上,更新完会同时更新缓存池中的数据,同时将缓存池中的数据标记为脏页。等到合适的时候将内存刷盘,更新到磁盘中。

缓存池的容量

Buffer Pool 是在 MySQL 启动的时候,向操作系统申请的一片连续的内存空间,默认配置下 Buffer Pool 只有 128MB

可以通过调整 innodb_buffer_pool_size 参数来设置 Buffer Pool 的大小,一般建议设置成可用物理内存的 60%~80%

缓存池的存储结构

在Inno db中,数据的存储以页为单位。一个页中有保存了多条数据。根据缓存一致性原理,查询的时候会将其附近的语句一起加载。那么在缓存池中,也会同样以页为单位进行存储,一个页的大小默认为16kb。每个页中的结构有数据页,索引页,插入缓存页,undo log页,自适应哈希索引页,锁信息页。为了能够更好的管理页,每个页开头都有一个控制块进行管理,控制块中的信息包括缓存页的表空间、页号、缓存页地址、链表节点等。

缓存池的功能

管理空闲页

img

采用链表结构,将控制块作为链表节点。通过设置一个free链表来快速定位到空闲页的位置。减少了查询的数据量。

管理脏页

img

基本上相当于空闲页的结构。

提高缓存命中率

原始LRU结构

LRU数据结构

LRU主要是为了提高数据读取效率而设计的。他的主要功能是最近访问的信息会出现在链表的头部,所以他被叫做:last receive user(最近使用)

最朴素的LRU结构的特点就是,

  • 查询数据的时候,如果数据在LRU链表中,就将其提前到链表的头部。
  • 如果数据不在链表中,就在内存中将数据插入LRU链表的头部,同时将LRU的最后一个元素退出队列。

但是这种结构很少被使用,因为他会导致一些问题:

  • 预读失效
  • 缓存池污染

解决预读失效

预读指的是因为内存的空间局部性原理,当查询到一个数据的时候也会将其临近内存地址的数据一起添加。为了减少磁盘io。针对于程序的空间局部性原理,是大部分情况下成立的,但是在一部分情况下预读的页完全没有被访问到。但是他会存储在链表头部,挤占后面链表的空间。大大降低了缓存的命中率。

解决预读失效的方法是将其划分为新生代和老年代,预读的页加入old区域的头部,等到真正读取到的时候,才进入young区域的头部。这样既兼顾了缓存一致性的原理,有防止因为预读失效而让热点数据过期的风险。等到新数据插入的时候,优先淘汰old区域的节点。young区域的最后一个节点进入old区域。

解决缓存池污染

当一条sql语句查询出大量数据的时候,他们可能只被查询了一次,但是却一起被加载到缓存区中。把原有的高频缓存数据挤占出去。

mysql在进入到 young 区域条件增加了一个停留在 old 区域的时间判断。

具体是这样做的,在对某个处在 old 区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间:

  • 如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该缓存页就不会被从 old 区域移动到 young 区域的头部;
  • 如果后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该缓存页移动到 young 区域的头部;

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

也就说,只有同时满足「被访问」与「在 old 区域停留时间超过 1 秒」两个条件,才会被插入到 young 区域头部,这样就解决了 Buffer Pool 污染的问题 。

脏页刷入磁盘的时机

下面几种情况会触发脏页的刷新:

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
  • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
  • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;