Redis 缓存穿透、击穿、雪崩:成因、方案与实战指南

2 阅读7分钟

Redis 缓存穿透、击穿、雪崩:成因、方案与实战指南

在高并发系统中,Redis 缓存是保护数据库的第一道城墙。但当这道城墙出现裂缝,数据库面临的可能不是压力,而是灭顶之灾。穿透、击穿、雪崩——三个听起来相似的名词,背后却是三种截然不同的灾难场景。


一、缓存穿透:无孔不入的幽灵请求

1.1 什么是缓存穿透?

一句话概括:请求的数据,缓存里没有,数据库里也没有。

Redis 查不到 → 请求穿透到数据库 → 数据库也查不到 → 返回空。整个过程中,缓存形同虚设,每一次请求都在无意义地消耗数据库资源。

典型场景

  • 恶意攻击:爬虫循环递增商品 ID(1001、1002、1003……),大量不存在的 Key 绕过缓存直击数据库
  • 业务 Bug:前后端 Key 规则不一致,前端传了一个根本不存在的用户 ID
  • 非法请求:id = -1id = 999999999 这种明显越界的参数

1.2 三道防线,层层拦截

防线方案原理优缺点
第一道:接口层校验参数合法性检查ID 必须为正整数、长度在合理范围内,不合规直接返回 400✅ 成本极低,拦截大量低级攻击 ❌ 无法拦截"格式合法但不存在"的 ID
第二道:缓存空值查库为空时,将空结果写入 Redis,设置短过期时间(2~5 分钟)后续相同请求直接命中空值缓存,不再穿透✅ 实现极简 ❌ 大量随机 Key 攻击会占用 Redis 内存
第三道:布隆过滤器启动时将所有合法 ID 存入布隆过滤器,请求先过滤再查缓存过滤器判定"一定不存在"→ 直接拦截;判定"可能存在"→ 放行✅ 内存占用极小,查询极快 ❌ 有误判率(约 1%),不支持删除

实战建议

  • 一般业务:缓存空值 + 接口校验 足够
  • 对抗恶意攻击:必须上 布隆过滤器
  • 布隆过滤器初始化示例(Redisson):
java
1RBloomFilter<Long> productFilter = redissonClient.getBloomFilter("product:bloom");
2productFilter.tryInit(1_000_000L, 0.01); // 预计100万元素,误判率1%
3

查询时先过滤:

java
1if (!bloomFilterService.mightContain(id)) {
2    return null; // 直接拦截,不查缓存不查库
3}
4

二、缓存击穿:一把钥匙毁掉一座城

2.1 什么是缓存击穿?

一句话概括:一个热点 Key 过期的瞬间,大量并发请求同时涌入数据库。

注意与穿透的区别:

缓存穿透缓存击穿
数据是否存在缓存和数据库都不存在缓存不存在,但数据库存在
影响范围所有无效请求集中在单个热点 Key
典型例子查不存在的 user_id=99999爆款商品缓存过期,1000 人同时查询

核心矛盾: 热点 Key(如秒杀商品、首页推荐)访问量远超普通 Key,缓存失效后没有缓冲,所有请求瞬间打穿到数据库。

2.2 三种解法,按场景选用

方案一:分布式互斥锁(最常用)

缓存失效时,只允许一个线程去查库并重建缓存,其他线程等待。

java
1RLock lock = redissonClient.getLock("lock:product:" + productId);
2if (lock.tryLock(0, 10, TimeUnit.SECONDS)) {
3    try {
4        // 双重检查:防止其他线程已重建缓存
5        String cached = redisTemplate.opsForValue().get(key);
6        if (cached != null) return JSON.parseObject(cached, Product.class);
7        
8        // 查库 + 回写缓存
9        Product product = productMapper.selectById(productId);
10        redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 30, TimeUnit.MINUTES);
11        return product;
12    } finally {
13        lock.unlock();
14    }
15} else {
16    Thread.sleep(50);
17    return getProductInfo(productId); // 递归重试
18}
19

⚠️ 隐患: 查库 + 重建缓存耗时过长时,其他线程会阻塞,高并发下吞吐量下降。禁止在高并发场景无脑使用!

方案二:热点数据"永不过期"

物理上不设置过期时间,逻辑上通过后台异步线程定时更新。

java
1// 缓存时不设 expire
2redisTemplate.opsForValue().set(key, JSON.toJSONString(product));
3
4// 后台定时任务每 29 分钟刷新
5@Scheduled(fixedRate = 29 * 60 * 1000)
6public void refreshHotCache() {
7    Product latest = productMapper.selectById(hotProductId);
8    redisTemplate.opsForValue().set(key, JSON.toJSONString(latest));
9}
10

✅ 性能最友好,无击穿风险
❌ 存在短暂数据不一致,适合非实时业务(如首页推荐)

方案三:缓存预热

系统启动或大促前,提前将热点数据加载到缓存,避免上线瞬间缓存失效。

适合热点数据明确的场景(电商首页、秒杀商品),配合方案二效果最佳。


三、缓存雪崩:万马奔腾式的系统崩溃

3.1 什么是缓存雪崩?

一句话概括:大量缓存同时失效,或 Redis 整体不可用,所有请求瞬间压垮数据库。

两大成因:

成因类型典型场景
集中过期批量设置相同过期时间(如所有商品统一 24 小时过期),到期瞬间集体失效
服务不可用Redis 单点故障、集群分片宕机、网络中断,缓存整体不可用

与击穿的区别:

缓存击穿缓存雪崩
失效 Key 数量1 个热点 Key大量 Key 同时失效
影响范围单点业务全局业务
数据库压力瞬间高 QPS持续性高压,可能直接宕机

3.2 "预防 + 加固 + 兜底" 三层防御体系

🛡️ 第一层:预防——打散过期时间(成本最低,效果最好)

核心思路:避免所有 Key 的过期时间完全一致。

java
1// ❌ 错误:所有商品统一 1 小时过期
2redis.setex("product:1001", 3600, data);
3redis.setex("product:1002", 3600, data);
4
5// ✅ 正确:基础时间 + 随机偏移
6int baseExpire = 3600;
7int randomExpire = new Random().nextInt(600); // 0~10 分钟随机
8redis.setex(key, baseExpire + randomExpire, value);
9

效果: 每个缓存的过期时间重复率大幅降低,集体失效概率趋近于零。

🛡️ 第二层:加固——Redis 高可用部署
部署模式适用场景特点
主从复制 + 哨兵中小规模主节点宕机后秒级切换
Redis Cluster大规模高并发分片存储,单分片故障不影响整体
多活集群(跨机房)核心金融/电商异地容灾,机房级故障自动切换

⚠️ 铁律:禁止单机部署!  至少保证 1 主 2 从,开启 RDB + AOF 混合持久化。

🛡️ 第三层:兜底——限流 + 熔断 + 降级

即使前两层都失效,这是保护数据库的最后一道防线:

  • 接口限流: 网关层限制单接口 QPS(如商品查询每秒最多 2000 次),超出直接返回"服务繁忙"
  • 服务熔断: 监控数据库响应,错误率超阈值时自动熔断,返回缓存旧数据或默认值
  • 降级策略: 核心业务返回旧缓存,非核心业务(评论、收藏)直接关闭,保住下单、查询等主链路

四、三种问题对比速查表

维度缓存穿透缓存击穿缓存雪崩
本质查了不存在的数据热点 Key 过期瞬间大量 Key 集中失效 / Redis 宕机
数据状态缓存❌ 数据库❌缓存❌ 数据库✅缓存❌ 数据库✅
影响范围全局(所有无效请求)单点(一个热点 Key)全局(所有业务)
核心方案布隆过滤器 + 缓存空值互斥锁 / 永不过期随机过期 + 高可用 + 熔断
最致命场景恶意攻击秒杀活动Redis 集群宕机

五、最佳实践清单

  1. 组合使用,不要单打独斗: 布隆过滤器(拦截非法请求)+ 缓存空值(兜底重复查询)+ 互斥锁(重建缓存)是生产环境的标准三件套
  2. 过期时间必加随机值: 这是性价比最高的单一操作,能同时防范击穿和雪崩
  3. 热点数据单独处理: 识别 Top N 热点 Key,走"永不过期 + 后台异步更新"路线
  4. 监控必须到位: 实时监控 Redis 命中率(低于 90% 立即告警)、QPS、过期 Key 数量、数据库连接数
  5. 压测验证: 大促前模拟缓存集中过期、Redis 宕机场景,验证防御方案有效性

写在最后

缓存穿透、击穿、雪崩,本质上都是  "缓存失效后的流量过载" ,但触发点不同,解法也不同。

  • 穿透防的是  "不该来的请求"
  • 击穿防的是  "不该同时来的请求"
  • 雪崩防的是  "不该一起失效的缓存"

理解这三个"不该",你就掌握了 Redis 缓存防御的核心逻辑。剩下的,就是根据业务场景选择合适的组合策略,然后——做好监控,定期压测,别等出了事再救火。