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 高可用、熔断降级、缓存预热
相关推荐
音符犹如代码3 小时前
ArrayList常见面试题二
java·开发语言·面试·职场和发展
Harold3 小时前
SpringBoot的jar包启动流程梳理
spring boot
NO.10243 小时前
11.4八股
java·linux·数据库
天工无极3 小时前
基于Spring AI实现法律咨询AI助手
java
乐悠小码3 小时前
Java设计模式精讲---01工厂方法模式
java·设计模式·工厂方法模式
cherry--3 小时前
集合(开发重点)
java·开发语言
寻星探路3 小时前
测试开发话题10---自动化测试常用函数(2)
java·前端·python
api_180079054604 小时前
请求、认证与响应数据解析:1688 商品 API 接口深度探秘
java·大数据·开发语言·mysql·数据挖掘
陈果然DeepVersion4 小时前
Java大厂面试真题:Spring Boot+Kafka+AI智能客服场景全流程解析(十二)
java·spring boot·ai·kafka·面试题·向量数据库·rag