前文我们已经讲了Buffer Pool最基础的数据存储单元,缓存页。缓存页里存储的就是一行一行的数据,同时每个缓存页都对应了一个描述数据。
那MySQL启动的时候,是如何初始化Buffer Pool的?又是如何从磁盘加载数据文件到缓冲页的呢?
MySQL启动的时候,会按照配置在内存中给Buffer Pool分配一块内存,作为Buffer Pool的内存空间。然后会按照默认的16K缓存页大小,在Buffer Pool中划分出一个个的缓存页和与它们一一对应的描述数据块,这些描述数据块比较小。
此时这些缓冲页都还是空的, 然后Buffer Pool会搞出一个free链表,把这些描述数据块放到这个free链表中,这个free链表是公用的,如果有多个Buffer Pool,它们会公用这个free链表。
如上图,这个free链表是个双向链表,它里面就是各个缓存页的描述数据块的指针,指向了各个Buffer Pool中的描述数据。
另外,这个free链表还有一个基础节点,它会引用双向链表的头节点尾节点,同时会记录free链表中有多少个节点,也就是多少个空闲的缓冲页。
现在有了free链表,我们从磁盘加载数据页到Buffer Pool缓存页的时候,就可以从free链表里获取一个描述数据块,然后就可以对应的获取到这个描述数据块对应的空闲缓存页。
把磁盘上的数据页加载到Buffer Pool的缓存页后,同时会把相关的描述数据写入缓存页的描述数据块去,比如这个数据页所属的表空间,页号,最后从free移除这个节点。
可能有同学疑问,我怎么知道数据页有没有加载到Buffer Pool?
我们在操作数据的时候,肯定要先看下这条数据对应的数据页是否已经加载到缓存页,如果已经加载了,就直接使用了。如果没有加载,就从free链表中找到一个空闲的缓存页,写入描述数据,再从free链表中移除这个节点。
其判断依据,是数据库的一个哈希表。
这个哈希表,用表空间号 + 数据页号作为key,缓冲页地址作为value。
当你要使用一个数据页时,先在这个哈希表里判断下这个页是否存在。如果存在,就直接根据value找到缓冲页了,如果不存在,就要去加载,然后把这个页的信息写入哈希表中。
当我们把数据页加载到缓存页后,就可可以操作数据了,当你完成了对缓存页数据的更新,那么缓存页的数据就和磁盘上的数据页里的数据不一致了,怎么刷到磁盘上去呢?
不能每次修改一条数据,就把所有缓存页都刷到磁盘去吧?有的可能只是读取才加载到Buffer Pool,并没有修改,没必要刷盘啊。
其实InnoDB里还有个flush链表,这个flush链表跟free链表类似,其每个节点保存的也是描述数据块的指针,当某个缓存页修改了,就把它对应的描述数据块放到flush链表中。如下图所示:
通过上面的分析,我们基本能知道,MySQL启动的时候,会分配一块Buffer Pool内存区,然后划分出一块一块的缓存页,这些页都是空的,都会加入到free链表中。当要访问数据的时候,先判断下缓存页是否在数据页缓存哈希表中,如果在就直接使用了,如果不在,就从磁盘文件中加载数据页,然后从free链表中删除这个节点。完成修改后,就把这个缓存页加入到flush链表,等待刷入磁盘。
接下来,我们需要思考一个问题,我们不停的把磁盘上的数据页加载到free链表中去,free链表就会不停的移除空闲缓存页,总会有free链表没有空闲缓存页的时候,这时候还要加载数据页到空闲缓存页怎么办?
Buffer Pool大小是固定的,free链表在初始的时候,也初始化了所有的空闲缓存页,数量也是固定的,当所有缓存页都被填满了后,就需要淘汰点一些缓存页。
淘汰哪些呢?
这就要引入缓存命中率了。
假设有两个缓存页,一个缓存页,1分钟之内访问了100次,另一个缓存页1分钟之内只有访问了10次,这时我们就可以说前者缓存命中率比后者高。
这时候必须要淘汰一个缓存页,肯定是淘汰掉缓存命中率低的那个了。
说到这里,很多同学肯定会想到它了,没错,就是LRU算法。
LRU,least recently used,最近最少使用的意思。
InnoDB通过引入LRU链表,就可以知道哪些缓存页最近最少被使用,当你要淘汰一些缓存页的时,就知道淘汰哪些缓存页了。
LRU链表的特性就是,你只要访问了某个节点,它就会把这个节点移动到链表头部去,也就是说最近被访问的节点一定在LRU链表的头部,访问少的节点,自然到了链表的尾部。
这样的话,当缓存页长时间没有被访问,就处于LRU链表的尾部,当要淘汰缓存页的时候,这些访问频率低的节点就可以淘汰掉,重新把需要的磁盘文件数据页加载到空闲的缓存页里来。
总结:
本文引入了3个链表:
free链表,管理Buffer Pool中空闲的缓存页;
flush链表,管理修改过需要刷入磁盘文件的缓存页;
lru链表,用lru算法管理哪些缓存页访问频率高,哪些缓存页访问频率低,当没有空闲缓存页的时候,就可以把访问频率低的缓冲页过期掉,从新加载需要的缓存页了。
END