MySQL 的查询缓存

MySQL 可以缓存查询结果,一旦缓存命中,MySQL 可以立刻返回结果,跳过解析、优化和执行阶段。

查询缓存系统会跟踪查询中设计的每个表,如果这些表发生变化,那么和这个表相关的缓存数据都会失效。

查询缓存对应用程序是透明的,无论是开启或关闭 MySQL 的查询缓存,对应用程序来说调用方式和结果都是一样的。

如何判断缓存命中

判断缓存命中的方法很简单,就是使用哈希表:

  1. 根据查询语句、要查询的数据库、客户端协议的版本等信息,计算哈希值
  2. 通过哈希值来查找缓存 对于查询语句,MySQL 不会做任何预处理(例如解析、正规化、去除空格注释等),直接使用应用层发送过来的 SQL 语句,以及其他原始信息。因此,任何字符上的不同(包括空格、注释)都会导致缓存的不命中。

当查询语句中有不确定的数据时,也不会被缓存。例如函数 NOW()、CURRENT_USER、用户变量等等。

查询缓存一般都可以提升查询性能,但也存在一定的额外开销:

  1. 查询开始之前必须先检查是否命中缓存
  2. 如果这个查询可以被缓存,而当前缓存中没有,就会将结果存入缓存
  3. 一旦发生写操作,涉及到的表就被改变,这些表的所有缓存都需要被设置为失效。

查询缓存如何使用内存

查询缓存是存放在 MySQL 专门的一块内存块中的,由 MySQL 自己管理。

MySQL 在查询返回结果之前,它就需要为缓存的数据分配空间,但是此时它不知道返回的数据有多大、需要分配多大的空间,因此,它总是分配参数 query_cache_min_res_unit 大小的缓存块。如果这个缓存块不够用,就再申请同样大小的一个缓存块;如果有剩余,就释放为空闲空间。

这样做会制造内存碎片:如果有两个查询同时需要缓存,但它们没有用足预分配的缓存块,则多于的内存就会被释放,这样就造成这个两个缓存之间存在「间隙」,这个「间隙」如果小于 query_cache_min_res_unit 就无法再被重复利用,因此就是「内存碎片」。除此之外,缓存失效等情况也都会导致碎片。

适用查询缓存的场景

缓存和失效都会带来额外的开销,因此需要权衡利弊来判断是否要启用查询缓存。

一般来说,对于复杂的 SELECT 语句(例如多表 JOIN 之后再排序和分页)和需要消耗大量资源的查询(例如汇总计算),都非常适合使用查询缓存。

判断查询缓存是否有效的直接数据是命中率,就是使用查询缓存直接返回结果占总查询的比率。

如果查询和更新操作混杂在一起,更新会导致缓存失效,所以如果更新操作比查询更频繁,就不适合使用查询缓存,因此,我们还可以查看另一个指标:命中和写入的比率(Qcache_hits : Qcache_inserts),一般 3:1 时查询缓存是有效的,最好能达到 10:1。

配置和维护查询缓存

查询缓存相关的配置选项较少,其中 limit 是能够缓存的最大查询结果,例如某个查询反悔了几百万条记录,就可能超限无法缓存;size 是查询缓存占用的总内存空间。query_cache_wlock_invalidate 这个一般保持默认配置 OFF 就行。

1
2
3
4
5
6
7
8
9
10
mysql> SHOW VARIABLES LIKE 'query_cache_%';
+------------------------------+---------+
| Variable_name | Value |
+------------------------------+---------+
| query_cache_limit | 1048576 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 1048576 |
| query_cache_type | OFF |
| query_cache_wlock_invalidate | OFF |
+------------------------------+---------+

查询缓存的内存空间的碎片数量可以通过 query_cache_min_res_unit 来调节,该参数越小,浪费的空间就越少,碎片也就越少,但会造成更频繁的内存块申请。

除此之外,FLUSH QUERY CACHE 语句可以进行碎片整理,RESET QUERY CACHE 语句可以清空缓存。

InnoDB 和查询缓存

由于 InnoDB 的 MVVC 机制,InnoDB 和查询缓存的交互比较复杂。举例来说,如果某个表上正被任何锁锁住,那么于该表相关的查询缓存都将被临时禁用;所有有加锁操作的事务也都不使用任何查询缓存。

通用查询缓存优化

  • 用多个小表代替一个大表对查询缓存有好处。因为对某个小表的修改只会让该表的缓存失效,不会对其他小表的缓存有影响。
  • 批量写入比单条写入效率高。因为前者缓存失效只要一次,后者要很多次
  • 缓存空间太大,会让过期操作的时候可能会导致服务器僵死。解决办法就是减小 query_cache_size 或禁用。
  • 无法在数据库或表级别控制查询缓存。但可以把 query_cache_type 设置成 DEMAND,然后通过 SQL_CACHE 和 SQL_NO_CACHE 来控制某个 SELECT 语句是否需要进行缓存。
  • 对于写密集型的应用、存在大量锁竞争的,关闭查询缓存效率更高。

除此之外,在客户端使用缓存,可以进一步减少与 MySQL 服务器通信的开销,也能减轻 MySQL 服务器的压力,这部分内容将单独介绍。


参考资料:「高性能 MySQL」