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

1. 缓存问题概述

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


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

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

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

3.1 缓存穿透

案例

ini 复制代码
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 实现)

javascript 复制代码
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 缓存击穿

案例

ini 复制代码
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,并使用异步更新机制,减少过期瞬间的冲击。 适用于超热点数据,但可能导致数据不一致。
提前更新缓存 主动刷新缓存,在即将过期前预加载数据,确保缓存持续有效。 适用于可预测的缓存更新场景。

互斥锁示例

ini 复制代码
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)。 适用于允许短时间一致性的业务。
自动重建缓存 使用后台线程异步刷新缓存,防止大规模过期后查询数据库。 适用于数据较稳定的场景。

随机过期时间示例

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

本地缓存 + Redis

vbnet 复制代码
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 缓存架构,确保高并发下的稳定性。

相关推荐
天草二十六_简村人14 分钟前
Rabbitmq消息被消费时抛异常,进入Unacked 状态,进而导致消费者不断尝试消费(上)
java·spring boot·分布式·后端·rabbitmq
多多*20 分钟前
使用事件监听器来处理并发环境中RabbitMQ的同步响应问题
java·开发语言·spring boot·分布式·docker·mybatis
农夫阿才23 分钟前
排序算法总结
java·算法·排序算法
失业写写八股文38 分钟前
如何选择栈与堆?堆跟栈的区别
java·后端
张张张3121 小时前
3.25学习总结 抽象类和抽象方法+接口+内部类+API
java·学习
Awesome Baron1 小时前
LeetCode hot 100 每日一题(16)——240. 搜索二维矩阵 II
java·leetcode·矩阵
云只上1 小时前
为什么后端接口返回数字类型1.00前端会取到1?
java·前端
陳長生.1 小时前
JAVA EE_多线程-初阶(一)
java·开发语言·java-ee
天机️灵韵1 小时前
Java动态生成Word终极指南:poi-tl与Aspose.Words性能对比及选型建议
java·vscode·word·模板