Skip to content

缓存穿透、击穿、雪崩

更新: 7/25/2026 字数: 0 字 时长: 0 分钟

缓存穿透、击穿、雪崩,这三个词发音相近、场景又都和"缓存没挡住、请求打到 DB"有关,特别容易记混。但它们的成因和防范手段完全不同:一个是查根本不存在的数据,一个是单个热点 key 失效,一个是大批 key 同时失效

一、三大问题分点定义

1. 缓存穿透(Cache Penetration)

缓存穿透:查不存在的数据

定义:查询一个缓存和数据库里都不存在的数据。因为 DB 里查不到,缓存永远不会被写入,于是每次这种请求都"穿透"缓存直击数据库。关键词:数据不存在。典型如恶意用 id=-1id=999999999 这类不可能存在的键疯狂请求,缓存完全失去屏障作用。

2. 缓存击穿(Cache Breakdown / Hotspot Invalid)

定义:某个热点 key(数据本身存在)在某一刻恰好过期,而此时正有大量并发请求它。缓存刚失效的一瞬间,这些并发请求全部同时落到 DB 去查同一条数据,并同时尝试重建缓存。关键词:单个热点 key 失效的瞬间。它打的是"一个点"。

3. 缓存雪崩(Cache Avalanche)

定义:大批 key 在同一时间段集中失效(或 Redis 实例整体宕机),导致海量请求在同一时刻全部落到 DB,数据库瞬间被打垮。关键词:大面积 key 同时失效。它打的是"一大片"。

二、三大问题核心区别

一句话抓住边界:

  • 穿透 = 查的数据根本不存在 → 缓存无从建立,请求反复打 DB。
  • 击穿 = 数据存在,但某个热点 key 刚好失效的瞬间被高并发击中 → 问题。
  • 雪崩 = 数据存在,但大量 key 同时失效或 Redis 挂了 → 问题。

用一个比喻串起来:缓存是挡在 DB 前的一道墙。穿透是有人专门钻墙上本来就没堵的洞(查不存在的数据);击穿是墙上某块最扛压的砖突然碎了(热点 key 失效);雪崩是整面墙同一时间集体垮塌(大批 key 同时失效)。

三、逐个击破:JS 全栈场景、成因与落地防范

缓存穿透

JS 全栈常见场景:C 端接口 GET /api/user/:idGET /api/product/:id 直接把前端传来的 id 拿去查缓存和库。攻击者或爬虫构造大量不存在的 id 高频刷接口,缓存全程不命中。

成因:查不到的数据不会被写进缓存,缺乏对"空结果"的处理机制。

JS 全栈防范方案:

① 缓存空值(最简单实用):查到 DB 也没有时,给这个 key 写一个短 TTL 的空标记,后续相同请求直接被缓存挡回。

js
// ioredis:缓存空值,短 TTL 防止穿透
async function getUser(id) {
  const key = `user:${id}`;
  const cached = await redis.get(key);
  if (cached !== null) {
    return cached === '' ? null : JSON.parse(cached); // 命中空值标记
  }
  const user = await db.findUser(id);
  if (!user) {
    await redis.set(key, '''EX'60);   // 空值只存 60s,避免长期占用
    return null;
  }
  await redis.set(key, JSON.stringify(user), 'EX'3600);
  return user;
}

② 参数合法性校验(第一道闸):在 Node 层用 zod/joi 或简单判断拦掉明显非法的 id(负数、超长、非数字),不让它们有机会打到 DB。

③ 布隆过滤器(海量 key 场景):用 bloom-filters 这类库,或 Redis 的 RedisBloom 模块,预先把所有存在的 id 灌进布隆过滤器。请求先问过滤器"这个 id 可能存在吗",明确不存在的直接拒掉(布隆过滤器不会漏判存在的,只会有极小概率把不存在的误判为存在,足够用)。

缓存击穿

缓存击穿:热点key失效

JS 全栈常见场景:秒杀商品详情、首页热门榜单、爆款活动页——这类被巨量并发访问的单个热点 key,一旦到期,瞬间成百上千请求同时回源。

成因:热点 key 过期的瞬间,并发请求同时发现缓存 miss,一起去查 DB 并重建缓存(即"惊群效应")。

JS 全栈防范方案:

① 互斥锁(只放一个请求去重建):用 Redis 的 SET NX 抢一把分布式锁,抢到的那个请求去查 DB、重建缓存,其余请求短暂等待后重试读缓存。

js
// ioredis:互斥锁防击穿,只让一个请求回源重建
async function getHotData(key) {
  let data = await redis.get(key);
  if (data !== null) return JSON.parse(data);

  const lockKey = `lock:${key}`;
  const locked = await redis.set(lockKey, '1''NX''PX'3000); // 抢锁,3s 自动释放
  if (locked) {
    try {
      const fresh = await db.query(key);
      await redis.set(key, JSON.stringify(fresh), 'EX'3600);
      return fresh;
    } finally {
      await redis.del(lockKey);
    }
  } else {
    await new Promise(r => setTimeout(r, 50)); // 没抢到锁,稍等再读缓存
    return getHotData(key);
  }
}

② 热点 key 逻辑过期 / 不设物理过期:对极热数据不设 TTL,改为在 value 里存一个"逻辑过期时间",由后台异步任务或抢到锁的请求去刷新,读请求永远能拿到(可能略旧的)数据,彻底避免 miss。

③ 提前预热与续期:活动开始前把热点数据提前写入缓存;对可预期的热点做定时续期,别让它在高峰期自然过期。

缓存雪崩

缓存雪崩:批量同时失效

JS 全栈常见场景:系统启动时批量把一堆数据用相同 TTL 灌进缓存;或定时任务在整点集中刷新一批缓存——TTL 到期时间高度一致,某一刻集体失效。极端情况是 Redis 实例宕机,所有缓存瞬间全没。

成因:大量 key 过期时间过于集中,或缓存服务整体不可用。

JS 全栈防范方案:

① TTL 加随机抖动(最有效的一招):给每个 key 的过期时间加一个随机偏移,把失效时间打散开。

js
// 基础 TTL + 随机抖动,避免大批 key 同时到期
const baseTTL = 3600;
const jitter = Math.floor(Math.random() * 600); // 0~10 分钟随机
await redis.set(key, JSON.stringify(val), 'EX', baseTTL + jitter);

② 多级缓存:Node 进程内加一层本地缓存(如 lru-cache),Redis 挂了时本地缓存还能兜一部分请求,减轻 DB 压力。

③ 熔断与降级:检测到 Redis 不可用或 DB 压力过大时,启用熔断(如 opossum),对非核心请求快速返回降级数据(默认值/空),保住核心链路不被拖垮。

④ Redis 高可用:用主从 + 哨兵或集群部署,避免单点宕机引发整体雪崩(这层通常运维负责,全栈了解即可)。

四、三大问题对比汇总表

维度缓存穿透缓存击穿缓存雪崩
数据是否存在不存在(DB 也没有)存在存在
失效范围无缓存可建单个热点 key大批 key / 整个 Redis
核心成因查不存在的数据,空结果不缓存热点 key 过期瞬间高并发回源大量 key 同时过期或 Redis 宕机
打击特征反复穿透打 DB打在"一个点"打在"一大片"
JS 全栈典型场景恶意刷不存在的 id、爬虫秒杀详情、热门榜单启动批量灌缓存、整点刷新、Redis 挂
首选防范缓存空值 + 参数校验 + 布隆过滤器互斥锁(SET NX)+ 逻辑过期TTL 随机抖动 + 多级缓存 + 熔断降级
JS 生态工具zod/joi、bloom-filters、RedisBloomioredis(SET NX)、后台刷新任务lru-cache、opossum、哨兵/集群

小结

抓住一句话就不会混:穿透查的是"没有的数据",击穿是"一个热点 key 没了",雪崩是"一大批 key 一起没了"。 对应到落地:穿透靠缓存空值 + 布隆过滤器堵住无效查询,击穿靠互斥锁保证只有一个请求回源,雪崩靠TTL 随机化 + 多级缓存 + 熔断把失效时间和风险打散。在 Node.js 里,这些几乎都能用 ioredis 的原子命令(SET NXEX/PX)配合 bloom-filterslru-cacheopossum 落地,不需要引入重型中间件。