Redis 缓存一起过期,把博客首页打慢了:我用互斥重建、空值缓存和随机 TTL 挡住回源风暴
一次首页缓存同时失效,请求全打到 MongoDB。我用互斥重建、空值缓存和随机 TTL,把缓存击穿和雪崩的压力降下来。

作者
ZHOUYI
那天首页接口突然变慢,监控看到 Redis 命中率掉到底,MongoDB 查询数飙升。排查后发现首页聚合缓存和热门文章缓存都设了固定 10 分钟,发布文章后同一批 key 几乎同时过期,前端请求瞬间全部回源。
先分清:击穿、雪崩
击穿更像某个热 key 过期后,大量请求同时重建;雪崩则是大量 key 同时过期或缓存服务抖动,导致数据库被集中打穿。我的场景两者都沾一点:首页热 key 是击穿,批量文章缓存同刻过期像小规模雪崩。
防波堤怎么搭
- 给 TTL 加随机偏移:比如基础 600 秒加 0~120 秒,避免同一批缓存集体到期。
- 重建缓存时加互斥锁:只让一个请求查库回写,其他请求走旧缓存或降级数据。
- 给空结果做短缓存:删除页、不存在的 slug 也写入占位值,避免反复穿透数据库。
关键代码
const ttl = 600 + Math.floor(Math.random() * 120);
const cache = await redis.get(key);
if (cache !== null) return JSON.parse(cache);
const locked = await redis.set(`${key}:lock`, '1', 'NX', 'PX', 3000);
if (!locked) return getHomeFallback();
try {
const data = await buildHomeData();
await redis.set(key, JSON.stringify(data), 'EX', ttl);
return data;
} finally {
await redis.del(`${key}:lock`);
}这里最容易踩坑的是锁过期和无限等待。锁的 PX 要比慢查询略长,但不要无限等待;拿不到锁时可以返回旧缓存、降级数据或稍后重试。在 Nuxt 的 useAsyncData 里也一样,服务端渲染阶段不要把每个请求都当成首次回源。首页这种读多写少页面,宁可短暂展示几十秒前的数据,也不要让所有请求都冲进 MongoDB。
TTL 和空值怎么定
有些文章被删除或 slug 不存在,如果只查库不缓存空结果,会被持续穿透。我会把空结果写成短 TTL 的占位值,比如 30~60 秒;如果内容频繁变化,再结合发布事件主动删除相关 key。注意 JSON 序列化时区分“空值占位”和“真的空字符串”,避免读取时误判。
随机 TTL 也不是越大越好。我最后把首页聚合缓存设成基础 10 分钟、随机 2 分钟,文章详情按内容更新频率分层。TTL 太长会让发布后更新变慢,太短又失去缓存意义,最好结合页面可接受的延迟来定。
“缓存不是“设个 TTL”就完事,过期策略、重建路径和失败降级要一起设计。”