2444 字
约 8 分钟
21
S12-A15 · 命中率之外:缓存效果的三个口径

一个缓存命中率 96%,团队认为效果很好。直到某次压测发现:缓存挂掉之后,数据库只承受了 1.6 倍的流量——说明这个缓存实际只挡住了很小一部分实际工作,那 96% 的命中大多是极便宜的、本来就不会打到数据库的请求。

命中率是一个容易被误读的指标,因为它只统计"有没有命中",不统计"命中省下了多少"。要判断缓存是否真的有用,至少还要看两个数:回源成本的节省量,以及失效时的放大倍数。


1. 三个口径各自回答什么

口径 公式 回答
命中率 命中次数 / 总请求 有多少请求没回源
加权命中率 Σ(命中 × 单次回源成本) / Σ(全部请求 × 成本) 省下了多少工作量
失效放大 缓存不可用时的回源量 / 正常回源量 缓存挂了会怎样

三个口径的使用顺序也有讲究:先算加权命中率判断"值不值得",再算失效放大判断"风险",最后才看命中率做日常监控。顺序反了,就会拿一个美化过的数字去做价值判断。

第一项最常见,也最容易美化:大量低成本的命中会把比率推高,而真正昂贵的那些请求可能一直没命中。

第二项把"成本"引入进来,才回答"这个缓存值不值得"。第三项是容量规划的依据——它决定"缓存故障"是不是一个灾难。

// 同一份数据的三个数字,结论完全不同
命中率:      96%(看起来很好)
加权命中率:  41%(昂贵的请求大量回源)
失效放大:    1.6 倍(数据库能承受,缓存其实可有可无)

2. 命中率为什么会被美化

美化方式 机制
低价值请求占比高 便宜请求命中多,拉高整体
分母含"不该走缓存"的请求 健康检查、静态资源
多次读取同一键 一次加载多次命中
缓存自身兜底(本地缓存) 统计层看不到外层

第三项很微妙:一个键加载一次后被读一万次,命中率看起来接近 100%,但它只省了一次数据库查询。这类"读多写零"的键对命中率贡献极大,对成本节省贡献极小。

第四项在两级缓存里常见:本地缓存命中被记成"命中",但它没有挡住任何回源——因为本地缓存本来就是从分布式缓存拿的。

3. 加权命中率怎么算

步骤 做法
1 标注成本 按接口或按键类型给"单次回源成本"(耗时或资源)
2 采集分布 记录每类请求的命中/未命中次数
3 加权求和 按成本加权,得到"节省比例"

采样方式也要注意:按请求采样,不要按接口采样。按接口取平均会掩盖"同一接口内 heavy 与 light 混合"的情况,而这种情况恰恰是最需要看清楚的。

关键在第一步:成本不必精确,分档就够。把回源成本分成"轻(内存计算)、中(单表查询)、重(多表聚合或远程调用)"三档,比追求精确的成本模型更实用。

// 三档成本模型:够用且可维护
light   : 本地计算、字典查询        权重 1
medium  : 单表索引查询、单次缓存读  权重 5
heavy   : 多表聚合、远程调用、搜索  权重 50

加权命中率 = Σ(命中数 × 权重) / Σ(总数 × 权重)
// 若 heavy 类的加权命中率很低,说明缓存没帮到最贵的那部分

4. 失效放大倍数

缓存状态 回源量
正常 1(基准)
缓存全挂 N 倍(等于原命中量)
缓存部分失效(热点键被淘汰) 可能接近 N
缓存与数据库同时降级 叠加

放大倍数还有一个二阶效应容易被忽略:回源量翻倍时,下游的耗时也会上升(数据库在更高负载下变慢),于是总时延可能涨三四倍而不是两倍。这就是"缓存故障往往演变成雪崩"的机制。

第二项的 N 就是"缓存挡住的比例"的倒数:挡住 60% 意味着失效时回源 2.5 倍。所以"缓存挂了能不能扛"这个问题,答案不在缓存配置里,而在数据库的余量里。

// 估算失效放大的最直接方式:看回源请求数与数据库容量
redis-cli info stats | grep -E "keyspace_hits|keyspace_misses"
// 假设命中 h、未命中 m:挡住比例 = h/(h+m)
// 放大倍数 ≈ (h+m)/m;若数据库余量 < 该倍数,缓存失效即故障

还有一个与容量规划直接相关的用法:用失效放大倍数反推"需要多少数据库余量"。如果缓存挡住了 60% 的请求,那么数据库至少要能承受 2.5 倍突发;这个数字应当写进容量表,而不是留在缓存团队的口头共识里。

// 容量表的第四行:缓存失效场景
正常回源 QPS:2000
缓存失效 QPS:5000(放大 2.5 倍)
数据库安全上限:6000
结论:可承受缓存失效,但余量仅 20%,需保留降级开关

5. 三个口径的联动判断

命中率 加权命中率 结论
高 高 缓存有效,关注失效放大
高 低 缓存只挡住了便宜请求,重新设计键
低 高 昂贵请求命中,便宜回源无妨——可接受
低 低 缓存基本没用,考虑移除

第三行是个反直觉但重要的情况:命中率低不代表缓存无效,如果没命中的都是便宜请求,那么回源成本本来就不高。此时优化的方向不是提高命中率,而是确保昂贵请求始终命中。

// 判断顺序:先看加权,再看整体
if weighted_hit > 0.7:  缓存有效 → 评估失效放大
elif weighted_hit < 0.3: 几乎无效 → 移除或重设计
else:                   局部有效 → 找出未命中的 heavy 类,单独优化

6. 排错:五种"命中率好但没效果"

现象 成因 处理方向
命中率 95% 但库压力没降 命中集中在便宜请求 算加权命中率
缓存挂了就雪崩 挡住比例高且库无余量 降级方案 + 余量评估
加了缓存 QPS 没涨 瓶颈不在数据库 先定位瓶颈
命中率忽高忽低 键设计不稳定或 TTL 抖动 统一键规则
两级缓存命中率高但慢 本地命中仍需一次远程校验 检查层级职责

排错时还可以直接对比两个时间点的数据:缓存正常时与某次缓存故障时的回源量。这个比值是最真实的放大倍数,比任何公式估算都可靠——前提是那次故障被记录下来。

第四项常源于"键里带时间戳"这类设计:每秒的键都不同,命中率自然低且波动。这类问题的修复不在缓存参数,而在键的定义。

7. 边界:什么情况只需看命中率

场景 为什么 做法
请求成本高度均匀 加权与简单平均等价 只看命中率
缓存只保护单一接口 成本单一 看该接口的命中率
纯静态内容 成本几乎为零 命中率只反映带宽
已有压测覆盖失效场景 放大倍数已知 不必重复算

最后一条与指标治理有关:把加权命中率写进看板,而不是只写命中率。只有有人看,键设计的问题才会被发现;只写命中率,等于给缓存系统发了一张永久的合格证。

判断口径一句话:不同请求的回源成本是否差一个数量级以上。差距大,必须加权;差距小,命中率就够。


8. 自测

  1. 三个口径分别回答什么问题?为什么命中率高可能意味着缓存无效?
  2. "一个键加载一次被读一万次"为什么对命中率贡献大、对成本节省贡献小?
  3. 加权命中率的成本为什么要分档而不是精确计算?三档模型怎么切?
  4. 失效放大倍数与"缓存挡住的比例"是什么关系?它决定了哪个工程决策?
  5. "命中率低但加权命中率高"意味着什么?此时优化方向是什么?
  6. 判断"是否必须加权"的那个问题是什么?
S12-A15 · 命中率之外:缓存效果的三个口径
https://www.clxhxhhr.top/posts/6430/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。