MySQL缓冲池Buffer Pool
一、引言
MySQL Buffer Pool 是 InnoDB 存储引擎中用于缓存数据和索引的内存区域。其作用是减少磁盘 I/O 操作,提高查询性能。
二、为什么需要Buffer Pool
我们在对数据库执行增删改操作的时候,不可能直接更新磁盘上的数据的,因为如果你对磁盘进行随机读写操作,那速度是相当的慢,随便一个大磁盘文件的随机读写操作,可能都要几百毫秒。如果要是那么搞的话,可能我们的数据库每秒也就只能处理几百个请求了!
因此为了提高系统的并发能力,我们是否可以将数据拷贝到内存里面修改完之后,异步刷新到内存,这样我们数据库的并发能力就会得到显著的提升,在我们MySQL数据库中,对数据库执行增删改操作的时候,实际上主要都是针对内存里的Bufer Pool中的数据进行的,也就是我们实际上主要是对数据库的内存里的数据结构进行了增删改。
三、Buffer Pool
我们首先一张图看看Buffer Pool 是一个什么东西,它是怎么工作的。
Buffer pool默认大小是128M,在正式的生产环境的下,我们完全要对buffer pool 要进行调整,比如我们是16核32G的机器,那么可以给buffer pool分配20G的内存
[server]
innodb_buffer_pool_size = 2147483648
3.1 数据页
数据页是MySQL中抽象出来的数据单位,MySQL实际对数据抽象出来一个数据页,他把很多行数据放在数据页里面,也就是说磁盘文件上会存放很多的数据页,每个数据页里面存放了很多行数据,我们要更新一行数据,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页直接给加载到Buffer Pool里去。
实际上默认情况下,磁盘中存放的数据页的大小是16KB,也就是说,一页数据包含了16KB的内容
而Bufer Pool中存放的一个一个的数据页,我们通常叫做缓存页,因为毕竟Bufer Pool是一个缓冲池,里面的数据都是从磁盘缓存到内存去的。
而Buffer Pool中默认情况下,一个缓存页的大小和磁盘上的一个数据页的大小是一一对应起来的,都是16KB。
3.2 缓存页
缓存页是 InnoDB 存储引擎在内存中缓存的数据页,用于加速数据访问。当查询需要访问某页数据时,InnoDB 会先检查该页是否在 Buffer Pool 中。如果在(缓存命中),则直接读取;如果不在(缓存未命中),则从磁盘读取并加载到 Buffer Pool 中。缓存页(Buffer Pool Page) 是内存中缓存的数据页,而每个缓存页都有一个对应的 描述信息块(Description Block),用于管理缓存页的元数据和控制信息。描述信息块是 InnoDB 对缓存页进行高效管理的关键数据结构。
数据库启动的时候,就会按照我们设置的Bufer Poo大小,去找操作系统申请一块内存区域,作为BufferPool的内存区域。然后当内存区域申请完毕之后,数据库就会按照默认的缓存页的16KB的大小以及对应的800个字节左右的描述数据的大小,在Bufer Pool中划分出来一个一个的缓存页和一个一个的他们对应的描述数据。
3.3 free链表
在了解完上面的知识后,我们在来思考这样一个问题,当我们的数据库运行起来之后,我们肯定会不停的执行增删改查的操作,此时就需要不停的从磁盘上读取一个一个的数据页放入Buffer Pool中的对应的缓存页里去,把数据缓存起来,那么以后就可以对这个数据在内存里执行增删改查了。但是此时在从磁盘上读取数据页放入Buffer Pool中的缓存页的时候,必然涉及到一个问题,那就是哪些缓存页是空闲的。
所以数据库会为Buffer Pool设计一个free链表,他是一个双向链表数据结构,这个free链表里,每个节点就是一个空闲的缓存页的描述数据块的地址,也就是说,只要一个缓存页是空闲的,那么他的描述数据块就会被放入这个free链表中。
刚开始数据库启动的时候,可能所有的缓存页都是空闲的,因为此时可能是一个空的数据库,一条数据都没有,所以此时所有缓存页的描述数据块,都会被放入这个free链表中。
我们可以看到上面出现了一个free链表,这个free链表里面就是各个缓存页的描述数据块,只要缓存页是空闲的,那么他们对应的描述数据块就会加入到这个free链表中,每个节点都会双向链接自己的前后节点,组成一个双向链表。这个free链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节点,也就是有多少个空闲的缓存页。
当我们需要把磁盘上的数据页读取到Bufer Pool中的缓存页里去的时候,首先,我们需要从free链表里获取一个描述数据块,然后就可以对应的获取到这个描述数据块对应的空闲缓存页,接着我们就可以把磁盘上的数据页读取到对应的缓存页里去,同时把相关的一些描述数据写入缓存页的描述数据块里去,比如这个数据页所属的表空间之类的信息,最后把那个描述数据块从free链表里去除就可以了。
看到这里,大家就完全明白,磁盘中的数据页是如何读取到Buffer Pool中的缓存页里去的了,而且这个过程中free链表是用来干什么的。
这里还有一个问题不知道大家发现了没有,我们怎么知道一个数据页有没有被缓存呢?
我们在执行增删改查的时候,肯定是先看看这个数据页有没有被缓存,如果没被缓存就走上面的逻辑,从free链表中找到一个空闲的缓存页,从磁盘上读取数据页写入缓存页,写入描述数据,从free链表中移除这个描述数据块。但是如果数据页已经被缓存了,那么就会直接使用了。所以其实数据库还会有一个哈希表数据结构,他会用表空间号+数据页号,作为一个key,然后缓存页的地址作为value。因此当你要使用一个数据页的时候,通过“表空间号+数据页号”作为key去这个哈希表里查一下,如果没有就读取数据页,如果已经有了,就说明数据页已经被缓存了。
3.4 flush链表
在了解完free链表之后,接下来我们再来看看另一个很重要的数据结构flush链表。
我们在执行增删改的时候,如果发现数据页没缓存,那么必然会基于free链表找到一个空闲的缓存页,然后读取到缓存页里去,但是如果已经缓存了,那么下一次就必然会直接使用缓存页。不管怎么样,我们要更新的数据页都会在Buffer Pool的缓存页里,供我们在内存中直接执行增删改的操作。接着我们肯定会去更新Buffer Pool的缓存页中的数据,此时一旦我们更新了缓存页中的数据,那么缓存页里的数据和磁盘上的数据页里的数据,是不是就不一致了?这个时候,我们的缓存页是脏数据,脏页。
我们都是知道一点的,最终这些在内存里更新的脏页的数据,都是要被刷新回磁盘文件的。
但是这里就有一个问题了,不可能所有的缓存页都刷回磁盘的,因为有的缓存页可能是因为查询的时候被读取到ButferPool里去的,可能根本没修改过。所以数据库在这里引入了另外一个跟free链表类似的flush链表,这个fush链表本质也是通过缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块,组成一个双向链表。凡是被修改过的缓存页,都会把他的描述数据块加入到flush链表中去,flush的意思就是这些都是脏页,后续都是要flush刷新到磁盘上去的,所以flush链表跟free链表几乎是一样的。
3.5 LRU算法
看到这里,大家也了解了free链表和flush链表的作用,现在,随着我们不停的把磁盘上的数据页加载到空闲的缓存页里去,free链表中的空闲缓存页是不是会越来越少呢?因为只要你把一个数据页加载到一个空闲缓存页里去,free链表中就会减少一个空闲缓存页。
所以,当我们不停的把磁盘上的数据页加载到空闲缓存页里去,free链表中不停的移除空闲缓存页,迟早有那么瞬间,我们会发现free链表中已经没有空闲缓存页了。这个时候,当我们还要加载数据页到一个空闲缓存页的时候,怎么办呢?
此时无法从磁盘上加载新的数据页到缓存页里去了,那么此时我们只有一个办法,就是淘汰掉一些缓存页。接着我们再把磁盘上你需要的新的数据页加载到这个腾出来的空闲缓存页中去。
MySQL为了解决这个问题引入了LRU链表,通过这个LRU链表,我们可以知道哪些缓存页是最近最少被使用的,那么当我们缓存页需要腾出来一个刷入磁盘的时候,不就可以选择那个LRU链表中最近最少被使用的缓存页了么?
我们从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到LRU链表头部去,那么只要有数据的缓存页,他都会在LRU里了,而且最近被加载数据的缓存页,都会放到LRU链表的头部去。
通过上图我们可以清晰的看见,刚加载进来缓存页就放在链表的头部,但是MySQL为了提高性能,当我们从磁盘上加载一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也加载到缓存里去,这就是我们所谓的预读机制。这种机制虽然带来了性能的提高,但是也引入了一个新的问题。
通过上图我们可以清晰的看到预读机制带来的问题,通过预读机制加载进来的数据放在了链表头部,之前加载进来且频繁访问的数据放在了链表后面,根据LRU的性质,就导致了之前加载进来且频繁访问的数据最先被淘汰,而没有人访问在链表头部加载进来的数据却没有被淘汰,这显然是不能被允许的。
3.5.1 基于冷热数据分离的思想设计LRU链表
为了解决预读带来的问题,采取冷热分离的思想,真正MVSQL在设计LRU链表的时候,采取的实际上是冷热数据分离的思想。真正的LRU链表,会被拆分为两个部分,一部分是热数据,一部分是冷数据,这个冷热数据的比例是由innodb_old_blocks_pct参数控制的,他默认是37,也就是说冷数据占比37%。
因此当数据页第一次被加载到缓存的时候,存放该数据的缓存页会被放在冷数据区域的链表头部,MySQL设定了一个规则,他设计了一个innodb old blocks time参数,默认值1000,也就是1000毫秒,也就是说,必须是一个数据页被加载到缓存页之后,在1s之后,你访问这个缓存页,他才会被挪动到热数据区域的链表头部去。至此就成功解决了由于预读带来的问题。
四、小结
MySQL的缓冲池是提高数据库性能的关键组件,通过有效的内存管理和数据缓存策略,它显著减少了磁盘I/O操作,提升了数据库的响应速度。掌握Buffer Pool对于开发人员来说是必不可少的一项内容。