MySQL高级特性四:查询缓存

会有一股神秘感。 提交于 2019-11-29 10:58:07

很多数据库产品都能够缓存查询的执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成截断。MySQL在某些场景下也可以实现,但是MySQL还有另一种不同的缓存类型:缓存完整的select查询结果,也就是查询缓存。

MySQL查询缓存保存查询返回的完整结果。当查询命中该缓存,MySQL会like返回结果,跳过了解析、优化和执行截断。

查询缓存系统会跟踪查询中涉及的每个表,如果这些表发生变化,那么和这个表相关的所有的缓存数据都将失效。这种机制效率看起来比较低,因为数据表变化时很有可能对应的查询结果没有变更,但是这种简单实现代价很小,而这点对于一个非常繁忙的系统来说非常重要。

查询缓存对应用程序是完全透明的。应用程序无需关心MySQL是通过查询返回的还是实际执行返回的结果。事实上,这两种方式执行的结果是完全相同的。换句话说,查询缓存无需使用任何语法。无论是MySQL开启或关闭查询缓存,对程序都是透明的。

随着现在的通用服务器越来越强大,查询缓存被发现是一个影响服务器扩展性的因素。它可能成为整个服务器的资源竞争单点,在多核服务器上还可能导致服务器僵死。所以大部分时候应该默认关闭查询缓存,如果查询缓存作用很大的话,可以配置个几十兆的小缓存空间。

1 MySQL如何判断缓存命中

MySQL判断缓存命中的方法很简单:缓存存放在一个引用表中,通过一个哈希值引用,这个哈希值包括了如下因素:

查询语句、当前要查询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。

当判断缓存是否命中时,MySQL不会解析、优化或者参数化查询语句,而是直接使用sql语句和客户端发送过来的其他原始信息。任何字符上的不同,例如空格、注释等都会导致缓存的不命中。所以在编写sql语句的时候,需要特别注意这点。通常使用同一的编码规则是一个好的习惯,在这里这个好习惯会让系统运行的更快。

当查询语句中有一些不确定的数据时,则不会被缓存。类似包含函数now()或current_date()的查询不会被缓存。事实上,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,或者任何包含列级别权限的表,都不会被缓存。

如果查询中包含一个不确定的函数,MySQL则不会检查查询缓存。这句话是不正确的,因为在检查查询缓存的时候,还没有解析SQL语句,所以MySQL并不知道查询语句中是否包含这类函数。在从检查查询缓存之前,MySQL只做一件事,就是通过一个大小写不敏感的检查来看看sql语句是否以sel开头。

准确的说法应该是:如果查询语句中包含任何的不确定函数,那么在查询缓存中是不可能找到缓存结果的。因为即使之前刚刚执行了这样的查询,结果也不会放在查询缓存中MySQL在任何时候只要发现不能被缓存的部分,就会禁止这个查询被缓存。

所以,如果希望换成一个带日期的查询,那么最好将日期提前计算好,而不要直接使用函数。

因为查询缓存是在完整的select语句的基础上的,而且只是在刚收到sql语句的时候才检查,所以子查询和存储过程都没办法使用查询缓存。在5.1之前的版本中绑定变量也无法使用查询缓存。

MySQL的查询缓存在很多时候可以提升查询性能,在使用的时候,有一些问题需要特别注意。首先,打开查询缓存对读和写操作都会带来额外的消耗:

① 读查询在开始之前必须先检查是否命中缓存。

② 如果这个读查询可以被缓存,那么当完成执行后,MySQL若发现查询缓存中没有这个查询,会将其结果存入查询缓存,这会带来额外的系统消耗。

③ 这对鞋操作也会有影响,因为当想某个表写入数据的时候,MySQL必须将对应表的所有缓存都设置失效。如果查询缓存非常大或者碎片很多,这个操作就可能会带来很大的系统消耗。

虽然如此,查询缓存仍然可能给系统带来性能提升。但是,如上所述,这些额外消耗也可能不断增加,在加上对查询缓存操作时一个加锁排他操作,这个消耗可能不容小觑。

对InnoDB、用户来说,事物的一些特性会限制查询缓存的使用。当一个语句在事物中修改了某个表,MySQL会将这个表的对应的查询缓存都设置失效,事实上InnoDB的多版本特性会暂时将这个修改对其他事物屏蔽。在这个事物提交前,这个表的相关查询时无法被缓存的,所以所有在这个表上面的查询,内部或外部的事物,都只能在改事物提交后才被缓存。因此,长时间运行的事物,会大大降低查询缓存的命中率。

如果查询缓存使用了很大量的内存,缓存失效操作就可能成为一个非常严重的问题瓶颈。如果缓存中存放了大量的查询结果,那么缓存失效操作时整个系统可能都会僵死一会。因为这个操作时靠一个全局锁操作保护的,所有需要做该操作的查询都需要等待这个锁,而且无论是检测是否命中缓存、还是缓存失效检测都需要等待这个全局锁。

2 查询缓存如何使用内存

查询缓存使完全存储在内存中的,所以在配置和使用它之前,我们需要了解它是如何使用内存的。除了查询结果之外,需要缓存的还有很多别的维护相关的数据。这和文件系统有些类似:需要一些内存专门用来确定哪些内存目前是可用的、哪些是已经用掉的、哪些用来存储数据表和查询结果之前的映射、哪些用来存储查询字符串和查询结果。

这些基本的管理维护数据结构大概需要40kb的内存资源,除此之外,MySQL用于查询缓存的内存被分成一个个的数据块,数据块是变长的。每一个数据块中,存储了自己的类型、大小和存储的数据本身,还外加指向前一个和后一个数据块的指针。数据块的类型有:存储查询结果、存储查询和数据表的映射、存储查询文本等。不同的存储块,在内存使用上没有什么不同,从用户角度来看无需区分他们。

当服务器启动的时候,它先初始化查询缓存需要的内存。这个内存池初始是一个完整的空闲块。这个空闲块的大小就是你锁配置的查询缓存大小减去用于维护原数据的数据结构所消耗的空间。

当有查询结果需要缓存的时候,MySQL先从大的空间块中申请一个数据块用于存储结果。这个数据块需要大于参数query_cache_min_res_unit的配置,即使查询结果远远小于此,人需要至少申请query_cache_min_res_unit空间。因为需要在查询开始返回结果的时候就分配空间,而此时是无法预知查询结果到底多大的,所以MySQL无法为每一个结果精确分配大小恰好的缓存空间。

因为需要先锁住空间块,然后找到合适大小数据库,所以相对来说,分配内存块是一个非常慢的操作。MySQL尽量避免这个操作的次数。当需要缓存一个查询结果的时候,它先选择一个尽可能小的内存块,然后将结果存入其中。如果数据块全部用完,但仍有剩余数据需要存储,那么MySQL会申请一块新的数据块;仍然是尽可能小的数据块,继续存储结果数据。当查询完成时,如果申请的内存空间还有剩余,MySQL会将其释放,并放入空闲内存部分。

我们上面说的分配内存块,并不是指通过函数malloc()向操作系统申请内存,这个操作只是初次创建查询缓存的时候执行一次。这里分配内存块是指在空闲块列表中找到一个合适的内存块,或者从正在使用的、待淘汰的内存块中回收在使用。也就是说,这里MySQL自己管理一大块内存,而不依赖操作系统的内存管理。

至此一切看起来都很简单。不过实际情况很复杂。例如,我们假设平均查询结果非常小,服务器在并发的想不通的两个链接返回结果,返回结果后MySQL回收剩余数据块空间时会发现,回收的数据块小于query_cache_min_res_unit,所以不能够直接在后续的内存块分配中使用。那么分配就更复杂一些。

在收缩第一个查询结果使用的缓存空间时,就会在第二个查询结果之间留下一个空隙,因为小于query_cache_min_res_unit而不能再次被查询缓存使用。这类空隙就成为了内存空间碎片,这在内存管理、文件系统管理上都是经典问题。有很多种情况都会导致碎片,例如缓存失效时,可能导致留下太小的数据块无法在后续缓存中使用。

3 什么情况下查询缓存能发挥作用

并不是什么情况下查询缓存都会提高系统性能的。缓存和失效都会带来额外的消耗,所以只有当缓存带来的资源节约大于其本身的资源消耗时才会给系统带来性能提升。这根具体的服务器压力模型有关。

理论上,可以通过观察打开或者关闭查询缓存时候的系统效率来决定是否需要开启查询缓存。关闭查询缓存时,每个查询都需要完整的执行,每一次写操作执行完成后立刻返回:打开查询缓存时,每次读请求先检查缓存是否命中,如果命中则立刻返回,否则就完整的执行查询,每次写操作则需要检查查询缓存中是否需要失效的缓存,然后在返回结果。

这个过程还比较简单明了,但是评估打开查询缓存是否能够带来性能提升却并不容易。还有一些外部的因素需要考虑,例如,查询缓存可以降低查询执行的时间,但是却不能减少查询结果传输的网络消耗,如果这个消耗时系统的主要瓶颈,那么查询缓存的作用也很小。

因为MySQL在show status中只能提供一个全局的性能指标,所以汉南根据此来判断查询缓存是否能够提升性能。很多时候,全局平均不能反映实际情况。例如,打开查询缓存可以使得一个很慢的查询变得非常快,但是也会让其他查询稍微慢一点点。有时候如果能够让某些关键的查询速度更快,稍微降低一下其他查询的速度是值得的。不过,这种情况我们推荐使用sql_cache来优化对查询缓存的使用。

对于那些需要消耗大量资源的查询通常都是非常适合缓存的。例如一些汇总计算查询count()等。总的来说,对于复杂的select语句都可以使用查询缓存,例如多表join后还需要做排序和分页,这类查询每次执行消耗都很大,但是返回的结果集却很小,非常适合查询缓存。不过需要注意的是,设计的表上update、delete和insert操作相比select来说要非常少才行。

一个判断查询缓存是否有效的直接数据是命中率,就是使用查询缓存返回结果占总查询的比率。当MySQL接收到一个select查询的时候,要么增加Qcache_hits的值,要么增加Com_select的值。所以查询缓存命中率可以由一下公式计算:Qcache_hits/(Qcache_hits+Com_select)。

不过查询缓存命中率是一个很难判断的数值。命中率多大才是好的命中率?只要查询缓存带来的效率提升大于查询缓存带来的额外消耗,即使百分之三十的命中率对系统性能提升也有很大好处。另外,缓存了哪些查询也很重要,例如,被缓存的查询本身消耗非常巨大,那么即使缓存命中率非常低,也任然会对系统性能提升有好处。所以,没有一个简单的规则可以判断查询缓存是否对系统有好处。

任何select语句没有从查询缓存中返回都称为缓存未命中。可能有一下几点原因:

① 查询语句无法被缓存,可能是因为查询中包含一个不确定的函数,或者查询结果太大而无法缓存。这都会导致状态值Qcache_not_cached增加。

② MySQL从未处理这个查询,所以结果也从不曾被缓存过。

③ 还有一种情况是虽然之前缓存了查询结果,但是由于查询缓存的内存用完了,MySQL需要将某些缓存清除,或者由于数据表被修改导致缓存失效。

如果你的服务器上有大量缓存未命中,但是实际上绝大多数查询都被缓存了,那么一定是有以下几种情况:

① 查询缓存还没有完成预热,也就是说,MySQL还没有机会将查询结果缓存起来。

② 查询语句之前从未执行过。

③ 缓存失效操作太多了。

缓存碎片、内存不足、数据修改都会导致缓存失效。如果配置了足够的缓存空间,而且query_cache_min_res_unit设置也合理的话,那么缓存失效应该主要是数据修改导致的。可以通过参数com_*来查看数据修改的情况,还可以通过Qcache_lowmem_prunes来查看有多少次失败时由于内存不足导致的。

在考虑缓存命中率的同时,通常还需要考虑缓存失效带来的额外消耗。一个极端的办法是,对某一个表先做一次只有查询的测试,并且所有的查询都命中缓存,而另一个相同的表则只做修改操作。这时,查询缓存的命中率就是百分百。但因为会给更新操作带来额外的消耗,所以查询缓存并不一定会带来总体效率的提升。这里,所有的更新语句都会做一次缓存失效检查,而检查的结果都是相同的,这会给系统带来额外的资源浪费。所以,如果你只是观察查询缓存的命中率的话,可能完全不会发现这样的问题。

在MySQL中如果更新操作和带缓存的读操作混合,那么查询缓存带来的好处通常很难衡量。更新操作会不断的使得缓存失效,而同时每次查询还会想缓存中在写入新的数据。所以只有当后续的查询能够在缓存失效前使用缓存才会有效的利用查询缓存。

如果缓存的结果在失效前没有被任何其他的select语句使用,那么这次缓存操作就是浪费时间和内存。我们可以通过查看com_select和qcache_inserts的相对值来看看是否一致有这种情况发生。如果每次查询操作都是缓存未命中,然后需要将查询结果放到缓存中,那么qcache_inserts的大小应该和com_select相当。所以在缓存完成预热后,我们总希望看到qcache_inserts远远小于com_select。不过由于缓存和服务器内部的复杂和多样性,任然很难说,这个比率是多少才是一个合适的值。

所以,上面的命中率和inserts和select比率都无法只管的反应查询缓存的效率。那么还有什么只管的办法能够反应查询缓存是否对系统有好处?这里推荐查看另一个指标:命中和写入的比率,即qcache_hits和qcache_inserts的比值。根据经验来看当这个比值岛屿3:1的时候,通常查询缓存时有效的,不过这个比率最好能够达到10:1.如果你的应用没有达到这个比率,那么久可以考虑禁用查询缓存了,除非你能够通过精确的计算得知:命中带来的性能提升大于缓存失效的消耗,并且查询缓存并没有成为系统的瓶颈。

每一个应用程序都会有一个最大缓存空间,甚至对一些纯读的应用来说也一样。最大缓存空间时能够缓存所有可能查询结果的缓存空间总和。理论上,对多数应用来说,这个数值都会非常大。而实际上,由于缓存失效的原因,大多数应用最后使用的缓存空间都比预想的要小。即使你配置了足够大的缓存空间,由于不断的失效,导致缓存空间一直都不会接近最大缓存空间。

通常可以通过观察查询缓存内存的实际使用情况,来确定是否需要缩小或扩大查询缓存。如果查询缓存空间长时间都有剩余,那么建议缩小;如果经常由于空间不足而导致查询缓存失效,那么则需要增大查询缓存。不过需要注意,如果查询缓存达到了几十兆这样的数量级,是有潜在危险的。

另外,可能还需要和系统的其他缓存一起考虑,例如InnoDB的缓存池,或者MyISAM的索引缓存。关于这点无法简单给出一个公式或者比率来判断,因为真正的平衡点与应用程序有很大的关系。

最好的判断查询缓存是否有效的办法还是通过查看某类查询时间消耗是否增大或者减小来判断。Percona Server通过扩展慢查询可以观察到一个查询是否命中缓存。如果查询缓存没有为系统节省时间,那么最好禁用它。

4 如何配置和维护查询缓存

一旦理解查询缓存工作的原理,配置起来就很容易了。它只有很少的参数可供配置:

① query_cache_type

是否打开查询缓存。可以设置OFF、ON或DEMAND、DEMAND表示只有在查询语句中明确写入sql_cache的语句才放入查询缓存。这个变量可以是会话级别的也可以是全局级别的。

② query_cache_size

查询缓存使用的总内存空间,单位是字节。这个值必须是1024的整倍数,否则实际分配的数据会和指定的大小有区别。

③ query_cache_min_res_unit

在查询缓存中分配内存块时的最小单位。

④ query_cache_limit

MySQL讷讷够缓存的最大查询结果。如果查询结果大于这个值,则不会被缓存。因为查询缓存在数据生成的时候就开始尝试缓存数据,所以只有当结果全部返回后,MySQL才知道查询结果是否超出限制。

如果超出,MySQL则增加状态值qcache_not_cached,并将结果从查询缓存中删除。如果你实现知道有很多这样的情况发生,那么建议在查询语句中加入sql_no_cache来避免查询缓存带来的额外消耗。

⑤ query_cache_wlock_invalidate

如果某个数据表被其他的链接锁住,是否仍然从查询缓存中返回结果。这个参数默认是OFF,这可能在一定程序上会改变服务器的行为,因为这使得数据库可能返回其他线程锁住的数据。将参数设置成ON,则不会从缓存中读取这类数据,但是这可能会增加锁等待。对于绝大多数应用来说无需注意这个细节,默认的通常没有问题。

 

配置查询缓存通常很简单,但是如果想知道修改这些参数会带来哪些改变,则是一项很复杂的工作。

减少碎片

没有什么方法能够完全避免碎片,但是选择合适的query_cache_min_res_unit可以帮你减少由碎片导致的内存空间浪费。设置合适的值可以平衡每个数据块的大小和每次存储结果时内存块申请的次数。这个值太小,则浪费的空间更少,但是会导致更频繁的内存块申请操作;如果这个值设置的太大,那么碎片会很多。调整合适的值其实是在平衡内存浪费和CPU消耗。

这个参数的最合适的大小和应用程序的查询结果的平均大小直接相关。可以通过内存实际消耗(query_cache_size-qcache_free_memory)/qcache_queries_in_cache计算单个查询的平均缓存大小。如果你的应用程序的查询结果很不均匀,有的结果很大,有的结果很小,那么碎片和反复的内存块分配可能无法避免。如果你发小缓存一个非常大的结果并没有什么意义,那么你可以通过query_cache_limit限制可以缓存的最大查询结果,借此来大大减少大的查询结果的缓存,罪证减少内存碎片的发生。

还可以通过参数qcache_free_blocks来观察碎片。参数qcache_free_blocks反应了查询缓存中空闲块的多少。最糟糕的情况是,任何两个存储结果的数据块之间都有一个非常小的空间块。所以如果qcache_free_blocks大小恰好达到qcache_total_blocks/2,那么查询缓存就有严重的碎片问题。而如果你还有很多空闲块,而状态值qcache_lowmem_prunes还不断的增加,则说明由于碎片导致了过早的在删除查询缓存结果。

可以使用命令flush query cache完成碎片整理。这个命令会将所有的查询缓存重新排序,并将所有的空闲空间都聚集到查询缓存的一块区域上。不过需要注意,这个命令并不会讲查询缓存清空,清空缓存由命令resert query cache完成。flush query cache会访问所有的查询缓存,在这期间任何其他的链接都无法访问查询缓存,从而会导致服务器僵死一段时间,使用这个命令的时候需要特别小心着点。另外,根据经验,建议保持查询缓存空间足够小,以便在维护时可以将服务器僵死控制在非常短的时间内。

提高查询缓存的使用率

如果查询缓存不在有碎片问题,但你仍然发现命中率很低,还可能是查询缓存的内存空间太小导致的。如果MySQL无法为一个新的查询缓存结果的时候,则会选择删除某个老的缓存结果。

当由于这个原因导致删除老的缓存结果时,会增加状态值qcache_lowmem_prunes。如果这个值增加的很快,那么可能是有下面两个原因导致的:

① 如果还有很快空闲块,那么碎片可能是问题原因。

② 如果这时没有空闲块了,就说明在这个系统压力下,你分配的查询缓存空间不够大。可以通过检查状态值qcache_free_memory来查看剩余的内存空间大小。

如果空闲块很多,碎片很少,也没有什么用于内存导致的缓存失效,但是命中率仍然很低,那么很可能说明,在你的系统压力下,查询缓存并没有什么好处。一定是什么原因导致查询缓存无法为系统服务,例如有大量的更新或者查询语句不能被缓存。

如果在观察命中率时,任然无法确定查询缓存是否给系统带来了好处,那么可以通过禁用它,然后观察系统性能,在重新打开它,观察性能变化,据此来判断查询缓存是否给系统带来了好处。可以通过将query_cache_size设置成0来关闭查询缓存。改变query_cahe_type的全局值并不会影响已经打开的链接,也不会讲查询缓存的内存释放给系统。

如何分析和配置查询缓存:

5 InnoDB和查询缓存

因为InnoDB有自己的MVCC机制,所以相比其他存储引擎,InnoDB和查询缓存的交互要更加复杂。MySQL4.0中,在事物处理中查询缓存时被禁用的,以后版本中InnoDB会控制在一个事物中是否可以使用查询缓存,InnoDB会同时控制对查询缓存的读和写操作。

事物是否可以访问查询缓存取决于当前事物ID,以及对应的数据表上是否有锁。每一个InnoDB表的内存数据字典都保存了一个事物ID号,如果当前事物ID小于事物ID,则无法访问查询缓存。

如果表上有任何的锁,那么对这个表的任何查询语句都是无法被缓存的。例如,某个事物执行了select for update语句,那么在这个锁释放之前,任何其他的事物都无法从查询缓存中读取与这个表相关的缓存结果。

当事物提交时,InnoDB持有锁,并使用当前的一个系统事物ID更新当前表的计数器。锁一定程度上说明事物需要对表进行修改操作,当然有可能事物获得锁,却不进行任何更新操作,但是如果想更新任何表的内容,获得相应锁则是前提条件。InnoDB将每个表的计数器设置成某个事物ID,而这个事物ID就代表了当前存在的且修改了该表的最大的事物ID。

那么下面的一些概念也就成立:

① 所有大于该表计数器的事物才可以使用查询缓存。例如当前系统的事物ID是5,且事物获取了该表的某些记录的锁,然后进行事物提交操作,那么事物1到4都不应该在读取或者想查询缓存写入任何相关的数据。

② 该表的计数器并不是直接更新为对该表进行加锁的事物ID,而是被更新成一个系统事物ID。所以,会发现该事物自身后续的更新操作也无法读取和修改查询缓存。

查询缓存存储、检索和失效操作都是在MySQL层面完成,InnoDB无法绕过或者延迟这个行为。但InnoDB可以在事物中显示的告诉MySQL何时应该让某个表的查询缓存都失效。在没有外键限制的时候这是必须的,例如某个sql语句有on delete cascade,呢么相关联表的查询缓存也是要一起失效的。

原则上,在InnoDB的MVCC架构下,当某些修改不影响到查询缓存的效率。还有一些需要注意:

① 用多个小表代替一个达标对查询缓存由好处。这个设计将会使得失效策略能够在一个更合适的粒度上进行。当然,不要让这个原则过分影响你的设计,毕竟其他的一些优势可能很容易弥补这个问题。

② 批量写入时只需要做一次缓存失效,所以相比单条写入效率更好。

③ 因为缓存空间太大,在过期操作的时候可能会导致服务器僵死。一个简单的解决办法就是控制缓存空间的大小query_cache_size,或者直接禁用查询缓存。

④ 无法在数据库或者表级别控制查询缓存,但是可以通过sql_cache和sql_no_cache来控制某个select语句是否需要进行缓存。你还可以通过修改会话级别的变量query_cache_type来控制查询缓存。

⑤ 对于写密级行的应用来说,直接禁用查询缓存可能会提高系统的性能。关闭查询缓存可以移除所有相关的消耗。

⑥ 因为对互斥信号量的竞争,有时直接关闭查询缓存对读密级型的应用也会有好处。如果你希望提高系统的并发,那么最好做一个相关的测试,对比打开和关闭查询缓存时候的性能差异。

如果不想所有的查询都进入查询缓存,但是又希望某些查询走缓存,那么可以将query_cache_type设置成demand,然后在希望缓存的查询中加上sql_cache。这虽然需要在查询中加入一些额外的语法,但是可以让你非常自由的控制哪些查询需要杯缓存。相反,如果希望缓存多数查询,而少数查询又不希望缓存,那么你可以使用关键字sql_no_cache。

7 查询缓存的替代方案

MySQL查询缓存工作的原则是:执行查询最快的方式就是不执行,但是查询仍然需要发送到服务器端,服务器也还需要做一点点工作。如果对于某些查询完全不需要与服务器通信效果会如何呢?这是客户端的缓存可以很大程度上帮你分担MySQL服务器的压力。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!