Redis 缓存击穿、穿透、雪崩问题及解决方案

160 阅读5分钟

1. 缓存问题概述

Redis 作为高性能缓存中间件,能够有效提高数据访问速度,但在实际使用中,可能会遇到 缓存击穿、缓存穿透、缓存雪崩 三大问题。这些问题可能会导致 缓存失效、数据库压力骤增、系统崩溃,因此必须采取合理的应对策略。


2. 缓存击穿、穿透、雪崩的区别

问题定义导致的后果典型场景
缓存穿透(Cache Penetration)查询 缓存和数据库中都不存在的数据,导致每次请求都要访问数据库。数据库压力骤增,影响系统性能。攻击者恶意请求不存在的 key,例如查询 id=-1
缓存击穿(Cache Breakdown)某个热点数据在缓存过期的瞬间,大量请求涌入数据库。数据库瞬时负载过高,可能导致系统崩溃。促销活动商品 ID 缓存过期时,流量暴增。
缓存雪崩(Cache Avalanche)大量缓存同时过期,导致大量请求打到数据库。数据库承受不了瞬时高并发,可能宕机。设定相同过期时间的大量缓存同时失效。

3. 具体案例分析与解决方案

3.1 缓存穿透

案例

java
复制编辑
// 伪代码示例:查询用户信息
String key = "user:1001";
String user = redis.get(key);
if (user == null) {  // Redis 中没有数据
    user = database.query("SELECT * FROM users WHERE id = 1001"); // 查询数据库
    redis.setex(key, 3600, user);  // 写入缓存
}
return user;

问题:如果用户 id=9999 不存在,Redis 没有缓存,每次查询都会打到数据库,造成高并发压力。

解决方案

方案具体措施优缺点
布隆过滤器使用 布隆过滤器(Bloom Filter) 维护一个所有合法 key 的集合,拦截非法请求。低内存占用,误判率较低,但不能删除数据。
缓存空值若查询数据库后发现数据不存在,将 null 存入 Redis,并设置短 TTL(如 60s)。有效防止短时间内的重复查询,但可能会缓存无用数据。
接口层拦截在应用层限制 ID 规则,如 ID 需大于 0,防止非法访问。适用于特定业务规则。

布隆过滤器示例(Java 实现)

java
复制编辑
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000);
bloomFilter.put("user:1001");  // 添加合法 key
if (!bloomFilter.mightContain("user:9999")) {
    return null;  // 直接拦截
}

3.2 缓存击穿

案例

java
复制编辑
// 高并发访问某个热点 key
String key = "hot-item:123";
String item = redis.get(key);
if (item == null) {  // 缓存过期
    item = database.query("SELECT * FROM items WHERE id = 123");  // 直接访问数据库
    redis.setex(key, 3600, item);  // 重新缓存
}

问题:当 "hot-item:123" 这个热门 key 过期的瞬间,大量请求直接冲向数据库,造成高并发压力。

解决方案

方案具体措施优缺点
互斥锁(Mutex)缓存过期时,只允许一个线程查询数据库,其他线程等待。避免数据库短时间高并发,但有一定等待时间。
设置热点数据永不过期设置较长 TTL,并使用异步更新机制,减少过期瞬间的冲击。适用于超热点数据,但可能导致数据不一致。
提前更新缓存主动刷新缓存,在即将过期前预加载数据,确保缓存持续有效。适用于可预测的缓存更新场景。

互斥锁示例

java
复制编辑
String key = "hot-item:123";
String item = redis.get(key);
if (item == null) {
    if (redis.setnx("lock:hot-item:123", "1")) { // 获取锁
        redis.expire("lock:hot-item:123", 30); // 设置锁过期时间
        item = database.query("SELECT * FROM items WHERE id = 123");
        redis.setex(key, 3600, item);
        redis.del("lock:hot-item:123"); // 释放锁
    } else {
        Thread.sleep(100); // 休眠后重试
    }
}

3.3 缓存雪崩

案例

如果我们在 00:00 统一设置大量缓存的 TTL=3600s,那么 01:00 这些缓存会同时过期,导致数据库压力激增。

解决方案

方案具体措施优缺点
随机过期时间给每个 key 设置不同的过期时间(如 3600 ± 600 秒),避免集中过期。实现简单,避免缓存同时失效。
分批加载使用双层缓存(L1+L2) ,当 Redis 失效时,先访问本地缓存(如 Guava Cache)。适用于允许短时间一致性的业务。
自动重建缓存使用后台线程异步刷新缓存,防止大规模过期后查询数据库。适用于数据较稳定的场景。

随机过期时间示例

java
复制编辑
int ttl = 3600 + new Random().nextInt(600); // 设置 3600 ~ 4200 秒随机过期
redis.setex("user:1001", ttl, user);

本地缓存 + Redis

java
复制编辑
LoadingCache<String, String> localCache = CacheBuilder.newBuilder()
    .maximumSize(1000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(new CacheLoader<String, String>() {
        public String load(String key) throws Exception {
            return redis.get(key);
        }
    });

4. 结论

问题核心原因最佳解决方案
缓存穿透访问缓存和数据库都不存在的 key布隆过滤器 + 缓存空值
缓存击穿热点 key 过期瞬间,大量请求打到数据库互斥锁 + 提前更新缓存
缓存雪崩大量 key 同时过期,数据库负载骤增随机 TTL + 分批加载

🔹 企业级应用中,通常结合多种方案,例如 布隆过滤器 + 互斥锁 + 随机过期时间,来优化 Redis 缓存架构,确保高并发下的稳定性。