Mysql基础(六):Buffer Pool详细讲解

目录

1、Buffer Pool认识

Buffer Pool总结因为增删改操

2、Buffer Pool的大小?

3、数据是如何放在Buffer Pool中的?

4、磁盘上的数据页和Buffer Pool中的缓存页是如何对应起来?

5、缓存页的描述信息?

6、数据库启动的时候,是如何初始化Buffer Pool的?

7、free链表

8、数据页缓冲哈希表

9、Buffer Pool的内存碎片

Buffer Pool中会不会有内存碎片?

怎么减少内存碎片呢?

10、flush链表

缓存页是脏数据/脏页?

为什么区分脏页呢?

哪些缓存页是脏页呢?

 11、Buffer Pool缓存页满了,怎么办?

12、怎么知道哪些缓存页经常被访问,哪些缓存页很少被访问?

13、LRU问题

触发MySQL的预读机制场景?

14、LRU优化----基于冷热分离

在LRU链表的冷数据区域中的都是什么样的数据呢?

如果在Redis里存放了很多缓存数据,那么此时会不会有类似冷热数据的问题?应该如何优化和解决呢?

15、定时刷新

总结:


1、Buffer Pool认识

        我们在对数据库执行增删改操作的时候,不可能直接更新磁盘上的数据的,因为如果你对磁盘进行随机读写操作,那速度是相当的慢, 随便一个大磁盘文件的随机读写操作,可能都要几百毫秒。如果要是那么搞的话,可能你的数据库每秒也就只能处理几百个请求了  
        你在对数据库执行增删改操作的时候,实际上主要都是针对内存里的Buffer Pool中的数据进行的,也就是你实际上主要是对数据库的内存里的数据结构进行了增删改。
        数据库中的数据实际上最终都是要存放在磁盘文件上的

Buffer Pool总结因为增删改操

        因为增删改操作首先就是针对这个内存中的Buffer Pool里的数据执行的,同时配合了后续的redo log、刷磁盘等机制和操作。所以Buffer Pool就是数据库的一个内存组件,里面缓存了磁盘上的真实数据,然后我们的Java系统对数据库执行的增删改操作,其实主要就是对这个内存数据结构中的缓存数据执行的。

2、Buffer Pool的大小?

        因为Buffer Pool本质其实就是数据库的一个内存组件,你可以理解为他就是一片内存数据结构,所以这个内存数据结构肯定是有一定的大小的,不可能是无限大的。
         这个Buffer Pool默认情况下是128MB ,还是有一点偏小了,我们实际生产环境下完全可以对Buffer Pool进行调整。比如我们的数据库如果是16核32G的机器,那么你就可以给Buffer Pool分配个2GB的内存,使用下面的配置就可以了。
/etc/my.cnf中
[server]
innodb_buffer_pool_size = 2147483648

3、数据是如何放在Buffer Pool中的?

        实际上MySQL对数据抽象出来了一个数据页的概念,他是把 很多行数据放在了一个数据页里,也就是说我 们的磁盘文件中就是会有很多的数据页,每一页数据里放了很多行数据
        所以实际上假设我们要更新一行数据,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页直接给加载到Buffer Pool里去
        也就是说,Buffer Pool中存放的是一个一个的数据页,

4、磁盘上的数据页和Buffer Pool中的缓存页是如何对应起来?

        实际上默认情况下,磁盘中存放的数据页的大小是16KB,也就是说,一页数据包含了16KB的内容。而Buffer Pool默认128M,则可以容纳8192个数据页。

        而 Buffer Pool中存放的一个一个的数据页,我们通常叫做缓存页 ,因为毕竟Buffer Pool是一个缓冲池,里面的数据都是从磁盘缓存到内存去的。
而Buffer Pool中默认情况下, 一个缓存页的大小和磁盘上的一个数据页的大小是一一对应起来的 ,都是16KB。

5、缓存页的描述信息?

        对于每个缓存页,他实际上都会有一个描述信息,这个描述信息大体可以认为是用来描述这个缓
存页的 比如包含如下的一些东西: 这个数据页所属的表空间、数据页的编号、这个缓存页在Buffer Pool中的地址 等。
        每个缓存页都会对应一个描述信息,这个描述信息本身也是一块数据,在Buffer Pool中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面。
        Buffer Pool中的 描述数据大概相当于缓存页大小的5%左右,也就是每个描述数据大概是800个字节左右的大小, 然后假设你设置的buffer pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能有个130多MB的样子,因为他里面还要存放每个缓存页的描述数据。

6、数据库启动的时候,是如何初始化Buffer Pool的?

          申请空间: 数据库只要一启动,就会按照你设置的Buffer Pool大小,稍微再加大一点,去找操作系统申请一块内存区域,作为Buffer Pool的内存区域。
        划分空间: 然后当内存区域申请完毕之后,数据库就会按照默认的缓存页的16KB的大小以及对应的800个字节左右的描述数据的大小,在Buffer Pool中划分出来一个一个的缓存页和一个一个的他们对应的描述数据。此时Buffer Pool的缓存页是都是空的,里面什么都没有,要等数据库运行起来之后,当我们要对数据执行增删改查的操作的时候,才会把数据对应的页从磁盘文件里读取出来,放入Buffer Pool中的缓存页中。

7、free链表

        当你的数据库运行起来之后,你肯定会不停的执行增删改查的操作,此时就需要不停的从磁盘上读 取一个一个的数据页放入Buffer Pool中的对应的缓存页里去,把数据缓存起来,那么以后就可以对这个数据在内存里执行增删改查了。

        但是此时在从磁盘上读取数据页放入Buffer Pool中的缓存页的时候,必然涉及到一个问题, 那就是哪些缓存页是空闲的?
        数据库会为Buffer Pool设计一个free链表 ,他是一个双向链表数据结构, 这个free链表里,每个节点就是一个空闲的缓存页的描述数据块的地址 ,也就是说,只要你一个缓存页是空闲的,那么他的描述数据块就会被放入这个free链表中。 刚开始数据库启动的时候,可能所有的缓存页都是空闲的,因为此时可能是一个空的数据库,一条数据都没有,所以此时所有缓存页的描述数据块,都会被放入这个free链表中
free链表: 这个free链表里面就是各个缓存页的描述数据块,只要缓存页是空闲的,那么他们对应的描述数据块就会加入到这个free链表中,每个节点都会双向链接自己的前后节点,组成一个双向链表。 这个free链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节点,也就是有多少个空闲的缓存页,是40字节大小的节点。
如何将磁盘上的页读取到Buffer Pool的缓存页中去?

        借助free链表,从free链表里获取一个描述数据块,然后对应的找到这个描述数据块对应的空闲缓存页,然后把磁盘上的数据页读到该缓存页中,同时把相关描述数据写到描述数据块中,并将该描述数据块从free链表中去除。

8、数据页缓冲哈希表

如何确定数据页是否被缓存?
        数据库还会有一个哈希表数据结构,他会用表空间号+数据页号,作为一个key,然后缓存页的地址作为value。
        当你要使用一个数据页的时候,通过“表空间号+数据页号”作为key去这个哈希表里查一下,如果没有就读取数据页,如果已经有了,就说明数据页已经被缓存了。 也就是说,每次你读取一个数据页到缓存之后,都会在这个哈希表中写入一个key-value对,key就是表空间号+数据页号,
value就是缓存页的地址,那么下次如果你再使用这个数据页,就可以从哈希表里直接读取出来他已经被放入一个缓存页了

9、Buffer Pool的内存碎片

Buffer Pool中会不会有内存碎片?

        会,因为Buffer Pool大小是你自己定的,很可能Buffer Pool划分完全部的缓存页和描述数据块之后,还剩一点点的内存,这一点点的内存放不下任何一个缓存页了,所以这点内存就只能放着不能用,这就是内存碎片。

怎么减少内存碎片呢?

        数据库在Buffer Pool中划分缓存页的时候,会让所有的缓存页和描述数据块都紧密的挨在一起,这样尽可能减少内存浪费,就可以尽可能的减少内存碎片的产生了

10、flush链表

缓存页是脏数据/脏页?

        你要更新的数据页都会在Buffer Pool的缓存页里,供你在内存中直接执行增删改的操作。接着你肯定会去更新Buffer Pool的缓存页中的数据,此时一旦你更新了缓存页中的数据,那么 缓存页里的数据和磁盘上的数据页里的数据,就不一致了。我们就说缓存页是脏数据,脏页。

为什么区分脏页呢?

        因为在内存里更新的脏页的数据,都是要被刷新回磁盘文件的。但是,不可能所有的缓存页都刷回磁盘的,因为有的缓存页可能是因为查询的时候被读取到Buffer Pool里去的,可能根本没修改过!

哪些缓存页是脏页呢?

        flush链表这个flush链表本质也是通过缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块,组成一个双向链表。凡是被修改过的缓存页,都会把他的描述数据块加入到flush链表中去,flush的意思就是这些都是脏页,后续都是要flush刷新到磁盘上去的。

        故flush链表就是记录脏数据页的。

 11、Buffer Pool缓存页满了,怎么办?

        如果所有的缓存页都被塞了数据了,此时无法从磁盘上加载新的数据页到 缓存页里去了,则就是淘汰掉一些缓存页。
淘汰缓存页: 把一个缓存页里被修改过的数据,给他刷到磁盘上的数据页里去,然后这个缓存页就可以清空了,让他重新变成一个空闲的缓存页。 接着你再把磁盘上你需要的新的数据页加载到这个腾出来的空闲缓存页中去。
缓存命中率:
        假设现在有两个缓存页,一个缓存页的数据,经常会被修改和查询,比如在100次请求中,有30次都是在查询和修改这个缓存页里的数据。那么此时我们可以说这种情况下,缓存命中率很高
为什么呢?因为100次请求中,30次都可以操作缓存,不需要从磁盘加载数据,这个缓存命中率就比较高了。
        另外一个缓存页里的数据,就是刚从磁盘加载到缓存页之后,被修改和查询过1次,之后100次请求中没有一次是修改和查询这个缓存页的数据的,那么此时我们就说缓存命中率有点低,因为大部分请求可能还需要走磁盘查询数据,他们要操作的数据不在缓存中。

12、怎么知道哪些缓存页经常被访问,哪些缓存页很少被访问?

LRU:一个新的LRU链表了,这个所谓的LRU就是Least Recently Used,最近最少使用的意思。

LRU工作原理:假设我们从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到LRU链表头部去,那么只要有数据的缓存页,他都会在LRU里了,而且最近被加载数据的缓存页,都会放到LRU链表的头部。然后假设某个缓存页的描述数据块本来在LRU链表的尾部,后续你只要查询或者修改了这个缓存页的数据,也要把这个缓存页挪动到LRU链表的头部去。

也就是说最近被访问过的缓存页,一定在LRU链表的头部

        当你的缓存页没有一个空闲的时候,你是不是要找出来那个最近最少被访问的缓存页去刷入磁盘?此时你就直接在LRU链表的尾部找到一个缓存页,他一定是最近最少被访问的那个缓存页!

然后你就把LRU链表尾部的那个缓存页刷入磁盘中,然后把你需要的磁盘数据页加载到腾出来的空闲缓存页中就可以了。 

13、LRU问题

MySQL的预读机制:当你从磁盘上加载一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也加载到缓存里去!举个例子,假设现在有两个空闲缓存页,然后在加载一个数据页的时候,连带着把他的一个相邻的数据页也加载到缓存里去了,正好每个数据页放入一个空闲缓存页! 实际上只有一个缓存页是被访问了,另外一个通过预读机制加载的缓存页,其实并没有人访问,此时这两个缓存页可都在LRU链表的前面。 

触发MySQL的预读机制场景?

(1) 有一个参数是innodb_read_ahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制, 把下一个相邻区中的所有数据页都加载到缓存里去。
(2) 如果Buffer Pool里缓存了一个区里的13个连续的数据页,而且这些数据页都是比较频繁会被访问的,此时就会直接触发预读机制, 把这个区里的其他的数据页都加载到缓存里去。 这个机制是通过参数innodb_random_read_ahead来控制的,他默认是OFF,也就是这个规则是关闭的。
        所以默认情况下, 主要是第一个规则可能会触发预读机制,一下子把很多相邻区里的数据页加载到缓存里 去,这些缓存页如果一下子都放在LRU链表的前面,而且他们其实并没什么人会访问的话,那就会如上图,导致本来就在缓存里的一些频繁被访问的缓存页在LRU链表的尾部。

全表扫描机制:能导致频繁被访问的缓存页被淘汰的场景,那就是全表扫描

SELECT * FROM USERS

        此时他没加任何一个where条件,会导致他直接一下子把这个表里所有的数据页,都从磁盘加载到Buffer Pool里去。 这个时候他可能会一下子就把这个表的所有数据页都一一装入各个缓存页里去!此时可能LRU链表中排在前面的一大串缓存页,都是全表扫描加载进来的缓存页!那么如果这次全表扫描过后,后续几乎没用到这个表里的数据呢? 此时LRU链表的尾部,可能全部都是之前一直被频繁访问的那些缓存页! 然后当你要淘汰掉一些缓存页腾出空间的时候,就会把LRU链表尾部一直被频繁访问的缓存页给淘汰掉了,而留下了之前全表扫描加载进来的大量的不经常访问的缓存页!

14、LRU优化----基于冷热分离

真正MySQL在设计LRU链表的时候,采取的实际上是冷热数据分离的思想。

之前一系列的问题都是因为所有缓存页都混在一个LRU链表里,才导致的么?
        真正的LRU链表,会被拆分为两个部分,一部分是热数据,一部分是冷数据,这个冷热数据的比例是由innodb_old_blocks_pct参数控制的,他默认是37,也就是说冷数据占比37%。

原理:数据页第一次被加载到缓存页之后,这个缓存页是放在LRU链表的冷数据区域的头部的,然后必须是1s过后访问换个缓存页,他才会被移动到热数据区域的链表头部。 

        MySQL设定了一个规则,他设计了一个
        
        innodb_old_blocks_time参数,默认值1000,也就是1000毫秒

        因为那种预读机制以及全表扫描机制加载进来的数据页,大部分都会在1s之内访问一下,之后可能就再也不访问了,所以这种缓存页基本上都会留在冷数据区域里。然后频繁访问的缓存页还是会留在热数据区域里。当你要淘汰缓存的时候,优先就是会选择冷数据区域的尾部的缓存页,这就是非常合理的了!

在LRU链表的冷数据区域中的都是什么样的数据呢?

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

如果在Redis里存放了很多缓存数据,那么此时会不会有类似冷热数据的问题?应该如何优化和解决呢?

我们在设计缓存机制的时候,经常会考虑热数据的缓存预加载。

        也就是说,每 天统计出来哪些商品被访问的次数最多,然后晚上的时候,系统启动一个定时作业,把这些热门商品的数据,预加载到Redis里 那么第二天是不是对热门商品的访问就自然会优先走Redis缓存了?

15、定时刷新

        并不是在缓存页满的时候,才会挑选LRU冷数据区域尾部的几个缓存页刷入磁盘,而是

        有一个后台 线程,他会运行一个定时任务,这个定时任务每隔一段时间就会把LRU链表的冷数据区域的尾部的一些缓存页,刷入磁盘里去,清空这几个缓存页,把他们加入回free链表去

        这个后台线程同时也会在MySQL不怎么繁忙的时候,找个时间把flush链表中的缓存页都刷入磁盘中,这样被你修改过的数据,迟早都会刷入磁盘的

总结:

注意: 我们写SQL时对应表 + 行,在MySQL内部是表空间 + 数据页。
         当我们操作增删改数据时,首先通过 “表空间号 + 数据页号” 作为key 去数据页缓存哈希表查一下,如果有说明已经缓存了,如果没有就读取数据页。读取数据页时:从free链表找到一个空闲的缓存页,从磁盘读取数据页到缓存页,写入描述数据,从free链表移除这个描述数据块。
  • 13
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值