查询缓存的概念
MariaDB / MySQL 查询缓存是内置于服务器中的一种机制,用于在内存中存储 SELECT 查询结果。当相同的查询再次提交时,服务器直接返回缓存结果而无需执行查询。这很简单、优雅,而且可能非常高效。
这里的关键词是"可能"。查询缓存是 MariaDB / MySQL 中最被误解和配置错误的功能之一。正确使用时,它可以将响应时间降低 10 倍。配置不当时,它可能摧毁性能。
三种模式
查询缓存有三种工作模式,由 query_cache_type 变量控制:
OFF (0)
查询缓存完全禁用。不缓存任何查询,不执行任何缓存检查。这是写密集型工作负载的最安全选项。
ON (1)
所有 SELECT 查询默认都会被缓存,除了用 SQL_NO_CACHE 提示标记的查询。这是最激进的模式。
-- 这个查询将被缓存
SELECT * FROM products WHERE category_id = 5;
-- 这个查询不会被缓存
SELECT SQL_NO_CACHE * FROM products WHERE category_id = 5;
ON DEMAND (2)
默认不缓存任何查询。只有明确用 SQL_CACHE 标记的查询才会被缓存。这是最可控的模式,通常也是最有效的。
-- 这个查询不会被缓存(默认行为)
SELECT * FROM products WHERE category_id = 5;
-- 这个查询会被缓存
SELECT SQL_CACHE * FROM products WHERE category_id = 5;
ON DEMAND 模式是我在大多数情况下推荐的。它迫使你思考哪些查询值得被缓存,而不是盲目缓存一切。
失效:阿喀琉斯之踵
查询缓存在对某张表执行写操作后,会使该表的所有缓存条目失效。不仅仅是受影响的行——而是整张表。
-- 假设 'products' 表有 1000 个 SELECT 被缓存
UPDATE products SET price = 19.99 WHERE product_id = 42;
-- → 'products' 的所有 1000 个缓存条目都被失效
这是一个粗暴但必要的机制,用以保证一致性。问题在于,对于频繁修改的表,缓存会不断被失效和重建,消耗的资源比没有缓存还多。
缓存大小设置
查询缓存大小由 query_cache_size 控制:
[mysqld]
query_cache_type = 2
query_cache_size = 64M
query_cache_limit = 2M
query_cache_min_res_unit = 2048
- query_cache_size:缓存总大小。不要超过 256 MB——超过这个值,全局缓存互斥锁会成为瓶颈。
- query_cache_limit:单个结果的最大大小。更大的结果不会被缓存。
- query_cache_min_res_unit:分配块大小。对于小结果减小此值可以降低碎片化。
监控缓存
Qcache_* 状态变量对于评估效果至关重要:
SHOW GLOBAL STATUS LIKE 'Qcache%';
关键指标:
| 变量 | 描述 |
|---|---|
Qcache_hits |
从缓存提供服务的查询数 |
Qcache_inserts |
添加到缓存的查询数 |
Qcache_not_cached |
未被缓存的查询(过大、提示等) |
Qcache_lowmem_prunes |
因内存不足而被淘汰的条目数 |
Qcache_free_memory |
缓存中的可用内存 |
Qcache_total_blocks |
分配的块总数 |
Qcache_free_blocks |
空闲块(高 = 碎片化) |
效率比率
最重要的比率是命中率:
命中率 = Qcache_hits / (Qcache_hits + Com_select) * 100
解读:
- > 40%:缓存有效,值得保持启用
- 20-40%:灰色地带,需要逐案评估
- < 20%:缓存无效,考虑禁用
第二个重要比率是淘汰率:
淘汰率 = Qcache_lowmem_prunes / Qcache_inserts * 100
如果此比率超过 10%,缓存太小——增大 query_cache_size 或减少缓存内容。
并发陷阱
查询缓存使用全局互斥锁。这意味着同一时间只有一个线程可以读取或写入缓存。在有 100 个并发连接的服务器上,这个互斥锁会成为严重的瓶颈。
这就是为什么 MySQL 8.0 完全移除了查询缓存。Oracle 团队认为在现代工作负载(高并发、频繁写入)中,互斥锁的开销超过了缓存的收益。
MariaDB 选择保留它,理由是它对特定使用场景仍然有用(读密集型负载、低并发、很少修改的表)。
查询缓存与 Galera:艰难的组合
在 Galera 集群中使用查询缓存是有问题的。Galera 将写操作复制到所有节点,但查询缓存对每个节点来说是本地的。结果是:
- 节点 A 接收一个 SELECT 并缓存结果
- 节点 B 通过 Galera 复制接收一个 UPDATE
- 节点 A 的缓存不会被失效——它不知道数据已经改变
- 节点 A 上的下一个 SELECT 返回过期数据
在 Galera 中安全使用查询缓存的唯一方法是设置 wsrep_causal_reads = ON,它会在每次读取前强制执行一致性检查。但这在很大程度上抵消了缓存的收益。
替代方案:MaxScale 缓存过滤器
对于需要分布式查询缓存的架构,MaxScale 缓存过滤器是更好的方案:
[query-cache]
type = filter
module = cache
storage = storage_inmemory
ttl = 10s
max_size = 256Mi
MaxScale 缓存是集中式的(在代理层),这消除了 Galera 节点之间的不一致问题。此外,MaxScale 可以基于查询类型智能地失效缓存,而不仅仅是基于被修改的表。
何时启用查询缓存
查询缓存在以下情况下是适用的:
- 工作负载以读为主(> 80% SELECT)
- 表很少被修改(配置表、参考数据)
- 并发程度中等(< 50 个并发连接)
- 相同的查询被频繁重复(有缓存未命中的 Web 应用)
- 你在单机服务器上,不是 Galera 集群
查询缓存在以下情况下不适用:
- 工作负载是读写混合的
- 表频繁被修改(事务表)
- 并发度高(全局互斥锁)
- 你在 Galera 集群上(缓存不一致)
结论
MariaDB 的查询缓存是一个强大但精细的工具。带 SQL_CACHE 提示的 ON DEMAND 模式提供了最佳控制。通过 Qcache_* 变量进行监控对于评估效果至关重要。对于分布式架构,MaxScale 缓存过滤器通常是更好的选择。
与任何性能工具一样,关键在于度量。启用、监控、调整。如果命中率始终低于 20%,就禁用它并继续前进。
本文最初发表于 Medium。
评论 (0)
暂无评论。
发表评论