缓存穿透、击穿与雪崩:处理方案详解及代码实践
1. 引言
在现代的高并发系统中,缓存是提高性能的关键组件之一。然而,缓存也存在一些常见问题,比如 缓存穿透、缓存击穿 和 缓存雪崩。这些问题如果不加以处理,可能会导致系统崩溃或性能严重下降。
本文将深入探讨这三种问题的成因以及解决方案,并通过代码示例帮助你更好地理解和应用这些技术。
2. 缓存穿透
2.1 定义
缓存穿透 是指查询一个既不在缓存也不在数据库中的数据。由于缓存未命中,每次请求都会直接打到数据库上,可能导致数据库压力过大甚至崩溃。
2.2 成因
- 恶意攻击(如利用不存在的ID频繁查询)
- 数据被删除但缓存未更新
2.3 解决方案
方案一:布隆过滤器(Bloom Filter)
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于集合中。
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnel;
import com.google.common.hash.PrimitiveSink;
// 示例:使用Guava库创建布隆过滤器
BloomFilter<String> bloomFilter = BloomFilter.create(
(Funnel<String>) (from, into) -> into.putString(from, Charsets.UTF_8),
1000000
);
bloomFilter.put("key1");
bloomFilter.mightContain("key1"); // 返回true
bloomFilter.mightContain("key2"); // 可能返回false
方案二:缓存空值(Null Caching)
如果查询结果为空,也可以将该结果缓存一段时间,避免重复查询数据库。
public String getFromCache(String key) {
String value = redis.get(key);
if (value == null) {
// 查询数据库
value = db.query(key);
if (value == null) {
// 缓存空值,防止穿透
redis.setex(key, 60, "");
} else {
redis.setex(key, 60, value);
}
}
return value;
}
3. 缓存击穿
3.1 定义
缓存击穿 是指某个热点数据在缓存中过期后,大量并发请求同时访问数据库,导致数据库瞬时压力剧增。
3.2 成因
- 热点数据缓存过期
- 并发量高
3.3 解决方案
方案一:永不过期策略
对于某些热点数据,可以设置为“永不过期”,并通过后台任务定期更新。
// 设置缓存永不超时
redis.set("hot_data_key", hotData);
// 后台线程定时更新
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
String newData = db.queryHotData();
redis.set("hot_data_key", newData);
}, 0, 5, TimeUnit.MINUTES);
方案二:互斥锁(Mutex Lock)
当缓存失效时,只允许一个线程去加载数据,其他线程等待。
public String getWithLock(String key) {
String value = redis.get(key);
if (value == null) {
synchronized (this) {
value = redis.get(key);
if (value == null) {
value = db.query(key);
redis.setex(key, 60, value);
}
}
}
return value;
}
4. 缓存雪崩
4.1 定义
缓存雪崩 是指大量缓存数据在同一时间失效,导致所有请求都落到数据库上,可能造成数据库崩溃。
4.2 成因
- 所有缓存设置了相同的过期时间
- 缓存服务器宕机
4.3 解决方案
方案一:随机过期时间
在设置缓存过期时间时,加上一个随机偏移量,避免所有缓存同时失效。
int expireTime = 60 + new Random().nextInt(10);
redis.setex(key, expireTime, value);
方案二:集群部署 + 高可用架构
使用 Redis 集群或主从复制机制,确保即使某一台缓存服务器宕机,也能保证整体服务的可用性。
# Redis Cluster 配置示例
cluster-enabled yes
cluster-node-timeout 5000
方案三:限流降级
在前端加一层限流逻辑,防止短时间内大量请求冲击数据库。
// 使用Guava的RateLimiter进行限流
RateLimiter rateLimiter = RateLimiter.create(1000);
if (rateLimiter.tryAcquire()) {
// 允许请求
} else {
// 返回错误或降级响应
}
5. 综合应用场景设计
我们来设计一个电商平台的商品详情页场景,结合以上三种缓存问题的解决方案。
5.1 场景描述
- 用户访问商品详情页,系统优先从缓存中获取数据
- 如果缓存中没有,则查询数据库并写入缓存
- 商品信息变更后,异步更新缓存
- 热门商品缓存永不过期
- 所有缓存设置随机过期时间
- 使用布隆过滤器防止恶意攻击
5.2 实现代码
public class ProductService {
private final RedisClient redis;
private final DBClient db;
private final BloomFilter<String> bloomFilter;
public Product getProductDetail(String productId) {
// 1. 检查布隆过滤器
if (!bloomFilter.mightContain(productId)) {
return null; // 直接拒绝非法请求
}
// 2. 从缓存获取
Product product = redis.get("product:" + productId);
if (product == null) {
synchronized (this) {
product = redis.get("product:" + productId);
if (product == null) {
// 3. 查询数据库
product = db.getProductById(productId);
if (product != null) {
// 4. 写入缓存,设置随机过期时间
int expireTime = 60 + new Random().nextInt(10);
redis.setex("product:" + productId, expireTime, product);
} else {
// 5. 缓存空值
redis.setex("product:" + productId, 60, "");
}
}
}
}
return product;
}
}
6. 总结
缓存穿透、击穿和雪崩是高并发系统中常见的三大缓存问题,合理的设计和实现可以显著提升系统的稳定性和性能。通过布隆过滤器、缓存空值、互斥锁、随机过期时间等手段,我们可以有效应对这些挑战。
希望本文对你理解这些概念有所帮助,欢迎留言交流~