使用 Redis 缓存时,我们常常会遇到以下三种高并发场景下的问题:
🧠 一、缓存三大问题概述
问题 | 描述 | 危害 |
---|
缓存雪崩 | 大量缓存 key 在同一时间失效,导致所有请求都打到数据库 | 数据库压力骤增,可能宕机 |
缓存击穿 | 某个热点 key 突然失效,大量并发访问直接冲击 DB | 同样可能导致数据库崩溃 |
缓存穿透 | 查询一个既不在缓存也不在数据库的 key,恶意攻击或非法查询 | 高频无效请求压垮系统 |
✅ 二、解决方案
问题 | 解决方案 | 实现方式 |
---|
缓存雪崩 | 设置不同的过期时间(随机值) | TTL + random |
缓存击穿 | 加互斥锁 / 缓存永不过期 / 逻辑过期时间 | Redisson 分布式锁 / 本地缓存 |
缓存穿透 | 缓存空值(NULL) / 布隆过滤器 | Redis 缓存 null / Guava BloomFilter / Redisson RBloomFilter |
🔒 三、Spring Boot 中实现方案
1️⃣ 防止 缓存雪崩
✅ 方案:设置随机过期时间
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
@Service
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
public CacheService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setWithRandomExpire(String key, Object value) {
int baseSeconds = 300;
int randomSeconds = (int)(Math.random() * 60);
redisTemplate.opsForValue().set(key, value, baseSeconds + randomSeconds, TimeUnit.SECONDS);
}
}
2️⃣ 防止 缓存击穿
✅ 方案一:Redisson 分布式锁 + 双重检查(推荐)
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redissonClient;
public CacheService(RedisTemplate<String, Object> redisTemplate, RedissonClient redissonClient) {
this.redisTemplate = redisTemplate;
this.redissonClient = redissonClient;
}
public Object getWithRedissonLock(String key, String lockKey, DataLoader dataLoader) {
Object cachedData = redisTemplate.opsForValue().get(key);
if (cachedData != null) {
return cachedData;
}
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(1, 30, TimeUnit.SECONDS)) {
cachedData = redisTemplate.opsForValue().get(key);
if (cachedData == null) {
cachedData = dataLoader.loadFromDB();
redisTemplate.opsForValue().set(key, cachedData, 300, TimeUnit.SECONDS);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return cachedData;
}
@FunctionalInterface
public interface DataLoader {
Object loadFromDB();
}
}
✅ 方案二:设置缓存永不过期 + 异步更新逻辑
@Scheduled(fixedRate = 300_000)
public void refreshCache() {
Object newData = loadDataFromDB();
redisTemplate.opsForValue().set("hot_data", newData);
}
3️⃣ 防止 缓存穿透
✅ 方案一:缓存空值(NULL)
public Object getWithNullCaching(String key) {
Object result = redisTemplate.opsForValue().get(key);
if (result != null) {
return result;
}
result = queryFromDatabase(key);
if (result == null) {
redisTemplate.opsForValue().set(key, "", 60, TimeUnit.SECONDS);
return null;
}
redisTemplate.opsForValue().set(key, result, 300, TimeUnit.SECONDS);
return result;
}
✅ 方案二:使用布隆过滤器(推荐用于高频非法 key)
使用 Redisson 的 RBloomFilter:
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
@Service
public class BloomFilterService {
private final RBloomFilter<String> bloomFilter;
public BloomFilterService(RedissonClient redissonClient) {
bloomFilter = redissonClient.getBloomFilter("bloom:filter");
bloomFilter.tryInit(1000000L, 0.03);
}
public boolean mightContain(String key) {
return bloomFilter.contains(key);
}
public void add(String key) {
bloomFilter.add(key);
}
}
然后在业务层:
public Object getCachedData(String key) {
if (!bloomFilterService.mightContain(key)) {
return null;
}
return cacheService.getWithRedissonLock(key, "lock:" + key, () -> {
return yourRepository.findById(key);
});
}
🛡️ 四、总结
问题 | 解决方法 | 是否推荐 | 场景建议 |
---|
缓存雪崩 | 随机过期时间 | ✅ 推荐 | 所有缓存数据 |
缓存击穿 | Redisson 分布式锁 | ✅ 推荐 | 热点数据 |
缓存击穿 | 永不过期 + 定时刷新 | ⚠️ 轻量级 | 不常变的数据 |
缓存穿透 | 缓存空值 | ✅ 推荐 | 少量非法 key |
缓存穿透 | 布隆过滤器 | ✅ 推荐 | 高频恶意请求场景 |
🧩 五、完整流程图
用户请求 key → 布隆过滤器校验 → key 是否存在?
↓ 是
查缓存是否命中?
↓ 是
返回缓存数据
↓ 否
获取分布式锁?
↓ 是
查缓存是否命中?(双重检查)
↓ 是
返回缓存数据
↓ 否
查询数据库
更新缓存
释放锁
↓
返回结果
📌 六、补充建议
建议 | 说明 |
---|
组合使用 | 布隆过滤器 + Redisson 锁 + 随机过期时间,组合使用更安全 |
监控报警 | 对于频繁穿透、击穿、雪崩等异常行为添加监控告警 |
缓存降级 | 当 Redis 故障时,可切换为本地缓存(如 Caffeine) |
多级缓存 | 本地缓存 + Redis 缓存,提升性能与容错能力 |