一、什么是缓存穿透?
缓存穿透是指查询一个根本不存在的数据,导致请求直接打到数据库上。这种情况通常发生在恶意攻击或者业务逻辑缺陷时,比如有人故意请求数据库中不存在的ID,导致缓存层形同虚设。
举个例子,假设我们有一个电商系统,用户通过商品ID查询商品信息。正常情况下,如果商品存在,我们会把商品信息缓存到Redis中,下次查询时直接从缓存获取。但如果有人故意请求不存在的商品ID(比如-1或者特别大的数字),每次请求都会绕过缓存直接查询数据库,导致数据库压力骤增。
// Java示例:存在缓存穿透风险的代码
public Product getProductById(String productId) {
// 先查缓存
Product product = redisTemplate.opsForValue().get("product:" + productId);
if (product == null) {
// 缓存没有,查数据库
product = productDao.findById(productId);
if (product != null) {
// 存入缓存
redisTemplate.opsForValue().set("product:" + productId, product, 1, TimeUnit.HOURS);
}
}
return product;
}
这段代码的问题在于,如果productId不存在,每次请求都会查询数据库,缓存完全没起作用。
二、如何解决缓存穿透?
1. 缓存空对象
最简单的办法是,即使数据库查不到数据,也在缓存中存一个空值(比如NULL或特定标记),这样后续请求就不会直接打到数据库。
// Java示例:缓存空对象解决穿透
public Product getProductById(String productId) {
// 先查缓存
Product product = redisTemplate.opsForValue().get("product:" + productId);
if (product == null) {
// 缓存没有,查数据库
product = productDao.findById(productId);
if (product != null) {
// 存入缓存
redisTemplate.opsForValue().set("product:" + productId, product, 1, TimeUnit.HOURS);
} else {
// 存入空对象,设置较短的过期时间
redisTemplate.opsForValue().set("product:" + productId, "NULL", 5, TimeUnit.MINUTES);
}
}
// 如果是空对象标记,返回null
return "NULL".equals(product) ? null : product;
}
优点:实现简单,能有效减少数据库压力。
缺点:如果恶意请求大量不同的不存在的key,会导致缓存中存储大量无效数据,占用内存。
2. 布隆过滤器(Bloom Filter)
布隆过滤器是一种概率型数据结构,可以高效地判断一个元素是否存在于集合中。它的特点是:
- 可能存在误判(判断存在时可能实际不存在,但判断不存在时一定不存在)。
- 占用空间小,适合海量数据场景。
我们可以把所有合法的商品ID预先加载到布隆过滤器中,查询前先检查商品ID是否可能存在,如果不存在就直接返回,避免查询数据库。
// Java示例:使用Guava的布隆过滤器
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
public class ProductService {
private BloomFilter<String> bloomFilter;
public ProductService() {
// 预计元素数量100万,误判率1%
bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.01);
// 初始化时加载所有合法商品ID
List<String> allProductIds = productDao.getAllProductIds();
for (String id : allProductIds) {
bloomFilter.put(id);
}
}
public Product getProductById(String productId) {
// 先检查布隆过滤器
if (!bloomFilter.mightContain(productId)) {
return null; // 肯定不存在,直接返回
}
// 后续逻辑和之前一样...
}
}
优点:内存占用低,适合大规模数据。
缺点:有一定的误判率,且数据更新时需要同步更新布隆过滤器。
三、提高缓存命中率的其他技巧
1. 合理设置缓存过期时间
缓存时间太短会导致频繁回源查询数据库,太长则可能导致数据不一致。我们可以采用:
- 基础过期时间 + 随机抖动,避免缓存雪崩。
- 热点数据永不过期,通过后台线程异步更新。
// Java示例:设置缓存过期时间带随机抖动
public void setProductCache(Product product) {
// 基础过期时间1小时 + 随机0-10分钟,防止同时失效
long expireTime = 60 + (long) (Math.random() * 10);
redisTemplate.opsForValue().set(
"product:" + product.getId(),
product,
expireTime,
TimeUnit.MINUTES
);
}
2. 多级缓存架构
除了Redis,还可以利用本地缓存(如Caffeine)作为一级缓存,Redis作为二级缓存,进一步减少Redis的压力。
// Java示例:多级缓存(Caffeine + Redis)
public Product getProductById(String productId) {
// 先查本地缓存
Product product = caffeineCache.getIfPresent(productId);
if (product == null) {
// 本地缓存没有,查Redis
product = redisTemplate.opsForValue().get("product:" + productId);
if (product == null) {
// Redis没有,查数据库...
} else {
// 回填本地缓存
caffeineCache.put(productId, product);
}
}
return product;
}
四、应用场景与总结
适用场景
- 高并发查询系统:如电商商品详情、社交动态等。
- 防止恶意攻击:比如爬虫频繁请求不存在的资源。
技术对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 缓存空对象 | 实现简单 | 可能缓存大量无效数据 |
| 布隆过滤器 | 内存占用低 | 有误判,需维护过滤器 |
注意事项
- 空对象缓存的过期时间不宜过长,避免占用内存。
- 布隆过滤器需要定期重建,确保数据准确性。
- 监控缓存命中率,及时发现异常。
总结
缓存穿透是分布式系统常见问题,合理使用空对象缓存或布隆过滤器可以有效缓解。同时,结合多级缓存、动态过期策略等手段,能进一步提升系统性能。
评论