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 失效 突然大流量请求 互斥锁、异步更新、热点永不过期
相关推荐
躺平大鹅11 小时前
Java面向对象入门(类与对象,新手秒懂)
java
初次攀爬者11 小时前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺12 小时前
搞懂@Autowired 与@Resuorce
java·spring boot·后端
Derek_Smart13 小时前
从一次 OOM 事故说起:打造生产级的 JVM 健康检查组件
java·jvm·spring boot
NE_STOP14 小时前
MyBatis-mybatis入门与增删改查
java
孟陬17 小时前
国外技术周刊 #1:Paul Graham 重新分享最受欢迎的文章《创作者的品味》、本周被划线最多 YouTube《如何在 19 分钟内学会 AI》、为何我不
java·前端·后端
想用offer打牌17 小时前
一站式了解四种限流算法
java·后端·go
华仔啊18 小时前
Java 开发千万别给布尔变量加 is 前缀!很容易背锅
java
也些宝19 小时前
Java单例模式:饿汉、懒汉、DCL三种实现及最佳实践
java