一句话总览:三个“缓存杀手”都是因为Redis没拦住请求,导致压力转移到数据库,但触发原因和影响范围完全不同。

一、三者对比(一图秒懂)

术语 一句话定义 触发条件 影响范围 危险等级
缓存击穿 一个热点Key过期,海量请求直击DB 热点Key在高并发瞬间过期 只影响这一个Key对应的业务 ⭐⭐⭐⭐⭐
缓存雪崩 大批量Key同时过期,DB瞬间爆了 大量Key设置了相同的过期时间 影响大面积业务,可能导致整体服务不可用 ⭐⭐⭐⭐⭐
缓存穿透 查询的Key在DB中根本不存在,每次都绕开缓存 恶意请求查询不存在的ID(如-1) 持续打DB,直到DB扛不住 ⭐⭐⭐⭐

二、缓存击穿(Cache Breakdown)

是什么?

一个超级热点数据(比如双11爆款商品、春运车票)在缓存中过期的瞬间,成千上万的并发请求同时发现缓存为空,全部穿透到数据库。

发生原因

  1. 热点数据设置了过期时间
  2. 在高并发窗口期内恰好过期
  3. 大量请求在同一毫秒发现缓存为空,同时查询DB

解决方案

方案 原理 适用场景 代码思路
互斥锁(SET NX) 只有1个请求能抢到锁去查DB,其他请求等待/重试 对实时一致性要求高 SET lock 1 NX EX 10
逻辑过期(永不过期) Redis不设物理过期,值里存逻辑过期时间,过期时先返回旧数据,异步更新 高并发读多写少(推荐) 结合BullMQ后台刷新
热点数据预热 流量低谷期(如凌晨)提前刷新热点Key 可预测的热点(如大促) 定时任务主动刷新

代码示例(逻辑过期 + BullMQ 方案——你现在的技能组合!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. 存数据时,多加一个逻辑过期时间字段
await SET('hot_goods:1001', JSON.stringify({
stock: 10,
price: 99.9,
logicExpire: Date.now() + 300000 // 5分钟后逻辑过期
}));

// 2. 读取时
async function getHotGoods(goodsId) {
const cache = await GET(`hot_goods:${goodsId}`);
const data = JSON.parse(cache);

// 如果逻辑过期了
if (data.logicExpire < Date.now()) {
// 抢锁,防止重复触发更新任务
const lock = await SET(`lock:${goodsId}`, 1, 'NX', 'EX', 10);
if (lock) {
// 通过BullMQ异步去查DB更新缓存(不阻塞用户请求!)
await refreshQueue.add('refresh-goods', { goodsId });
}
// 关键:先返回旧数据给用户
return data;
}
return data;
}

// 3. BullMQ Worker 后台静默更新
refreshQueue.process(async (job) => {
const { goodsId } = job.data;
const newData = await queryDB(goodsId);
await SET(`hot_goods:${goodsId}`, JSON.stringify(newData));
});

三、缓存雪崩(Cache Avalanche)

是什么?

大量缓存Key在同一时间集中过期,或者Redis服务本身宕机,导致海量请求直接压垮数据库。

发生原因

  1. 设计失误:所有Key设置了相同的过期时间(如统一凌晨0点过期)
  2. Redis宕机:Redis服务挂了,所有请求直接走DB
  3. 服务器重启:缓存被批量清空后瞬间重建

解决方案

方案 说明
过期时间加随机值 给过期时间加一个随机偏移量(如 300秒 + random(0,100)),避免集体死亡
Redis高可用 使用主从+哨兵Redis Cluster,保证Redis不宕机
多级缓存 本地缓存(Caffeine/Guava)+ Redis 双层兜底
限流降级 使用Sentinel/Hystrix等组件,当DB压力过大时直接拒绝部分请求

代码示例(过期时间加随机值)

1
2
3
4
5
6
7
8
// ❌ 错误写法:所有Key都在同一秒过期
await SET('goods:1001', data, 'EX', 3600);
await SET('goods:1002', data, 'EX', 3600);

// ✅ 正确写法:过期时间加入随机偏移
const baseExpire = 3600;
const randomOffset = Math.floor(Math.random() * 300); // 0~300秒随机
await SET('goods:1001', data, 'EX', baseExpire + randomOffset);

四、缓存穿透(Cache Penetration)

是什么?

查询一个在数据库中根本不存在的数据(比如用 user_id = -1 查询),因为缓存里没有,每次都穿透到数据库,DB每次都查不到。

发生原因

  1. 恶意攻击:黑客故意用大量不存在的ID发起请求
  2. 业务漏洞:前端/客户端传入了非法参数

解决方案

方案 说明
缓存空值(Null Value) null 也缓存起来(SET key null EX 60),下次请求直接返回空,不打DB
布隆过滤器(Bloom Filter) 在请求到达Redis前,用布隆过滤器判断Key是否可能存在于DB中,不存在的直接拦截
参数校验 业务层先校验ID格式(如 ID > 0),非法的直接返回

五、对比

对比维度 缓存击穿 缓存雪崩 缓存穿透
问题本质 热点Key过期瞬间的单点爆破 大量Key同时过期的集体自杀 查询不存在数据的无效攻击
影响Key数量 1个热点Key N个Key 无限(恶意构造)
核心解决思路 只让1个人去查DB,其他人等着或拿旧数据 分散过期时间,避免同时死亡 把“不存在”也缓存起来,或用过滤器提前拦截
最佳实践方案 逻辑过期 + 异步刷新(BullMQ) 过期时间 + 随机值 布隆过滤器
简单记忆 “热点Key过期瞬间高并发穿透” “大量Key同时过期导致DB被打” “查不存在的数据绕开缓存直击DB”