Appearance
缓存穿透、击穿、雪崩
更新: 7/25/2026 字数: 0 字 时长: 0 分钟
缓存穿透、击穿、雪崩,这三个词发音相近、场景又都和"缓存没挡住、请求打到 DB"有关,特别容易记混。但它们的成因和防范手段完全不同:一个是查根本不存在的数据,一个是单个热点 key 失效,一个是大批 key 同时失效。
一、三大问题分点定义
1. 缓存穿透(Cache Penetration)

定义:查询一个缓存和数据库里都不存在的数据。因为 DB 里查不到,缓存永远不会被写入,于是每次这种请求都"穿透"缓存直击数据库。关键词:数据不存在。典型如恶意用 id=-1、id=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/:id、GET /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 可能存在吗",明确不存在的直接拒掉(布隆过滤器不会漏判存在的,只会有极小概率把不存在的误判为存在,足够用)。
缓存击穿

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、RedisBloom | ioredis(SET NX)、后台刷新任务 | lru-cache、opossum、哨兵/集群 |
小结
抓住一句话就不会混:穿透查的是"没有的数据",击穿是"一个热点 key 没了",雪崩是"一大批 key 一起没了"。 对应到落地:穿透靠缓存空值 + 布隆过滤器堵住无效查询,击穿靠互斥锁保证只有一个请求回源,雪崩靠TTL 随机化 + 多级缓存 + 熔断把失效时间和风险打散。在 Node.js 里,这些几乎都能用 ioredis 的原子命令(SET NX、EX/PX)配合 bloom-filters、lru-cache、opossum 落地,不需要引入重型中间件。