Java

缓存 TTL 设计不当怎么排查?一次整点集中过期把数据库打高的事故复盘

真正把数据库打高的,往往不是 TTL 短,而是 TTL 太整齐。一次整点活动里,批量预热配上统一 30 分钟 TTL,让一批商品缓存在流量最高的时候一起失效,事故感就是从这一分钟长出来的。

  • Redis
  • TTL
  • 缓存
  • 数据库回源
  • 性能排查
9 分钟阅读

TTL 这个词很容易被说轻。很多系统最初都是先给个 5 分钟、10 分钟、30 分钟,跑起来再说。可真到事故现场,TTL 从来不只是“缓存活多久”的小参数,它决定的是:流量会不会在同一时间窗里重新砸回数据库。

我印象最深的一次,是 21:00 整点活动开始后的第 31 分钟。数据库 QPS 突然翻倍,商品详情和优惠页一起抖,群里第一反应是活动流量超过预估了。

但再往前翻一分钟,问题已经写在 Redis 指标里了:expired_keys 在 21:31 那一分钟出现了异常尖刺,而且尖刺主要集中在活动商品相关前缀。真正打高数据库的,不是流量本身,而是一批原本被预热过的 key,在最忙的时候一起过期了。

这次事故的第一现场,是 expired_keys 尖刺

那天最早变坏的不是接口 RT,也不是数据库连接池,而是下面三条线同时对齐:

  • expired_keys 从平时每分钟 8 千左右,跳到 21:31 的 14.6 万
  • product:detail:*activity:price:* 两个前缀 miss QPS 同时拉高
  • 数据库只读库上的商品详情查询在 40 秒后开始陡增

这个顺序很关键。它说明当时最该盯的不是“数据库为什么突然顶不住”,而是“为什么恰好这分钟有一大批活动 key 一起死掉”。

根因不是 TTL 太短,而是 TTL 太齐

继续查预热脚本,发现活动前为了省事,运营同学在 21:00 前统一跑了一轮热门商品预热,应用把这些 key 全部写成同样的 TTL:1800 秒。

这在平峰其实没那么吓人,可一旦活动流量本身就高,它会产生一个很糟糕的效果:

  • 热门商品在 21:00 被成批写入
  • 21:31 左右又会成批失效
  • 失效窗口正好撞上活动第二波流量峰值

于是你看到的不是“缓存慢慢老化”,而是数据库在某一分钟突然挨一拳。

为什么这次不像热 key 打穿,也不像调用路径绕过缓存

因为证据形状不一样。

如果是单个热点 key 打穿,回源会集中在少数对象上,TopN 占比会特别高;如果是调用路径绕过缓存,新老版本、不同实例或者不同接口通常会有明显分叉。

但这次不是。

那次新增 miss 分布在成批活动商品上,前 20 个 key 只占新增 miss 的 19%,并不集中;同时所有详情实例和价格实例都在同一分钟开始变差,说明不是某一组机器偷偷绕过了缓存。

真正统一它们的,是失效时间点。

现场为什么很容易被误判成“活动流量太猛”

因为从结果层看,所有图都像是高峰把数据库打满了:

  • 只读库 QPS 上来
  • 连接池等待变长
  • 接口 RT 跟着抖
  • 应用线程池 active 升高

如果不把时间线拉到 Redis 的过期指标那一层,你很容易把整件事理解成“高峰来了,缓存没扛住”。这句也不算错,但太粗了。真正能指导动作的是更具体那句:缓存不是没扛住,而是在同一时刻成批卸载了保护。

这两种理解会直接决定处理方式。

这次止血动作为什么不是简单把 TTL 全拉长

先说结果:当时没有直接把所有 TTL 全改成 2 小时,而是做了三件更窄的事。

  1. 对活动商品缓存增加 0 到 600 秒随机偏移
  2. 把预热任务改成分 15 分钟滚动写入,而不是整点一次性刷完
  3. 对最热的 300 个商品做异步续期,避免第二轮一起掉

动作下去以后,第二天同一活动时段里:

  • expired_keys 尖刺没再出现
  • 商品详情回源峰值比前一天低了 63%
  • 数据库只读库 QPS 仍有波峰,但没有再出现断崖式尖刺

这说明真正要修的不是“TTL 长度”本身,而是“过期节奏”。

为什么统一 TTL 在高峰期特别危险

因为缓存最有价值的时候,恰恰是系统余量最小的时候。

平峰里,哪怕有一批 key 一起过期,数据库也许还能吸收;可一到活动高峰、晚高峰或者发布窗口,数据库、连接池和线程池都更紧。这时 TTL 再整齐,等于把一批本来该平滑释放的 miss 压成同一分钟爆出来。

所以 TTL 事故常常不是因为“配置特别离谱”,而是因为一个看起来普通的配置,在最坏的时间点和最坏的流量叠在了一起。

这种事故里最值钱的验证方式是什么

不是光看 TTL 文本配置,而是把四个时间点对齐:

  • 预热或批量写入时间
  • expired_keys 尖刺时间
  • miss QPS 抬头时间
  • 数据库回源抬头时间

只要这四段顺序能对上,TTL 就不再是一个抽象怀疑,而是现场里真正的起点。

什么时候更该先怀疑 TTL 节奏问题

如果你看到的是下面这种现场,就很值得先往 TTL 上收:

  • miss 在固定时间窗里突然起尖刺
  • 多个 key 前缀一起变差,但不是集中在少数热点对象
  • expired_keys 和数据库回源有明显时间相关性
  • 问题常出现在预热、定时任务、整点活动之后

这种形状和热点击穿、调用路径绕过缓存都不太一样,它更像缓存自己在某个时间点集体松手了。

回到 TTL 这件事本身

TTL 真正麻烦的地方,不在于它短不短,而在于它会不会把大量 key 排成同一拍子。只要节奏被排齐,高峰流量就会顺着那一分钟一起掉下去。

所以排查 TTL 事故时,我现在最先看的已经不是“配了多少秒”,而是:这些 key 是不是被同一时间写进去,又会不会在同一时间一起过期。 这一步看清,后面才谈得上随机打散、分层寿命和滚动预热。