MySQL 8.0正式移除了查询缓存(Query Cache)模块,这一改动让不少从旧版本迁移过来的开发者感到意外。查询缓存原本的设计目标是将SELECT语句及其结果集直接保存在内存中,当同样的查询再次发生时免去解析和执行开销。但在真实的生产环境中,该机制在并发写入频繁的场景下暴露出明显的性能瓶颈,最终促使官方在8.0中将其彻底删除。

查询缓存的基本工作方式
在MySQL 5.7及之前版本中,可以通过参数query_cache_type开启查询缓存。当一条SELECT语句进入时,系统先计算其哈希值,若命中缓存则直接返回结果,否则执行查询并将结果写入缓存。相关配置如下:
-- 查看查询缓存状态 SHOW VARIABLES LIKE 'query_cache%'; -- 开启查询缓存(旧版本) SET GLOBAL query_cache_type = ON; SET GLOBAL query_cache_size = 134217728; -- 128MB
并发写操作下的核心瓶颈
1. 表级缓存失效过于粗暴
查询缓存的失效逻辑是:只要某张表发生了任意INSERT、UPDATE、DELETE或DDL操作,这张表上所有已缓存的查询结果都会立即被标记为无效。这意味着即便只是修改了一行数据,其他完全不相关、只读取不同行的SELECT缓存也会全部清空。
- 写操作越频繁,缓存命中率越低
- 缓存区不断被填充又迅速被清空,产生大量无效内存回收
- 在写多读少的业务中,查询缓存几乎无法发挥作用
2. 全局互斥锁带来的串行化问题
查询缓存区在内部由一把全局锁保护。无论是检查缓存、写入缓存还是失效缓存,线程都必须先获取这把锁。在高并发场景下,大量连接会阻塞在锁竞争上。
并发请求流程示意: 线程A:获取QC锁 -> 检查缓存 -> 释放锁 线程B:获取QC锁 -> 写缓存 -> 释放锁 线程C:获取QC锁 -> 失效缓存 -> 释放锁 由于锁唯一,A/B/C实际只能串行执行
3. 维护缓存本身的开销
每次查询都需要计算哈希、管理缓存条目,写操作还要遍历并清理对应表的缓存。当并发写达到一定程度,这些管理成本超过了缓存命中带来的收益,系统吞吐量反而下降。
一个简单的并发测试对比
下面用伪代码说明在并发写压力下开启与关闭查询缓存的差异:
import threading
def worker_with_qc():
# 模拟开启查询缓存:每次写操作触发全表缓存失效
for i in range(1000):
execute_update("UPDATE user SET score=score+1 WHERE id=1")
execute_select("SELECT * FROM product WHERE id=2") # 缓存已被清
def worker_without_qc():
# 关闭查询缓存:直接走引擎查询
for i in range(1000):
execute_update("UPDATE user SET score=score+1 WHERE id=1")
execute_select("SELECT * FROM product WHERE id=2")
# 启动50个并发线程
threads = [threading.Thread(target=worker_with_qc) for _ in range(50)]
在worker_with_qc中,product表的查询缓存因为user表的更新而被反复淘汰,缓存完全失效;而worker_without_qc没有这部分开销,整体延迟更低。
MySQL官方给出的替代方案
删除查询缓存后,官方建议将缓存能力下沉到应用层或使用专用缓存系统:
| 方案 | 说明 |
|---|---|
| 应用层缓存 | 使用Redis等中间件缓存热点数据,控制粒度更细 |
| ProxySQL | 在代理层实现查询路由与结果缓存 |
| 调整业务读写比 | 将复杂报表查询分流到只读副本 |
总结
MySQL 8.0删除查询缓存并不是因为缓存思想本身有问题,而是旧实现中表级失效与全局锁的设计无法适应高并发写场景。对于使用SELECT频繁且写操作稀少的系统,原本能获益;但多数现代业务写并发高,查询缓存反而成为性能包袱。理解这一背景,有助于我们在新版本中更合理地设计数据访问层。