# 20. Redis 是如何处理缓存穿透、缓存击穿和缓存雪崩等问题的?

# 标准答案

Redis 处理缓存三大问题的方式如下:

  • 缓存穿透:利用 布隆过滤器缓存空值 防止数据库被恶意查询。
  • 缓存击穿:对热点 key 设置 互斥锁(Mutex) 或使用 永不过期策略 确保缓存重建不发生并发冲突。
  • 缓存雪崩:采用 随机过期时间双层缓存限流降级 等措施,防止大量缓存失效导致数据库崩溃。

# 1️⃣ 缓存穿透(Cache Penetration)

# 问题描述

缓存穿透指的是查询一个数据库中不存在的 key,因为缓存也没有这个 key,查询会直接打到数据库,导致数据库负载增加甚至崩溃。例如:攻击者不断请求 userId=-1 这样的非法数据,由于缓存不会存储不存在的 key,每次查询都会穿透到数据库。

# 解决方案

#方法 1:布隆过滤器(Bloom Filter)(推荐)

  • 布隆过滤器是一种高效的哈希算法,用于快速判断 key 是否存在,从而拦截无效请求
  • 具体做法:
    1. 在 Redis 之前增加一个布隆过滤器,存储所有可能存在的 key。
    2. 查询时,先在布隆过滤器中判断 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:缓存空值

  • 如果数据库查询返回 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️⃣ 缓存击穿(Cache Breakdown)

# 问题描述

缓存击穿是指某个热点 key 过期,大量请求同时查询该 key,由于缓存没有命中,所有请求直接打到数据库,造成数据库压力过大甚至宕机。例如:一个明星热点新闻的 key 失效后,大量用户访问导致数据库崩溃。

# 解决方案

#方法 1:互斥锁(Mutex)(推荐)

  • 在缓存过期时,只有一个线程能去查询数据库并重建缓存,其他线程等待。
  • 具体实现:
    1. 查询 Redis,如果缓存未命中,尝试用 SETNX 获取互斥锁
    2. 成功获取锁的线程查询数据库,并更新缓存。
    3. 其他线程等待锁释放,获取新缓存数据。
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:热点数据永不过期

  • 对于热点 key,可以不设置过期时间,或者使用定期异步刷新策略(后台线程定期更新缓存)。
  • 例如,秒杀活动的商品库存信息,可以定期同步而不是等缓存自动过期。
redis.set("hotKey", value); // 不设过期时间
// 由后台异步任务定期刷新
1
2

# 3️⃣ 缓存雪崩(Cache Avalanche)

# 问题描述

缓存雪崩指的是大量缓存同时失效,导致所有请求都直接打到数据库,数据库负载急剧上升,可能直接宕机。例如:定时批量缓存刷新时,所有缓存同时过期,造成流量冲击数据库。

# 解决方案

#方法 1:随机过期时间(推荐)

  • 给不同的 key 设定随机的过期时间,避免同一时刻大规模缓存失效。
int expireTime = 3600 + new Random().nextInt(600); // 1 小时 ± 10 分钟
redis.setex("key", expireTime, value);
1
2
  • 优点:简单易用,减少数据库压力。

#方法 2:双层缓存架构(L1 + L2)

  • 在本地缓存(如 Caffeine、Guava Cache)和 Redis 之间增加一层缓冲,减少 Redis 直接失效对数据库的冲击。
  • 读取流程:
    1. 先查询本地缓存(L1);
    2. 本地缓存未命中,再查询 Redis(L2);
    3. 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

#方法 3:限流 & 降级

  • 限流:使用 Redis 令牌桶或漏斗算法限制 QPS。
  • 降级:当数据库压力过大时,可以返回默认值或者静态页面,保证服务可用性。
if (!rateLimiter.tryAcquire()) {
    return "系统繁忙,请稍后再试";
}
1
2
3

# 深入追问

🔹 Redis 的分布式锁如何防止缓存击穿?
🔹 布隆过滤器如何优化 Redis 查询性能?
🔹 如何防止缓存雪崩导致数据库宕机?
🔹 在电商秒杀系统中,如何优化热点 key 缓存?

# 相关面试题

🔹 如何在高并发场景下保证缓存的稳定性?
🔹 Redis 如何实现分布式限流?
🔹 缓存击穿和缓存雪崩有什么不同?
🔹 如何使用 Redis Pipeline 优化缓存请求?