从零起步学习Redis || 第九章:缓存雪崩,缓存击穿,缓存穿透三大问题的成因及实战解决方案

0 阅读4分钟

前言

在高并发系统中,我们通常会使用 Redis 来做缓存,以减轻数据库压力、提高系统性能。然而,在实际开发中,如果缓存机制设计不当,就容易出现三种经典问题:

  • 缓存雪崩(Cache Avalanche)
  • 缓存击穿(Cache Breakdown)
  • 缓存穿透(Cache Penetration)

这三种问题看起来类似,但根本原因和解决方案却不相同。本文将逐一分析它们的 成因、影响、解决方案及代码实践


 一、缓存雪崩(Cache Avalanche)

 1. 概念

缓存雪崩是指:

在同一时间,大量缓存数据同时过期或缓存服务宕机,导致所有请求直接打到数据库,数据库压力骤增甚至崩溃。

 2. 成因

  • 所有缓存 key 设置了相同的过期时间(如都在凌晨 0 点失效);
  • Redis 宕机;
  • 应用重启或缓存被批量清空。

示例:

商品信息缓存设置为 2 小时失效,2 小时后所有 key 同时过期,大量请求瞬间涌向数据库。

 3. 解决方案

✅ 方案1:给过期时间加随机值(均匀平均过期时间)

避免同一时刻大量 key 同时失效。

int expire = 3600 + new Random().nextInt(600); // 1小时~1小时10分钟
redisTemplate.opsForValue().set("product:" + id, product, expire, TimeUnit.SECONDS);

✅ 方案2:缓存预热

在系统启动或流量高峰前,提前加载热点数据进缓存。

✅ 方案3:服务降级机制

当缓存不可用时,临时返回默认值、兜底数据或提示稍后再试。

✅ 方案4:多级缓存

例如:

  • 一级:本地缓存(Caffeine、Guava)
  • 二级:Redis

✅ 方案5:互斥锁

当大量业务线程请求Redis发现数据不存在(过期),使用互斥锁锁住一个线程(保证同一时刻只有一个线程去拉取数据到Redis),其他线程没有锁就无法请求,只能等待或者返回空值

✅ 方案6:后台更新缓存

数据直接设置为不过期,如果数据库有变化,再更新Redis中的数据

问题:不过期可能会出现内存淘汰,此时如何处理?

答:业务线程发现缓存数据失效后,用消息队列通知后台线程更新缓存


 二、缓存击穿(Cache Breakdown)

 1. 概念

缓存击穿是指:

某个热点 key 在失效的瞬间,有大量并发请求同时访问该 key,缓存未命中,导致瞬间大量请求打到数据库。

 2. 成因

  • 热点 key 过期(例如秒杀商品、首页 banner 等);
  • 瞬间有大量请求访问该热点 key。

 3. 解决方案

✅ 方案1:分布式锁(互斥锁)

防止多个线程同时去加载数据库。

String key = "product:" + id;
String lockKey = "lock:" + key;

Object cache = redisTemplate.opsForValue().get(key);
if (cache == null) {
    if (tryLock(lockKey)) { // 抢到锁
        Object dbData = queryFromDB(id);
        redisTemplate.opsForValue().set(key, dbData, 60, TimeUnit.SECONDS);
        releaseLock(lockKey);
    } else {
        Thread.sleep(100); // 没抢到锁的稍等再查缓存
        return redisTemplate.opsForValue().get(key);
    }
}
return cache;

✅ 方案2:逻辑过期(永不过期策略)

缓存中保存数据和过期时间字段,过期后后台异步更新,而非直接删除缓存。

✅ 方案3:异步预刷新

使用定时任务在 key 即将过期时提前刷新。


 三、缓存穿透(Cache Penetration)

 1. 概念

缓存穿透是指:

查询一个 缓存和数据库中都不存在的 key,每次请求都绕过缓存直接打到数据库,导致数据库压力过大。

 2. 成因

  • 用户或攻击者请求非法或不存在的 key;
  • 数据库查无此数据,缓存未保存任何结果;
  • 下次相同请求又打到数据库。

示例:

攻击者请求 /product?id=-9999,Redis 没有,数据库也没有。下次再请求又重复打 DB。

 3. 解决方案

✅ 方案1:缓存空对象

查询结果为空时,也写入缓存(可设置较短 TTL)。

Object dbData = queryFromDB(id);
if (dbData == null) {
    redisTemplate.opsForValue().set("product:" + id, "NODATA", 60, TimeUnit.SECONDS);
} else {
    redisTemplate.opsForValue().set("product:" + id, dbData, 300, TimeUnit.SECONDS);
}

✅ 方案2:参数校验

对请求参数合法性进行检查,如 ID < 0 或非法字符直接拦截。

✅ 方案3:布隆过滤器(Bloom Filter)

在访问 Redis 之前,先通过布隆过滤器判断 key 是否可能存在。

  • 不存在 → 直接拦截请求;
  • 可能存在 → 再访问 Redis。
if (!bloomFilter.mightContain("product:" + id)) {
    return null; // 直接拦截
}
return redisTemplate.opsForValue().get("product:" + id);


 四、对比总结

问题类型现象成因解决方案
缓存雪崩大量缓存同时失效,DB 压力暴增同一时间过期、Redis 宕机随机过期时间、预热、降级、多级缓存
缓存击穿热点 key 失效瞬间被并发访问热点 key 过期分布式锁、逻辑过期、异步刷新
缓存穿透查询不存在数据反复访问 DBkey 不存在且无缓存缓存空值、参数校验、布隆过滤器

 五、结语

Redis 缓存的使用是高并发系统中提升性能的关键手段,但如果不处理好缓存的失效机制,系统反而可能因为缓存问题而“自爆”。