InnoDB的Buffer Pool简介

这篇非常重要!这篇非常重要!这篇非常重要!重要的事情说三遍,这篇是后续事务和锁的基础,一定要看懂这篇,反正我写的已经够白话了,你要再看不懂呢,那你告诉我,我改还不行么~下边是建议正文:
1. 最好使用电脑观看。
2. 如果你非要使用手机观看,那请把字体调整到最小,这样观看效果会好一些。
3. 碎片化阅读并不会得到真正的知识提升,要想有提升还得找张书桌认认真真看一会书,或者我们公众号的文章。
4. 如果觉得不错,各位帮着转发转发,如果觉得有问题或者写的哪不清晰,务必私聊我~
5. 本公众号的文章都是需要被系统性学习的,在阅读本篇文章前最好已经阅读过下边几篇文章,要不然可能会有阅读不畅的体验:
表空间的编号
我们在唠叨的时候就已经说过,存储引擎是使用来存储的,又可以被分为系统表空间和独立表空间。为了方便管理,每个都会有一个字节的编号,值得注意的一点是,系统表空间的编号始终为,也会根据一定规则给其他独立表空间也编上号~
所以,当我们查看或修改某个的数据的时候,实际上需要同时知道表空间的编号和该页的编号,也就是的组合才能定位到某一个具体的。如果你有认真看前边唠叨的那篇文章,肯定记得每个的编号也是占用个字节,而在一个内页的编号是不能重复的,个字节是个二进制位,也就是说:一个表空间最多拥有2³²个页,默认情况下一个页的大小为16KB,也就是说一个表空间最多存储64TB的数据~
 
缓存的重要性
     所谓的只不过是对文件系统上一个或几个实际文件的抽象,我们的数据说到底还是存储在磁盘上的。但是各位也都知道,磁盘的速度慢的跟乌龟一样,怎么能配得上“快如风,疾如电”的呢?所以将内存作为缓存也是无奈之举。MySQL服务器在处理客户端的请求时,当需要访问某个页的数据时,就会把完整的页的数据全部加载到内存中,也就是说即使我们只需要访问一个数据页的一条记录,那也需要先把整个页的数据加载到内存中。这是为了不必每次请求都去访问一下磁盘,那得多慢啊~
对某个的访问类型分为两种,一种是只读访问,一种是写入访问。只读访问好办,就是把磁盘上的加载到内存中读而已;而如果需要修改该页的数据就有点尴尬了,首先会把数据写到内存中的页中,然后在某个合适的时刻将修改过的页同步到磁盘上,同步的时候断电咋办?数据就丢了么?那支付宝或者微信支付的童鞋们还不得紧张死,天天祈祷神千万不要宕机,宕机的话好多人就要家破人亡了?当然不是了,凡是成熟的数据库系统都会有一套完整的机制来保证写入过程要么完整的完成,要么就把已经写入的数据恢复到之前没写的情况,总之不会家破人亡的~ 我们后边几篇文章的内容就是仔细唠叨这个过程是怎么实现的,哈哈,是不是有点儿小刺激~ 不过本篇文章作为先导篇,大家先得搞清楚这块缓存是个神马玩意儿,是用啥方式来管理这块儿缓存的~
 
InnoDB的Buffer Pool
      啥是个Buffer Pool?设计的大叔为了缓存磁盘中的,向操作系统申请了一片连续的内存,他们给这片内存起了个名,叫做(中文名是),那它有多大呢?这个其实看我们机器的配置,如果你是土豪,你有内存,你分配个几百G作为也可以啊,当然你要是没那么有钱,设小点也行呀~ 默认情况下只有大小。当然如果你嫌弃这个太大或者太小,可以在启动服务器的时候配置参数的值,它表示的大小,就像这样:其中,的单位是字节,也就是我指定的大小为。需要注意的是,也不能太小,最小值为(当小于该值时会自动设置成)。

Buffer Pool内部组成
我们已经知道这个其实是一片连续的内存空间,那现在就面临这个问题了:怎么将磁盘上的页缓存到内存中的中呢?直接把需要缓存的页向里一个一个往里怼么?不不不,为了更好的管理这些被缓存的,为每一个缓存页都创建了一些所谓的,这些控制信息包括该页所属的表空间编号、页号、页在中的地址,一些锁信息以及信息(锁和我们之后会具体唠叨,现在可以先忽略),当然还有一些别的控制信息,我们这就不全唠叨一遍了,挑重要的说嘛哈哈~
       每个缓存页对应的控制信息占用的内存大小是相同的,我们就把每个页对应的控制信息占用的一块内存称为一个吧,控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,所以整个对应的内存空间看起来就是这样的:咦?控制块和缓存页之间的那个是个什么玩意儿?你想想啊,每一个控制块都对应一个缓存页,那在分配足够多的控制块和缓存页后,可能剩余的那点儿空间不够一对控制块和缓存页的大小,自然就用不到喽,这个用不到的那点儿内存空间就被称为了。当然,如果你把的大小设置的刚刚好的话,也可能不会产生~
 
FREE链表的管理
       当我们最初启动服务器的时候,需要完成对的初始化过程,就是分配的内存空间,把它划分成若干对控制块和缓存页。但是此时并没有真实的磁盘页被缓存到中(因为还没有用到),之后随着程序的运行,会不断的有磁盘上的页被缓存到中,那么问题来了,从磁盘上读取一个页到中的时候该放到哪个缓存页的位置呢?或者说怎么区分中哪些缓存页是空闲的,哪些已经被使用了呢?我们最好在某个地方记录一下哪些页是可用的,我们可以把所有空闲的页包装成一个节点组成一个链表,这个链表也可以被称作(或者说空闲链表)。因为刚刚完成初始化的中所有的缓存页都是空闲的,所以每一个缓存页都会被加入到中,假设该中可容纳的缓存页数量为,那增加了的效果图就是这样的:
图片
 
       从图中可以看出,我们为了管理好这个,特意为这个链表定义了一个,里边儿包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。我们在每个的节点中都记录了某个缓存页控制块的地址,而每个缓存页控制块都记录着对应的缓存页地址,所以相当于每个Free链表节点都对应一个空闲的缓存页。
有了这个事儿就好办了,每当需要从磁盘中加载一个页到中时,就从中取一个空闲的缓存页,并且把该缓存页对应的的信息填上,然后把该缓存页对应的节点从链表中移除,表示该缓存页已经被使用了~
 
缓存页的哈希处理
 
       我们前边说过,当我们需要访问某个页中的数据时,就会把该页加载到中,如果该页已经在中的话直接使用就可以了。那么问题也就来了,我们怎么知道该页在不在中呢?难不成需要依次遍历中各个缓存页么?一个中的缓存页这么多都遍历完岂不是要累死?
      再回头想想,我们其实是根据来定位一个页的,也就相当于是一个,就是对应的,怎么通过一个来快速找着一个呢?哈哈,那肯定是哈希表喽~
所以我们可以用作为,作为创建一个哈希表,在需要访问某个页的数据时,先从哈希表中根据看看有没有对应的缓存页,如果有,直接使用该缓存页就好,如果没有,那就从中选一个空闲的缓存页,然后把磁盘中对应的页加载到该缓存页的位置。
 
FLU链表的管理
 
       如果我们修改了中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为(英文名:)。当然,最简单的做法就是每发生一次修改就立即同步到磁盘上对应的页上,但是频繁的往磁盘中写数据会严重的影响程序的性能(毕竟磁盘慢的像乌龟一样)。所以每次修改缓存页后,我们并不着急立即把修改同步到磁盘上,而是在未来的某个时间点进行同步,至于这个同步的时间点我们后边的文章会特别详细的说明的,现在先不用管哈~
但是如果不立即同步到磁盘的话,那之后再同步的时候我们怎么知道中哪些页是,哪些页从来没被修改过呢?总不能把所有的缓存页都同步到磁盘上吧,假如被设置的很大,比方说,那一次性同步这么多数据岂不是要慢死!所以,我们不得不再创建一个存储脏页的链表,凡是修改过的缓存页都会被包装成一个节点加入到这个链表中,因为这个链表中的页都是需要被刷新到磁盘上的,所以也叫,有时候也会被简写为。链表的构造和差不多,这就不赘述了。
 
LRU链表的管理
 
      缓存不够的窘境,对应的内存大小毕竟是有限的,如果需要缓存的页占用的内存大小超过了大小,也就是中已经没有多余的空闲缓存页的时候岂不是很尴尬,发生了这样的事儿该咋办?当然是把某些旧的缓存页从中移除,然后再把新的页放进来喽~ 那么问题来了,移除哪些缓存页呢?
为了回答这个问题,我们还需要回到我们设立的初衷,我们就是想减少和磁盘的交互,最好每次在访问某个页的时候它都已经被缓存到中了。假设我们一共访问了次页,那么被访问的页已经在缓存中的次数除以就是所谓的,我们的期望就是让越高越好~ 从这个角度出发,回想一下我们的微信聊天列表,排在前边的都是最近很频繁使用的,排在后边的自然就是最近很少使用的,假如列表能容纳下的联系人有限,你是会把最近很频繁使用的留下还是最近很少使用的留下呢?废话,当然是留下最近很频繁使用的了~
 
简单的LRU链表
       管理的缓存页其实也是这个道理,当中不再有空闲的缓存页时,就需要淘汰掉部分最近很少使用的缓存页。不过,我们怎么知道哪些缓存页最近频繁使用,哪些最近很少使用呢?呵呵,神奇的链表再一次派上了用场,我们可以再创建一个链表,由于这个链表是为了的原则去淘汰缓存页的,所以这个链表可以被称为(Least Recently Used)。当我们需要访问某个时,可以这样处理:
如果该页不在中,在把该页从磁盘加载到中的缓存页时,就把该缓存页包装成节点塞到链表的头部。
如果该页在中,则直接把该页对应的节点移动到链表的头部。
也就是说:只要我们使用到某个缓存页,就把该缓存页调整到的头部,这样尾部就是最近最少使用的缓存页喽~ 所以当中的空闲缓存页使用完时,到的尾部找些缓存页淘汰就OK啦,真简单,啧啧…
 
划分区域的LRU链表
高兴的太早了,上边的这个简单的用了没多长时间就发现问题了,有的小伙伴可能会写一些需要扫描全表的查询语句(比如没有建立合适的索引或者压根儿没有WHERE子句的查询),扫描全表意味着什么?意味着将访问到该表所在的所有数据页!假设这个表中记录非常多的话,那该表会占用特别多的,当需要访问这些页时,会把它们统统都加载到中,这也就意味着吧唧一下,中的所有数据页都被换了一次血,其他查询语句在执行时又得执行一次从磁盘加载到的操作。而这种全表扫描的语句执行的频率也不高,每次执行都要把中的缓存页换一次血,这严重的影响到其他查询对Buffer Pool的使用,严重的降低了缓存命中率!这能忍么?这肯定不能忍啊!再想想办法,把这个按照一定比例分成两截,分别是:
一部分存储使用频率非常高的缓存页,所以也叫做,或者称。另一部分存储使用频率不是很高的缓存页,所以也叫做,或者称。为了方便大家理解,我们把示意图做了简化,各位领会精神就好:
图片
 
        大家要特别注意一个事儿:我们是按照某个比例将 LRU链表 分成两半的,不是某些节点固定是young区域的,某些节点固定式old区域的,随着程序的运行,某个节点所属的区域也可能发生变化。所以把一个完整的分成了和两个部分之后,修改链表的方式也就可以变一变了:
如果某个页第一次从磁盘加载到中,则放到区域的头部。
如果该页已经在中,则将其放到区域的头部,也就是的头部。
这样搞有啥好处呢?在没有空闲的缓存页时,我们可以从old区域中淘汰一些页,而不影响young区域中的缓存页。这样全表扫描的页虽然也会进入中,但是由于首次缓存时只会放到区域,区域不受影响,也就是只会对造成部分换血,而不是全部换血,这在一定程度上降低了全表扫描对的缓存命中率的影响。
那这个划分成两截的比例怎么确定呢?对于存储引擎来说,我们可以通过查看系统变量的值来确定区域在中所占的比例,比方说这样:
从结果可以看出来,默认情况下,区域在中所占的比例是,也就是说区域大约占的。这个比例我们是可以设置的,我们可以在启动时修改参数来控制区域在中所占的比例,比方说这样修改配置文件:
这样我们在启动服务器后,区域占的比例就是。当然,如果在服务器运行期间,我们也可以修改这个系统变量的值,不过需要注意的是,这个系统变量属于,一经修改,会对所有客户端生效,所以我们只能这样修改:
更进一步优化LRU链表
这就说完了么?没有,早着呢~ 首次从磁盘上加载到的页会放到区域,第二次访问该页的时候便会被放到区域,那如果在很短的时间内进行了两次全表扫描操作岂不是会把区域的节点都移动到区域了,那相当于又把给破坏掉了,咋办?
我们可以设置一个间隔时间,当第二次访问区域的某个缓存页时(该缓存页没有被淘汰掉),如果距离上一次访问的时间小于这个时间,那就不把这个缓存页放到区域,这个过程称之为;而如果距离上一次访问的时间不小于这个时间,那就把这个缓存页放到区域,这个过程称之为。这样就可以降低在短时间内有大量全表扫描对的缓存命中率的影响。中这个间隔时间是由系统变量控制的,你看:
在我的电脑上的值是,它的单位是毫秒,也就意味着如果在1秒内发生了多次全表扫描,这些在区域的页也不会被加入到区域的~ 当然,像一样,我们也可以在服务器启动或运行时设置的值,这里就不赘述了,你自己试试吧~
还有一个问题,对于区域的缓存页来说,我们每次访问一个缓存页就要把它移动到的头部,这样开销是不是太大啦,毕竟在区域的缓存页都是热点数据,也就是可能被经常访问的,这样频繁的对进行节点移动操作是不是不太好啊?是的,为了解决这个问题其实我们还可以提出一些优化策略,比如只有被访问的缓存页其于区域的(这个值可调节)之后,才会被移动到头部,这样就可以降低调整的频率,从而提升性能。
还有木有什么别的针对的优化措施呢?当然有啊,你要是好好学,写篇论文,写本书都不是问题,可是我们毕竟是一个介绍MySQL基础知识的文章,再说多了篇幅就受不了了,适可而止,想了解更多的优化知识,自己去看源码或者更多关于链表的知识喽~ 另外,不同的大公司,可能会针对自己的业务对链表进行自己的定制,优化是无穷尽的,但是千万别忘了我们的初心:尽量提高Buffer Pool的缓存命中率。
其他的一些链表
为了更好的管理中的缓存页,除了我们上边提到的一些措施,设计的大叔们还引进了其他的一些,比如用于管理解压页,用于管理存储没有被解压的压缩页,用来管理被压缩的页等等,反正是为了更好的管理这个引入了各种链表,构造和我们介绍的链表都差不多,具体的使用方式就不啰嗦了,大家有兴趣深究的再去找些更深的书或者直接看源代码吧~
InnoDB中对各种列表的处理
上边对各种链表的介绍,只是从我们初学者的学习原理的角度去看的,设计的大叔在真正实现这些链表上又下了一番苦功夫,都是为了节省内存和挺高性能而做的努力。比方说
实际的链表节点并不是独立于的而存在的,而是被放在了缓存页控制块中。
虽然为了不同的目的我们提出了很多的链表,但是每个缓存页控制块其实只有一个节点和一个通用的链表节点。为了节省内存,针对缓存页所处于的不同状态,对缓存页控制块的通用链表节点进行了复用,比方说在该缓存页空闲时,该节点代表的节点;比方说该缓存页被修改时,该节点代表的节点,吧啦吧啦~
当然,如果你看不懂我上边在说啥(本来也没打算让你看懂),那就不用看了,这只是设计的大叔在针对具体的场景做的优化方案,待你真正动手去设计一个存储引擎时,你才会考虑如何更好地实现我们上边的这些原理,现在可以先跳过,设计的大叔们为了更好地实现,使用了好几万行代码,要说清楚这些,怎么也得好几篇文章了,我没那个时间,等以后牛逼有空了,我再来仔细说清楚针对这些链表实现的具体细节~
 
多个Buffer Pool实例
       我们上边说过,本质是向操作系统申请的一块连续的内存空间,在多线程环境下,为了保护缓存页可能会对缓存页进行加锁处理啥的(具体怎么锁我们后边的文章会唠叨),在特别大而且多线程并发访问特别高的情况下,单一的可能会影响请求的处理速度。所以在特别大的时候,我们可以把它们拆分成若干个小的,每个都称为一个,它们都是独立的,独立的去申请内存空间,独立的管理各种链表,独立的吧啦吧啦,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。我们可以在服务器启动的时候通过设置的值来修改的个数,比方说这样:
这样就表明我们要创建2个。那每个实际占多少内存空间呢?其实使用这个公式算出来的:
也就是总共的大小除以个数,结果就是每个占用的大小。
不过也不是说实例创建的越多越好,分别管理各个也是需要性能开销的,设计的大叔们规定:Buffer Pool的大小小于1G的时候设置多个实例是无效的,InnoDB会默认把innodb_buffer_pool_instances 的值修改为1。而我们鼓励在大小大于1G的时候设置多个Buffer Pool实例。
InnoDB中查看的状态信息
作为MySQL的管理人员,我们有时候需要查看一下中的情况,设计的大叔们给我们提供了这样的查询请求,就像这样(为了突出重点,我们只把输出中关于的部分提取了出来):
虽然这里头的参数我们并不能全部看懂,但是有一些还是很眼熟的:
代表该可以容纳多少缓存,注意,单位是!
代表当前还有多少空闲缓存页,也就是中还有多少个节点。
代表链表中的页的数量,包含和两个区域的节点数量。
代表链表区域的节点数量。
代表脏页数量,也就是中节点的数量。
代表从区域移动到区域的节点数量,代表区域没有移动到区域就被淘汰的节点数量。后边跟着移动的速率。
代表读取,创建,写入了多少页。后别跟着读取、创建、写入的速率。
代表中节点的数量。
代表非大小的页的数量,这些非大小的页都是被管理的,我们也没多唠叨它,看不懂就忽略它吧~
其他的参数我们目前不需要理解,之后遇到的话会仔细说的~
 
 
总结
我们可以通过的组合可以定位到某一个具体的。磁盘太慢,用内存作为缓存很有必要。本质上是向操作系统申请的一段连续的内存空间,可以通过来调整它的大小由控制块和缓存页组成,每个控制块和缓存页都是一一对应的,在填充足够多的控制块和缓存页的组合后,剩余的空间可能产生不够填充一组控制块和缓存页,这部分空间不能被使用,也被称为。
使用了许多来管理。
中每一个节点都代表一个空闲的缓存页,在将磁盘中的页加载到时,会从中寻找空闲的缓存页。
为了快速定位某个页是否被加载到,使用作为,缓存页作为,建立哈希表。
在中被修改的页称为,脏页并不是立即刷新,而是被加入到中,待之后的某个时刻同步到磁盘上。
分为和两个区域,可以通过来调节区域所占的比例。首次从磁盘上加载到的页会被放到区域的头部,如果在间隔时间后该页该页没有被淘汰掉并且仍在区域时,会把它放到链表的头部,也就是区域的头部。在没有可用的空闲缓存页时,会首先淘汰掉区域的一些页。
我们可以通过指定来控制的个数,每个中都有各自独立的链表,互不干扰。
可以用下边的命令查看的状态信息:
 
题外话
写文章挺累的,有时候你觉得阅读挺流畅的,那其实是背后无数次修改的结果。如果你觉得不错请帮忙转发一下,万分感谢~
 
转载于https://view.inews.qq.com/a/20180424G0YLHQ00?uid=&from=singlemessage
原作者:我们都是小青蛙   2018-04-24
原内容有很多文字和内容上的错误,阅读不是特别通顺,后续有时间在进行整理……

猜你喜欢

转载自blog.csdn.net/xiaoyi23000/article/details/80263176