mysql 应该是我们大部分同学所接触到的第一款数据库了(我是sqlserver哈哈哈)。我们今天来看一下mysql中的Buffer Pool,它位于存储引擎层和物理层之间,作用是利用缓存技术在一定程度上提高mysql的效率。
提到缓存很多同学就会说到:哎,不就是缓存嘛,这个我知道呀,在mysql8.0版本不是被删掉了嘛。哦~不是呦,此缓存非彼缓存。被删掉的那个和本文所提到的可不是同一个东西。
首先作用不一样
- 查询缓存(即被删掉的这个缓存)所缓存的数据是将上次通过sql语句查询所返回的内容缓存,节省的操作是:服务层->存储引擎层->物理层 的操作。
- 而Buffer Pool是作用于存储引擎层->物理层的这部分IO操作。
其次缓存的内容不一样
- 查询缓存缓存的是将上次sql所作用的行列数据缓存,以key-value形式存储,key是查询语句,value是查询内容。如果sql语句满足条件,那么直接从value里取值。
- bufferpool则会把数据氛围若干个页,每个缓存页默认大小为16kb。除了缓存「索引页」和「数据页」,还包括了 undo 页,插入缓存、自适应哈希索引、锁信息等等。
虽然都是为了提高mysql工作效率的,但是查询缓存的作用却十分的鸡肋。因为对于更新比较频繁的表,查询缓存的命中率很低的,因为只要一个表有更新操作,那么这个表的查询缓存就会被清空。如果刚缓存了一个查询结果很大的数据,还没被使用的时候,刚好这个表有更新操作,查询缓冲就被清空了,相当于缓存了个寂寞。所以,MySQL 8.0 版本直接将查询缓存删掉了。
好了,言归正传,我们开始研究Buffer Pool是什么。我们都知道mysql的存储是基于硬盘的,而对于硬盘的操作无疑是非常低效的,但是在安全性上来考虑,落盘是最安全的,于是出现了许多基于缓存的数据库来弥补mysql在这些方面的不足,如redis、Memcached等。mysql一想,什么玩意,简直孤陋寡闻,我也内含缓存技术的好吧。Buffer Pool就是其中的优秀产物。
上面我们提到了,Buffer Pool内缓存索引页,数据页,undo页,插入缓存、自适应哈希索引、锁信息等等。在其内部,对于每一个页都创建了一个控制块,从而更方便的来管理这些页。他们是一一对应的。控制块中保存了对应页的表空间,页号,缓存地址,链表节点。
对于Buffer Pool的学习我整体归纳为:三种链表和三种页。
记得有次有位同学给我装逼,说他有次被问到Buffer Pool的数据结构,“简直离谱,我都没想到能问这么深。”然后我问他是啥,他说是链表。我觉得他的答案是不严谨的,Buffer Pool内部其实是有多种数据结构的。就比如页表就是哈希表加双向链表构成的。
1.Free链表:管理空闲页
Buffer Pool是一片连续的内存空间,但是当mysql运行一段时间后,它内部的空闲空间不一定是连续的了。但是当我们需要从磁盘中加载一个页到Buffer Pool中时,我们就需要快速的知道哪里有空间。可是空闲空间不连续,如果一个一个去遍历那效率就太差了。所以Buffer Pool专门为此维持了一个链表,用来记录Buffer Pool内部的空闲页。
Free链表的节点是一个个控制块,每个控制块对应着该控制块的缓存页。当从磁盘加载的时候,会从Free链表中取一个缓存块,吧对应的控制块信息填上,然后从Free链表中删除
2.Frush链表:管理脏页
设计 Buffer Pool 除了能提高读性能,还能提高写性能,也就是更新数据的时候,不需要每次都要写入磁盘,而是将 Buffer Pool 对应的缓存页标记为脏页,然后再由后台线程将脏页写入到磁盘。
那为了能快速知道哪些缓存页是脏的,于是就设计出 Flush 链表,Frush链表中存储的也是控制块,只不过对应的缓存页不是空闲页了,而是脏页。
3.LRU链表:提高缓存命中率
对于Buffer Pool而言,它为mysql的提供了缓存功能。有了缓存,自然就会出现热点数据选取,缓存命中率等问题。一起来看看BufferPool是怎样解决这些问题的。
Buffer Pool 的大小是有限的,对于一些频繁访问的数据我们希望可以一直留在 Buffer Pool 中,而一些很少访问的数据希望可以在某些时机可以淘汰掉,从而保证 Buffer Pool 不会因为满了而导致无法再缓存新的数据,同时还能保证常用数据留在 Buffer Pool 中。
要实现这个,最容易想到的就是LRU算法,该算法的思路是,链表头部的节点是最近使用的,而链表末尾的节点是最久没被使用的。那么,当空间不够了,就淘汰最久没被使用的节点,从而腾出空间。
- 在Buffer Pool中则是将最新访问的数据加到链表头部
- 当访问的页不在了,不仅要把页加入到链表头部,还要淘汰链表末尾的节点
但是简单的LRU算法并没有被采用,因为它并不适用于mysql。mysql中有以下场景不适合用该算法:
- 预读失效
- Buffer Pool污染
什么是预读失效?
对于这个问题,我们先了解下mysql的预读机制。由于数据的局部性,mysql在读数据的时候一般会把其相邻的数据页一并加载进来,目的是为了减少磁盘IO。
但是也有可能这些被加载进来的相邻页并没有被访问到,这就是预读失败。这就有可能导致LRU链表前面的位置是一些不怎么被访问的数据,而被淘汰的那些却有可能是热点数据。
mysql是怎么处理这个问题的呢,它将LRU链表分为了young和old两段,两个区域的比例可调,默认是7:3。如果数据被预读了,那么只会将其添加到old区域,当它第一次被访问的时候,才会被提前到young区域,而young区域不会淘汰数据,该区域末尾的数据会在经历一次头插后进入old区域,而此时old区域的最后一个数据则会被淘汰掉。
虽然划分young和old两个区域可以有效避免预读失效。但是却无法避免Buffer Pool污染。那怎么才能避免Buffer Pool污染呢
什么是Buffer Pool污染?
比如我现在用范围查询,查了大量数据,那由于Buffer Pool的大小限制,这些数据则会存入当缓存池中,挤占原本的热点数据。假设现在Buffer Pool的长度是10,存着1-10的页,young区域的长度是7,存1-7,剩下的存在old区域。此时有个范围查询,查询了17-21页的数据,这五个页的数据会进入young区域,那么现在young区域的数据则为17.18.19.20.21.1.2,old区域的数据为3.4.5,可以看到,原本存在于young区域的数据被淘汰了,这就是Buffer Pool污染。
那么要怎么解决这个问题呢。其实也很简单,提高页进入young区域的门槛就行了,比如设定一个时间戳。只有当数据被查询后,在这个时间戳内再次被查询到后,才会将数据存入young区域,这样的机制也符合热点数据的性质。
在Buffer Pool中的数据是以页码的形式存储的,这些页又分为三大类
- 空闲页
- 干净页
- 脏页
空闲页即Free链表维持的页码,他们没有被使用。干净页即正在被使用,且与硬盘保持一致的页。脏页即投入使用,但是已经被更改,但是却又没有被刷到硬盘当中。
这个时候就会出现两个问题,第一是老生常谈的,如果脏页还未刷到硬盘,MYSQL挂掉怎么办,第二是什么时候数据会被刷到脏页当中呢?
InnoDB 的更新操作采用的是 Write Ahead Log 策略,即先写日志,再写入磁盘,通过 redo log 日志让 MySQL 拥有了崩溃恢复能力。
下面几种情况会触发脏页的刷新:
- 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
- Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
- MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
- MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;