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 失效 突然大流量请求 互斥锁、异步更新、热点永不过期
相关推荐
天天摸鱼的小学生1 分钟前
【Java Enum枚举】
java·开发语言
爬山算法14 分钟前
Redis(168) 如何使用Redis实现会话管理?
java·数据库·redis
程语有云19 分钟前
生产事故-那些年遇到过的OOM
java·内存·oom·生产事故
雨中飘荡的记忆22 分钟前
Spring Test详解
java·后端·spring
sugar__salt31 分钟前
网络编程套接字(二)——TCP
java·网络·网络协议·tcp/ip·java-ee·javaee
颜颜yan_32 分钟前
跨越x86与ARM:openEuler全架构算力实战评测
java·arm开发·架构
毕设源码-朱学姐33 分钟前
【开题答辩全过程】以 陪诊就医小程序设计与实现为例,包含答辩的问题和答案
java
程序员果子36 分钟前
零拷贝:程序性能加速的终极奥秘
linux·运维·nginx·macos·缓存·centos
动感小麦兜43 分钟前
NAS学习
java·开发语言·eureka
可爱の小公举1 小时前
Redis技术体系全面解析
数据库·redis·缓存