Java

缓存命中率下降怎么排查?一次发布后 key 设计被打碎的排查过程

命中率突然下降,不一定是流量变散了,也不一定是 Redis 出了故障。一次发布里,因为 key 拼接多带了渠道和实验参数,同一份商品数据被拆成了成倍增长的变体 key,命中率就是这样掉下去的。

  • Redis
  • 缓存
  • 命中率
  • 性能排查
  • 系统设计
9 分钟阅读

缓存命中率下降时,团队很容易先分成两派:一派说业务流量变长尾了,掉一点很正常;另一派说缓存方案肯定写坏了,不然不会掉得这么快。

我后来越来越不喜欢这种开场,因为它太容易把人带去争观点,而不是看证据。真到现场,最值钱的往往不是先判断“哪一派对”,而是先抓到 命中率为什么是从这一分钟开始掉的

有次商品搜索页就是这样。14:07 发了一版灰度,14:10 命中率开始从 92% 往下掉,到 14:18 已经只剩 71%。最开始很多人都怀疑新活动把搜索词打散了,但真正把问题钉死的,不是搜索词分布,而是 Redis 里的 key 基数突然放大了。

第一眼该看的,不是热词分布,而是 key 基数

那次现场最早能说明问题的,其实是这两个数字:

  • 每分钟搜索请求数只比发布前多了 6%
  • search:product:list:* 这一组 key 的唯一 key 数,却在 10 分钟里涨了 3.8 倍

如果真是业务流量自然变长尾,请求量、搜索词分布、热门词占比通常会一起动。但那次不是。请求量没怎么变,热门词前十也基本没变,真正变的是:同一批查询,被拼成了更多互不复用的 key。

这一步一看见,排查方向其实就收了:先别急着讲 TTL、容量、数据库,先去看发布前后 key 规则到底差了什么。

根因是一次“看起来合理”的参数补充

翻代码之后发现,灰度版本里为了支持渠道和实验分组分析,搜索结果缓存 key 从:

search:product:list:{keyword}:{pageNo}

改成了:

search:product:list:{keyword}:{pageNo}:{channel}:{expId}

改法本身不是完全没道理。问题在于,这两个新增维度里,channel 有十几种,expId 又会跟 AB 实验动态变化,而搜索结果本身并没有真的按这两个维度分叉那么多。

结果就是一份原本可以共享的热门搜索结果,被拆成了一堆近似内容的变体 key。

为什么这次更像 key 设计被打碎,而不是流量自己变散

因为几个证据能互相咬住。

第一,热门词榜单没明显变化,说明请求兴趣本身没突然长尾化。

第二,只有灰度实例命中率先掉,全量实例还在原水平附近。要是流量自然变化,全量和灰度不该分叉得这么明显。

第三,同一个关键词在 Redis 里出现了大量近似 key,例如:

  • search:product:list:手机壳:1:app:A12
  • search:product:list:手机壳:1:app:B07
  • search:product:list:手机壳:1:h5:A12

它们返回的数据几乎一样,却没法互相复用。

只要看到这一层,命中率下降就不再是抽象现象,而是 key 复用能力被人为打碎了。

为什么数据库和接口变慢来得比命中率晚一点

这次也很典型。14:10 到 14:13 之间,先掉的是 Redis 命中率;数据库和接口 RT 是 3 分钟后才跟上来的。

顺序大概是:

  • 新 key 规则让热门查询共享失败
  • miss 变多,搜索结果开始更多回源 Elasticsearch 和数据库补充信息
  • 聚合链路耗时被拉长
  • 接口 p95 和线程池排队才开始明显变差

这个顺序很重要。它证明数据库慢不是第一现场,而是命中率下降继续传导下去的结果。

止血动作为什么是回滚 key 规则,而不是先调 TTL

当时做得最有效的一步特别朴素:把 key 规则先回滚,只保留真正影响结果集的参数,把 channelexpId 从缓存主 key 里拿掉,实验差异改成结果返回后的轻量处理。

动作下去 5 分钟后:

  • search:product:list:* 的 key 基数开始回落
  • 命中率从 71% 回到 88%
  • 搜索页 p95 从 1.3s 回到 420ms

这也解释了为什么那次不该先调 TTL。TTL 只能让碎掉的 key 活得久一点,不能把它们重新合并成可复用的一份数据。

这类事故为什么容易被“业务变长尾”带偏

因为从结果看,它确实很像流量变散了:

  • 热门词占比下降一点
  • miss 变多
  • 回源上升
  • 搜索接口变慢

但“流量真的变散”与“同一流量被拆成更多 key”,现场长得很像,治理方式却完全不同。前者可能要改缓存模型,后者先把 key 设计修回来就行。

所以我现在碰到命中率突然掉,第一反应不是去猜业务,而是先问一句:同一份逻辑数据,现在还会不会落到同一个 key 上?

什么信号最值得优先看

如果你怀疑是 key 设计问题,比总命中率更值得先拉的是这几组:

  • 发布前后的 key 基数变化
  • 灰度实例和全量实例的命中率差异
  • 热门对象有没有生成大量近似 key
  • 新增参数里,哪些其实并不真正影响结果

只要这些证据对上,排查就会比继续看总览快很多。

回到这次事故留下的一条判断

缓存命中率下降,并不一定是缓存留不住数据,也不一定是业务天然更长尾。很多时候,只是我们把原本能共用的一份结果,亲手拆成了很多份彼此隔离的缓存。

所以这类问题里,最先该看的不是“要不要调大 TTL”,而是 key 规则有没有把共享面切碎。一旦切碎,命中率掉下去几乎是必然的。