查询缓存为何在 MySQL 8.0 中被移除

查询缓存为何在 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.7query_cache_type = 0已废弃但代码还在
MySQL 8.0查询缓存代码被完全删除

移除的原因

  1. 维护成本高:查询缓存的代码复杂,Bug 多(著名的 #65184 等)
  2. 收益为负:大多数场景下开启比关闭更差
  3. 架构演进:现代 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 等缓存层。对于聚合查询,可以考虑建汇总表代替。

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容