MySQL的查询缓存
很多数据库产品都能够缓存査询的执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段。MySQL在某些场景下也可以实现,但是MySQL还有另一种不同的缓存类型:缓存完整的SELECT査询结果,也就是“査询缓存”。本节将详细介绍这类缓存。
MySQL査询缓存保存査询返回的完整结果。当査询命中该缓存,MySQL会立刻返回结果,跳过了解析、优化和执行阶段。
查询缓存系统会跟踪査询中涉及的每个表,如果这些表发生变化,那么和这个表相关的所有的缓存数据都将失效。这种机制效率看起来比较低,因为数据表变化时很有可能对应的査询结果并没有变更,但是这种简单实现代价很小,而这点对于一个非常繁忙的系统来说非常重要。
査询缓存对应用程序是完全透明的。应用程序无须关心MySQL是通过査询缓存返回的结果还是实际执行返回的结果。事实上,这两种方式执行的结果是完全相同的。换句话说,查询缓存无须使用任何语法。无论是MySQL开启或关闭査询缓存,对应用程序都是透明的。
随着现在的通用服务器越来越强大,査询缓存被发现是一个影响服务器扩展性的因素。 它可能成为整个服务器的资源竞争单点,在多核服务器上还可能导致服务器僵死。后面我们将详细介绍如何配合査询缓存,但是很多时候我们还是认为应该默认关闭査询缓存,如果査询缓存作用很大的话,那就配置一个很小的査询缓存空间(如几十兆)。后面将解释如何判断在你的系统压力下打开查询缓存是否有好处。
1.MySQL如何判断缓存命中
MySQL判断缓存命中的方法很简单:缓存存放在一个引用表中,通过一个哈希值引用,这个哈希值包括了如下因素,即査询本身、当前要査询的数据库、客户端协议的版本等一些其他可能会影响返回结果的信息。
当判断缓存是否命中时,MySQL不会解析、“正规化”或者参数化査询语句,而是直接使用SQL语句和客户端发送过来的其他原始信息。任何字符上的不同,例如空格、注释——任何的不同--都会导致缓存的不命中。所以在编写SQL语句的时候,需要特别注意这点。通常使用统一的编码规则是一个好的习惯,在这里这个好习惯会让你的系统运行得更快。
当査询语句中有一些不确定的数据时,则不会被缓存。例如包含函数NOW()或者CURRENT_DATE()的查询不会被缓存。类似的,包含CURRENT_USER或者CONNECTION_ID()的査询语句因为会根据不同的用户返回不同的结果,所以也不会被缓存。事实上,如果査询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,或者任何包含列级别权限的表,都不会被缓存。(如果想知道所有情况,建议阅读MySQL官方手册。)
我们常听到:“如果査询中包含一个不确定的函数,MySQL则不会检査査询缓存”。这个说法是不正确的。因为在检査査询缓存的时候,还没有解析SQL语句,所以MySQL并不知道査询语句中是否包含这类函数。在检査査询缓存之前,MySQL只做一件事情,就是通过一个大小写不敏感的检査看看SQL语句是不是以SEL开头。
准确的说法应该是:“如果査询语句中包含任何的不确定函数,那么在査询缓存中是不可能找到缓存结果的”。因为即使之前刚刚执行了这样的査询,结果也不会放在査询缓存中。MySQL在任何时候只要发现不能被缓存的部分,就会禁止这个査询被缓存。
所以,如果希望换成一个带日期的査询,那么最好将日期提前计算好,而不要直接使用函数。例如:
... DATE_SUB(CURRENT_DATE, INTERVAL 1 DAY) -- Not cacheable! ... DATE_SUB('2007-07-14’, INTERVAL 1 DAY) -- Cacheable
因为査询缓存是在完整的SELECT语句基础上的,而且只是在刚收到SQL语句的时候才检査,所以子査询和存储过程都没办法使用査询缓存。在MySQL5.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会将其释放,并放入空闲内存部分。图7-3展示了这个过程。
我们上面说的“分配内存块”,并不是指通过函数malloc()向操作系统申请内存,这个操作只在初次创建査询缓存的时候执行一次。这里“分配内存块”是指在空闲块列表中找到一个合适的内存块,或者从正在使用的、待淘汰的内存块中回收再使用。也就是说,这里MySQL自己管理一大块内存,而不依赖操作系统的内存管理。
至此,一切都看起来很简单。不过实际情况比图7-3要更复杂。例如,我们假设平均査询结果非常小,服务器在并发地向不同的两个连接返回结果,返回完结果后MySQL回收剩余数据块空间时会发现,回收的数据块小于query_cache_min_res_unit,所以不能够直接在后续的内存块分配中使用。如果考虑到这种情况,数据块的分配就更复杂些,如图7-4所示。
在收缩第一个查询结果使用的缓存空间时,就会在第二个査询结果之间留下一个“空隙”—一个非常小的空闲空间,因为小于query_cache_min_res_unit而不能再次被查询缓存使用。这类“空隙”我们称为“碎片”,这在内存管理、文件系统管理上都是经典问题。有很多种情况都会导致碎片,例如缓存失效时,可能导致留下太小的数据块无法在后续缓存中使用。
3.什么情况下查询缓存能发挥作用
并不是什么情况下査询缓存都会提髙系统性能的。缓存和失效都会带来额外的消耗,所以只有当缓存带来的资源节约大于其本身的资源消耗时才会给系统带来性能提升。这跟具体的服务器压力模型有关。
理论上,可以通过观察打开或者关闭査询缓存时候的系统效率来决定是否需要开启査询缓存。关闭査询缓存时,每个査询都需要完整的执行,每一次写操作执行完成后立刻返回;打开査询缓存时,每次读请求先检査缓存是否命中,如果命中则立刻返回,否则就完整地执行査询,每次写操作则需要检査査询缓存中是否有需要失效的缓存,然后再返回。
这个过程还比较简单明了,但是要评估打开査询缓存是否能够带来性能提升却并不容易。 还有一些外部的因素需要考虑,例如,査询缓存可以降低查询执行的时间,但是却不能减少査询结果传输的网络消耗,如果这个消耗是系统的主要瓶颈,那么査询缓存的作用也很小。
因为MySQL在SHOW STATUS中只能提供一个全局的性能指标,所以很难根据此来判断査询缓存是否能够提升性能。很多时候,全局平均不能反映实际情况。例如,打开査询缓存可以使得一个很慢的査询变得非常快,但是也会让其他査询稍微慢一点点。有时候如果能够让某些关键的査询速度更快,稍微降低一下其他査询的速度是值得的。不过,这种情况我们推荐使用SQL_CACHE来优化对査询缓存的使用。(Percona和MariaDB对MySQL慢日志进行了改进,会记录慢日志中的查询是否命中查询缓存。)
对于那些需要消耗大量资源的査询通常都是非常适合缓存的。例如一些汇总计算查询,具体的如C0UNT()等。总地来说,对于复杂的SELECT语句都可以使用査询缓存,例如多表JOIN后还需要做排序和分页,这类査询每次执行消耗都很大,但是返回的结果集却很小,非常适合査询缓存。不过需要注意的是,涉及的表上UPDATE、DELETE和INSERT操作相比SELECT来说要非常少才行。
一个判断查询缓存是否有效的直接数据是命中率,就是使用査询缓存返回结果占总查询的比率。当MySQL接收到一个SELECT查询的时候,要么增加Qcache_hits的值,要么增加Com_select的值。所以査询缓存命中率可以由如下公式计算:Qcache_hits/(Qcache_hits+Com_select)。
不过,査询缓存命中率是一个很难判断的数值。命中率多大才是好的命中率?具体情况要具体分析。只要査询缓存带来的效率提升大于査询缓存带来的额外消耗,即使30%命中率对系统性能提升也有很大好处。另外,缓存了哪些査询也很重要,例如,被缓存的査询本身消耗非常巨大,那么即使缓存命中率非常低,也仍然会对系统性能提升有好处。所以,没有一个简单的规则可以判断査询缓存是否对系统有好处。
任何SELECT语句没有从查询缓存中返回都称为“缓存未命中”。缓存未命中可能有如下 几种原因:
- 査询语句无法被缓存,可能是因为查询中包含一个不确定的函数(如CURRENT_ DATA),或者查询结果太大而无法缓存。这都会导致状态值Qcache_not_cached增加。
- MySQL从未处理这个査询,所以结果也从不曾被缓存过。
- 还有一种情况是虽然之前缓存了查询结果,但是由于查询缓存的内存用完了,MySQL需要将某些缓存“逐出”,或者由于数据表被修改导致缓存失效。(后续会详细介绍缓存失效。)
如果你的服务器上有大量缓存未命中,但是实际上绝大数査询都被缓存了,那么一定是有如下情况发生:
- 査询缓存还没有完成预热。也就是说,MySQL还没有机会将査询结果都缓存起来。
- 查询语句之前从未执行过。如果你的应用程序不会重复执行一条查询语句,那么即使完成预热仍然会有很多缓存未命中。
- 缓存失效操作太多了。
缓存碎片、内存不足、数据修改都会造成缓存失效。如果配置了足够的缓存空间,而且query_cache_min_res_unit设置也合理的话,那么缓存失效应该主要是数据修改导致的。可以通过参数Com_*来査看数据修改的情况(包括Com_update,Com_delete,等等),还可以通过Qcache_lowmem_prunes来査看有多少次失效是由于内存不足导致的。
在考虑缓存命中率的同时,通常还需要考虑缓存失效带来的额外消耗。一个极端的办法是,对某一个表先做一次只有査询的测试,并且所有的査询都命中缓存,而另一个相同的表则只做修改操作。这时,査询缓存的命中率就是100%。但因为会给更新操作带来额外的消耗,所以査询缓存并不一定会带来总体效率的提升。这里,所有的更新语句都会做一次缓存失效检査,而检查的结果都是相同的,这会给系统带来额外的资源浪费。所以,如果你只是观察査询缓存的命中率的话,可能完全不会发现这样的问题。
在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的整数倍,否则MySQL实际分配的数据会和你指定的略有不同。
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反映了査询缓存中空闲块的多少,在图7-4的配置中我们看到,有两个空闲块。最糟糕的情况是,任何两个存储结果的数据块之间都有一个非常小的空闲块。所以如果Qcache_free_blocks大小恰好达到Qcache_total_blocks/2,那么査询缓存就有严重的碎片问题。而如果你还有很多空闲块,而状态值Qcache_lowmem_prunes还不断地增加,则说明由于碎片导致了过早地在删除査询缓存结果。
可以使用命令FLUSH QUERY CACHE完成碎片整理。这个命令会将所有的査询缓存重新排序,并将所有的空闲空间都聚集到查询缓存的一块区域上。不过需要注意,这个命令并不会将査询缓存清空,清空缓存由命令RESET QUERY CACHE完成。FLUSH QUERY CACHE会访问所有的査询缓存,在这期间任何其他的连接都无法访问查询缓存,从而会导致服务器僵死一段时间,使用这个命令的时候需要特别小心这点。另外,根据经验,建议保持查询缓存空间足够小,以便在维护时可以将服务器僵死控制在非常短的时间内。
提高查询缓存的使用率
如果査询缓存不再有碎片问题,但你仍然发现命中率很低,还可能是査询缓存的内存空间太小导致的。如果MySQL无法为一个新的査询缓存结果的时候,则会选择删除某个老的缓存结果。
当由于这个原因导致删除老的缓存结果时,会增加状态值Qcache_lowmem_prunes。如果这个值增加得很快,那么可能是由下面两个原因导致的:
- 如果还有很多空闲块,那么碎片可能是罪魁祸首(参考前面的小节)。
- 如果这时没什么空闲块了,就说明在这个系统压力下,你分配的查询缓存空间不够大。你可以通过检査状态值Qcache_free_memory来査看还有多少没有使用的内存。
如果空闲块很多,碎片很少,也没有什么由于内存导致的缓存失效,但是命中率仍然很低,那么很可能说明,在你的系统压力下,查询缓存并没有什么好处。一定是什么原因导致查询缓存无法为系统服务,例如有大量的更新或者査询语句本身都不能被缓存。
如果在观察命中率时,仍然无法确定査询缓存是否给系统带来了好处,那么可以通过禁用它,然后观察系统的性能,再重新打开它,观察性能变化,据此来判断査询缓存是否给系统带来了好处。可以通过将query_cache_size设置成0,来关闭査询缓存。(改变query_cache_type的全局值并不会影响已经打开的连接,也不会将査询缓存的内存释放给系统。)你还可以通过系统测试来验证,不过一般都很难精确地模拟实际情况。
图7-5展示了一个用来分析和配置査询缓存的流程图。
5.InnoDB和查询缓存
因为InnoDB有自己的MVCC机制,所以相比其他存储引擎,InnoDB和査询缓存的交互要更加复杂。MySQL4.0版本中,在事务处理中査询缓存是被禁用的,从4.1和更新的InnoDB版本开始,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架构下,当某些修改不影响其他事务读取一致的数据时,是可以使用査询缓存的。但是这样实现起来会非常复杂,InnoDB做了一个简化,让所有有加锁操作的事务都不使用任何査询缓存,这个限制其实并不是必须的。
6.通用查询缓存优化
库表结构的设计、査询语句、应用程序设计都可能会影响到査询缓存的效率。除了前文介绍的之外,这里还有一些要点需要注意:
- 用多个小表代替一个大表对査询缓存有好处。这个设计将会使得失效策略能够在一个更合适的粒度上进行。当然,不要让这个原则过分影响你的设计,毕竟其他的一些优势可能很容易就弥补了这个问题。
- 批量写入时只需要做一次缓存失效,所以相比单条写入效率更好。(另外需要注意,不要同时做延迟写和批量写,否则可能会因为失效导致服务器僵死较长时间。)
- 因为缓存空间太大,在过期操作的时候可能会导致服务器僵死。一个简单的解决办法就是控制缓存空间的大小(query_cache_size),或者直接禁用査询缓存。
- 无法在数据库或者表级别控制査询缓存,但是可以通过SQL_CACHE和SQL_NO_CACHE来控制某个SELECT语句是否需要进行缓存。你还可以通过修改会话级别的变量query_cache_type来控制査询缓存。
- 对于写密集型的应用来说,直接禁用査询缓存可能会提髙系统的性能。关闭查询缓存可以移除所有相关的消耗。例如将query_cache_size设置成0,那么至少这部分就不再消耗任何内存了。
- 因为对互斥信号量的竞争,有时直接关闭査询缓存对读密集型的应用也会有好处。如果你希望提髙系统的并发,那么最好做一个相关的测试,对比打开和关闭查询缓存时候的性能差异。
如果不想所有的査询都进入査询缓存,但是又希望某些査询走査询缓存,那么可以将query_cache_type设置成DEMAND,然后在希望缓存的査询中加上SQL_CACHE。这虽然需要在査询中加入一些额外的语法,但是可以让你非常自由地控制哪些査询需要被缓存。相反,如果希望缓存多数査询,而少数查询又不希望缓存,那么你可以使用关键字SQL_N0_CACHE。
7.查询缓存的替代方案
MySQL査询缓存工作的原则是:执行査询最快的方式就是不去执行,但是查询仍然需要发送到服务器端,服务器也还需要做一点点工作。如果对于某些査询完全不需要与服务器通信效果会如何呢?这时客户端的缓存可以很大程度上帮你分担MySQL服务器的压力。
8.总结
完全相同的査询在重复执行的时候,査询缓存可以立即返回结果,而无须在数据库中重新执行一次。根据我们的经验,在高并发压力环境中査询缓存会导致系统性能的下降,甚至僵死。如果你一定要使用査询缓存,那么不要设置太大内存,而且只有在明确收益的时候才使用。那该如何判断是否应该使用査询缓存呢?建议使用Percona Server,观察更细致的日志,并做一些简单的计算。还可以査看缓存命中率(并不总是有用)、“INSERTS和SELECT比率”(这个参数也并不直观)、或者“命中和 写入比率”(这个参考意义较大)。査询缓存是一个非常方便的缓存,对应用程序完全透明,无须任何额外的编码,但是,如果希望有更高的缓存效率,建议使用或者其他类似的解决方案。