我们在对数据库进行增删改操作的时候是不可能直接对磁盘文件上的数据进行操作的,因为一个大磁盘文件的随机读写操作可能都要几百毫秒,那么数据库每秒也就只能处理几百个请求了。所以很自然的就要引入缓存的概念,MySQL的InnoDB存储引擎中就是使用Buffer Pool来实现数据的缓存,增删改的时候先将数据加载到Buffer Pool中,然后配合undo log、redo log以及binlog来保证数据的正常更新。
那么Buffer Pool中的保存的数据结构到底是什么样的,我们一起来看看。
1. 如何配置Buffer Pool的大小
Buffer Pool 本质上一块内存,所以它不可能是无限大的,默认情况下是128MB,在实际生产环境中我们可以对它的大小进行配置。例如,我们的机器是16核32G配置,那么可以给Buffer Pool分配2GB内存大小,通过以下配置:
[server]
innodb_buffer_pool_size=2147483648
2. 数据页
数据库中的表示一行一行存在的,但是Buffer Pool中的数据却不是直接按行保存的。首先MySQL抽象出一个数据页的概念,一个数据页中包含多行数据,磁盘数据文件中也包含了很多的数据页,所以也是通过数据的形式从磁盘中加载到Buffer Pool中。也就是说如果我们想去更新某一行数据,数据库会加载这行数据所在的页到Buffer Pool中再进行后续操作。如下图所示:
实际上,默认情况下一个数据页的大小是16KB,而默认的Buffer Pool大小是128MB,所以Buffer Pool中也会加载很多数据页,通常叫做缓存页。
3. 描述数据
对于每个缓存页,加载到Buffer Pool之后,都会有产生一个描述信息,具体内容包括:这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址
以及一些其他信息。这个描述信息也是一块数据,大概是800个字节左右的大小,所以如果设置的Buffer Pool为128MB,实际上的大小会超出一些大概在130多MB左右,多出的空间就是保存描述信息的。描述信息会放在Buffer Pool最前面,然后才是数据页,如下图:
4. free链表
在数据库启动的时候,数据库就会去找操作系统申请一块区域作为Buffer Pool的内存区域。然后就会按照数据页和描述信息的大小将Buffer Pool划分成一个个的区域,但是此时这些区域里面都是空的。只有当我们要对数据进行增删改查的时候,才会从磁盘文件把对应的数据页加载进来。那么我们怎么知道那些缓存页是空闲的呢?
实际上数据库为Buffer Pool设计了一个free 链表(空闲缓存页描述链表)
。它是一个双向链表数据结构,这个free 链表中,每个节点就代表了一个空闲的缓存页的描述信息的地址。也就是说,如果一个缓存页是空闲的,那么它的描述信息的地址肯定就在 free 链表中。所以刚开始数据库启动时,这个free链表是包含了所有的缓存页的描述信息地址的
。此外,这个free链表有一个基础节点,它会引用链表的头结点和尾节点,里面存储了链表中有多少个描述信息。如下图:
5. 将磁盘上的页读取到Buffer Pool的缓存页中的步骤
(1)从free链表中获取一个空闲缓存页的数据描述块
(2)将磁盘中的某一个数据页加载到这个数据描述块对应的空闲缓存页中
(3)从free链表中移除这个数据描述块
6. 已加载数据页哈希表
我们在执行增删改查的时候,肯定首先看这个数据所在的数据页有没有被缓存,如果有就不用从磁盘加载了。如何知道数据页有没有被缓存呢?MySQL中还有一个哈希数据结构,它会用表空间号+数据页号作为key,用数据页的地址作为value,以此来表示一个数据页已经被加载到缓存中
。如下图:
也就是说,每次从磁盘加载一个数据页到Buffer Pool中的时候,除了会从free链表中移除对应的空闲缓存页描述数据,还会吧这个数据页的地址保存到一个哈希表中。这样下次再使用这个数据页的时候,就不用从磁盘加载,可以直接从Buffer Pool中获取了。
7. LRU链表
没由于Buffer Pool的大小是有限的,所以在所有空闲缓存页全部加载满了之后,就需要淘汰一写缓存页,也就是从Buffer Pool中移除这些缓存页,然后加载新的需要使用的缓存页进来。那么要选择哪个缓存页淘汰呢?
7.1 简单LRU算法
这里我们需要引入一个缓存命中率
的概念。也就是需要把缓存命中率低的缓存页淘汰出去。这里就需要用到LRU(Least Recently Used)算法了,也就是最近最少使用算法。也就是将加载进Buffer Pool中的缓存页的描述信息保存在一个双向LRU链表中,后续涉及到某个缓存页的操作就需要将这个缓存页移动到LRU链表头部,这样LRU链表的尾部总是最少被访问的缓存页,在淘汰的时候直接从链表尾部选择一个描述块,从Buffer Pool中移除它对应的缓存页即可
。如下图:
实际上这个LRU链表中的节点和上面的free链表中的节点还是同一份数据,只不过通过不同的指针联系在一起组成了不同的链表而已。
7.2 简单LRU算法的问题
但是这种简单的LRU算法会存在一些问题。首先是MySQL的预读机制
会在加载数据页的时候,把磁盘上相邻的其他数据页一起加载到Buffer Pool中,并且会将连续的描述信息插入到LRU链表中。这样就带来了一个问题,就是原先处于链表末尾的数据页实际上的访问次数是大于这些通过预读机制加载进来的相邻数据页的,但是却有更大的可能被淘汰,这显然是不合理的。如下图:
7.3 冷热分离的LRU算法
MySQL中是基于冷热分离的思想
来设计LRU链表从而解决上述问题的。实际上MySQL中的LRU链表分成两个部分,一部分是热数据,一部分是冷数据。默认冷数据占比37%。这个时候LRU链表看起来是下面这个样子的:
(1)数据页第一次加载到Buffer Pool中的时候,描述数据会放在冷数据区域的头部。
(2)如果数据页加载到Buffer Pool中之后的1秒内访问了这个数据页,那么它的描述信息会被移动到热数据区域的头部。
这样就解决了前面提到的由于预加载机制带来的淘汰问题了。并且在淘汰缓存页的时候,直接选择冷数据区域末尾的数据页淘汰即可
,因为这些数据页是加载进来之后1秒甚至更久都没有访问的。
8. flush 链表
实际上在淘汰数据页的时候,我们还需要把需要淘汰的数据页中的脏数据刷入磁盘中,避免数据丢失。如果Buffer Pool中的某个缓存页中的数据被修改了,那么就需要把它对应的描述信息插入到flush链表中。flush链表就是待刷盘数据页的描述信息组成的链表。如下图:
THE END.