Java

缓存预热做了还是抖?我会先把时间线、命中路径和 TTL 放在一张图上

预热任务成功、Redis 里也有 key,但切流一来还是抖,这种现场往往不是“有没有预热”这么简单。更常见的是预热范围打偏、命中路径变了、TTL 没活到接流量那一刻,或者 Redis 热了、本地状态没热。

  • Redis
  • 缓存预热
  • 流量切换
  • 命中率
  • 性能排查
18 分钟阅读

缓存预热这件事,最容易把人带进一个很危险的误区:任务成功了,就默认读链已经稳了。

我遇到过最典型的一次,是发布群里大家都说“预热已经跑完”,Redis 里抽样看 key 也都在,结果灰度一放量,命中率还是掉,数据库连接池 pending 立刻抬头。最后查下来,不是没人做预热,而是做的那批 key 根本不是新版本真正会打到的那一批;同时预热开始得太早,到了正式切 50% 流量的时候,部分热点 TTL 已经只剩十几分钟。

所以这类问题我通常不先争“有没有做预热”,而是先把下面四个点钉死:

  • 预热完成的时间
  • 开始接真实流量的时间
  • 真实请求命中的 key 路径
  • 命中率、回源量、数据库 RT 开始抬头的时间

只要这四个点落在一张时间线上,很多争论会自己消失。

我第一眼会先钉哪条时间线

先别急着翻预热脚本,先把现场摆平。

我一般会先拉一条很土但非常有用的时间线:

14:02  预热任务完成,日志打印 warm_count=120348
14:20  灰度实例启动完成
14:25  开始切 10% 流量
14:27  cache.hit.rate 从 93% 掉到 68%
14:28  db.qps 从 3k 涨到 11k
14:29  Hikari pending 从 0 到 37
14:31  P99 RT 从 180ms 到 1.6s

这条线的价值在于,它能先回答一个问题:数据库慢,是根因,还是预热没接住之后的结果?

如果命中率先掉、回源量先涨、数据库和连接池是后面才被拖高,那我通常不会先把锅甩给数据库。数据库当然可能也有问题,但它在这条链上更像第二段,而不是第一段。

我会先看四类证据,而不是先看预热脚本写得漂不漂亮

1. 预热进去的,和真实高峰请求打到的,是不是同一批 key

很多“做了预热还是抖”的现场,问题不在有没有预热,而在预热对象和真实热点不是一回事

最常见的几种偏差是:

  • 预热的是昨天的热点,不是今天活动页的新热点
  • 预热了主对象 key,没预热组合 key、列表页 key 或衍生统计 key
  • 预热脚本按总量取 Top N,但线上真正打爆的是少数超热点用户或商品
  • 新版本 key 规则改了,旧脚本还在按旧规则写

这一轮我会直接对比两份样本:

# 预热任务产出的 key 列表
warm_keys_2026-03-24.txt

# 真实高峰 5 分钟内最热的请求参数 / key 样本
access_top_keys_14_25_14_30.txt

如果这两份东西根本对不上,后面再讨论 TTL、切流、数据库,优先级都得往后放。

2. 真实流量切过来以后,是不是还走在同一条命中路径上

这是第二个高发误判点。

我见过不少现场,Redis 里明明有值,但请求还是 miss,最后发现根本不是 Redis 没被预热,而是真实请求压根没走到那条命中路径上。比如:

  • 新版本把 key 前缀从 product:detail:{id} 改成了 product:v2:{id}
  • 旧实例先查本地缓存,新实例先查另一层对象缓存
  • 流量切到新机房以后,访问的是另一套 Redis 分片
  • 灰度条件带上了租户或地域维度,实际请求落点变了

这种问题我会直接查一条真实请求的日志,把“参数 -> 生成的 key -> 查询的缓存层 -> 是否回源”串起来,而不是只看 Redis 里有没有数据。

例如像下面这种日志,就比一句“缓存里明明有值”有用得多:

requestId=9fd... skuId=84219 tenant=hz
cacheKey=product:v2:hz:84219
localCache=MISS
redis=MISS
dbQuery=SELECT ... WHERE sku_id=84219

为什么我通常不先把数据库当成第一嫌疑人

因为这类故障里,数据库很容易只是被打疼的那一层。

如果你已经看到下面这种顺序:

  1. 切流开始
  2. 命中率掉
  3. miss QPS 涨
  4. 数据库 RT 和连接池等待跟着抬

那我会先把注意力留在“预热为什么没接住真实流量”,而不是直接钻数据库索引。

当然,如果一开始数据库就慢,命中率却很平,那是另一回事。但只要链路顺序明显是 miss 在前、数据库在后,就别把因果倒过来。

预热做了还是抖,最常见的四种真问题

一种是预热范围打偏了

这个最好理解,也最常见。

比如活动商品页真正高频的 key 是:

product:v2:hz:84219
product:v2:hz:84220
product:v2:hz:84221

结果预热脚本打的是:

product:detail:84219
product:detail:84220
product:detail:84221

从“有数据”这个角度看,大家都没说谎;但从“真实流量能不能命中”这个角度看,等于没做。

一种是预热时机和切流时间根本没对齐

我比较怕这种情况:凌晨把预热任务跑了,白天下午才开始放量;TTL 又只有 2 小时,甚至 30 分钟。

这时你表面上完成了预热,实际上只是提前把热数据做了一次短暂展示,真正流量进来时,它早就不热了。

这类问题别只看 key 在不在,要直接看剩余 TTL:

redis-cli TTL product:v2:hz:84219

如果离切流只剩几十秒、几分钟,或者大量 key 的剩余寿命集中在同一个窗口,那结论已经很明确:不是没预热,是预热活得不够久。

一种是 Redis 热了,但实例还是冷的

很多团队说“我们已经把 Redis 预热好了”,但真实读链并不只靠 Redis。

有些接口后面还挂着:

  • 本地缓存
  • 连接池
  • JIT / 类加载
  • 对象反序列化后的二级缓存
  • 首次回填时的批量查询或聚合逻辑

于是你会看到一种很绕的现场:Redis 命中率不算太差,但 P99 还是抖,因为新实例刚接流量时,本地状态还没热起来,连接池也还没稳定,热点对象的反序列化和回填路径照样把尾延迟拉长。

还有一种是回填链本身太慢,预热只能挡住第一波

预热不是无限护盾。

它能挡住第一轮热点 miss,但如果漏下去的那部分请求一回源就把数据库、线程池、连接池打到等待区,后面的 miss 还是会迅速放大。现场通常会看到:

  • 不是所有 key 都 miss
  • 但少量 miss 已经足够把回源链打慢
  • 一旦回填速度赶不上新 miss 速度,整条链就开始抖

这时问题就从“预热范围”扩展成了“回填能力”。

我自己更习惯的排查顺序

真在线上处理时,我一般按这个顺序走:

第一步:把预热完成、切流开始、命中率掉点、数据库抬头放进一张时间线

先把因果顺序看清,不然后面很容易一路追错。

第二步:拿真实高峰请求样本,对比预热 key 样本

别只按 key 总数比较,要按真实流量占比看。十万个冷 key,不如几十个超热点 key 值钱。

第三步:抽一条真实请求,顺着命中路径把日志串完整

重点不是“Redis 里有没有值”,而是这条请求到底查了哪个 key、落在哪个缓存层、为什么会 miss。

第四步:查剩余 TTL,而不是只查配置里的 TTL 文本

配置写着 2 小时,不代表切流时还剩 2 小时。现场更重要的是剩余寿命和失效分布。

第五步:把本地缓存、连接池和回源耗时一起拉出来

如果 Redis 已经基本命中,但尾延迟还是不好,就别把眼睛只留在 Redis。

这类问题最容易误判的地方

误判一:Redis 里有 key,就等于预热成功

不对。预热成不成功,最终看的是真实流量是否命中这批状态

误判二:预热任务成功,就说明时机也没问题

也不对。很多事故不是动作失败,而是动作做早了、做偏了、做在错误路径上了。

误判三:切流后数据库 RT 变高,所以根因一定在数据库

很可能数据库只是被 miss 和回源放大以后拖高的那一层。

最后收一句

“预热做了还是抖”,真正要盯的不是动作有没有执行,而是三件事有没有同时对上:

  • 预热的是不是那批真实会被打到的对象
  • 真实请求是不是走在同一条命中路径上
  • 预热效果有没有活到真正接流量的那一刻

这三件事只要错一件,后面看到的数据库抬头、连接池等待、接口 RT 抖动,都只是更晚出现的结果。