数据库缓存池|青训营笔记

157 阅读10分钟

这是我参加「第三节青训营~后端场」笔记创作活动的第7篇笔记

一,概述 我们都知道数据库的数据是存储到磁盘的,但是磁盘的速度是不能满足我们对于数据的存取的要求的,所以一个数据库需要有自己的缓存,这就是InnoDB的缓冲池(Buffer Pool)

在MySQL服务启动的时候就会和操作系统申请一片连续的内存当作缓存池,缓冲池默认大小为128M,可以通过配置innodb_buffer_pool_size参数的值来改变缓冲池的大小(该参数单位是字节)

缓冲池大小最小值为5M

二,Buffer Pool内部组成

Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是一样的,都是16KB,为了更好的管理这些在Buffer Pool中的缓存页,设计InnoDB的大叔为每一个缓存页都创建了一些所谓的控制信息,这些控制信息包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息\

每个缓存页对应的控制信息占用的内存大小是相同的,我们可以称这些控制信息为控制块,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边

碎片区域是不足以构建一个控制块和缓存页的内存碎片,如果缓冲池的大小设置得刚刚好的话,可以没有碎片\

每个控制块大约占用缓存页大小的5%,而我们设置的innodb_buffer_pool_size并不包含这部分控制块占用的内存空间大小,也就是说InnoDB在为Buffer Pool向操作系统申请连续的内存空间时,这片连续的内存空间一般会比innodb_buffer_pool_size的值大5%左右。

Free 链表

在我们刚申请了缓冲池的空间的时候并没有从磁盘上加载页到缓冲池,而是随着程序运行而慢慢加载,那么这个时候就需要知道哪些缓存页是还没加载磁盘的物理页的。

这个时候就需要Free链表(空闲链表),通过将空闲的缓存页的控制块连成一个双向链表来表示空闲的缓存页\

链表的基节点占用的内存空间并不包含在为Buffer Pool申请的一大片连续内存空间之内,而是单独申请的一块内存空间。

有了free链表,每当需要从磁盘中加载一个页到Buffer Pool中时,就从free链表中取一个空闲的缓存页,并且把该缓存页对应的控制块的信息填上(就是该页所在的表空间、页号之类的信息),然后把该缓存页对应的free链表节点从链表中移除,表示该缓存页已经被使用了

缓存页的哈希处理

当物理页已经缓存到了缓存池了,但是我们需要知道有哪些页被缓存到缓存池了,这个时候如果只是遍历缓存页的话那效率就比较低了

所以我们可以通过表空间号 + 页号来定位一个页的,以表空间号 + 页号为key,缓存页就是对应的value,通过哈希表来存储,这样就可以在常数时间内才知道一个缓存页了

当我们想要访问某个页的数据的时候,根据在哈希表中根据表空间号 + 页号看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从free链表中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。

flush链表的管理 当我们修改了缓冲池中某个缓存页的数据,那么该数据页就与磁盘上的 数据页不一致了,这样的缓存页我们称作脏页。

处理脏页最简单的办法是立即同步到磁盘上,但是磁盘io是很缓慢的,每次修改就进行一次磁盘io的效率是很低的,所以我们可以选择先不刷新脏页,等到特定的时机再一起刷新到磁盘。\

要统一处理脏页,就必须记录哪些缓存页是脏页(总不能直接把所以缓存页的都刷新回磁盘吧),这个时候就需要flush链表了

flush链表凡是修改过的缓存页对应的控制块作为一个节点加入到链表中,和free链表是很相同的\

LRU链表

缓存池的空间是有限的,并且页没法容纳所以物理页,所以我们在没用空间可以存储新的缓存页的时候,就需要把一些缓存页从内存中置换出去

InnoDB的缓冲池采用的是最少使用算法来进行页面置换。

普通LRU链表

对于普通的LRU算法,只需要简单的维护一个LRU链表,在每次访问页的时候,可以这样处理:

●如果该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页对应的控制块作为节点塞到链表的头部。 ●如果该页已经缓存在Buffer Pool中,则直接把该页对应的控制块移动到LRU链表的头部。 ●如果缓冲池中的空闲缓存页已经使用完了,就到LRU链表尾部找些缓存页淘汰就可以了

划分区域的LRU链表 普通的LRU链表,并不能解决InnoDB的问题,因为InnoDB有预读的功能,同时对于全表查询也会为普通LRU链表带来问题

预读

●线性预读:InnoDB提供了一个系统变量innodb_read_ahead_threshold(默认56),如果顺序访问了某个区(extent)的页面超过这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到Buffer Pool的请求 ●随机预读:如果Buffer Pool中已经缓存了某个区的13个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其的页面到Buffer Pool的请求(随机预读默认是关闭的)

全表查询 全表查询就是把所有的表都查询一遍

预读和全表查询为LRU带来一些问题,那就是,他们都有可能加载一些不经常访问的页进入缓冲池而替换掉那些真正需要的热页,这样大大降低了缓存命中率

所以InnoDB采用了一种特殊的LRU链表:

InnoDB的LRU链表会按比例(该比例是old区域占37%或者是3/8)分成两段:

●一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称young区域。 ●另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称old区域。

预读的优化: 每次预读的时候从磁盘中加载的缓存页,都会将缓存页的控制块放到old区域的头部,这样预读的数据就不会影响到热数据了,并且如果old区域的缓存页在后续访问中不被访问到,也会逐渐被淘汰出去缓存区

全表扫描的优化: 对于全表扫描,由于是访问每一行的,相当于短时间内访问同一个页多次,如果只是简单的添加到old区域,在多次访问后,他还是会被添加到young区域,从而挤掉热数据

所以InnoDB规定,在对某个处在old区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该页面就不会被从old区域移动到young区域的头部,否则将它移动到young区域的头部。上述的这个间隔时间是由系统变量innodb_old_blocks_time控制的(默认1000ms)

进一步优化: 对于young区域,如果每个缓冲页被访问就将其移到LRU链表的头部,那样的话开销有点大,所以InnoDB设计了,只有访问的页在young区域的1/4的位置后,才将其移到到LRU链表头部(也就是说在young区域的1/4区域中的页,在其再次被访问到的时候是不会移动到LRU链表头部的)

刷脏的时机

后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。主要有两种刷新路径: ●从LRU链表的冷数据中刷新一部分页面到磁盘。 后台线程会定时从LRU链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为BUF_FLUSH_LRU。 ●从flush链表中刷新一部分页面到磁盘。后台线程也会定时从flush链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST。 ●有时候如果刷脏速度太慢,并且每有空闲页可以缓存,这时就会尝试看看LRU链表尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将LRU链表尾部的一个脏页同步刷新到磁盘(和磁盘交互是很慢的,这会降低处理用户请求的速度)。这种刷新单个页面到磁盘中的刷新方式被称之为BUF_FLUSH_SINGLE_PAGE。 ●有时候系统特别繁忙时,也可能出现用户线程批量的从flush链表中刷新脏页的情况,很显然在处理用户请求过程中去刷新脏页是一种严重降低处理速度的行为(毕竟磁盘的速度慢的要死),这属于一种迫不得已的情况

多个Buffer Pool实例 在Buffer Pool特别大的时候,我们可以把它们拆分成若干个小的Buffer Pool,每个Buffer Pool都称为一个实例,它们都是独立的,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。我们可以在服务器启动的时候通过设置innodb_buffer_pool_instances的值来修改Buffer Pool实例的个数

当innodb_buffer_pool_size的值小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1。而我们鼓励在Buffer Pool大于或等于1G的时候设置多个Buffer Pool实例。