文章目录

  • 前文
  • 简单介绍查询缓存
  • 查询缓存定义
  • 查询缓存是否开启控制
  • 缓存(buffer pool)的架构与控制
  • 缓存大小控制
  • 缓存的设计与架构
  • 缓存的LRU设计
  • 总结


前文

  继上篇:MySQL进阶必备知识(三):带你最快入门索引!!,让我们继续MySQL的学习记录–>缓存(Buffer Pool缓存池)。对于缓存的学习也能让我们了解到MySQL对于优化用户查询速度而设计的缓存架构,同时学习到散列表+LRU链表这样的数据结构!   在介绍buffer pool,先和查询缓存区别开,前者相当于是管理整个MySQL的内存,后者则是单纯为了查询而做出的改动。前者至今扔在MySQL里充当重要的角色,而后者在最新版本8.0已经被官方移除了,因为考虑到维护缓存的困难性以及缓存失效频繁而做出的变更。这里简单介绍二者的功能:

  • buffer pool缓存的是整张表的数据,是以数据页的形式,无论sql如何变化,只要定位到对应的数据页,就能从内存返回结果集;
  • 查询缓存即Qcache缓存的是SQL和结果集的一一对应,当SQL改了则无法取到对应的结果集;当结果集所对应的表被更改了,则整个结果集废弃。相应的由两个参数控制:query_cache_size和query_cache_type,前者控制这个查询缓存所占内存大小,后者控制是否开启。

简单介绍查询缓存

查询缓存定义

  查询缓存定义:就是当用户查询的SQL跑过一遍后,当执行同样的SQL时,MySQL不会再去定位数据页、分析执行计划,而是直接把之前查询的结果返回给用户,不过大前提是:在这两遍的查询过程中,这个表不能有任何的DDL、DML语句,不然MySQL会清空缓存,而这也是缓存维护起来最困难的一点!

查询缓存是否开启控制

  即使在8.0版本以下,我们也可以通过参数控制是否开启缓存,即query_cache_type,该参数有三种设置:query_cache_type有3个值 0代表关闭查询缓存OFF,1代表开启ON,2(DEMAND)代表当sql语句中有SQL_CACHE关键词时才缓存。建议选第三种,这样当我们有业务需求的时候(比如该表的变更几乎没有,是一个静态表!),可以通过加入SQL_CACHE来查询缓存。
  明白了缓存的定义,了解下查询缓存是在哪进行的?我们知道,查询的过程要经过这么几个阶段:

  1. 建立连接:客户端和服务端建立连接,此时MySQL会初始化资源
  2. 查询缓存
  3. 分析器:分析词法错误、确认字段是否存在等
  4. 优化器:优化查询速度,比如链表查询、多个索引查询,MySQL会选择最优生成执行计划
  5. 执行器:上述还是在server端发生,而该阶段则是在引擎端执行优化后的执行计划
  6. 返回结果
      可以看到查询缓存位于第二阶段,所以如果缓存存在的话,那返回的速度可以说是非常快了!但是在这里推介大家关闭查询缓存,因为其带来的效率不及其丢失的性能。

缓存(buffer pool)的架构与控制

缓存大小控制

  缓存的大小控制:innodb_buffer_pool_size(默认128M),即当MySQL启动时,会向操作系统申请一块内存用于缓存,随着缓存的增加,这块内存不会超过128M。那如果想增加怎么办?通过innodb_buffer_pool_instances来控制Buffer Pool的实例数量,如果你想有1G的缓存内存,也好办,设置为9个以上即可~
  另外MySQL5.75引入了innodb_buffer_pool_chunk_size来解决Buffer Pool扩容的问题,即每次想扩容Buffer Pool,通过申请一个个chunk来申请,不需再申请一个Buffer Pool instance,而这个参数默认是128M。

缓存的设计与架构

  说完重要参数,让我们理解MySQL是如何设计缓存的!我们知道,在那么多数据结构里,如链表、数组、队列、栈、树、图,要最快查询某个值是否存在,那就是用散列表,所以MySQL的缓存就是基于散列表的key-value形式,当匹配到对应的数据页存在时,就找到对应的数据页!然后再通过LRU链表去查询!也即是先通过散列表确认是否存在缓存,存在后再通过链表查询。
  而整个缓存实际上是由控制块和缓存页构成,每个控制块对应着对应的缓存页信息:比如表空间号+页号等,而所谓的Buffer Pool大小为128M指的就是缓存页的大小,控制块则一般占5%,所以每次会多申请6M的内存空间用于存放控制块。另外MySQL会维护两个链表来处理空闲页+脏页(即被修改的页):free链表+flush链表,前者控制空闲页,后者控制脏页!
  所以**整个缓存的逻辑是这样:**刚初始化缓存时,会生成部分控制块和缓存页,将这些划入到free链表,当有select语句时,就判断在散列表是否存在,很明显初始化时不存在,此时就从free链表取空闲页来缓存查询结果,当free链表为空,则会向操作系统申请内存(不超过你的参数限制)来填补空闲页。由此就完成缓存的生成和消耗!而脏页则是当缓存页被修改时(即此时缓存页与磁盘不一致),MySQL就会将缓存页移到flush链表,并且从散列表移除,再由MySQL后台将flush链表刷新到磁盘里。
  整个缓存的逻辑还是比较简单,而在这之上,MySQL通过LRU链表(按照最近最少使用的原则去淘汰缓存页)来最大程度的节省缓存内存。

缓存的LRU设计

  在LRU的设计过程中,官方也在不断的优化,
  最初的LRU:即当缓存页不存在时,就从free链表拿,同时将控制块放入LRU头部;而存在时,则通过LRU头部去获取控制块再找到对应的缓存页!这个方式也行,不过有两个问题:

  1. 当MySQL的预读(read ahead)模式开启时,当顺序访问了某个区(管理数据页的单位,)的页面超过了这个区设定的值,将会触发异步预读,将下个区的内容整个加载到Buffer pool里。
  2. 当MySQL全表扫描时,会将所有扫描的数据页全部加入到Buffer Pool里

  这两个问题的存在,直接导致Buffer Pool很容易就满了,对内存很不友好!
  优化版LRU:为了解决上述问题,引入了young和old两个概念,即把LRU链表分为两部分,一部分为young:缓存最新最热的数据,另一部分为old:缓存只使用1次的数据,比如全表扫描、预读等等。这两个区域的比例关系由参数:innodb_old_blocks_pct控制的,默认是37,即old所占比例是37%。使用逻辑:即每次select语句都是放入old区域,而old区域有个限制:如果两次查询的不超过1s,则不会将其加入young区域,所以全表扫描也好、预读也好,因为只查询1次,所以并不会加入young区域。
  最终版LRU:到这里LRU还有很多优化的地方,在最终版LRU里的一个很大的优化点是将young区域再次处理,即如果是存在于young区域前1/4的内容无论再怎么访问也不会将其加入LRU头部,而在1/4后的内容才会加入头部!这是为了解决移动的消耗,比如在前1/4区域,本来就是很热的查询,这样就不用次次都移到头部!

总结

  关于缓存就介绍到这啦,一开始我一直把查询缓存和buffer pool弄混了,然后一直理解不了架构这么好的缓存为啥8.0要放弃,直到现在才弄清楚他们二者的区别!另外buffer pool的优化非常强大,我们可以借鉴其设计思想来设计代码~