什么是 Buffer Pool

Buffer Pool 的背景

我们都知道数据库中的数据最终是要放到磁盘文件上的,但是我们在对数据库执行增删改操作的时候,不可能直接更新磁盘上的数据,因为磁盘的读写是随机的读写操作,速度很慢。所以我们在对数据库的数据进行操作时都是在内存 Buffer Pool 中的。
当数据库读取这个数据呢,就会先从这个 buffer 中取。如果 buffer 中没有呢就从磁盘中取,读取完之后再放到 buffer 缓冲区中。当向数据库写入数据时也会首先向这个 buffer 中写入数据,定期将 buffer 中的数据刷新到磁盘上进行持久化的操作。看到这里你可能有疑问,如果在数据还没有持久化到磁盘的时候断电了,是否会出现数据丢失。那么 MySQL 为了防止数据丢失,引入了 redo log 机制,在对内存里的数据进行增删改的时候,同时也会将对应的日志写入 redo log 中。(后续会详细讲)
image.png

Buffer Pool 简介

了解了 Buffer Pool 的背景,那么数据库中的 Buffer Pool 是多大呢?Buffer Pool 的大小是可配置的,在默认情况下是 128MB,有点偏小,我们会对其进行调整。比如:我们数据如果是 16 核 32G 的机器,那么我们会给 Buffer Pool 分配 2GB 的内存,操作指令为:

[server] innodb_buffer_pool_size = 2147483648

了解了 Buffer Pool 的大小,我们再看看 Buffer Pool 是如何存放数据的?
MySQL 对数据抽象出了一个数据页的概念,它是把很多行数据放在了一个数据页里,也就是说我们的磁盘文件中就是会有很多的数据页,每一页里放了很多行数据。假设我们要更新一条数据,此时数据库会找到这行数据所对应的数据页直接给加载到 Buffer Pool 里去。

磁盘中存放的数据页的大小是 16KB,而 Buffer Pool 中存放的一个一个的数据页,我们通常叫做缓存页。对于每个缓存页,他实际上都会有一个描述信息,这个描述信息大体可以认为是用来描述这个缓存页的。比如:这个数据页所属的表空间、数据页的编号、这个缓存页在 Buffer Pool 中的地址以及其他。每个缓存页都会对应一个描述信息,这个描述信息本身也是一块数据。

值得注意,Buffer Pool 中的描述数据大概相当于缓存页大小的 5% 左右,大概 800 个字节左右。假设你设置的 Buffer Pool 大小是 128MB,实际上 Buffer Pool 真正的最终大小会超出一些,可能有 130 多 MB 的样子,因为它里面还要存放缓存页的描述数据。
image.png

free 链表(双向)

当数据库运行起来之后,我们就会去执行增删改查的操作,此时就需要不停地从磁盘上读取一个一个数据页放到 Buffer Pool 中的对应的缓存页里面,把数据缓存起来,然后再内存里执行操作。那如何判断哪些缓存页是空闲的?
数据库会为 Buffer Pool 设计一个 free 链表,他是一个双向链表结构,这个链表中每个节点就是一个空闲的缓存页的描述数据块的地址。
工作流程大致是:我们首先需要从 free 链表里获取一个数据块,然后可以对应的获取到这个数据块对应的空闲缓存页。接着我们就可以把磁盘上的数据页读取到对应的缓存页里去,同时把相关的一些描述写入缓存页的描述数据块里去,最后把这个数据块从 free 链表里去除就可以了。

flush 链表(双向)

在聊 flush 链表前,我们先聊一下脏数据页。什么是脏数据页,就是当我们更新 Buffer Pool 中的数据时,此时一旦你更新了缓存页中的数据,那么缓存页里的数据和磁盘上的数据页里的数据,就不一致了。这个时候,我们就说缓存页是脏数据,脏页。

最终这些在内存里更新的脏页数据都是要被刷回磁盘文件的。但有一个问题,如果缓存页可能因为查询的时候被读取到 Buffer Pool 里去的,可能就根本没修改过。所以数据库在这里引入了另外一个跟 free 链表类似的 flush 链表。
凡是被修改过的缓存页都会把他的描述数据加入到 flush 链表中去,flush 的意思是这些都是脏页,后续都要 flush 刷新到磁盘上去的。

LRU 链表

由上面的内容我们可以得知,当我们要执行 CURD 操作时,是需要将磁盘上的数据页加载到缓存页里来。那么要加载数据到缓存页的时候,必然是要加载到空闲的缓存页里去的,所以必须要从 free 链表中找一个空闲的缓存页,然后把磁盘上的数据页加载到空闲缓存页里。
image.png
因此,随着数据不断地加载到空闲缓存页里去,free 链表中就会不断减少空闲缓存页,迟早有那么一瞬间 free 链表中已经没有空闲缓存页了。这个时候我们就需要淘汰掉一些缓存数据。那么什么叫做淘汰呢?就是把一个缓存页里被修改过的数据刷新到磁盘上的数据页里去,然后这个缓存页就可以清空了,变成了一个空闲的缓存页。

LRU 表大致的工作原理:简单来说,我们从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到 LRU 链表表头去,那么只要有数据的缓存页,他都会在 LRU 里,而且最近被加载数据的缓存页,都会放到 LRU 链表的头部。假设原本某个缓存页的描述数据块本来在 LRU 链表的尾部,后续你只要查询或者修改了这个缓存页的数据,也要把这个缓存页挪动到 LRU 链表的头部去,也就是说最近被访问过的缓存页,一定在 LUR 链表头部。
此后,当缓存页没有空闲的时候,就去 LRU 链表尾部找到一个缓存页,然后将这个缓存页刷入磁盘中就可以了。