# 20. Redis 是如何处理缓存穿透、缓存击穿和缓存雪崩等问题的?
# 标准答案
Redis 处理缓存三大问题的方式如下:
- 缓存穿透:利用 布隆过滤器 或 缓存空值 防止数据库被恶意查询。
- 缓存击穿:对热点 key 设置 互斥锁(Mutex) 或使用 永不过期策略 确保缓存重建不发生并发冲突。
- 缓存雪崩:采用 随机过期时间、双层缓存、限流降级 等措施,防止大量缓存失效导致数据库崩溃。
# 1️⃣ 缓存穿透(Cache Penetration)
# 问题描述
缓存穿透指的是查询一个数据库中不存在的 key,因为缓存也没有这个 key,查询会直接打到数据库,导致数据库负载增加甚至崩溃。例如:攻击者不断请求 userId=-1
这样的非法数据,由于缓存不会存储不存在的 key,每次查询都会穿透到数据库。
# 解决方案
# ✅ 方法 1:布隆过滤器(Bloom Filter)(推荐)
- 布隆过滤器是一种高效的哈希算法,用于快速判断 key 是否存在,从而拦截无效请求。
- 具体做法:
- 在 Redis 之前增加一个布隆过滤器,存储所有可能存在的 key。
- 查询时,先在布隆过滤器中判断 key 是否可能存在,不存在则直接返回,不查询 Redis 和数据库。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class BloomFilterExample {
private static BloomFilter<Integer> bloomFilter =
BloomFilter.create(Funnels.integerFunnel(), 1000000, 0.01);
public static void main(String[] args) {
bloomFilter.put(123);
System.out.println(bloomFilter.mightContain(123)); // true
System.out.println(bloomFilter.mightContain(999)); // false
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 优点:高效、低内存占用,可拦截无效请求。
- 缺点:有一定误判率(不过可以调节误判率)。
# ✅ 方法 2:缓存空值
- 如果数据库查询返回
null
,则将该 key 的值缓存为null
,并设置一个短期过期时间(如 5~10 分钟)。 - 下次查询时,直接返回
null
,避免频繁访问数据库。 - 适用于数据较少、更新不频繁的场景。
String value = redis.get(key);
if (value == null) {
value = db.query(key);
if (value == null) {
redis.setex(key, 300, "null"); // 5 分钟
} else {
redis.setex(key, 3600, value);
}
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 2️⃣ 缓存击穿(Cache Breakdown)
# 问题描述
缓存击穿是指某个热点 key 过期,大量请求同时查询该 key,由于缓存没有命中,所有请求直接打到数据库,造成数据库压力过大甚至宕机。例如:一个明星热点新闻的 key 失效后,大量用户访问导致数据库崩溃。
# 解决方案
# ✅ 方法 1:互斥锁(Mutex)(推荐)
- 在缓存过期时,只有一个线程能去查询数据库并重建缓存,其他线程等待。
- 具体实现:
- 查询 Redis,如果缓存未命中,尝试用
SETNX
获取互斥锁。 - 成功获取锁的线程查询数据库,并更新缓存。
- 其他线程等待锁释放,获取新缓存数据。
- 查询 Redis,如果缓存未命中,尝试用
String value = redis.get(key);
if (value == null) {
if (redis.setnx("lock:" + key, "1") == 1) { // 获取锁
redis.expire("lock:" + key, 10); // 10 秒超时,防止死锁
value = db.query(key);
redis.setex(key, 3600, value); // 设置缓存
redis.del("lock:" + key); // 释放锁
} else {
Thread.sleep(100); // 休眠后重试
value = redis.get(key);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- 优点:防止大量请求同时查询数据库。
- 缺点:增加了一定的复杂性,需要保证锁不会死锁。
# ✅ 方法 2:热点数据永不过期
- 对于热点 key,可以不设置过期时间,或者使用定期异步刷新策略(后台线程定期更新缓存)。
- 例如,秒杀活动的商品库存信息,可以定期同步而不是等缓存自动过期。
redis.set("hotKey", value); // 不设过期时间
// 由后台异步任务定期刷新
1
2
2
# 3️⃣ 缓存雪崩(Cache Avalanche)
# 问题描述
缓存雪崩指的是大量缓存同时失效,导致所有请求都直接打到数据库,数据库负载急剧上升,可能直接宕机。例如:定时批量缓存刷新时,所有缓存同时过期,造成流量冲击数据库。
# 解决方案
# ✅ 方法 1:随机过期时间(推荐)
- 给不同的 key 设定随机的过期时间,避免同一时刻大规模缓存失效。
int expireTime = 3600 + new Random().nextInt(600); // 1 小时 ± 10 分钟
redis.setex("key", expireTime, value);
1
2
2
- 优点:简单易用,减少数据库压力。
# ✅ 方法 2:双层缓存架构(L1 + L2)
- 在本地缓存(如 Caffeine、Guava Cache)和 Redis 之间增加一层缓冲,减少 Redis 直接失效对数据库的冲击。
- 读取流程:
- 先查询本地缓存(L1);
- 本地缓存未命中,再查询 Redis(L2);
- Redis 也未命中,则查询数据库,并更新两层缓存。
Cache<String, String> localCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(10000)
.build();
public String getValue(String key) {
String value = localCache.getIfPresent(key);
if (value == null) {
value = redis.get(key);
if (value != null) {
localCache.put(key, value);
}
}
return value;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ✅ 方法 3:限流 & 降级
- 限流:使用 Redis 令牌桶或漏斗算法限制 QPS。
- 降级:当数据库压力过大时,可以返回默认值或者静态页面,保证服务可用性。
if (!rateLimiter.tryAcquire()) {
return "系统繁忙,请稍后再试";
}
1
2
3
2
3
# 深入追问
🔹 Redis 的分布式锁如何防止缓存击穿?
🔹 布隆过滤器如何优化 Redis 查询性能?
🔹 如何防止缓存雪崩导致数据库宕机?
🔹 在电商秒杀系统中,如何优化热点 key 缓存?
# 相关面试题
🔹 如何在高并发场景下保证缓存的稳定性?
🔹 Redis 如何实现分布式限流?
🔹 缓存击穿和缓存雪崩有什么不同?
🔹 如何使用 Redis Pipeline 优化缓存请求?