在MySQL执行一条更新语句文章结尾给出了此图,其中BufferPool是MySQL中一个重要的组件。
1. 概念
1.1 BufferPool
BufferPool本质就是MySQL的一个内存组件,作为一个缓冲池,就是一片内存数据结构,所以这个内存数据结构肯定是有一定的大小的,不可能是无限大的。
它的默认大小为128M。不过它的大小是可以改变的,并且可以设置多个。比如有一台16核32G内存的机器,可以分配给MySQL 20G内存,划分为10个buffer pool。
1.2 数据页
数据页是MySQL中抽象出来的数据单位,它将多行数据存入一个数据页,而磁盘文件中会有多个数据页。
我们都知道数据库的核心数据模型就是表+字段+行的概念,也就是说我们都知道数据库里有一个一个的表,一个表有很多字段,然后一个表里有很多行数据,每行数据都有自己的字段值。 但是这只是MySQL的数据的逻辑结构,真正存入磁盘文件的最小单位是数据页。
所以更新数据的最小单元是数据页,即使我们只更新/查询一条数据也要把整个数据页全部读入BufferPool中。
默认情况下,每个数据页的大小为16k,也就是说,一页数据包含了16KB的内容。
Linux文件系统中,默认一个块的大小为4k。
1.3 缓存页
Buffer Pool中存放的一个一个的数据页,我们通常叫做缓存页,因为毕竟Buffer Pool是一个缓冲池,里面的数据都是从磁盘缓存到内存去的。默认情况下,一个缓存页的大小和磁盘上的一个数据页的大小是一一对应起来的,都是16KB
1.4 脏页
缓存页被修改后,修改后的数据未被写入磁盘的数据页中,此时的缓存页被称为脏页。
1.5 缓存页对应描述信息
每个缓存页,他实际上都会有一个描述信息,这个描述信息大体可以认为是用来描述这个缓存页的比如包含如下的一些东西:这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址以及别的一些杂七杂八的东西。
每个缓存页都会对应一个描述信息,这个描述信息本身也是一块数据,在Buffer Pool中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面所以此时我们看下面的图,Buffer Pool实际看起来大概长这个样子。

Buffer Pool中的描述数据大概相当于缓存页大小的5%左右,也就是每个描述数据大概是800个字节左右的大小,然后假设你设置的buffer pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能有个130多MB的样子,因为他里面还要存放每个缓存页的描述数据。
1.6 Free链表
Free链表是由未被使用的缓存页的描述信息组成的链表。
对于free链表而言,只有一个基础节点是不属于Buffer Pool的,他是40字节大小的一个节点,里面就存放了free链表的头节点的地址,尾节点的地址,还有free链表里当前有多少个节点。
工作原理:
- 数据库刚启动时,还没有从磁盘中读取任何数据页到内存(Buffer Pool)中,那此时Buffer Pool中所有的缓存页其实都是空的。
- 此时所有缓存页的数据描述信息都在free链表中。
- 当加载磁盘文件中的数据页时,会将相应的描述信息从Free链表中删除。
1.7 LRU链表
LRU即Least Recently Used最少使用的意思,该链表记载了每个缓存页被访问的命中程度。
如概念图所示,LRU链表分为两个部分,一个是热数据区域(红色部分,占5/8),一个是冷数据区域(蓝色部分,占3/8)。
工作原理:
- 数据页从磁盘文件中加载到缓存页,并将free链表缓存页的描述信息节点转到LRU链表的冷数据区域的头节点部位。
- 当过一秒后,仍有请求命中(包括:增删改查)该缓存页后,则将该缓存页的描述信息放到LRU链表的热数据区域的头节点处。
- LRU链表热数据区域对应的缓存页当被访问时,也会移动到热数据区域头节点处。不过为了提高效率,前1/4部分对应的缓存页当被访问时,不会移动。
优点:
冷热数据分离方案可以有效阻止全表扫描和数据页预读,防止热数据被挤下去。
1.8 Flush链表
Flush链表中的基础节点指向的是被修改过(包括:增删改)的缓存页的描述信息。
2. 数据库启动的时候,是如何初始化Buffer Pool的?
数据库只要一启动,就会按照你设置的Buffer Pool大小,稍微再加大一点,去找操作系统申请一块内存区域,作为Buffer Pool的内存区域。
然后当内存区域申请完毕之后,数据库就会按照默认的缓存页的16KB的大小以及对应的800个字节左右的描述数据的大小,在Buffer Pool中划分出来一个一个的缓存页和一个一个的他们对应的描述数据。
然后当数据库把Buffer Pool划分完毕之后,看起来就是之前我们看到的那张图了。
只不过这个时候,Buffer Pool中的一个一个的缓存页都是空的,里面什么都没有,要等数据库运行起来之后,当我们要对数据执行增删改查的操作的时候,才会把数据对应的页从磁盘文件里读取出来,放入Buffer Pool中的缓存页中。
3. 如何得知哪些缓存页是空闲的?
接着我们来看下一个问题,当你的数据库运行起来之后,你肯定会不停的执行增删改查的操作,此时就需要不停的从磁盘上读取一个一个的数据页放入Buffer Pool中的对应的缓存页里去,把数据缓存起来,那么以后就可以对这个数据在内存里执行增删改查了。
但是此时在从磁盘上读取数据页放入Buffer Pool中的缓存页的时候,必然涉及到一个问题,那就是哪些缓存页是空闲的?因为默认情况下磁盘上的数据页和缓存页是一 一对应起来的,都是16KB,一个数据页对应一个缓存页。所以我们必须要知道Buffer Pool中哪些缓存页是空闲的状态。
所以数据库会为Buffer Pool设计一个free链表,他是一个双向链表数据结构,这个free链表里,每个节点就是一个空闲的缓存页的描述数据块的地址,也就是说,只要你一个缓存页是空闲的,那么他的描述数据块就会被放入这个free链表中。
刚开始数据库启动的时候,可能所有的缓存页都是空闲的,因为此时可能是一个空的数据库,一条数据都没有,所以此时所有缓存页的描述数据块,都会被放入这个free链表中。
free链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节点,也就是有多少个空闲的缓存页。
free链表,他本身其实就是由Buffer Pool里的描述数据块组成的,你可以认为是每个描述数据块里都有两个指针,一个是free_pre,一个是free_next,分别指向自己的上一个free链表的节点,以及下一个free链表的节点。
通过Buffer Pool中的描述数据块的free_pre和free_next两个指针,就可以把所有的描述数据块串成一个free链表,大家可以自己去思考一下这个问题。上面为了画图需要,所以把描述数据块单独画了一份出来,表示他们之间的指针引用关系。
对于free链表而言,只有一个基础节点是不属于Buffer Pool的,他是40字节大小的一个节点,里面就存放了free链表的头节点的地址,尾节点的地址,还有free链表里当前有多少个节点。
4. 将数据页内容读到缓存页中过程
首先,需要从free链表里获取一个描述数据块,然后就可以对应的获取到这个描述数据块对应的空闲缓存页,下图所示。
接着把磁盘上的数据页读取到对应的缓存页里去,同时把相关的一些描述数据写入缓存页的描述数据块里去,比如这个数据页所属的表空间之类的信息,最后把那个描述数据块从free链表里去除就可以了,如下图所示。
5. 数据页是否被缓存
在执行增删改查的时候,肯定是先看看这个数据页有没有被缓存,如果没被缓存就走上面的逻辑,从free链表中找到一个空闲的缓存页,从磁盘上读取数据页写入缓存页,写入描述数据,从free链表中移除这个描述数据块。
但是如果数据页已经被缓存了,那么就会直接使用了。
所以其实数据库还会有一个哈希表数据结构,他会用表空间号+数据页号,作为一个key,然后缓存页的地址作为value。
当你要使用一个数据页的时候,通过“表空间号+数据页号”作为key去这个哈希表里查一下,如果没有就读取数据页,如果已经有了,就说明数据页已经被缓存了。我们看下图,又引入了一个数据页缓存哈希表的结构。
也就是说,每次你读取一个数据页到缓存之后,都会在这个哈希表中写入一个key-value对,key就是表空间号+数据页号,value就是缓存页的地址,那么下次如果你再使用这个数据页,就可以从哈希表里直接读取出来他已经被放入一个缓存页了。
6. 预读机制
- 所谓预读机制,说的就是当你从磁盘上加载一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也加载到缓存里去
- 什么时候会触发预读机制?
- 有一个参数是innodb_read_ahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓存里去
- 如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制,把这个区里的其他的数据页都加载到缓存里去
- 全表扫描的时候,select * from tableName 会把该表所有的数据页都缓存到buffer pool当中
7. Buffer Pool的缓存页以及几个链表的使用回顾
- 数据库启动时,会申请内存创建buffer pool,buffer pool分成一个个缓存页及其缓存页描述信息块,描述信息块加入到free链表中
- 数据加载到一个缓存页,free链表里会移除这个缓存页,然后lru链表的冷数据区域的头部会放入这个缓存页
- 如果查询了一个缓存页,那么此时就会把这个缓存页在lru链表中移动到热数据区域去,或者在热数据区域中也有可能会移动到头部去
- 如果更新了缓存页,会把该缓存页加入到flush链表中
- 如果缓存页不够用了,会把lru冷数据区尾部的缓存页刷盘,清空;该缓存页从lru链表和flush链表中移除,加入到free链表中
- mysql后台线程也会定时把lru冷数据区尾部的缓存页刷盘,清空;定时把flush链表中的缓存页刷盘,清空,加入到free链表中
- 总结
- 一边不停的加载数据到缓存页里去,不停的查询和修改缓存数据,然后free链表中的缓存页不停的在减少,flush链表中的缓存页不停的在增加,lru链表中的缓存页不停的在增加和移动
- 另外一边,你的后台线程不停的在把lru链表的冷数据区域的缓存页以及flush链表的缓存页,刷入磁盘中来清空缓存页,然后flush链表和lru链表中的缓存页在减少,free链表中的缓存页在增加
8. 脏页刷盘时机
- 定时把LRU尾部的部分缓存页刷入磁盘:
首先第一个时机,并不是在缓存页满的时候,才会挑选LRU冷数据区域尾部的几个缓存页刷入磁盘,而是有一个后台线程,他会运行一个定时任务,这个定时任务每隔一段时间就会把LRU链表的冷数据区域的尾部的一些缓存页,刷入磁盘里去,清空这几个缓存页,把他们加入回free链表去!
所以实际上在缓存页没用完的时候,可能就会清空一些缓存页了。所以如果在一个动态的运行效果中,大概就是你不停的加载数据到一些空闲的缓存页里去,然后这些缓存页可能被使用,会在lru链表中各种移动。然后同时有一个后台线程还不停的把冷数据区域的一些不用的缓存页刷入磁盘中,清空一些缓存页出来。
- 把flush链表中的一些缓存页定时刷入磁盘 :
数据区域里的很多缓存页可能也会被频繁的修改,这些数据总归要刷入磁盘,所以这个后台线程同时也会在MySQL不怎么繁忙的时候,找个时间把flush链表中的缓存页都刷入磁盘中,这样将被你修改过的数据刷入磁盘!
只要flush链表中的一波缓存页被刷入了磁盘,那么这些缓存页也会从flush链表和lru链表中移除,然后加入到free链表中去!
所以可以理解为,一边不停的加载数据到缓存页里去,不停的查询和修改缓存数据,然后free链表中的缓存页不停的在减少,flush链表中的缓存页不停的在增加,lru链表中的缓存页不停的在增加和移动。另外一边,你的后台线程不停的在把lru链表的冷数据区域的缓存页以及flush链表的缓存页,刷入磁盘中来清空缓存页,然后flush链表和lru链表中的缓存页在减少,free链表中的缓存页在增加。
引用
https://blog.csdn.net/wuhenyouyuyouyu/article/details/93377605 缓冲池(buffer pool),这次彻底懂了!!!
