说一说Mysql缓存池BufferPool基本原理

前言:

     最近面试了一些个5年8年的程序员,普遍发现了一个问题,底层的基本原理知道的少之又少,只停留在CRUD。其实还是很建议每一位同学关注一些底层的实现原理,当然不是要自己再造轮子,而是学习别人想法的同时当你了解他的底层的实现原理,在编写代码中才能把你的代码写的更优更健壮。

    废话到此结束   开始正题~~~~


一、为什么要有缓存池

  我们在使用sql查询的时候,都应该会发现一个问题,第一次对于数据查询的时候(数据量w级别无索引),查询速度可能在秒级别,而再进行第二次第三次查询的时候就快了很多,可能都知道有缓存的那么一个概念,但具体怎么实现的,是值得我们深究的一个问题。

mysql InnoDB的存储引擎是基于磁盘的,并且是按照页的方式管理。

在数据库系统中,CPU的处理速度与磁盘是千倍的差距,为了最大可能的优化之间的差距,于是有了缓存池的概念。

简单来说,就是分配了一块【内存空间】,通过内存的速度来优化磁盘速度较慢的瓶颈。

二、缓存池的基本原理

【读操作】

 当一个读请求进来,读取页操作的时候,首先把磁盘读到的页存放在缓存池中,下一次读取相同的页时,首先判断该页是不是在缓存池中,缓存命中,则直接读取该页,否则读取磁盘上的页,并把该页放到缓存池中。

(看到这,可能会有同学举手问,这不是和redis+mysql实现缓存读取一个原理吗?没错,你可以这么理解,可以把这里的redis当作mysql的缓存池)

【写操作】

 对于数据库页的修改操作,首先会修改缓存池中的页,然后再以一定频率刷新到磁盘,所以真正落地到磁盘并不是实时的,而是通过checkpoint的机制刷新回磁盘。

(看到这,又有同学举手问,这不是缓存读写一致性的原理吗?没错,你又可以这么理解)

无论是读操作还是写操作,都是对缓存池进行操作,而不是直接对磁盘进行操作。

三、缓存池结构

Buffer Pool是一片连续的内存空间,InnoDB存储引擎是通过页的方式对这块内存进行管理的。

其中数据页和索引页会用掉大多的内存。

那么问题来了,innodb是如何管理缓存池的这么多页呢?

为了更好的管理这些缓存的页,innodb为每一个缓存页都创建了一些标识信息:

  • 表空间编号(space id)
  • 页号(page numeber)
  • 页在buffer pool的地址
  • 一些锁信息以及LSN信息日志序列号

【课外小知识,科普一下什么是LSN】

就是日志序号,全称 (LSN:Log sequence number) 标识特定日志文件记录在日志文件中的位置。

用来维护数据库一致性和完整性。除其他作用外,LSN 还对于分区数据库环境中的落实和回滚操作、崩溃和前滚恢复以及数据库操作同步起非常重要的作用。

日志文件中 LSN 的增长率与数据库活动直接相关联。也就是说,随着事务发生并且条目被写入日志文件,LSN 会不断增大。数据库中的活动越多,LSN 增长得越快。】

每个缓存页对应的控制信息占用的内存空间大小是相同的,我们把每个页对应的控制信息占用的一块内存称为【控制块】

控制块】和缓存页是一一对应的,它们都被存放到buffer pool中,其中控制块被放到buffer pool的前边,缓存页被存放到buffer pool的后面。大概就是这样

缓存池参数设置,my.ini配置文件中可查看默认给定的大小值

  • innodb_buffer_pool_size:缓存池的大小最多应设置为物理内存的 80%

  • innodb_buffer_pool_instances:设置有多少个缓存池,通常建议把缓存池个数设置为 CPU 的个数,多个缓存池可以减少数据库内部的资源竞争,增加数据库并发访问的能力

  • innodb_old_blocks_pct:老生代占整个 LRU 的链长比例,默认是 3:7

  • innodb_old_blocks_time:老生代停留时间窗口,单位是毫秒,默认是 1000,即同时满足“被访问”与“在老生代停留时间超过 1 秒”两个条件,才会被插入到新生代头部

四、缓存池管理

一共有三种数据结构,Free链表、LRU链表、Flush链表

1、Free链表

当启动mysql的时候,需要对Buffer Pool进行初始化,也就是分配内存空间,把它划分成若干对控制块和缓存页,但是此时并没有真正的磁盘页被缓存到Buffer Pool中,之后随着程序的运行,有数据了之后才会不断有磁盘上的页被缓存到Buffer Pool中。

在使用过程中,为了记录哪些缓存页是有用的,(因为随着数据被读写操作之后,会有一些脏页产生,下面再说)会把所有空闲的页包装成一个节点组成一个链表,这种链接就成为空闲链表(Free链表)。因为刚刚完成初始化的Buffer Pool中所有缓存页都是空闲的,所以一开始所有的缓存页都会被加到Free链表中。

为了便于管理这些Free链表,为这些链表定义了一些【控制信息】,里面包含链表的头节点、尾节点地址,以及当前链表中节点的数量信息。

另外会在每个Free链表的节点中都记录每个【缓存页控制块】的地址,也就是缓存页所在的内存空间地址,而每个缓存页控制块都记录着对应的缓存页地址,所以相当于每个Free链表都对应一个空闲的缓存页。

我从网上偷了张图,人家画的真好看

2、Lru链表

Lru链表用来管理已经读取的页,当数据库刚启动时,Lru链表是空的,此时页也都放在Free链表中,当需要读取数据时,会从Free链表中申请一个页,把从磁盘中读取的数据放到这个申请的页中,这个页的集合叫做Lru链表。

3、Flush链表

Flush链表用来管理被修改的页,Buffer Pool中被修改的页也被称之为【脏页】,脏页既存在Lru链表中,也存在于Flush链表中,Flush链表中存的是一个指向Lru链表中具体数据的指针。

因此只有Lru链表中的页第一次被修改时,对应的指针才会存到Flush中,若之后再修改这个页,则是直接更新到Lru链表中对应的数据。

(这里可能不好明白,为什么只有第一次被修改,Flush才会存放对应的指针?就好比你有一个配置表,还有一个管理表,关联关系是id字段,id是唯一的,之后你随便改配置表中的name,通过id关联还是可以查的到的)

【读操作】

Buffer Pool一个最主要的功能就是【加速读】,加速读就是当访问一个数据页面的时候,如果这个数据页存在Buffer Pool中,就不需要再访问磁盘。当我们需要访问某个页中的数据时,就会把该页加载到Buffer Pool中。

那么问题又来了,如何快速查找在Buffer Pool中的页呢?

为了避免查询数据时扫描Lru,其实就是根据表空间号+页号来定位一个页,也就相当于表空间号+页号为一个key,缓存页就是对应的value,创建一个哈希表,在需要访问某个页时,先从哈希表中根据表空间号+页号看看有没有对应的缓存页。

如果有,直接使用该缓存页就好

否则,那就从Free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。每当需要从磁盘中加载一个页到Buffer Pool中,就从Free链表取一个空闲的缓存页,并且把该缓存页对应的控制块信息填上,然后从Free链表中把该缓存页节点移除,表示该缓存页已经被使用了,并且写入Lru链表中。

在初始化的时候,Buffer Pool中所有的页都是空闲页,需要读取数据时,就会从Free链表中申请页,但是物理内存不可能无线增大,Free链表会被用完的。

因此!!!需要考虑把已经缓存的页从Buffer Pool中删除一部分,缓存命中率越高,磁盘IO交互次数就越少,为了提高缓存命中率,InnoDB在传统的Lru算法基础上,解决了两个问题:1、预读失效 2、缓存池污染

【写操作】

Buffer Pool另一个主要功能就是【加速写】,(但是吧,Mysql并发写最多也就是1000/s,所以对于高并发或者大数据的场景都不会考虑使用mysql,扛不住QPS的重锤。)当需要修改一个页面的时候,先将这个页面在缓存池中进行修改,记下相关的重做日志。

被修改的页面真正刷新到磁盘,这个是后台刷新线程来做的,前面的页面更新是在缓存池中先进行的,那它就和磁盘上的页不一致了,这样的缓存页就叫【脏页】。

那么问题又来了,这些被修改的页什么时候以什么顺序刷新到磁盘呢?

最简单的做法就是每发生一次修改 就立即刷新到磁盘,但是磁盘IO交互就会导致性能的问题,所以并不是实时的刷新,在未来的某个时间点进行同步,由后台刷新线程依次刷新到磁盘。

其实简单理解,就是在内存空间中数据到一定程度后,开始启动依次有序追加到磁盘。减少一次次的寻址操作。

但是如果不立即同步到磁盘的话,那之后再同步的时候如何判断Buffer Pool中哪些页是脏页,哪些页从没有被修改过呢?

InnoDB并没有一次性把所有的缓存页都同步到磁盘上,InnoDB创建一个存储脏页的链表,凡是在Lru链表中被修改过的页都需要加入到这个链表中,因为这个链表的页每次都要被刷新到磁盘上的,所有这个链表也叫Flush链表,链表的构造和Free链表一致。

这里的脏页修改指的是此页被加载进Buffer Pool后第一次被修改,只有第一次修改时才需要加入到Flush链表中,对于已经存在Flush链表中的页,如果这个页再次被修改就不会再放到Flush链表中。

需要注意,脏页数据实际还在Lru链表中,而Flush链表中的脏页记录只是通过指针指向Lru链表中的脏页。并且在Flush链表中的脏页是根据oldest_lsn(这个值表示这个页第一次被更改时的 lsn 号,对应值 oldest_modification,每个页头部记录)进行排序刷新到磁盘的,值越小表示要最先被刷新,避免数据不一致。


猜你喜欢

转载自blog.csdn.net/Lee_SmallNorth/article/details/115393112