查询缓存(Query Cache)曾是 MySQL”同一条 SQL 直接返回结果”的内置优化,却因一把全局锁和粗暴的表级失效粒度,在高并发下从加速手段沦为性能瓶颈,最终在 8.0 被彻底删除。

设计意图:命中即返回

Query Cache 的思路很直接:SELECT 语句到达 Server 层、通过权限校验之后,先对 SQL 文本计算哈希,拿哈希值去缓存里找结果集——命中就直接返回给客户端,连解析器、优化器、存储引擎都不用进。缓存 key 由 SQL 原文、当前数据库、客户端字符集、协议版本等共同算出,因此大小写、空格、注释的差异都会被视为不同查询;含 NOW()、RAND() 这类不确定函数的查询则直接不可缓存。

听起来很美好,但它只在”读多写极少、查询高度重复、结果集小”的静态场景下才有正收益。

为什么高并发下成为瓶颈

两个结构性缺陷决定了它的命运。

全局互斥锁。 Query Cache 的所有操作——读前查缓存、写结果进缓存、失效清理——都由同一把全局互斥锁保护。并发一高,大量线程堵在锁上,SHOW PROCESSLIST 里成片出现 Waiting for query cache lock,所谓”缓存加速”变成了”锁排队”,多核机器上问题被进一步放大。

表级失效粒度。 任何对表的写操作,哪怕只改一行,都会让涉及该表的全部缓存条目失效;失效时还要持独占锁扫描缓存区,缓存开得越大持锁越久,期间连不相关表的缓存访问都被阻塞。写入稍频繁,命中率立刻崩塌,刚存进去的结果下一秒就被清空,纯做无用功。此外按 query_cache_min_res_unit 分块的内存管理容易产生碎片,维护成本远超收益。

5.7.20 起官方将其默认关闭并标记废弃,8.0 直接移除,query_cache_type、query_cache_size 等参数不复存在。决策逻辑很清晰:一个只在小众场景有正收益、却给所有用户带来并发风险与代码复杂度的功能,不如删掉。

8.0 之后靠什么缓存

MySQL 并未放弃缓存,只是把缓存换到了更合适的层次:

  • InnoDB Buffer Pool:缓存数据页与索引页,与 SQL 文本无关,失效粒度是页,配合改进的 LRU 管理,这才是数据库内缓存的正解。
  • 应用层缓存:进程内用 Caffeine 这类本地缓存承接极高频热点,跨实例用 Redis。缓存粒度由业务掌控(按对象、按页面),失效策略(TTL、主动失效、延迟双删)可精细定制,还能横向扩容。

应用层缓存的正确姿势:key 设计包含业务维度与版本号,避免发布后读到旧结构数据;只缓存热点小对象,别把大结果集塞进 Redis 撑爆内存;配套治理穿透(空值缓存、布隆过滤器)、击穿(互斥重建、逻辑过期)与雪崩(TTL 加随机抖动)。

小结

Query Cache 的失败在于粒度太粗:key 粗(整段 SQL 文本)、锁粗(全局一把)、失效粗(整表作废)。现代方案把缓存拆到页级(Buffer Pool)和业务级(Redis/本地缓存),各得其所。

面试追问:

  1. 为什么写一行就要清掉整张表的缓存?(缓存系统只跟踪到表粒度,无法判断结果集具体依赖哪些行)
  2. 升级 8.0 后配置文件里还留着 query_cache_size 会怎样?(参数已移除,启动报未知变量错误,需清理)
  3. Buffer Pool 和 Query Cache 的本质区别是什么?(前者缓存数据页、与 SQL 无关;后者缓存 SQL 文本对应的结果集,失效粒度天壤之别)