针对 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 高可用、熔断降级、缓存预热 |