一、缓存雪崩 (Cache Avalanche)
什么是缓存雪崩?
缓存雪崩是指在某一时刻,大量缓存数据同时失效,导致大量请求直接打到后端数据库,造成数据库压力骤增甚至宕机的现象。这种情况就像雪崩一样,一旦发生就会引发连锁反应。
大量key同时过期 → Redis无数据 → 请求打到MySQL
产生原因
1. 大量缓存同时设置相同的过期时间
2. Redis服务宕机或重启
3. 热点数据集中过期
解决方案
1. 设置随机过期时间
// 给缓存设置随机过期时间,避免集中失效
redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(300 + new Random().nextInt(300)));
2. 缓存预热,在系统启动或低峰期,预先加载热点数据到缓存中
@Component
public class CacheWarmUpService {
@PostConstruct
public void warmUp() {
// 预加载热点数据
List<Product> hotProducts = productRepository.findHotProducts();
for (Product product : hotProducts) {
redisTemplate.opsForValue().set(
"product:" + product.getId(),
JSON.toJSONString(product),
Duration.ofHours(1));
}
}
}
3. 限流与降级,使用Hystrix等组件进行服务降级
@HystrixCommand(fallbackMethod = "getProductFallback")
public Product getProduct(Long productId) {
String productJson = redisTemplate.opsForValue().get("product:" + productId);
if (productJson != null) {
return JSON.parseObject(productJson, Product.class);
}
Product product = productRepository.findById(productId);
if (product != null) {
redisTemplate.opsForValue().set(
"product:" + productId,
JSON.toJSONString(product),
Duration.ofMinutes(30));
}
return product;
}
public Product getProductFallback(Long productId) {
// 降级处理,返回默认值或错误信息 return new Product(productId, "商品暂时不可用", BigDecimal.ZERO);
}
缓存穿透 (Cache Penetration)
二、什么是缓存穿透?
缓存穿透是指查询一个不存在的数据,由于缓存和数据库中都没有该数据,每次请求都会直接打到数据库。恶意攻击者可能利用这个漏洞对系统进行攻击。
缓存穿透:查询不存在的数据 → Redis无数据 → 请求打到MySQL
产生原因
1. 查询不存在的数据
2. 恶意攻击,大量请求不存在的key
3. 缓存未正确处理空值
解决方案
1. 布隆过滤器,布隆过滤器是一种空间效率很高的概率型数据结构,可以快速判断一个元素是否可能存在
@Service
public class BloomFilterService {
private BloomFilter<String> bloomFilter;
@PostConstruct
public void initBloomFilter() {
// 初始化布隆过滤器
bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 预期插入元素数量
0.01); // 误判率
}
public User getUserById(Long userId) {
// 先用布隆过滤器判断用户是否存在
if (!bloomFilter.mightContain("user:" + userId)) {
return null; // 直接返回,不查询数据库
}
String userJson = redisTemplate.opsForValue().get("user:" + userId);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
User user = userRepository.findById(userId);
if (user != null) {
redisTemplate.opsForValue().set(
"user:" + userId,
JSON.toJSONString(user),
Duration.ofMinutes(30));
} else {
// 缓存空值,防止持续穿透
redisTemplate.opsForValue().set(
"user:" + userId,
"",
Duration.ofMinutes(5));
}
return user;
}
}
2. 缓存空值,即使查询结果为空,也将空结果缓存一段时间
public User getUserById(Long userId) {
String cached = redisTemplate.opsForValue().get("user:" + userId);
if (cached != null) {
return cached.isEmpty() ? null : JSON.parseObject(cached, User.class);
}
User user = userRepository.findById(userId);
if (user != null) {
redisTemplate.opsForValue().set(
"user:" + userId,
JSON.toJSONString(user),
Duration.ofMinutes(30));
} else {
// 缓存空值
redisTemplate.opsForValue().set(
"user:" + userId,
"",
Duration.ofMinutes(5));
}
return user;
}
缓存击穿 (Cache Breakdown)
三、什么是缓存击穿?
缓存击穿是指某个热点key在失效的瞬间,大量并发请求同时打到数据库,导致数据库压力骤增。与雪崩不同的是,击穿只针对一个特定的key。
缓存击穿:热点key失效瞬间 → Redis无数据 → 大量请求打到MySQL
产生原因
1. 热点数据过期
2. 高并发访问同一热点数据
3. 缓存失效与请求到达的时间窗口
解决方案
1. 互斥锁,使用分布式锁确保只有一个线程去加载数据
public class CacheService {
public String getData(String key) {
String value = redisTemplate.opsForValue().get(key);
if (value == null) {
return loadFromDatabase(key);
}
return value;
}
private String loadFromDatabase(String key) {
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
// 获取分布式锁
Boolean acquired = redisTemplate.opsForValue().setIfAbsent(
lockKey, lockValue, Duration.ofSeconds(10));
if (acquired != null && acquired) {
try {
// 双重检查,防止重复加载
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 查询数据库
value = database.get(key);
if (value != null) {
redisTemplate.opsForValue().set(
key, value, Duration.ofMinutes(10));
} else {
// 缓存空值
redisTemplate.opsForValue().set(
key, "", Duration.ofMinutes(5));
}
return value;
} finally {
// 释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue);
}
} else {
// 等待其他线程加载完成
try {
Thread.sleep(100);
return getData(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
}
2. 逻辑过期,在value中包含过期时间信息,由应用层判断是否过期
public class CacheValue {
private Object data;
private long expireTime;
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
// getters and setters
}
public String getDataWithLogicExpire(String key) {
String json = redisTemplate.opsForValue().get(key);
if (json != null) {
CacheValue cacheValue = JSON.parseObject(json, CacheValue.class);
if (!cacheValue.isExpired()) {
return cacheValue.getData().toString();
}
// 异步更新缓存
asyncUpdateCache(key);
// 返回旧值
return cacheValue.getData().toString();
}
// 缓存不存在,加载数据
return loadAndCacheData(key);
}
总结
问题类型 | 影响范围 | 解决思路 | 核心策略 |
---|---|---|---|
缓存雪崩 | 大量key | 避免集中失效 | 随机过期时间、限流降级 |
缓存穿透 | 不存在数据 | 过滤无效请求 | 布隆过滤器、缓存空值 |
缓存击穿 | 单个热点key | 控制并发访问 | 分布式锁、逻辑过期 |