缓存命中率看着正常但回源还是很高?一次“平均值掩盖高价值 miss”的排查记录
平均命中率还在 95% 以上,数据库回源却压不住,问题往往不在图错了,而在真正昂贵的那批请求正在持续 miss。比起讨论“命中率算不算正常”,更该先把高价值 miss 从平均值里剥出来。
有一类缓存事故特别容易把人带偏:监控大盘上的命中率看着不难看,甚至还能稳在 95% 以上,可数据库回源就是降不下来,接口 RT 也始终贴着高位。
上个月有次订单确认页变慢,现场就是这样。值班同学先把 Redis 命中率截图发出来:95.6%,比平时只低了 1 个点。有人顺势判断,缓存不是主因,先去查数据库。
但数据库那边追了十几分钟,只能看到结果层:只读库 QPS 高升、连接池紧、order_snapshot 相关 SQL 变多。真正把主线拉回来的,是把命中率从“全站平均”拆成“核心链路命中”。
平均值为什么会骗人
那次事故里,便宜请求其实命中得很好。
- 首页轻量配置、推荐位、营销文案几乎都还在命中
- 真正出问题的是订单确认页里最贵的那段:优惠资格校验和库存快照查询
一旦把指标拆开,画面马上变了:
| 指标 | 事故前 | 事故时 |
|---|---|---|
| 全站缓存命中率 | 96.7% | 95.6% |
confirmOrder 链路命中率 | 93.9% | 71.8% |
| 优惠资格 key 命中率 | 91.2% | 48.5% |
| 只读库回源 QPS | 2.1k/s | 6.4k/s |
所以真正该问的不是“平均命中率正常不正常”,而是:最贵的那批请求,有没有被平均值藏起来。
这次事故的首证据,不是 miss 总量,而是 SQL 成本结构
那天把新增回源 SQL 按耗时和调用次数排了一遍,发现新增最明显的是两类查询:
select ... from coupon_user_quota where user_id = ? and activity_id = ?select ... from inventory_snapshot where sku_id in (...)
它们的共同点不是调用量绝对最高,而是一旦 miss,单次代价很贵。一个是要扫资格状态,一个要拼库存分片结果。总 miss 量并没有高到离谱,但只要这些查询反复落到数据库,确认页就会先慢下来。
这也是为什么全站命中率还能看,可业务已经有事故感:命中的多数是便宜请求,没命中的偏偏是最不该 miss 的那一小段。
真正的问题,是一段高价值缓存被写路径不断打空
继续往下查,发现优惠资格这段缓存不是自然过期,而是被一条补偿任务持续清掉。
那天 18:40 上了一版营销活动修正逻辑,补偿程序会按用户批量回刷资格。每刷一批,就顺手删一次 coupon:quota:user:{userId}:activity:{activityId}。按设计看没错,但活动高峰里,这类 key 本来就是确认页最频繁读取的缓存。
于是现场变成了一个很别扭的样子:
- 大部分缓存都还健康
- 只有高价值资格缓存不断被删
- 每次删掉以后,下一波确认请求又把查询打回数据库
这就是为什么平均命中率不够用。它会把“便宜请求大量命中”和“昂贵请求持续 miss”揉成一个温和的数字。
怎么确认不是数据库先慢,而是缓存先漏
那次最有用的不是再看一层总图,而是把补偿任务时间点和回源曲线叠在一起。
19:02 开始,补偿任务每 30 秒扫一批用户资格;从这一刻起:
coupon:quota相关 key 的 miss 先起- 2 到 3 秒后,只读库上的资格查询 QPS 跟着抬高
- 再往后,订单确认页的线程池 active 和获取连接耗时才一起往上走
时间顺序很干净:先是高价值缓存留不住,再是昂贵查询回源,再是数据库和应用资源被拖紧。
如果数据库是第一现场,顺序不会长这样。
这次止血为什么不是“把命中率再拉高一点”
当时没有先去改整站 TTL,也没有先补一堆概念性优化,而是只盯那批高价值 miss 做了两件事:
- 把补偿任务从逐批删缓存,改成写库完成后只标记脏数据,由异步线程合并刷新
- 给
coupon:quota这段缓存加 90 秒本地兜底,避免短时间重复回源
这两个动作下去以后,顺序很清楚:
- 资格查询 SQL 每秒调用数先掉
- 只读库连接池等待在 3 分钟内回落
confirmOrder的 p95 从 920ms 回到 260ms- 全站平均命中率只回升了 0.7 个点
这其实特别能说明问题:救场的关键不在把平均命中率修得多好看,而在把那一小段昂贵 miss 收住。
这类场景最容易误判成什么
最容易误判成两种东西。
第一种,是“数据库自己不行了”。因为从结果看,确实是数据库查询变多、连接池变紧、接口变慢。但如果不把高价值 miss 单独拆出来,你很难看到它其实是被缓存结构拖下去的。
第二种,是“命中率都 95% 了,缓存应该问题不大”。这句话只在所有 miss 代价接近时才成立。现实里不是这样:有的 miss 只是查个轻量配置,有的 miss 会把确认链路里最贵的一段直接送进数据库。
什么时候该先这么拆,而不是继续看总命中率
如果你现场同时满足下面三个条件,就别再围着平均值打转了:
- 全局命中率变化不大
- 数据库回源和核心接口 RT 却明显变差
- 新增回源主要集中在少数高成本 SQL 或少数核心链路
这时候更像“平均值掩盖高价值 miss”,而不是缓存整体健康。
回到这篇真正想说的那句话
命中率看着正常,不等于回源压力就低。很多线上事故难的地方,不是没有指标,而是一个过于体面的平均值,把最该处理的那一小撮 miss 藏住了。
所以比起反复讨论“95% 算不算正常”,更值钱的一步是把命中率拆到核心链路、热点前缀和高成本 SQL 上。只有这样,你才看得见数据库到底是在为谁买单。