MySQL原理简介—4.深入分析Buffer Pool

大纲

1.Buffer Pool是什么

2.如何配置Buffer Pool的大小

3.数据页是MySQL中抽象出来的数据单位

4.数据页如何对应Buffer Pool中的缓存页

5.缓存页对应的描述信息是什么

6.Buffer Pool简单总结

7.数据库启动时如何初始化Buffer Pool

8.free链表可判断哪些缓存页是空闲的

9.free链表占用多少内存空间

10.如何读取数据页到Buffer Pool的缓存页

11.如何知道数据页有没有被缓存

12.空闲缓存页与free链表总结

13.Buffer Pool中会不会有内存碎片

14.脏数据到底为什么会脏

15.flush链表可判断哪些缓存页是脏页

16.flush链表的伪代码

17.flush链表和脏页总结

18.如果Buffer Pool中的缓存页不够了怎么办

19.淘汰缓存页与缓存命中率

20.引入LRU链表来判断哪些缓存页是不常用的

21.基于冷热数据分离思想设计LRU链表

22.Buffer Pool的缓存页以及几个链表总结

23.LRU链表冷数据区域的缓存页何时刷盘

24.Buffer Pool在访问时是否需要加锁

25.多个Buffer Pool优化并发能力

26.通过chunk动态调整运行期的Buffer Pool

27.生产环境应给Buffer Pool设置多少内存、多少Buffer Pool、多大的chunk

28.show engine innodb status输出详解

关键字:MySQL内存数据的更新机制

1.Buffer Pool是什么

Buffer Pool是MySQL数据库中一个非常关键的组件。数据库中的数据最终都是存放在磁盘文件上的。但是在对数据库执行增删改查操作时,不可能直接更新磁盘上的数据。因为如果直接对磁盘进行随机读写操作,那速度是相当的慢的。随便一个大磁盘文件的随机读写操作,可能都要几百毫秒,这样数据库每秒也就只能处理几百个请求。

数据库执行增删改操作时,是基于内存Buffer Pool中的数据进行的。同时为了防止在更新完内存中的数据之后,由于机器宕机而造成数据丢失,数据库引入了redo日志机制,即增删改时会把修改也写入redo日志中。

Buffer Pool就是数据库的一个内存组件,里面缓存了磁盘上的真实数据。当执行更新时,会写undo日志、修改Buffer Pool数据、写redo日志;当提交事务时,会将redo日志刷磁、binlog刷盘、添加commit标记。最后后台IO线程会随机把Buffer Pool里的脏数据刷入到磁盘数据文件中。

2.如何配置Buffer Pool的大小

由于Buffer Pool本质就是数据库的一个内存组件,所以Buffer Pool是有大小的,不能无限大。

Buffer Pool的默认大小是128MB,有点偏小。在实际生产环境下可以对Buffer Pool进行调整。比如对于16核32GB的数据库,可以给Buffer Pool分配2GB大小的内存。

[server]
innodb_buffer_pool_size = 2147483648

3.数据页是MySQL中抽象出来的数据单位

MySQL是如何将数据放在Buffer Pool中的?我们日常使用的数据库的数据模型是表 + 字段 + 行。数据库里有一个个表,一个表有很多字段,一个表有很多行数据。所以数据是否是一行一行地放在Buffer Pool里面的?

MySQL会把很多行数据放在一个数据页里。然后磁盘文件中会有很多数据页,每一页放了很多行数据。

假设要更新一行数据,此时数据库会找到这行数据所在的数据页。然后从磁盘文件中把这行数据所在的数据页加载到Buffer Pool里。因此Buffer Pool中存放的是一个一个的数据页。

4.数据页如何对应Buffer Pool中的缓存页

默认情况下,磁盘中存放的数据页大小是16KB。Buffer Pool中存放的一个个数据页,通常叫做缓存页。因为Buffer Pool是一个缓冲池,里面的数据都是从磁盘加载到内存的。所以Buffer Pool中一个缓存页的大小等于磁盘上一个数据页的大小,都是16KB。

5.缓存页对应的描述信息是什么

每个缓存页都有对应的描述信息,如缓存页所属表空间、数据页编号、缓存页在Buffer Pool的地址等。

每个缓存页的描述信息本身也是一块数据。每个缓存页的描述数据放在Buffer Pool最前面,然后各个缓存页放在后面。

Buffer Pool中的描述数据大概相当于缓存页大小的5%,也就是每个描述数据占大概是16KB/20=800个字节。

如果设置了Buffer Pool的大小是128MB,则实际上Buffer Pool真正的大小可能是135MB,因为每个缓存页还有对应的描述数据。因此,Buffer Pool的结构看起来像如下的样子:

6.Buffer Pool简单总结

一.缓冲池Buffer Pool的默认大小是128MB

缓冲池Buffer Pool的大小根据服务器的配置来调整。比如服务器的配置是16核32GB,可以给缓冲池Buffer Pool分配2GB内存。

二.数据页是MySQL抽象出来的数据单位

磁盘文件中有很多数据页,每一页中放了很多行数据。如果数据库要更新某一行数据,首先会找到这行数据所在的数据页,然后把这行数据页加载到缓冲池Buffer Pool中。缓冲池Buffer Pool中存放的一个一个的数据页,也被称为缓存页。数据页默认大小为16KB,数据页和缓存页的大小是一样的。

三.Buffer Pool中每个缓存页都有对应的描述数据

描述数据包括:缓存页所属的表空间、数据页的编号、缓存页在Buffer Pool中的地址等。每个缓存页的描述数据放Buffer Pool最前面,各个缓存页放在后面。Buffer Pool里的一个描述数据大小相当于一个缓存页的5%,约800字节。

7.数据库启动时如何初始化Buffer Pool

数据库的Buffer Pool里会包含很多个缓存页,同时每个缓存页还有对应的描述数据。

数据库启动时,会按照设置的Buffer Pool大小,去操作系统申请一块内存区域,作为Buffer Pool的内存区域。申请完毕后,数据库会按照默认的缓存页大小及对应的描述数据大小,在Buffer Pool中划分一个个缓存页和对应的描述数据。

然后当数据库把Buffer Pool划分完毕后,里面的缓存页都是空的。需要等数据库运行起来后执行增删改查操作时:才会把对应的数据页从磁盘里读取出来,放入Buffer Pool中的缓存页里。

8.free链表可判断哪些缓存页是空闲的

当数据库运行起来后,肯定会不停地进行增删改查操作。此时会从磁盘上读取一个个的数据页放入到Buffer Pool中的缓存页里。

默认情况下,磁盘上的数据页和缓存页是一一对应的,都是16KB。Buffer Pool把数据缓存起来后,就可以对数据在内存里执行增删改查。

但是当数据库从磁盘上读取数据页放入Buffer Pool中的缓存页时,首先需要解决一个问题:哪些缓存页是空闲的?

为此,数据库为Buffer Pool设计了一个free链表,它是一个双向链表。在这个free链表里,每个节点就是一个空闲缓存页的描述数据块的地址。只要一个缓存页是空闲的,则其描述数据块的地址就会被放入free链表中。所以数据库刚启动时,如果此时所有的缓存页都是空闲的,那么所有缓存页的描述数据块就会被放进该free链表里。

下图展示了一个free链表,这个free链表里的元素就是各个缓存页的描述数据块。只要缓存页是空闲的,则缓存页对应的描述数据块就会加入到free链表中。每个节点都会双向链接自己的前后节点,组成一个双向链表。

此外,这个free链表还有一个基础节点。该基础节点会引用链表的头节点和尾节点,以及存储链表中的描述数据块节点数,也就是会有多少个空闲的缓存页。

9.free链表占用多少内存空间

描述数据块,在Buffer Pool里有一份,在free链表里也有一份。这样是不是内存里就出现了两个一模一样的描述数据块了?

其实不是的。因为这个free链表,本身就是由Buffer Pool里的描述数据块组成的。可以认为每个描述数据块都有两个指针,一个free_pre、一个free_next。这两个指针分别指向自己在free链表的上一个节点、以及下一个节点。

通过Buffer Pool中的描述数据块的free_pre和free_next两个指针,就可以把所有的描述数据块串成一个free链表(双向链表)。

对于free链表而言,只有一个基础节点是不属于Buffer Pool的。基础节点是40字节大小的一个节点。里面存放了free链表的头节点和尾节点地址、及当前链表里有多少个节点。

10.如何读取数据页到Buffer Pool的缓存页

首先需要从free链表里获取一个描述数据块,然后就可以获取到这个描述数据块对应的空闲缓存页,接着把磁盘上的数据页读取到该空闲缓存页里去,同时把相关的一些描述数据写入该空闲缓存页的描述数据块里,最后把那个描述数据块从free链表里移除。

从free链表移除一个描述数据块的为代码如下。假设有一个描述数据块02,它的上一个节点是描述数据块01,下一个节点是描述数据块03,那么描述数据块02的结构是:

//描述数据块
DescriptionDataBlock {
    //这个块就是block02
    block_id = block02
    //在free链表中的上一个节点是block01
    free_pre = block01;
    //在free链表中的下一个节点是block03
    free_next = block03;
}

现在尾节点block03被使用了,要从free链表中移除,那么此时可以直接把block02节点的free_next设置为null即可:

//描述数据块
DescriptionDataBlock {
    //这个块就是block02
    block_id = block02
    //在free链表中的上一个节点是block01
    free_pre = block01;
    //在free链表中的下一个节点是空的
    free_next = null;
}

11.如何知道数据页有没有被缓存

我们在执行增删改查时,首先要看这个数据页有没有被缓存:如果已被缓存,则直接使用;如果没有被缓存,就从free链表中找到一个空闲的缓存页。然后从磁盘上读取数据页写入缓存页,同时写入描述数据,接着在free链表中移除该描述数据块。

所以为了判断数据页有没有被缓存,InnoDB会有一个哈希表。这个哈希表使用表空间号+数据页号作为key,而缓存页地址作为value。当要使用一个数据页时,通过表空间号+数据页号作为key去哈希表查询。如果哈希表中没有就读取磁盘的数据页,如果有则说明数据页已被缓存。

因此,数据库每次读取数据页到缓存后,都会往哈希表中写入一个kv对。key就是表空间号+数据页号,value就是缓存页地址。这样下次如果要使用该数据页,就可以从哈希表里直接读取出缓存页地址,然后根据缓存页地址到Buffer Pool中读取出具体的缓存页数据。

12.空闲缓存页与free链表总结

(1)数据库启动时会按照设置的Buffer Pool大小向OS申请内存

当数据库向OS申请到设置的Buffer Pool大小的内存后,就会在缓冲池中划分出一个个空闲缓存页和相应的描述数据块。

(2)Buffer Pool有一个叫free链表的双向链表

free链表的每个节点是一个空闲缓存页的描述数据块的地址,通过free链表可知哪些缓存页是空闲的。

(3)根据free链表的节点可得到一个空闲缓存页

从free链表中获取一个节点后,根据该节点就能找到对应的空闲缓存页。接着就可以将磁盘中的数据页读取到该空闲缓存页里。同时把该数据页的描述数据写到该空闲缓存页对应的描述数据块里。以及把表空间号 + 数据页号作为key,缓存页地址作为value,写到哈希表。这样下次读取该数据页时可通过key查哈希表,直接从缓冲池里进行读取。

(4)增删改查一条数据时InnoDB引擎会怎么处理

首先InnoDB会获取到对应数据的"表空间号 + 数据页号"。然后根据"表空间号 + 数据页号"作为key,去哈希表中进行查询。如果能查到缓存页地址,则去Buffer Pool中读取对应的缓存页数据。如果哈希表查不到,则说明要将磁盘的数据页读取到缓冲区的缓存页里。

于是会先从free链表里获取一个节点,然后找到其描述数据块的地址。通过该地址可得到一个空闲缓存页,就能把数据页读取到该空闲缓存页里。同时会把描述数据也写到该缓存页的描述数据块里,以及把表空间号 + 数据页号作为key,缓存页地址作为value,写到哈希表。

(5)SQL语句中表和行、表空间和数据页的关系

一.表、列和行都是逻辑概念

数据库里有一个表,表里有几个字段有多少行,这些都是逻辑上的概念。我们作为数据库使用方,并不关注它们具体在数据库磁盘怎么存储。

二.表空间、数据页都是物理概念

在物理层面,表里的数据都放在一个表空间中。表空间由一堆磁盘上的数据文件组成,这些文件里都存放了表的数据。而这些数据又是由一个个的数据页组织起来的。所以表空间、数据页是物理层面的概念。

13.Buffer Pool中会不会有内存碎片

Buffer Pool中会有内存碎片。由于Buffer Pool大小是可以设置的,所以Buffer Pool划分完整的缓存页和描述数据块后,可能还剩一点内存。而这点内存放不下任何一个缓存页,只能放着不能用,这就是内存碎片。

如何减少内存碎片?

数据库在Buffer Pool中划分缓存页时,会让所有缓存页和描述数据块都紧密挨在一起,这样就能尽可能减少内存浪费、减少内存碎片。如果Buffer Pool里的缓存页是东一块西一块,则必然导致缓存页的内存间有很多内存空隙,从而产生大量内存碎片。

14.脏数据到底为什么会脏

MySQL在执行增删改语句时,如果在哈希表中发现数据页没有缓存,则会基于free链表找到一个空闲的缓存页,然后将数据页读取到缓存页里。如果在哈希表中发现数据页已缓存,那么会直接使用缓存页。

因此,无论如何,要更新的数据页都会在Buffer Pool的缓存页里。MySQL是基于Buffer Pool内存来执行具体的增删改查操作的。

所以,当MySQL去更新Buffer Pool的缓存页中的数据时,一旦更新完,则缓存页里的数据和磁盘上数据页的数据就不一致了。这时就说该缓存页是脏数据,或者脏页。

15.flush链表可判断哪些缓存页是脏页

在Buffer Pool里,有些缓存页经过修改是脏页,有些则只有查而不是脏页。所以为了方便数据库从缓存页中区分出脏页,数据库引入了一个跟free链表类似的flush链表。

flush链表也是通过缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块组成一个双向链表的,其中这两个指针分别是flush_pre、flush_next。

凡是被修改过的缓存页,都会把它的描述数据块加入到flush链表中。flush的意思就是这些都是脏页,后续都是要flush刷新到磁盘上去的。

16.flush链表的伪代码

下面用伪代码来展示一下这个flush链表的构造过程。比如现在缓存页01被修改了数据,那么它就是脏页了,此时就必须把它加入到flush链表中。

假设缓存页01的描述数据块如下所示:

//描述数据块
DescriptionDataBlock {
    //这是缓存页01的数据块
    block_id = block01
    //在free链表中的上一个节点和下一个节点(由于这个缓存页已经存储了数据页放在了缓冲池里, 所以肯定不在free链表了)
    //所以free链表中的两个指针都是null
    free_pre = null
    free_next = null
    //在flush链表中的上一个节点和下一个节点
    //现在因为flush链表中就它一个节点, 所以也是null
    flush_pre = null
    flush_next = null
}
//flush链表的基础节点
FlushLinkListBaseNode {
    //基础节点指向链表起始节点和结束节点的指针
    //flush链表中目前就一个缓存页01, 所以指向它的描述数据块
    start = block01
    end = block01
    //flush链表中有几个节点
    count = 1
}

现在flush链表的基础节点就指向了一个block01的节点。假如接下来缓存页02也被更新了,这时候缓存页02也是脏页。那么缓存页02的描述数据块也要被加入到flush链表中去。

//描述数据块
DescriptionDataBlock {
    //这是缓存页01的数据块
    block_id = block01
    //在free链表中的上一个节点和下一个节点(由于这个缓存页已经存储了数据页放在了缓冲池里, 所以肯定不在free链表了)
    //所以free链表中的两个指针都是null
    free_pre = null
    free_next = null
    //在flush链表中的上一个节点和下一个节点
    //现在因为flush链表中就它是起始节点, 所以它的flush_pre指针是null
    flush_pre = null
    //然后flush链表中它的下一个节点是block02, 所以flush_next指向block02
    flush_next = block02
}
//描述数据块
DescriptionDataBlock {
    //这是缓存页02的数据块
    block_id = block02
    //在free链表中的上一个节点和下一个节点(由于这个缓存页已经存储了数据页放在了缓冲池里, 所以肯定不在free链表了)
    //所以free链表中的两个指针都是null
    free_pre = null
    free_next = null
    //在flush链表中的上一个节点和下一个节点
    //现在因为flush链表中就它是尾节点, 所以它的上一个节点是block01, 它的下一个节点是null
    flush_pre = block01
    flush_next = null
}

由此可见,当数据库更新缓存页时,通过变换缓存页中的描述数据块的flush链表的指针,可以把脏页的描述数据块组成一个双向链表,也就是flush链表。flush链表的基础节点会指向起始节点和尾节点。通过flush链表,就可记录哪些缓存页是脏页了。

17.flush链表和脏页总结

一.通过free链表来管理所有空闲的数据页

加载磁盘的数据页时,先通过free链表拿到空闲的缓存页地址,然后再把磁盘的数据页写到这个Buffer Pool中的缓存页里。

二.通过哈希表来管理所有在Buffer Pool缓存过的数据页

根据哈希表可以快速在Buffer Pool中查找出缓存的数据页。

三.通过flush链表来管理所有被更新后的等待被刷盘的缓存页

free链表和flush链表都通过使用地址指针来大大减少内存的占用。free链表和flush链表的节点都是由缓存页的描述数据块来实现的。free链表和flush链表都通过两个指针来构成双向链表。

18.如果Buffer Pool中的缓存页不够了怎么办

上面介绍了:Buffer Pool中缓存页的划分、free链表的使用、数据页是如何加载到缓存页、对缓存页修改后flush链表如何记录脏页。

当数据库执行增删改查时,都会把磁盘上的数据页加载到缓存页里。而且在加载过程中必然要把磁盘的数据页加载到空闲的缓存页里。所以才会首先从free链表中找一个空闲的缓存页,然后把磁盘上的数据页加载到该空闲的缓存页里。

随着数据库不停地把磁盘上的数据页加载到空闲的缓存页里,free链表中的空闲缓存页就会越来越少,直到free链表没有空闲缓存页;这时就要淘汰掉一些缓存页。

淘汰缓存页:也就是把一个缓存页里被修改过的数据刷到磁盘的数据页里,然后这个缓存页就可以清空了,让它重新变成一个空闲的缓存页。既然要把缓存页的数据刷入磁盘,那么应该把哪些缓存页的数据刷入磁盘?

19.淘汰缓存页与缓存命中率

要处理"应该把哪些缓存页的数据给刷入磁盘"的问题,就需要缓存命中率。

假设现在有两个缓存页。一个缓存页的数据,经常被修改和查询。比如在100次请求中有30次都是在查询和修改这个缓存页里的数据,那么此时可认为这个缓存页的数据的缓存命中率很高。另一个缓存页的数据,偶尔被修改和查询。比如从磁盘加载到缓存页后的100次请求只修改和查询过1次,那么此时可认为这个缓存页的数据的缓存命中率有点低。

这时倾向于将命中率低的缓存页进行淘汰,让经常访问的缓存页留下来,淘汰掉很少被访问的缓存页。

20.引入LRU链表来判断哪些缓存页是不常用的

(1)简单LRU链表的工作原理

(2)简单LRU链表可能存在的预读问题

(3)触发MySQL预读机制的情况

(4)简单LRU链表可能存在的全表扫描问题

(5)总结

要知道哪些缓存页经常被访问、哪些缓存页很少被访问,可借助LRU链表。LRU就是Least Recently Used,最近最少使用的意思。

(1)简单LRU链表的工作原理

假设InnoDB从磁盘加载一个数据页到缓存页时,就把这个缓存页的描述数据块放到LRU链表头部去。

那么只要一个缓存页有数据,那么该缓存页就会在LRU里。并且最新加载数据的缓存页,会被放到LRU链表的头部。

假设某个缓存页的描述数据块本来在LRU链表的尾部,后面只要查询或者修改了这个缓存页的数据,也会把其描述数据块挪动到LRU链表头部。

总之,就是保证最近被访问过的缓存页,一定在LRU链表的头部。这样当缓冲区没有空闲的缓存页时,可以在LRU链表尾部找一个缓存页。而这个缓存页就是最近最少被访问的那个缓存页。然后把LRU链表尾部的那个缓存页刷入磁盘从而腾出一个空闲的缓存页,最后把需要的磁盘数据页加载到这个空闲的缓存页中即可。

这个LRU链表需要一定长度,不能只有2个节点。否则如果先是节点1被访问100次,接着到节点2被访问。这样虽然链表尾部是节点1,但实际上节点1是最近最少被访问的。

(2)简单LRU链表可能存在的预读问题

在LRU链表的尾部,一定是最近最少被访问的那个缓存页。但这个LRU机制在实际运行中,面对MySQL的预读机制,会有问题。

MySQL预读,指的是从磁盘加载一个数据页时,可能会连带着把这个数据页相邻的其他数据页,也加载到缓存里。比如现在有两个空闲缓存页,在加载一个数据页时,就会连带着把其相邻的一个数据页也加载到缓存里去。但是接下来只有一个缓存页被访问了,另外一个通过预读机制加载的缓存页,其实并没被访问,而此时这两个缓存页可能都在LRU链表前面。

上图中,前两个缓存页都是刚加载进来的。但是第二个缓存页是通过预读机制带着加载进来的。这个缓存页被放到了链表的前面,但实际上没人访问。除了第二个缓存页外,第一个缓存页以及最后两个缓存页一直都有访问。

这时如果没有空闲缓存页了,那么在加载新的数据页时,就要从LRU链表尾部把最近最少使用的一个缓存页拿出来清空腾出空闲。但对于上述情况,这是不合理的,合理的应该是把第二个缓存页清空。

(3)触发MySQL预读机制的情况

情况一:参数innodb_read_ahead_threshold默认值是56,意思是如果顺序访问一个区的多个数据页的数量超过了该阀值。就会触发预读机制,把下一个相邻区中的所有数据页都加载到缓存里去。

情况二:Buffer Pool里缓存一个区13个连续的会被频繁访问的数据页,此时就会直接触发预读机制,把这个区里的其他数据页也加载到缓存里。该情况通过参数innodb_random_read_ahead控制,默认OFF表示关闭。

所以,默认情况下第一种情况很可能会触发预读机制。并且第一种情况会一下子把相邻区中很多数据页加载到缓存里。这些缓存页如果都放在LRU链表前面,并且没什么访问了。这样就会导致一些频繁被访问的缓存页放到了LRU链表的尾部。最后造成频繁被访问的缓存页反而被清空掉。而被清空掉的缓存页很快又要从磁盘中重新加载进入缓冲区。这时不但不合理还很影响性能。

(4)简单LRU链表可能存在的全表扫描问题

全表扫描,就是类似于执行这样的SQL语句:select * from users。此时没有加任何一个where条件,这个会导致MySQL把该表所有的数据页,都从磁盘加载到Buffer Pool里。

这时LRU链表中排在前面的缓存页,可能都是全表扫描加载进来的缓存页。而如果这次全表扫描后,后面几乎没有用到这个表里的数据。那此时LRU链表的尾部,也可能都是之前一直被频繁访问的缓存页。这样也会把频繁访问的缓存页给淘汰掉,最后留下不经常访问的全表扫描加载进来的缓存页。

(5)总结

所以如果使用简单的LRU链表机制,其实是漏洞百出的。因为预读机制、全表扫描会把未来并不经常访问的数据页加载到缓存页里,从而导致那些频繁被访问的缓存页不得不处于LRU链表尾部。如果此时恰好需要把一些缓存页刷入磁盘或者清空以腾出空闲的缓存页,那么就会把频繁被访问的缓存页给清空了。

简单LRU链表可能存在的问题:

问题一:预读机制导致相邻数据页也一块被加载到缓冲池。此时在LRU链表中排前面的,可能都是通过预读机制加载进来的。

问题二:全表扫描可能会一下子把一个表的所有数据页都加载到缓冲池,此时在LRU链表中排前面的,可能都是通过全表扫描加载进来的。

触发预读机制的情况:

情况一:参数innodb_read_ahead_threshold的默认值是56。如果顺序访问一个区里多个数据页,访问的数据页的数量超过此阈值。那么就会触发预读机制,将下一个相邻区中所有数据页加载到缓冲池。

情况二:如果缓冲池中缓存了一个区里的13个连续的被频繁访问的数据页,那么就会触发预读机制,将这个区里其他数据页也加载到缓冲池。这种情况由参数innodb_random_read_ahead控制,默认关闭。

21.基于冷热数据分离思想设计LRU链表

(1)LRU链表分为冷数据区域和热数据区域

(2)冷热数据分离如何解决预读和全表扫描问题

(3)LRU链表的热数据区域是如何进行优化的

(4)LRU链表的冷数据区域中都是些什么数据

(5)Redis的冷热数据处理

(1)LRU链表分为冷数据区域和热数据区域

为解决简单LRU链表带来的预读和全表扫描问题,InnoDB设计LRU链表时用了冷热数据分离的思想。

InnoDB的LRU链表,会被拆分为两个部分。一部分是热数据,一部分是冷数据。冷热数据的比例由innodb_old_blocks_pct参数控制,默认是37。这时候,LRU链表看起来如下:

当数据页第一次被加载到内存时,缓存页对应的描述数据块节点会被放在LRU链表的冷数据区域的头部。被加载到内存的数据页,如果在默认1s后继续被访问,则该缓存页对应的描述数据块节点会被挪动到热数据区域的链表头部。对应的innodb_old_blocks_time参数默认就是设置为1s。

如果数据加载到缓存页之后过了1s+的时间,该缓存页被访问,则对应的描述数据块会被放入热数据区域的链表头部。如果数据加载到缓存页之后在1s内,该缓存页被访问,则对应的描述数据块不会被放入热数据区域。

(2)冷热数据分离如何解决预读和全表扫描问题

这套冷热数据分离的机制包含三个方案:

方案一:缓存页分冷热数据加载

方案二:冷数据转化为热数据进行时间限制

方案三:淘汰缓存页时优先淘汰冷数据区域

根据这套方案,简单LRU链表遇到的预读和全表扫描问题都能解决。比如通过预读机制和全表扫描加载进来的数据页,大都在1s内访问一次后就不再访问了,所以这些数据基本留在冷数据区域。当要淘汰缓存页时,优先选择冷数据区域尾部的缓存页,这就很合理了。这样就不会让刚加载进来的缓存页占据LRU链表的头部,导致频繁访问的缓存页突然在LRU链表的尾部而被错误淘汰掉。

(3)LRU链表的热数据区域是如何进行优化的

如果访问了热数据区域中的一个缓存页,是否应该马上把它移动到热数据区域的链表头部?由于热数据区域里的缓存页可能是被经常访问的,所以不建议频繁移动,否则影响性能。

因此LRU链表的热数据区域的访问规则是:只有在热数据区域的后3/4部分的缓存页被访问了,才会移动到链表头部;如果是热数据区域的前面1/4部分的缓存页被访问了,那么不需要移动。这样尽可能减少链表中的节点频繁移动。

(4)LRU链表的冷数据区域中都是些什么数据

大部分都是预读加载进来的缓存页,加载进来1s之后没人访问。或者全表扫描或者一些大的查询语句加载一堆数据到缓存页,结果都是1s之内访问一下,后续就不再访问的数据。

(5)Redis的冷热数据处理

通过对key延长过期时间就可以区分出冷热数据了。具体就是默认key在一定时间过期。热key每次访问都延长过期时间,冷key过期就不在Redis里了。

常见的场景就是电商系统里的商品缓存数据。假设有1亿商品,设计缓存机制时必须考虑热数据的缓存预加载。比如每天统计出哪些商品被访问的次数最多,然后系统晚上启动一个定时任务,把热门商品数据预加载到Redis里。

22.Buffer Pool的缓存页以及几个链表总结

Buffer Pool在被使用时,会频繁从磁盘上加载数据页到缓存页里。然后free链表、flush链表、LRU链表都会被同时使用,这三个链表都是双向链表。

一.当加载一个数据页到一个缓存页时

InnoDB就会从free链表里移除这个缓存页。然后会把这个缓存页放入到LRU链表的冷数据区域头部。

二.当修改一个缓存页时

InnoDB就会在flush链表中记录这个脏页。而且可能会把该缓存页从LRU链表的冷数据区域移动到热数据区域头部。

三.当查询一个缓存页时

InnoDB可能会把该缓存页从LRU链表冷数据区域移动到热数据区域头部,或者从LRU链表的热数据区域其他位置移动到热数据区域头部。

总之,MySQL在执行增删改查时:首先会大量操作缓存页以及对应的几个链表。然后当缓存页满时,会基于LRU链表淘汰缓存页。也就是先把要淘汰的缓存页刷入磁盘,然后清空该缓存页。接着再把需要的数据页加载到空闲的缓存页中。

23.LRU链表冷数据区域的缓存页何时刷盘

(1)LRU链表的冷数据区域的缓存页刷盘的几个时机

(2)如何避免缓存页都用完了(设置Buffer Pool很大的内存空间)

(1)LRU链表的冷数据区域的缓存页刷盘的几个时机

时机一:定时把LRU尾部的部分缓存页刷入磁盘

第一个时机并不是在缓存页满的时候,才会将缓存页刷入磁盘。而是有一个后台定时任务线程,该线程会定时把LRU链表的冷数据区域尾部的一些缓存页刷入磁盘。然后清空几个缓存页,并将这些缓存页加回free链表。

时机二:把flush链表中的一些缓存页定时刷入磁盘

如果仅仅是把LRU链表中冷数据区域的缓存页刷入磁盘,还是不够的。因为在LRU链表的热数据区域里很多缓存页可能也会被频繁的修改,这些缓存页不可能永远都不刷入磁盘中。

所以这个后台线程同时也会在MySQL不怎么繁忙时,找个时间把flush链表中的缓存页都刷入磁盘中。只要flush链表中的缓存页被刷入磁盘,则这些缓存页也会从flush链表和LRU链表中移除,然后加入到free链表中。

时机三:实在没有空闲缓存页时

假设所有的free链表都被使用,同时flush链表中有很多被修改过的缓存页,以及LRU链表中也有很多缓存页进行冷热数据分离。此时如果要从磁盘加载数据页到一个空闲缓存页中,就会从LRU链表的冷数据区域尾部找到一个缓存页,刷入磁盘和清空。

(2)如何避免缓存页都用完了(设置Buffer Pool很大的内存空间)

此时需要先把一个缓存页刷入磁盘腾出空闲缓存页,再从磁盘读取数据页。这种情况要执行两次磁盘IO,性能低下。一次是缓存页刷入磁盘,一次是从磁盘读取数据页加载到缓存页。

由于InnoDB在使用缓存页的过程中,会有一个后台线程定时地把LRU链表冷数据区域的一些缓存页刷入磁盘。所以缓存页是一边被使用,一边被后台线程定时地释放。如果缓存页被使用得很快,而后台线程释放缓存页的速度很慢,那么必然频繁出现缓存页被使用完的情况。

从InnoDB角度看,它无法控制缓存页被使用的速度。因为缓存页被使用的速度依赖于外部服务调用的并发程度。另外InnoDB的后台线程会定时释放一批缓存页,这个过程也很难优化。因为如果频繁释放也会造成磁盘IO频繁,从而影响性能。

所以最后可以依靠InnoDB的Buffer Pool的大小来避免。如果MySQL要抗高并发的访问,那么机器必然要配置很大的内存空间,起码是32G+、64GB、128GB。此时就可以设置Buffer Pool很大的内存空间,如20GB、48GB,80GB。

这样在高并发场景下:虽然Buffer Pool的缓存页被频繁使用,但后台线程也在定时释放缓存页。由于Buffer Pool内存很大,所以可能需要较长时间才会导致缓存页用完。需要的时间越长,那么就越可以撑到数据库访问高峰期已过去。只要高峰一过,后台线程又不停地基于flush链表和LRU链表释放缓存页,那么空闲的缓存页数量又会慢慢多起来。

24.Buffer Pool在访问时是否需要加锁

Buffer Pool本质上是一大块内存,由一大堆的缓存页和描述数据块组成。然后通过free链表、flush链表、LRU链表、哈希表来辅助运行。

如果MySQL同时接收到多个请求,启用了多个线程来处理这些请求。每个线程负责处理一个请求,那么就会出现多个线程并发访问Buffer Pool。此时这些线程都是在访问着内存里的一些共享数据结构:比如缓存页、各种链表,那么是否必须加锁?

多线程并发访问一个Buffer Pool,必然是要加锁的。一个线程完成一系列操作后,才能接着下一个线程执行操作;比如加载数据页到缓存页、更新free链表、更新LRU链表、更新flush链表。

加锁之后数据库的性能怎样?

即便在Buffer Pool里,多个线程加锁串行排队执行,它的性能也不会太差。因为大部分情况下,每个线程都是查询或更新缓存页里的数据。这些操作都是发生在内存里,基本都是微秒级(内存是百纳秒级)。

更新free、flush、LRU这些链表,也都是基于指针的操作,性能也很高。但如果线程拿到锁之后,需要从磁盘里读取数据页加载到缓存页中时,由于发生了磁盘IO,那么耗时就会久一点。

25.多个Buffer Pool优化并发能力

MySQL的生产经验,就是给MySQL设置多个Buffer Pool来优化并发能力。如果Buffer Pool的内存小于1GB,MySQL默认只会给一个Buffer Pool。如果Buffer Pool的内存较大如8G,那么可给MySQL设置多个Buffer Pool。

下面的配置就给Buffer Pool设置了8GB的内存。并且有4个Buffer Pool,每个Buffer Pool的大小就是2GB:

[server]
innodb_buffer_pool_size = 8589934592
innodb_buffer_pool_instances = 4

这样MySQL运行时就会有4个Buffer Pool,每个Buffer Pool负责管理一部分缓存页和描述数据块,每个Buffer Pool拥有独立的free、flush、LR链表。

这时即便多个线程并发来访问也可以把压力分开,比如有的线程访问这个Buffer Pool,有的线程访问另外的Buffer Pool。

通过多个Buffer Pool,MySQL多线程并发访问的性能就会得到提升,多个线程可以在不同的Buffer Pool中加锁和执行自己的操作。

26.通过chunk动态调整运行期的Buffer Pool

MySQL有个chunk机制,这指的是Buffer Pool是由很多个chunk组成的。innodb_buffer_pool_chunk_size参数可以控制chunk的大小,默认是128MB。

假设给MySQL的Buffer Pool设置了总大小是8GB,并且有4个Buffer Pool,那么每个Buffer Pool就是2GB。此时每个Buffer Pool就是由一系列的128MB的chunk组成的,每个Buffer Pool会有16个chunk,每个Buffer Pool里的每个chunk里才是一系列的描述数据块和缓存页,每个Buffer Pool里的多个chunk共享一套free、flush、LRU链表。

当动态调整Buffer Pool大小时,如Buffer Pool大小由8GB动态加到16GB。那么此时只要申请一系列的128M大小的chunk,并且每个chunk是连续的128M内存即可,然后把这些申请到的chunk内存分配到Buffer Pool。这样就无要额外申请16G的连续内存空间,然后把已有的数据进行拷贝。

所以,InnoDB的Buffer Pool的真实数据结构是:由多个Buffer Pool组成的,每个Buffer Pool由多个chunk组成。

27.生产环境应给Buffer Pool设置多少内存、多少Buffer Pool、多大的chunk

通常建议给Buffer Pool设置机器内存的50%~60%左右大小。因为操作系统内核也要使用内存。

所以32GB内存的机器,可以给Buffer Pool设置20GB的内存;128GB内存的机器,可以给Buffer Pool设置80GB的内存。

确定Buffer Pool总大小后,就需要设置多少个Buffer Pool以及chunk大小。Buffer Pool总大小 = (chunk大小 * chunk数量) * Buffer Pool数量

默认chunk的大小是128MB。如果机器的内存是32GB,设置了Buffer Pool总大小是20GB。那么Buffer Pool数量可以设置为10个,每个Buffer Pool由16个chunk组成。因为此时一个Buffer Pool = chunk大小 * chunk数量 = 128MB * 16 = 2GB。10个Buffer Pool就是总大小20GB。

当然也可设置Buffer Pool数量为16个,每个Buffer Pool由10个chunk组成。此时一个Buffer Pool = chunk大小 * chunk数量 = 1280MB。16个Buffer Pool也是总大小20GB。

当然也可设置Buffer Pool数量为32个,每个Buffer Pool由5个chunk组成。此时一个Buffer Pool = chunk大小 * chunk数量 = 640MB。32个Buffer Pool也是总大小20GB;

28.show engine innodb status输出详解

> show engine innodb status ;
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 274857984  //意思是Buffer Pool最终的总大小是多少
Dictionary memory allocated 116177  
Buffer pool size   16382  //意思是Buffer Pool一共能容纳多少个缓存页
Free buffers       16002  //意思是free链表中一共有多少个空闲的缓存页是可用的
Database pages     380    //意思是lru链表中一共有多少个缓存页
Old database pages 0    //冷数据区域里的缓存页数量
Modified db pages  0    //flush链表中的缓存页数量
Pending reads      0    //等待从磁盘上加载进缓存页的数量
Pending writes: LRU 0, flush list 0, single page 0    //即将从lru链表中刷入磁盘的数量,即将从flush链表中刷入磁盘的数量
Pages made young 0, not young 0  //在lru冷数据区域里访问之后转移到热数据区域的缓存页数量,在lru冷数据区域1s内被访问了没进入热数据区域的缓存页数量
0.00 youngs/s, 0.00 non-youngs/s  //每秒从冷数据区域进入热数据区域的缓存页数量,每秒在冷数据区域里被访问了但不能进入热数据区域的缓存页数量
Pages read 345, created 35, written 37  //已经读取、创建和写入了多少个缓存页
0.00 reads/s, 0.00 creates/s, 0.00 writes/s  //每秒读取、创建和写入的缓存页数量
No buffer pool page gets since the last printout
Buffer pool hit rate xxx / 1000,         //每1000次访问有多少次直接命中Buffer Pool里的缓存
young-making rate xxx / 1000 not xx / 1000   //每1000次访问有多少次访问让缓存页从冷数据区域移动到热数据区域以及没移动的缓存页数量
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 380, unzip_LRU len: 0      //lru链表里缓存页的数量
I/O sum[0]:cur[0], unzip sum[0]:cur[0]  //最近50s读取磁盘页的总数和正在读取磁盘页的总数

#牛客创作赏金赛#
MySQL底层原理与应用 文章被收录于专栏

MySQL底层原理与应用

全部评论

相关推荐

点赞 6 评论
分享
牛客网
牛客企业服务