docs/database/mysql/mysql-query-cache.md
缓存是一个有效且实用的系统性能优化手段,无论是操作系统,还是各类应用软件与 Web 服务,均广泛采用了缓存机制。
然而,有经验的 DBA 都建议生产环境中把 MySQL 自带的 Query Cache(查询缓存)给关掉。而且,从 MySQL 5.7.20 开始,就已经默认弃用查询缓存了。在 MySQL 8.0 及之后,更是直接删除了查询缓存的功能。
这又是为什么呢?查询缓存真就这么鸡肋么?
带着如下几个问题,我们正式进入本文。
MySQL 体系架构如下图所示:
为了提高完全相同的查询语句的响应速度,MySQL Server 会对查询语句进行 Hash 计算得到一个 Hash 值。MySQL Server 不会对 SQL 做任何处理,SQL 必须完全一致 Hash 值才会一样。得到 Hash 值之后,通过该 Hash 值到查询缓存中匹配该查询的结果。
也就是说,一个查询语句(select)到了 MySQL Server 之后,会先到查询缓存看看,如果曾经执行过的话,就直接返回结果集给客户端。
通过 show variables like '%query_cache%'命令可以查看查询缓存相关的信息。
8.0 版本之前的话,打印的信息可能是下面这样的:
mysql> show variables like '%query_cache%';
+------------------------------+---------+
| Variable_name | Value |
+------------------------------+---------+
| have_query_cache | YES |
| query_cache_limit | 1048576 |
| query_cache_min_res_unit | 4096 |
| query_cache_size | 599040 |
| query_cache_type | ON |
| query_cache_wlock_invalidate | OFF |
+------------------------------+---------+
6 rows in set (0.02 sec)
8.0 以及之后版本之后,打印的信息是下面这样的:
mysql> show variables like '%query_cache%';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| have_query_cache | NO |
+------------------+-------+
1 row in set (0.01 sec)
我们这里对 8.0 版本之前show variables like '%query_cache%';命令打印出来的信息进行解释。
have_query_cache: 该 MySQL Server 是否支持查询缓存,如果是 YES 表示支持,否则表示不支持。query_cache_limit: MySQL 查询缓存的最大查询结果,查询结果大于该值时不会被缓存。query_cache_min_res_unit: 查询缓存分配的最小块的大小(字节)。当查询进行的时候,MySQL 把查询结果保存在查询缓存中,但如果要保存的结果比较大,超过 query_cache_min_res_unit 的值,此时 MySQL 将在检索结果的同时保存数据,也就是说,有可能在一次查询中,MySQL 要进行多次内存分配的操作。适当的调节 query_cache_min_res_unit 可以优化内存。query_cache_size: 为缓存查询结果分配的内存的数量,单位是字节,且数值必须是 1024 的整数倍。MySQL 5.7 官方文档显示默认值为 1048576(1 MB),设置为 0 时禁用查询缓存。不同小版本的默认值存在差异,建议在配置文件中显式指定,不依赖默认行为。query_cache_type: 设置查询缓存类型,默认为 ON。设置 GLOBAL 值可以设置后面的所有客户端连接的类型。客户端可以设置 SESSION 值以影响他们自己对查询缓存的使用。query_cache_wlock_invalidate:如果某个表被锁住,是否返回缓存中的数据,默认处于关闭状态,生产环境通常建议保持此默认配置。query_cache_type 可能的值(query_cache_type 在 MySQL 5.6/5.7 中是动态变量,但有前提:若实例启动时 query_cache_type=0,服务器会跳过查询缓存互斥锁的分配,此时通过 SET GLOBAL 动态修改将报错,必须修改配置文件并重启;若启动时非 0,则可通过 SET GLOBAL query_cache_type=N 在线生效,无需重启):
Select SQL_NO_CACHE 开头的查询。Select SQL_CACHE 开头的查询。建议:
query_cache_size 不建议设置得过大。过大的空间不但挤占实例其他内存结构的空间,而且会增加在缓存中搜索的开销。建议根据实例规格,初始值设置为 10MB 到 100MB 之间的值,而后根据运行使用情况调整。
建议通过将 query_cache_size 设置为 0 来禁用查询缓存,而非仅依赖 query_cache_type。两者虽都是动态变量,但 query_cache_size=0 会完全跳过缓存内存分配和检查路径,禁用更彻底。
8.0 版本之前,my.cnf 加入以下配置,重启 MySQL 开启查询缓存
query_cache_type=1
query_cache_size=614400
或者,当实例启动时 query_cache_type 非 0 的情况下,也可以通过以下命令在线开启查询缓存(若启动值为 0 则该命令会报错,需修改配置文件后重启):
set global query_cache_type=1;
set global query_cache_size=614400;
手动清理缓存可以使用下面三个 SQL:
flush query cache;:清理查询缓存内存碎片。reset query cache;:从查询缓存中移除所有查询。flush tables; 关闭所有打开的表,同时该操作会清空查询缓存中的内容。now()、curdate()、last_insert_id()、rand() 等。query_cache_limit(默认 1 MB)时不会被缓存。SQL_NO_CACHE 的查询。查询缓存 SELECT 选项示例:
SELECT SQL_CACHE id, name FROM customer;# 会缓存
SELECT SQL_NO_CACHE id, name FROM customer;# 不会缓存
查询缓存是完全存储在内存中的,所以在配置和使用它之前,我们需要先了解它是如何使用内存的。
MySQL 查询缓存使用内存池技术,自己管理内存释放和分配,而不是通过操作系统。内存池使用的基本单位是变长的 block, 用来存储类型、大小、数据等信息。一个结果集的缓存通过链表把这些 block 串起来。block 最短长度为 query_cache_min_res_unit。
当服务器启动的时候,会初始化缓存需要的内存,是一个完整的空闲块。当查询开始返回结果时,由于此时无法预知完整的结果集有多大,MySQL 会先向内存池申请一个大小为 query_cache_min_res_unit 的基础数据块。如果结果集超出该块容量,则会在生成结果的过程中持续按需申请新的数据块,并将其通过链表拼接起来。
分配内存块需要先锁住空间块,所以操作很慢,MySQL 会尽量避免这个操作,选择尽可能小的内存块,如果不够,继续申请,如果存储完时有空余则释放多余的。
随着并发读写的进行,不同大小的缓存块被无序且随机地释放,加上分配时剩余的微小空间(小于 query_cache_min_res_unit)无法被复用,内存池中会迅速产生大量不连续的空闲内存块(类似操作系统层面的外部碎片),进而触发更频繁的内存整理消耗。
优点:
LOCK_query_cache 全局互斥锁的激烈竞争会导致大量线程处于等锁状态(可通过 SHOW PROCESSLIST 看到 Waiting for query cache lock),实际 TPS/QPS 反而大幅下降。缺点:
LOCK_query_cache)来保证并发安全。一旦涉及到高并发,成千上万条查询语句同时争抢该互斥锁进行缓存检查或写入,极其激烈的锁冲突和线程上下文切换开销将成为致命的性能瓶颈。在 MySQL Server 中打开查询缓存对数据库的读和写都会带来额外的消耗:
LOCK_query_cache 共享锁。高并发下,大量读请求同时争抢锁会形成排队。query_cache_size 越大持锁时间越长。Query Cache 的单一全局互斥锁设计导致写操作会阻塞所有其他读写请求,这也是 MySQL 8.0 移除它的首要原因。可以通过以下命令查看查询缓存的使用情况,判断是否值得开启:
SHOW STATUS LIKE 'Qcache%';
关键指标说明:
| 状态变量 | 含义 |
|---|---|
Qcache_hits | 缓存命中次数 |
Qcache_inserts | 写入缓存的查询次数 |
Qcache_not_cached | 未被缓存的查询次数(不可缓存或未命中) |
Qcache_lowmem_prunes | 因内存不足而被淘汰的缓存条目数,持续升高说明缓存空间不足或碎片严重 |
Qcache_free_memory | 缓存剩余空闲内存(字节) |
命中率参考公式:
命中率 = Qcache_hits / (Qcache_hits + Qcache_inserts + Qcache_not_cached)
若命中率长期低于 50%,说明工作负载不适合 Query Cache,建议关闭。此外,还需关注 Qcache_lowmem_prunes 与 Qcache_inserts 的比值:若比值极高,意味着刚写入缓存的数据很快因内存碎片或空间不足被剔除,此时开启缓存是纯负收益。Qcache_lowmem_prunes 持续增长时,可执行 FLUSH QUERY CACHE 整理内存碎片,或适当降低 query_cache_min_res_unit 的值。
MySQL 中的查询缓存虽然能够提升数据库的查询性能,但查询缓存机制本身也引入了额外的管理开销,每次查询后都要做一次缓存操作,失效后还要销毁。
查询缓存是一个适用较少情况的缓存机制。如果你的应用对数据库的更新很少,那么查询缓存将会作用显著。比较典型的如博客系统,一般博客更新相对较慢,数据表相对稳定不变,这时候查询缓存的作用会比较明显。
简单总结一下查询缓存的适用场景:
对于一个更新频繁的系统来说,查询缓存的作用是很微小的,在某些情况下开启查询缓存会带来性能的下降。
简单总结一下查询缓存不适用的场景:
《高性能 MySQL》这样写到:
根据我们的经验,在高并发压力环境中查询缓存会导致系统性能的下降,甚至僵死。如果你一 定要使用查询缓存,那么不要设置太大内存,而且只有在明确收益的时候才使用(数据库内容修改次数较少)。
确实是这样的!实际项目中,更建议使用本地缓存(比如 Caffeine)或者分布式缓存(比如 Redis) ,性能更好,更通用一些。