深度理解Mysql(三):Buffer pool内部结构

930 阅读6分钟

一.概述

在前面的讲述中,我们已经连接到innodb引擎中,会有缓冲池(buffer pool),undo log,redolog,binlog,并且在修改事务的时候,会把数据相应的写到对应的日志中,这篇文章,我们将继续深入理解buffer pool内部结构

二.Buffer pool 内部结构

2.1 buffer pool 大小

image.png

默认大小是128M,这个设置一般而言,对于线上部署的项目是有些偏小的,如果线上是16核32G,可以设置buffer pool的大小为2G,或者根据压测结果合理设置

2.2 buffer pool存储数据

我们知道数据库模型是 表+字段+行,一个表又会有很多行数据,每行数据又有多个字段,可能有人认为数据就是一行一行存放在bufferpool中,但是实际上又不是这种; 实际上mysql对数据又抽象了一层叫数据页的概念,就是会把很多数据存放在数据页中,在查询和更新数据的时候,是以数据页为单位进行操作的,如图所示:

image.png

实际上,当我们要更新一行数据的时候,会把包含这行数据所在的数据页,一起加载到buffer pool中,如下图:

image.png

一个数据页大小是16k,当数据页存入缓冲池中,叫做缓存页,并且每个缓存页都会有一个对应的描述信息,如下图:

image.png

每个缓存页对应的描述数据,大概占缓存页大小的5%左右,也就是800字节,如果使用默认的buffer pool是128M,实际上buffer pool大小会超过一些,大概有130多M,因为会存放一些描述信息.

2.3 buffer pool 哪些是空闲?

在mysql服务起来的时候,mysql会向操作系统申请128M的空间作为buffer pool,最开始的时候,里面都是空的,但是在运行很多sql后, 就会不停的把数据从磁盘读取到buffer pool中,同时IO线程也会定时的刷新bufferpool数据到磁盘中,必然会涉及一个问题,哪些缓存页是空闲的,因此数据库会设计一个free链表,他是一个双向链表接口,在这个free链表中,每个节点就是一个空闲的缓存页的描述信息的地址,也就是说,只要这个缓存页是空闲的,他的描述信息地址就会放在这个free连中

image.png

2.4 怎么知道数据页是否已经加载到缓存页中

我们在crud的时候,首先看数据页是否被缓存,如果没有缓存,就会从free中找一个空闲页,然后从磁盘读取数据,写入缓存页中,把这个缓存页从free链表中删除,如果已经被缓存过,就直接使用,那么是怎么知道是都被缓存的呢??

数据库还有一个hash结构,key = 表空间号+数据页号,value =缓存页的地址

当你要使用一个数据页时候,通过"表空间+数据页号"去hash中查一下,如果有就说明被缓存过了,如图所示

image.png

2.5 flush链表: 记录那些缓存页被修改

数据被加载到缓存页后,程序执行的时候,会先对内存中数据进行修改,然后在某个时间将数据刷新到磁盘中,这个时候,就需要有一个地方记录哪些缓存页被修改了,参考free链表,有一个flush链表,会记录哪些缓存页被修改,如下图所示:

image.png

三.淘汰策略

通过上面的讲解,我们知道在缓冲池中,会有free链表记录哪些是空闲的缓存页,fursh链表记录哪些是修改过数据的缓存页,会有这样一个问题:

在刚开始时候,所有的缓存页都在free链表中,随着数据不停的从磁盘加载到内存中,free链表的数量会越来越少,最后一刻,free链表为空了,这个时候说明已经没有可用的缓存页了,怎么办??

这个时候,就需要淘汰一些缓存数据,就是把缓存页中,修改过的数据给刷新到磁盘中,然后这个缓存页就能被再次使用了,问题又来了,要先清空哪些缓存页呢??

3.1 LRU判断缓存是否需要清空

作为开发人员,一般在内存型数据库,比如redis都会有一些淘汰算法,最常用的就是LRU,也就是Least Recently Used(最近最少使用) 简单介绍下LRU原理: 假如从磁盘加载一个数据页到缓存页,就把这个缓存页的描述数据块放在LRU链表的头部,只要缓存页被使用,就会出现在LRU中,而且最新被加载的缓存页,都会放在LRU链表的头部; 假如某个缓存页在lru的尾部,在查询或者修改这个缓存页的时候,也会把这个缓存页一定到lru的头部,也就是最近访问过的缓存页,一定在lru的头部,如下图所示:

image.png

这个时候如果想要腾空一些数据,只需要从lru链表的尾部节点找到缓存页,刷新到磁盘就能清空这个缓存页了

这样看似简单,实际上还是会有问题的

3.3 mysql预读问题导致lru的问题

我们知道,mysql有个预读机制,在读取一页数据的时候,它认为大概率你也会查询相邻的一些数据,所以就会把一些相邻的数据页也加载到缓存页中,这个时候,可能会导致预读加载进来的数据放在lru最前面,但是放在lru尾部的数据,可能也是经常被访问,只是在这个瞬间,被预读加载的缓存页挤到尾部了,如图所示

image.png 这样的话,通过预读机制读进来的数据,就会放在头部,尾部的数据可能经常使用,但是在这一瞬间,就被挤到尾部了,所以这个时候并不是不我们希望的;

3.2 哪些情况会触发mysql的预读机制

  1. innodb_read_ahead_threshold 的值,默认是56,意思就是如果顺序的访问了一个区里的多个 数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓 存里去

2.如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会 直接触发预读机制,把这个区里的其他的数据页都加载到缓存里去 这个参数数innodb_random_read_ahead默认值是false,也就是默认不会触发 image.png

平时写的sql比如有那种氛围查询或者全表扫描,比如 select * from users where id >10; select * from users; 会把很多表的数据装进缓存页中,这个时候可能就会触发mysql的预读机制,会把相邻的数据页也读入缓存中,所以说lru这种简单的把命中的数据放在头部不是一种很好的方式,在下篇文章中会继续深入讲解的

ps:公司的年终奖今年没有了,伤心啊,杭州现在行情这么差了么.....

大哭.png