很多数据库产品都能够缓存查询的执行计划,对于相同类型的SQL就可以跳过SQL解析和执行计划生成阶段,MySQL在某些场景下也可以实现,但是MySQL还有另外一种不同的缓存类型:缓存完整的SELECT查询结果,也就是“查询缓存”。
1. MySQL查询缓存保存查询返回的完整结果,当查询命中该缓存,MySQL会立即返回结果,跳过了解析、优化、执行阶段。
2. 查询缓存系统会跟踪查询涉及的每张表,如果这些表发生变化,那么和这个表相关的所有缓存数据都将失效
3. 查询缓存对应用来说是完全透明的
4. 随着通用服务器越来越强大,查询缓存被发现是一个影响服务器扩展性的原因
Mysql判断缓存命中的方法很简单:将缓存放在一个引用表中,通过一个哈希值引用这个哈希值包括如下因素:查询本身、查询的数据库、客户端协议版本等一些会影响返回结果的信息。如何字符上的不同,例如空格、注释都有可能导致缓存的不命中。
当查询有一些不确定的函数时,缓存不会被命中,例如包含函数now()、current_date()、用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表、包含列级权限的表。
如果查询中包含任何不确定函数,那么在查询缓存中是不可能找到缓存结果的。
MySQL查询缓存很多时候可以提升查询性能,首先打开缓存对读和写操作都会带来额外的消耗:
l 读查询在开始前必须检查是否命中缓存
l 如果查询被缓存,那么当完成执行后,MySQL若发现查询缓存没有,将其结果存入查询缓存,会带来额外的系统消耗。
l 这对写的操作也会有影响,当向某个表写入数据时候,MySQL必须将对应表的所有缓存都设置失效。如果查询缓存非常大或者碎片非常多,这个操作可能会带来很大系统消耗。
查询缓存是完全存储在内存中的,所以在配置和使用它之前,我们需要先了解它是如何使用内存的,出来查询结果外,需要缓存的还有很多别的维护相关的数据,例如需要一些专门用来确定哪些内存目前是可用的,哪些是已经用掉的,哪些用来存储数据表和查询结果之前的映射,哪些用来存储查询字符串和查询结果,大概需要40KB内存资源。
每个数据块中,存储了自己的类型、大小和存储的数据本身,外加指向前一个和后一个数据块的指针,数据块的类型有:存储查询结果、存储查询和数据表的映射、存储查询文本等等。
当服务器启动时,它先初始化查询缓存需要的内存,这个内存池初始化是一个完整的空闲块,当查询结果需要缓存时候,MySQL先从大空间块申请一个数据块用于存储结果,这个数据块需要大于参数query_cache_min_res_unit的配置,即使查询结果远远小于此,仍然需要至少申请query_cache_res_unit空间。因为需要在查询开始返回结果时候就分配空间,而此时是无法预知结果是多大的,所以MySQL 无法为每个查询结果精确分配恰好的缓存空间,当需要缓存一个查询结果时,它先选择一个尽可能小的内存块,然后如果数据全部用完仍有数据要存储,那么会申请新的数据块,当查询完成后,如果有剩余空间,MySQL会将其释放,并放入空闲内存部分。
我们假设平均查询都非常小,服务器在并发向不同的两个连接返回结果,返回完结果后MySQL 回收剩余数据块空间发现,回收的数据块小于query_cache_min_res_unit,所以不够再后续的内存分配中使用,就造成了碎片:
最简单就是打开或者关闭查询缓存时候的系统效率来决定是否开启查询缓存,但是不一定能准确,查询缓存可以降低查询执行的时间,淡却不能减少查询结果传输的网络消耗,这消耗是系统的主要瓶颈,那么查询缓存的作用就很小了。
对于那些需要消耗大量资源的查询通常是非常适合缓存的。例如汇总计算查询:COUNT(),例如多表JOIN后还需要排序和分页,但是这些表上UPDATE/DELETE/INSER要比较少。
通常查询命中率(查询缓存返回结果/总查询比率)可以参考是否适用缓存。
任何SELECT 语句没有从查询缓存中返回都称为“缓存未命中”,原因有如下:
1. 查询语句无法被缓存,可能因为查询包含不确定函数,导致状态值qcache_not_cached增加
2. MySQL从未处理过这个查询,所以结果也不曾被缓存过
3. 缓存的内存用完了,某些缓存被逐出。
4. 查询缓存没有预热
5. 查询语句之前从未执行过
6. 缓存失效操作太多了、缓存碎片、内存不足、修改数据
Query_cache_type:是否打开查询缓存:OFF/ON/DEMAND
Query_cache_size:查询缓存使用的总内存空间,单位是字节。必须是1024整数倍否则MySQL实际分配的数据会和你指定的略有不同
Query_cache_min_res_unit:查询缓存中分配内存块时最小单位
Query_cache_limit:MySQL能够缓存的最大查询结果,如果大于这个值,那么不会被缓存,如果炒作,MySQL则增加状态值qcache_not_cached,将结果从查询缓存中删除。
Query_cache_wlock_invalidate:如果某个数据表被其他的连接锁住,是否仍然查询缓存中返回结果,默认是OFF,数据库可能会返回其他线程锁住的数据,ON则不会从缓存中读取这类数据。
减少碎片
没有什么办法能够避免碎片,但是选择合适的query_cache_min_res_unit可以帮助减少由碎片导致的内存空间浪费,设置合适的值可以平衡每个数据块的大小和每次存储结果时内存块申请的次数,调整合适的值可以平衡内存和CPU消耗。通过query_cache_size-qcache_free_monory/qcache_queries_in_cache计算单个查询的平均缓存大小。通过qcache_free_blocks来观察碎片,反映了查询缓存中空闲块的多少,如果qcache_free_blocks恰好达到qcache_total_blocks/2那么查询缓存就有严重的碎片问题,可以使用flush query cache完成碎片整理。
提高查询缓存的使用率
如果查询缓存不再有碎片问题,但是发现仍然命中率很低,那么可能是查询缓存的内存空间太小的原因导致,如果MySQL无法为一个新的查询缓存结果的时候,则会选择删除某个老的缓存结果。当由于这个原因导致删除老的缓存结果时,会增加状态值Qcache_lowmem_prunes,如果这个值增加很快,那么原因有如下:
1、 如果还有很多空闲块,那么碎片可能是罪魁祸首
2、 如果这个时没什么空间块了,就说明这个系统压力下,你分配的查询缓存空间不够大,可以通过qcache_free_memory查看多少没有使用的内存
3、 开启查询缓存没有什么好处禁用试试看,通过时候这query_cache_size设置0,关闭缓存。
事务是否可以访问查询缓存取决于当前事务ID,以及对应的数据表上是否有锁,每个InnoDB内存数据字典都保存了一个事务ID号,如果当前事务ID小于该事务ID,则无法访问查询缓存。
如果表上有任何的锁,那么对这个表的任何查询语句都是无法被缓存的。
当提交事务时,InnoDB持有锁,并使用当前一个事务ID更新当前表的计数器,锁一定程度上说明事务需要对表进行修改操作,当然有可能事务获得锁,却不进行任何更新操作,但是如果想更新任何表的内容,获得相应锁应是前提条件。
所有大于该表计数器的事务才可以使用查询缓存,例如当前系统的事务ID是5,且事务获取了该表的某些记录的锁,然后将事务提交操作,那么事务1至4,都不应该再读取或者向查询缓存写入任何相关的数据。
该表的计数器并不是直接更新为对该表进行加锁的事务ID,而是被更新成一个系统事务ID。
查询缓存存储、检索和失效操作都是在MySQL层面完成,InnoDB无法绕过或者延迟这个行为。当某些修改不影响其他事务读取一致的数据时,是可以使用查询缓存。
库表结构的设计、查询语句、应用程序设计都会影响到查询缓存的效率,除了上面那些,以下也是需要注意的地方:
1、 多个小表代替一个大表对查询缓存有好处,这个设计会使得失效策略能够在更合适的粒度上进行。当然不要让这个原则过分影响你的设计,毕竟其他的一些优势可以很容易就弥补了这个问题。
2、 批量写入时需要做一次缓存失效,所以相比单条写入效率更好,不要同时做延迟写和批量写,否则可能会因为失效导致服务器僵死较长时间
3、 因为缓存空间太大,在过期操作的时候可能会导致服务器僵死,控制缓存空间大小(query_cache_size),或者直接禁用查询缓存
4、 无法在数据库或者表级别控制查询缓存,但是可以通过SQL_CACHE和SQL_NO_CACHE来控制某个SELECT 语句是否需要进行缓存,还可以通过会话级别的变量query_cache_type来控制查询缓存
5、 对于写密集型的应用来说,直接禁用查询缓存可能会提高系统的性能,关闭查询缓存可以移除所有相关的消耗。
6、 因为互斥信号量的竞争,有时直接关闭查询缓存对读密集型的应用也会有好处。如果希望提高系统的并发,那么做一个打开和关闭查询缓存时性能差异测试。
如果只想某些查询走查询缓存,那么可以将query_cache_type设置成DEMAND,然后在查询中加上SQL_CACHE,可以非常自由地控制哪些需要缓存。