一、当缓存变成筛子:什么是缓存穿透?
想象你管理着拥有百万藏书的图书馆(数据库),最近新采购了智能书架系统(Redis缓存)。某天有人频繁查询《哈利波特与永动机》这种不存在的书籍,导致智能书架每次都显示"未找到",图书管理员不得不反复跑仓库确认。这种无效请求击穿缓存直接访问数据库的现象,就是典型的缓存穿透。
示例场景:
// Spring Boot + Redis 查询示例
public Product getProduct(String id) {
// 1.先查Redis
Product product = redisTemplate.opsForValue().get(id);
if (product != null) return product;
// 2.查数据库(穿透风险点)
product = productDao.findById(id);
// 3.写入Redis(常规方案失效处)
if (product != null) {
redisTemplate.opsForValue().set(id, product, 30, TimeUnit.MINUTES);
}
return product;
}
当请求的id在数据库和缓存都不存在时,每次请求都会穿透到数据库,就像有人拿着不存在的地图坐标反复问路,最终拖垮整个问讯处。
二、穿透攻击的三种武器库
1. 空对象缓存:用虚拟盾牌抵挡攻击
// 空对象缓存方案(Spring Data Redis)
public Product getProductWithNullCache(String id) {
// 设置特殊标记值
String NULL_CACHE = "N/A";
Object cacheObj = redisTemplate.opsForValue().get(id);
if (NULL_CACHE.equals(cacheObj)) {
return null; // 直接拦截已记录的不存在请求
}
Product product = productDao.findById(id);
if (product == null) {
// 缓存空值但设置较短过期时间
redisTemplate.opsForValue().set(id, NULL_CACHE, 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(id, product, 30, TimeUnit.MINUTES);
}
return product;
}
注意事项:需要定期清理历史空值记录,防止长期占用内存
2. 布隆过滤器:构建请求防火墙
// 使用Guava布隆过滤器(Java技术栈)
public class BloomFilterService {
private BloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
// 预计元素量100万,误判率0.1%
bloomFilter = BloomFilter.create(Funnels.stringFunnel(), 1000000, 0.001);
// 初始化时加载所有有效ID
productDao.findAllIds().forEach(bloomFilter::put);
}
public boolean mightContain(String id) {
return bloomFilter.mightContain(id);
}
}
// 在查询方法中增加校验
public Product getProductWithBloomFilter(String id) {
if (!bloomFilterService.mightContain(id)) {
return null; // 直接拦截非法请求
}
// 后续流程与常规查询相同...
}
性能对比:内存消耗约1.8MB/百万数据,查询耗时约0.1毫秒
3. 互斥锁:建立流量缓冲带
// 使用Redisson分布式锁(Redis技术栈)
public Product getProductWithLock(String id) {
Product product = redisTemplate.opsForValue().get(id);
if (product != null) return product;
RLock lock = redissonClient.getLock("product_lock:" + id);
try {
if (lock.tryLock(2, 5, TimeUnit.SECONDS)) {
// 获得锁后二次检查缓存
product = redisTemplate.opsForValue().get(id);
if (product == null) {
product = productDao.findById(id);
// 即使为空也进行缓存
redisTemplate.opsForValue().set(id, product != null ? product : "N/A",
30, TimeUnit.MINUTES);
}
}
} finally {
lock.unlock();
}
return product;
}
适用场景:高并发环境下对热点数据的保护
三、方案选择的战场地图
应用场景对照表
方案类型 | 适用场景 | 典型QPS |
---|---|---|
空对象缓存 | 数据维度单一的中型系统 | 3000-5000 |
布隆过滤器 | ID规则明确的电商/社交系统 | 10000+ |
分布式锁 | 热点数据保护的金融系统 | 500-2000 |
技术方案性能对比
空对象缓存
- 优点:实现简单,见效快
- 缺点:可能缓存大量无效数据
- 内存消耗:每个空对象约100字节
布隆过滤器
- 优点:内存效率极高(1MB/50万数据)
- 缺点:存在误判率(需业务容忍)
- 更新延迟:新增数据需要同步更新
互斥锁方案
- 优点:保证数据一致性
- 缺点:增加系统复杂度
- 性能损耗:锁操作增加约5ms延迟
四、防御工事构建要点
- 监控预警系统:设置穿透请求阈值告警(如:空查询率>30%触发报警)
- 动态调整策略:根据流量特征自动切换防护方案
- 数据预热机制:在业务低峰期预加载热点数据
- 服务降级方案:在数据库压力过大时返回默认值
- 密钥校验机制:对请求参数进行格式校验(如ID必须为数字)
五、攻防战的经验总结
在实战中,我们通常会采用组合策略:使用布隆过滤器作为第一道防线,配合空对象缓存作为第二层防护,对特别重要的数据再增加分布式锁保护。就像古代城池的防御体系,既有护城河(布隆过滤器),又有城墙(空缓存),重要区域还有卫兵把守(分布式锁)。
需要特别注意两个误区:一是过度依赖单一方案,二是忽视方案的可维护性。曾经有个电商系统使用纯布隆过滤器方案,结果在新品上架时因为过滤器更新延迟导致用户无法看到新品,这就是典型的方案选型失误。
未来的防护体系将趋向智能化发展,基于机器学习的请求特征分析、动态流量整形等技术会逐渐普及。但无论技术如何演进,理解业务特征、做好基础防护的原则永远不会过时。就像再先进的防盗门,也要记得随手关门才能真正发挥作用。