Java + Spring Boot + Redis技术栈,在实际使用缓存时遇到 缓存击穿、缓存穿透、缓存雪崩

针对 Java + Spring Boot + Redis 技术栈,在实际使用缓存时遇到 缓存击穿、缓存穿透、缓存雪崩 是非常常见的,下面我会结合这个技术组合,给出 具体原因、问题场景、以及对应的解决方案和代码/配置示例,帮助你在实际项目中更好地应对这些问题。


一、缓存击穿(Cache Breakdown)------ 热点 key 突然失效,大量请求直达数据库

🎯 场景举例

比如"热门商品详情"(如 iPhone 15 商品ID=1001)被缓存,且缓存 key product:1001 设置了过期时间,假设是 30 分钟。当这个 key 恰好在某个高峰期过期 ,而又有大量用户同时访问该商品,就会导致 大量请求穿透到数据库,造成 DB 压力剧增。

✅ 解决方案(Spring Boot + Redis)

方案1:使用 互斥锁(分布式锁),只允许一个线程重建缓存

使用 Redis 的 SETNX(或 Redisson 的分布式锁)来实现:

依赖(如果使用 Redisson,推荐):
xml 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.x.x</version>
</dependency>
示例代码(伪代码逻辑,简化版):
java 复制代码
@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private RedissonClient redissonClient; // 使用 Redisson 分布式锁

public Product getProductById(Long id) {
    String cacheKey = "product:" + id;
    // 1. 先查缓存
    Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
    if (product != null) {
        return product;
    }

    // 2. 缓存没有,尝试获取锁
    RLock lock = redissonClient.getLock("lock:product:" + id);
    try {
        // 尝试加锁,最多等10秒,锁持有15秒后自动释放
        if (lock.tryLock(10, 15, TimeUnit.SECONDS)) {
            try {
                // 双重检查,可能其他线程已经重建好缓存
                product = (Product) redisTemplate.opsForValue().get(cacheKey);
                if (product != null) {
                    return product;
                }

                // 3. 查数据库
                product = productRepository.findById(id).orElse(null);
                if (product != null) {
                    // 4. 写入缓存,设置过期时间,比如30分钟
                    redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
                } else {
                    // 可选:缓存空对象,防止穿透
                    redisTemplate.opsForValue().set(cacheKey, new NullProduct(), 5, TimeUnit.MINUTES);
                }
                return product;
            } finally {
                lock.unlock();
            }
        } else {
            // 没抢到锁,稍后重试或者返回默认/错误信息
            Thread.sleep(100);
            return getProductById(id); // 简单重试,生产环境建议限流或返回兜底数据
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("获取锁失败", e);
    }
}

⚠️ 注意:生产环境中应做好重试机制、超时控制与降级策略,不要轻易重试或递归调用。

方案2:逻辑过期 + 后台刷新(适合读多写少)

不设置真正的过期时间,而是在 value 里保存一个过期时间字段,定时检查并异步刷新缓存。


二、缓存穿透(Cache Penetration)------ 查询不存在的数据,绕过缓存

🎯 场景举例

用户请求了一个 不存在的商品 ID(比如 999999999),这个 ID 在数据库中根本不存在,所以每次查询:

  • 缓存中没有
  • 数据库中也没有
    → 导致每次都打到数据库,如果有人恶意发起大量此类请求,DB 就会扛不住。

✅ 解决方案

方案1:缓存空对象(Null Object Pattern)

当查询数据库发现数据不存在时,仍然将一个特殊的标记(如 null 或自定义的 NullProduct 对象)存入 Redis,并设置较短的过期时间,例如 3~5 分钟,避免频繁查库。

代码片段(接上面):
java 复制代码
if (product == null) {
    // 缓存空对象,防止穿透
    redisTemplate.opsForValue().set(cacheKey, new NullProduct(), 5, TimeUnit.MINUTES);
    return null;
}

其中 NullProduct 是你自定义的一个类,代表"空结果",可用于前端展示或逻辑判断。

方案2:使用 布隆过滤器(Bloom Filter)

在查询数据库之前,先用布隆过滤器判断该 key(如商品 ID)是否 可能存在,如果布隆过滤器判断"一定不存在",则直接返回,无需查缓存和数据库。

如何集成布隆过滤器?

可以使用 Google 的 Guava BloomFilter (适合单机)或 RedisBloom(适合分布式,需引入 Redis 模块)。

简易示例(Guava,适合简单场景):
java 复制代码
// 初始化时将所有合法商品ID加入布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(
    Funnels.longFunnel(), 
    1000000,  // 预期元素数量
    0.01      // 误判率
);

// 添加合法ID
bloomFilter.put(1001L);
bloomFilter.put(1002L);

// 查询时先判断
if (!bloomFilter.mightContain(id)) {
    return null; // 一定不存在
}
// 再查缓存、查DB

⚠️ 生产环境中若数据量大、分布式部署,建议使用 RedisBloom 模块(需 Redis 支持)或自建布隆过滤器服务。


三、缓存雪崩(Cache Avalanche)------ 大量缓存同时失效 / Redis 宕机

🎯 场景举例

假如你给所有商品缓存都设置了 过期时间为 30 分钟 ,并且这些 key 的过期时间基本一致,那么在 某个 30 分钟的整点时刻,大量缓存同时失效,导致大量请求直接打到数据库,造成服务崩溃。

或者,Redis 宕机或网络抖动,也会导致缓存完全不可用,所有请求直连 DB。

✅ 解决方案

方案1:设置 随机过期时间,避免同时失效

不要所有 key 都设置相同的过期时间,比如:

java 复制代码
// 原始:30分钟
// 改为:30分钟 + 随机0~10分钟
int expireTime = 30 + new Random().nextInt(10);
redisTemplate.opsForValue().set(cacheKey, product, expireTime, TimeUnit.MINUTES);

这样可以有效让缓存 错开失效时间,避免同时雪崩。

方案2:多级缓存策略

  • 一级缓存:本地缓存(如 Caffeine、Guava Cache)
  • 二级缓存:Redis

即使 Redis 出现问题,本地缓存依然能挡住一部分流量。

示例(Caffeine 作为本地缓存):
xml 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>
java 复制代码
LoadingCache<Long, Product> localCache = Caffeine.newBuilder()
    .expireAfterWrite(1, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .build(this::loadProductFromRedisOrDB);

方案3:Redis 高可用

  • 使用 Redis 哨兵(Sentinel)集群(Cluster) 模式,提高缓存可用性,防止单点故障。
  • 如果 Redis 宕机,应有 降级策略(比如返回旧数据、默认值、错误提示等)。

方案4:熔断降级(如 Sentinel、Hystrix)

当数据库压力过大或 Redis 不可用时,可以快速失败或返回兜底内容,避免服务雪崩。


四、其他增强措施(推荐)

✅ 缓存预热

在系统启动时或低峰期,提前将热点数据加载到缓存中,避免冷启动时大量请求直接访问数据库。

✅ 监控与报警

  • 使用 Spring Boot Actuator + Prometheus + Grafana 监控:
    • 缓存命中率
    • Redis 响应时间、QPS
    • 数据库负载
  • 设置报警规则,如缓存未命中率过高、Redis 连接失败等。

✅ 接口限流

对于高频访问接口,可使用 Sentinel、RateLimiter、Redis + Lua 实现接口限流,防止恶意刷接口。


总结(Java + Spring Boot + Redis 应对三大问题)

问题 原因简述 推荐解决方案(Spring Boot + Redis)
缓存击穿 热点 key 突然失效,大量请求直达 DB 互斥锁(Redisson)、逻辑过期、后台刷新、双重检查锁
缓存穿透 查询不存在的数据,绕过缓存直连 DB 缓存空对象(Null Object)、布隆过滤器(Guava / RedisBloom)、参数校验
缓存雪崩 大量 key 同时失效 / Redis 宕机 设置随机过期时间、多级缓存(Caffeine + Redis)、Redis 高可用、熔断降级、缓存预热
相关推荐
超级大只老咪7 小时前
数组相邻元素比较的循环条件(Java竞赛考点)
java
小浣熊熊熊熊熊熊熊丶7 小时前
《Effective Java》第25条:限制源文件为单个顶级类
java·开发语言·effective java
毕设源码-钟学长7 小时前
【开题答辩全过程】以 公交管理系统为例,包含答辩的问题和答案
java·eclipse
啃火龙果的兔子7 小时前
JDK 安装配置
java·开发语言
星哥说事7 小时前
应用程序监控:Java 与 Web 应用的实践
java·开发语言
派大鑫wink7 小时前
【JAVA学习日志】SpringBoot 参数配置:从基础到实战,解锁灵活配置新姿势
java·spring boot·后端
xUxIAOrUIII8 小时前
【Spring Boot】控制器Controller方法
java·spring boot·后端
Dolphin_Home8 小时前
从理论到实战:图结构在仓库关联业务中的落地(小白→中级,附完整代码)
java·spring boot·后端·spring cloud·database·广度优先·图搜索算法
醇氧8 小时前
org.jetbrains.annotations的@Nullable 学习
java·开发语言·学习·intellij-idea
Java&Develop8 小时前
Aes加密 GCM java
java·开发语言·python