1.前言

  其实早就想说说innodb的缓存技术了,但是一直感觉自己可能说不下来,因此这一节我就慢慢的说

2.缓存重要性

  innodb的缓存是为了弥补了cpu和磁盘之间执行速度的巨大鸿沟,应该cpu的执行速度比磁盘读写速度要远远高于,因此需要在cpu和磁盘之间弄一个缓存,计算机可以把磁盘的数据先加载到缓存中,然后再有cpu去缓存中去取然后执行,这样可以大大提高计算机的处理速度。对于mysql的innodb来说,innodb的buffer pool就是充当这个功能的。

3.Buffer pool是啥?

  为了缓存磁盘中的页,innodb设计者在Mysql服务器启动时就向操作系统申请了一片连续的内存,他们在这片内存起了一个名字叫Buffer pool(缓冲池)。

4.Buffer pool内部组成

  buffer pool对应的一片连续的内存被划分为若干个页面,页面大小与innodb表空间用的页面大小一致,默认是16KB,为了与磁盘中的页面区分开来,我们这里把这些Buffer Pool中的页面称为缓冲页,为了更好地管理Buffer Pool中的这些缓冲页,设计者为每一个缓冲页都创建了一些控制信息。这些控制信息包括该页所属的表空间编号、页号、缓冲页在Buffer Pool中的地址、链表节点信息等。

  每个缓冲页对应的控制信息占用的内存大小是相同的,我们把每个页对应的控制信息占用的一块内存一个控制块,控制块和缓冲页是一一对应的,它们都存放到buffer pool中,其中控制块存放到buffer pool的前面,缓冲页存放到Buffer pool的后面,所以整个buffer pool对应的内存空间看起来如下图:

  

Buffer MySQL Pool 流程图 mysql innodb buffer pool_控制块

  这里可以看到控制块和缓冲页之间有个碎片,那么碎片是什么?每一个控制块都对应一个缓冲页,那么在分配足够多的控制块和缓冲页后,剩余的那点儿空间可能不够一对控制块和缓冲页的大小,自然也就用不到了。这个用不到的内存空间就被称为碎片。在debug模式下,每个控制块大约占用缓冲页大小的5%(非debug模式下会更小一点),在mysql5.7.22版本的debug模式下,每个控制块占用的大小是808字节,而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小。也就是说innodb在为buffer pool向操作系统申请连续的内存空间时,这片连续的空间会比innodb_buffer_pool_size的值大5%左右。

5.Free链表管理

  当我们最初启动mysql服务器的时候,需要完成buffer pool的初始化过程。就是先向操作系统申请buffer pool的内存空间,然后把它划分成若干对控制块和缓冲页。但是此时并没有真实的磁盘页被缓存到buffer pool中(因为还没用到),之后随着程序运行,会不断地有磁盘上的页被缓存到buffer pool中。

  那么问题来了,从磁盘上读取一个页到buffer pool中时,该放到哪个缓冲页的位置呢?或者说怎么区分buffer pool中哪些缓冲页是空闲的,哪些已经被使用了呢?我们最好在某个地方记录buffer pool中哪些缓冲页是可用的。这个时候缓冲页对应的控制块就排上了大用场了--我们可以把所用空闲的缓冲页对应的控制块作为一个节点放到一个链表中,这个链表页可以称为free链表(或者说空闲链表)。刚刚完成初始化的buffer pool中,所有的缓冲页都是空闲的,所以每一个缓冲页对应的控制块都会加入到free链表中。假设该buffer pool中可容纳的缓冲页数量为n,那么增加了free链表的效果如下:

Buffer MySQL Pool 流程图 mysql innodb buffer pool_缓存_02

  从上面可以看出,为了管理好这个free链表,这里特意为这个链表定义了一个基节点,里面包含链表的头节点地址、尾节点地址,以及当前链表中节点的数量等信息。这里需要注意的是,链表的基节点占用的内存空间并不包括在为buffer pool申请的一大片连续内存空间之内。而是一块单独申请的内存空间。

  有了这个free链表之后事情就好办了,每当需要从磁盘中加载一个页到buffer pool中的时,就冲free链表中取一个空闲的缓冲页,并且把该缓冲页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把缓冲页对应的free链表节点(也就是对应的控制块从链表中移除,表示该缓冲页已经被使用了)。这里要清楚我们真正从链表中获取的是控制块,通过控制块可以访问到真正的页。同理,"遍历buffer pool中的缓冲页"的意思是"遍历buffer pool中各个缓冲页对应的缓冲块"。

  缓冲页的哈希处理

  当我们需要访问某个页中的数据时,就会把该页从磁盘加载到buffer中,如果该页已经在buffer pool的话,直接使用就可以了,那么问题也就来了,我们怎么知道该页在不在buffer pool中呢?难不成需要依次遍历buffer pool中的各个缓冲页么?一个buffer pool中的缓冲页这么多,都遍历岂不是要累死?

  再回头想想,我们其实是根据表空间号+页号来定位一个页的,也就相当于表空间+页号是一个key(键),缓冲页控制块就是对应的value(值)。怎么通过一个key来快速找到一个value呢?  这里用的是哈希表。。。

  所以我们可以用表空间号+页号作为可以,用缓冲页控制块的地址作为value来创建一个哈希表。在需要访问某个页的数据时,先从哈希表中根据表空间号+页号看看是否有对应的缓冲页。如果有,直接使用该缓冲页就好;如果有,就从free链表中选一个空闲的缓冲页,然后把磁盘中对应的页加载到该缓冲页的位置。

6.flush链表的管理

  如果我们修改了buffer pool中的某个缓冲页的数据,它就与磁盘上的页不一致了,这样的缓冲页页称为脏页。当然,我们可以每当修改完某个缓冲页时,就立即将其刷新到磁盘中对应的页上。但是频频繁的往磁盘中写数据会严重影响程序的性能,所以每次修改缓冲页后,我们并不着急立即把修改刷新到磁盘上,而是在未来的某个时间点进行刷新。

  但是,如果不立即将修改的数据刷新到磁盘,那之后再刷新的时候我们怎么知道buffer pool中哪些页是脏页,哪些是从来没有用的页呢?,所以这里不得不再创建一个存储脏页的链表,凡是被修改过的缓冲页对应的控制块都会作为一个节点加入到这个链表中。因为这个链表节点对应的缓冲页都是需要被刷新到磁盘上的。所以也称为flush链表。flush链表的构造与free链表差不多。假设buffer pool在某个时间点的脏页数量为n,那么对应的flush链表如下图:

  

Buffer MySQL Pool 流程图 mysql innodb buffer pool_控制块_03

 如果一个缓冲页是空闲的,那它肯定不可能是脏页,如果一个缓冲页是脏页,那它肯定就不是空闲的,也就是说,某个缓冲页对应的控制块不可能既是free链表的节点,也是flush链表的节点。

总结:这里的free链表可以简单地理解为在缓冲区中所欲空闲的页组成的链表,而flush链表可以简单地理解为所有被修改的页(脏页)所组成的链表。

7.LRU链表管理

  LRU链表其实对buffer pool缓冲区空间的一个管理方法,你可以想想看,如果一直加载数据页到缓冲区中,迟早有一天缓冲区会被加载满了的,这时肯定有一个机制用来释放那么没用的数据页的,该机制就是LRU算法(最近最少使用)

  我们可以把该算法看成一个链表,该链表分为两个部分,一部分是用来存储使用频率非常高的缓冲页,这一部分链表也被称为热数据,或者称为young区域,另一部分是用来存储使用频率不是很高的缓冲页,这一部分链表被称为冷数据,或者称为old区域。

  innodb设计通过某个比例将LRU链表分为两半,这里可以通过参数innodb_old_blocks_pct 看到旧的链表所占的比例情况,

  有了这两个区域,innodb设计者就可能针对buffer pool命中率的情况进行优化了。

  • 针对预读的页面可能不进行后续访问的优化。设计者规定,当磁盘上的某个页面在初次加载到buffer pool中某个缓冲页时,该缓冲页对应的控制块会放到old区域的头部。这样一来,预读到buffer pool却不进行后续的页面的访问就会被逐渐从old区域逐出,而不会影响young区域中使用比较频繁的缓冲区。
  • 针对全表扫描时,短时间内访问大量使用频率非常低的页面的优化。在进行全表扫描时,虽然首次加载到buffer pool中的页放到了old区域的头部,但是后续会被马上访问到,每次进行访问时又会把该页放到young区域的头部,这样仍然会把哪些使用频率比较高的页面给排挤下去。因此设计者认为在执行全表扫描的过程中,即使某个页面中有很多条记录,尽管每读取一条记录都算是访问一次页面,但是这个过程所花费的时间也是非常少的。所以我们只需要规定,在对某个处于old区域的缓冲页进行第一个访问时,就在它对应的控制块中记录下这个访问时间,如果后续的访问时间与第一次访问的时间再某个时间间隔内,那么该页面就不会从old区移动到young区域的头部,否则将它移动到young区域的头部,这个间隔时间是由系统变量innodb_old_bolcks_time控制的
root@localhost 11:52:  [(none)]> show variables like '%innodb_old_blocks_time%';
+------------------------+-------+
| Variable_name          | Value |
+------------------------+-------+
| innodb_old_blocks_time | 1000  |
+------------------------+-------+
这个参数默认值是1000,单位是ms,也就意味着对于从磁盘加载到LRU链表中old区域的某个页来说,如果第一次和最后一次访问该页面的时间间隔小于1ms,那么该页是不会加入到young区域的,很明显,在一次全表扫描的过程中,多次访问一个页面(也就是读取同一个页面的多条记录)的时间不会超过1s.