一、当缓存变成筛子:什么是缓存穿透?

想象你管理着拥有百万藏书的图书馆(数据库),最近新采购了智能书架系统(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

技术方案性能对比

  1. 空对象缓存

    • 优点:实现简单,见效快
    • 缺点:可能缓存大量无效数据
    • 内存消耗:每个空对象约100字节
  2. 布隆过滤器

    • 优点:内存效率极高(1MB/50万数据)
    • 缺点:存在误判率(需业务容忍)
    • 更新延迟:新增数据需要同步更新
  3. 互斥锁方案

    • 优点:保证数据一致性
    • 缺点:增加系统复杂度
    • 性能损耗:锁操作增加约5ms延迟

四、防御工事构建要点

  1. 监控预警系统:设置穿透请求阈值告警(如:空查询率>30%触发报警)
  2. 动态调整策略:根据流量特征自动切换防护方案
  3. 数据预热机制:在业务低峰期预加载热点数据
  4. 服务降级方案:在数据库压力过大时返回默认值
  5. 密钥校验机制:对请求参数进行格式校验(如ID必须为数字)

五、攻防战的经验总结

在实战中,我们通常会采用组合策略:使用布隆过滤器作为第一道防线,配合空对象缓存作为第二层防护,对特别重要的数据再增加分布式锁保护。就像古代城池的防御体系,既有护城河(布隆过滤器),又有城墙(空缓存),重要区域还有卫兵把守(分布式锁)。

需要特别注意两个误区:一是过度依赖单一方案,二是忽视方案的可维护性。曾经有个电商系统使用纯布隆过滤器方案,结果在新品上架时因为过滤器更新延迟导致用户无法看到新品,这就是典型的方案选型失误。

未来的防护体系将趋向智能化发展,基于机器学习的请求特征分析、动态流量整形等技术会逐渐普及。但无论技术如何演进,理解业务特征、做好基础防护的原则永远不会过时。就像再先进的防盗门,也要记得随手关门才能真正发挥作用。