查询缓存为何在 MySQL 8.0 中被移除
什么是查询缓存
查询缓存(Query Cache)是 MySQL 5.7 及之前版本的一个特性,它会缓存 SELECT 语句的完整结果集和对应的 SQL 语句:
客户端执行 SELECT * FROM users WHERE id = 1
↓
查询缓存检查:这个 SQL 的 MD5 哈希是否在缓存中?
├── 命中 → 直接返回缓存的结果集(极快)
└── 未命中 → 执行查询 → 将结果存入缓存
查询缓存的工作原理
缓存键
缓存键 = SQL 语句的精确 MD5 哈希 + 当前数据库名 + 协议版本
SELECT * FROM users WHERE id = 1 → MD5(0x4A3B...)
SELECT * FROM users WHERE id = 1 → 相同哈希(命中)
select * from users WHERE id = 1 → 不同哈希(因为大小写不同)
SELECT * FROM users WHERE id = 2 → 不同哈希(参数不同)
缓存失效
任何对表的修改操作都会导致该表相关的所有查询缓存失效:
UPDATE users SET name = 'new' WHERE id = 1
↓
清空所有涉及 users 表的查询缓存
↓
下一次 SELECT 重新计算写入缓存
查询缓存的核心问题
1. 缓存失效非常频繁
这是查询缓存最大的问题。即使用户表有 1000 万条数据,插入一条新记录就会让所有缓存失效。对着高频更新的表,查询缓存几乎永远无效:
-- 想象一个博客系统
-- 下列查询都被缓存了
SELECT * FROM posts WHERE id = 1; -- 存入缓存
SELECT * FROM posts WHERE id = 2; -- 存入缓存
...
-- 新加一条评论 → posts 表有变更
INSERT INTO comments VALUES (...); -- comments 本身没事
-- 但如果 posts 表也有更新时间字段
UPDATE posts SET comment_count = ...; -- 所有 posts 的查询缓存全部失效!
2. 缓存操作需要加锁
查询缓存的读写需要加 Query Cache Latch(互斥锁),在 MySQL 5.6 及之前版本,这在高并发场景下会成为严重的性能瓶颈:
连接 A:SELECT → 获取 QCache 锁 → 检查是否命中 → 释放锁
连接 B:SELECT → 等待 QCache 锁...
连接 C:UPDATE → 等待 QCache 锁...(要清空缓存)
多个线程竞争同一个互斥锁,导致 MySQL 吞吐量下降,甚至开启查询缓存比关闭时性能更差。
3. 内存管理低效
- 缓存存储在
query_cache_size分配的内存中 - 存储结构类似链表,有碎片化问题
- 大结果集会占据大片缓存空间,挤掉其他有用缓存
- 无法精确控制每个表/每个查询的缓存策略
4. 缓存命中率低
经典的生产环境经验:查询缓存的命中率通常只有 1-5%。
原因:
– 大多数表都在频繁更新
– SQL 语句必须在字符上完全一致(包括大小写、空格、注释)
– 带有非确定性函数(NOW()、RAND()、UUID())的查询不可缓存
– 子查询、UNION、临时表等操作结果不可缓存
为什么互联网公司都关了查询缓存
早在 2010 年左右,MySQL DBA 的最佳实践就已经变成了:
query_cache_type = 0 # 关闭查询缓存
query_cache_size = 0 # 不分配空间
理由很简单:
– 默认开启反而让大多数业务性能更差
– 与其依赖数据库缓存结果集,不如用 Redis/Memcached 等外部缓存
– 应用层的缓存更可控:过期策略、缓存粒度、缓存预热
MySQL 8.0 的移除
MySQL 8.0 正式移除了查询缓存模块:
MySQL 5.7:query_cache_type = 0(已废弃,但代码还在)
MySQL 8.0:查询缓存代码被完全删除
移除的原因
- 维护成本高:查询缓存的代码复杂,Bug 多(著名的 #65184 等)
- 收益为负:大多数场景下开启比关闭更差
- 架构演进:现代 MySQL 应该用 InnoDB Buffer Pool + 外部缓存
替代方案
1. MySQL 内部
虽然没有查询缓存,但 InnoDB Buffer Pool 本身就是数据库级别的缓存:
innodb_buffer_pool_size = 10G # 缓存数据页和索引页
大量重复查询通过 Buffer Pool 直接从内存获取——不需要查询缓存。
2. 应用层缓存(推荐)
# 伪代码
def get_user(id):
user = redis.get(f"user:{id}")
if user:
return user
user = db.query("SELECT * FROM users WHERE id = %s", id)
redis.setex(f"user:{id}", 3600, user) # 缓存 1 小时
return user
3. Proxy 层缓存
- ProxySQL:支持查询缓存规则
- MyCAT:中间件层缓存
- 负载均衡器配合缓存策略
4. 物化视图
MySQL 原生不支持物化视图,但可以通过触发器或定时任务模拟:
-- 创建汇总表并定期更新
CREATE TABLE post_stats AS
SELECT date, COUNT(*) as post_count FROM posts GROUP BY date;
-- 每 5 分钟更新一次
INSERT INTO post_stats
SELECT CURDATE() as date, COUNT(*) as post_count FROM posts
ON DUPLICATE KEY UPDATE post_count = VALUES(post_count);
面试常问题
Q:为什么 MySQL 的查询缓存在高并发下反而拖慢性能?
A:操作查询缓存需要获取全局互斥锁(Query Cache Latch),高并发下锁竞争激烈。而且写操作会清空整表缓存,导致读操作在重新填充缓存时也要竞争锁。结果是开启了缓存比不开还慢。
Q:查询缓存真的毫无用处吗?
A:不是。在一些非常特定的场景下有用:
– 只读数据库(不做 UPDATE/INSERT)
– 配置表(几乎不更新)
– 查询结果极小的场景
但即使如此,也不如用 Buffer Pool 或应用层缓存。
Q:MySQL 8.0 移除了查询缓存,该用什么替代?
A:优先用 InnoDB Buffer Pool(缓存数据页),然后根据业务需要在应用层加 Redis 等缓存层。对于聚合查询,可以考虑建汇总表代替。


暂无评论内容