Redis:缓存雪崩、穿透、击穿的技术解析和实战方案

🚨 1、简述

随着系统规模扩大,Redis 缓存被广泛用于数据预热、热点数据防护和高并发系统优化。然而在高并发环境中,缓存雪崩、穿透、击穿等问题若处理不当,可能导致系统雪崩式崩溃。

本文从原理、原因出发,结合实际项目经验,讲解如何应对这三大常见问题,并给出对应实践方案。


💣 2、缓存雪崩(Cache Avalanche)

✅ 定义:

大量缓存同时失效,导致请求直接打到数据库或下游服务,造成系统瞬时过载甚至崩溃。

🧨 触发场景:

🔹 同一时间大量缓存设置了相同过期时间。

🔹 Redis 故障、重启或崩溃,导致全缓存失效。

✅ 解决方案:

方案 实现方式
缓存过期时间加随机 给不同缓存设置随机 TTL(如 ±5%)
本地限流/降级 Hystrix、Sentinel、RateLimiter 等
缓存预热 应用启动时加载关键缓存数据
多级缓存 引入本地缓存(如 Caffeine)减少对 Redis 的依赖
异步重建缓存 通过队列异步刷新热点数据缓存

🛠 实践样例:设置随机过期时间

java 复制代码
int baseExpire = 60 * 10; // 10分钟
int random = new Random().nextInt(60); // 加1分钟以内的随机值
redisTemplate.opsForValue().set("product:123", data, baseExpire + random, TimeUnit.SECONDS);

🕳️ 3、缓存穿透(Cache Penetration)

✅ 定义:

请求的数据既不在缓存中,也不在数据库中,攻击者通过大量随机 key 造成数据库压力激增。

🧨 触发场景:

🔹 攻击者构造不存在的 key 请求接口。

🔹 key 本身无意义(如 user:-1)

✅ 解决方案:

方案 实现方式
缓存空对象 查询结果为 null,也缓存一段时间(短 TTL)
参数校验 请求层校验 ID 合法性,如 ID > 0
布隆过滤器 使用布隆过滤器拦截不存在的 key 请求
接口限流 控制访问频率,防止恶意请求刷爆系统

🛠 实践样例:缓存空值

java 复制代码
String key = "user:1000";
String userJson = redisTemplate.opsForValue().get(key);

if (userJson == null) {
    User user = userService.getById(1000L);
    if (user == null) {
        // 缓存一个占位符,防止穿透
        redisTemplate.opsForValue().set(key, "", 5, TimeUnit.MINUTES);
    } else {
        redisTemplate.opsForValue().set(key, toJson(user), 10, TimeUnit.MINUTES);
    }
}

🛠 实践样例:布隆过滤器(Guava)

java 复制代码
BloomFilter<Long> filter = BloomFilter.create(Funnels.longFunnel(), 100_0000);
filter.put(12345L);

if (!filter.mightContain(requestedId)) {
    return null; // 拦截非合法请求
}

🔥 4、缓存击穿(Cache Breakdown)

✅ 定义:

某个热点 key 失效瞬间,多个并发请求同时击中数据库。

🧨 触发场景:

🔹 热点数据过期,瞬时大量请求访问该 key。

🔹 并发场景下没有做好互斥更新控制。

✅ 解决方案:

方案 实现方式
热点缓存永不过期 主动更新替代被动过期
加互斥锁 只有一个线程能加载数据,其余等待或返回旧值
异步更新缓存 提前设置过期,后台刷新
多级缓存 本地缓存兜底,如 Caffeine + Redis

🛠 实践样例:互斥锁方式防止击穿

java 复制代码
public User getUserById(Long userId) {
    String key = "user:" + userId;
    String cache = redisTemplate.opsForValue().get(key);

    if (cache != null) return parseUser(cache);
    // 加锁防击穿
    String lockKey = "lock:user:" + userId;
    boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 30, TimeUnit.SECONDS);

    if (!lock) {
        try { Thread.sleep(50); } catch (InterruptedException ignored) {}
        return getUserById(userId); // 递归重试
    }

    try {
        User user = db.queryById(userId);
        if (user != null) {
            redisTemplate.opsForValue().set(key, toJson(user), 10, TimeUnit.MINUTES);
        } else {
            redisTemplate.opsForValue().set(key, "", 3, TimeUnit.MINUTES);
        }
        return user;
    } finally {
        redisTemplate.delete(lockKey); // 释放锁
    }
}

🎯 5、总结

🔹 Redis 作为缓存层时,不应将其设计为单点依赖 。

🔹 搭配使用 Caffeine(本地)+ Redis(远程)+ MQ异步回源 是中大型系统推荐策略。

🔹 限流、熔断、降级 机制应作为系统稳定性的基本保障。

问题 定义 触发条件 核心解决策略
雪崩 批量缓存同时失效 TTL 设置一致或 Redis 崩溃 随机 TTL、本地缓存、限流
穿透 请求的 key 无效 缓存和数据库都无数据 空值缓存、布隆过滤器、参数校验
击穿 某一热点 key 失效 突然大流量请求 互斥锁、异步更新、热点永不过期
相关推荐
考虑考虑31 分钟前
JDK26的JEP 500属性
java·后端·java ee
LMT流氓兔36 分钟前
第五单元——第十二课:把脚本整理成项目
java·数据库·python·langchain·1024程序员节
yuniko-n42 分钟前
【JUC】四种内存屏障示例
java·1024程序员节
一条泥憨鱼1 小时前
【Redis | 黑马点评】附近商铺功能源码解读
数据库·redis·缓存
念何架构之路2 小时前
Go reflect 包:结构体反射 —Field、Tag 与 VisibleFields
java·数据库·golang
舰长1152 小时前
将百万数据插入到 Redis,有哪些实现方案?
数据结构·redis·开源
liangbo72 小时前
03-JDK 10/11 局部变量推断与 LTS:var 到底该怎么用
java
Percep_gan3 小时前
调用天翼云的对象存储ZOS,很详细
java·1024程序员节
若亦_3 小时前
数据结构>>优先级队列(堆)
java·数据结构
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 查询圈子个人动态:查询我的动态需求分析
java·开发语言·交友