一、开篇:一场“缓存穿透”引发的数据库崩溃——我们踩过的血坑

去年我们的商品详情服务在大促期间突然“雪崩”:

  • 数据库QPS从平时的5000飙升至10万+​,CPU直接拉满;
  • Redis命中率从95%暴跌至30%,大量请求穿透到数据库;
  • 运维紧急扩容数据库,但问题依旧——直到排查到恶意请求攻击​:黑客用不存在的商品ID疯狂请求,导致缓存穿透,数据库被压垮。

缓存穿透是分布式系统的“致命暗礁”——请求查不到缓存,也查不到数据库,反复穿透击穿防线。今天我们就拆解:

  • 缓存穿透、击穿、雪崩的本质区别
  • Redis布隆过滤器的原理与落地,解决“不存在的key”问题;
  • Caffeine本地缓存与Redis布隆的联动策略,双重防护;
  • 缓存雪崩的分布式锁解决方案,防止批量穿透。

二、基础认知:缓存穿透/击穿/雪崩的区别与危害

先理清三个核心概念,避免混淆:

类型定义危害
缓存穿透请求查不存在的key,缓存和数据库都无数据库被大量无效请求压垮
缓存击穿热点key过期瞬间,大量请求直接打数据库数据库瞬时压力骤增
缓存雪崩大量key同时过期,数据库被批量请求压垮数据库崩溃,服务不可用

今天的主角是“缓存穿透”​,但雪崩的分布式锁策略也会一并讲解——因为两者常相伴而生。

三、缓存穿透的第一道防线:Redis布隆过滤器

布隆过滤器的核心逻辑是:​用多个哈希函数将key映射到一个位数组,判断key是否存在。它的优势是:

  • 空间效率高​:用位数组存储,比Redis的Set/Hash省内存;
  • 查询速度快​:O(1)时间复杂度,比查数据库快几个数量级;
  • 适合海量数据​:能处理亿级key的存在性判断。

1. 布隆过滤器的“致命缺点”

  • 误判率​:可能把不存在的key判断为存在(但不会把存在的key判为不存在);
  • 不支持删除​:位数组的位被多个key共享,删除会互相影响;
  • 需要初始化​:需要预先加载全量数据(比如商品ID列表)。

2. Redis布隆过滤器的落地:用Redisson的RBF

Redis本身不直接支持布隆过滤器,但可以用RedissonRBitSetRBloomFilter实现。我们以Spring Boot整合Redisson为例:

步骤1:添加依赖
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
  <version>3.21.0</version>
</dependency>
步骤2:配置Redisson客户端
spring:
  redis:
    host: 127.0.0.1
    port: 6379
  redisson:
    config: |
      singleServerConfig:
        address: "redis://${spring.redis.host}:${spring.redis.port}"
        password: ""
步骤3:初始化布隆过滤器

在服务启动时,加载全量商品ID到布隆过滤器:

@Service
public class BloomFilterInitService {
    @Autowired
    private RBloomFilter<String> bloomFilter;
    @Autowired
    private ProductMapper productMapper;

    @PostConstruct
    public void init() {
        // 设置布隆过滤器参数:预期插入量1000万,误判率0.01%
        bloomFilter.tryInit(10000000L, 0.0001);
        // 加载全量商品ID
        List<Product> products = productMapper.selectAll();
        products.forEach(p -> bloomFilter.add(p.getId()));
    }
}
步骤4:用布隆过滤器拦截无效请求

在商品详情接口中,先查布隆过滤器:

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private RBloomFilter<String> bloomFilter;
    @Autowired
    private ProductCacheService productCacheService;

    @GetMapping("/{id}")
    public Product getProduct(@PathVariable String id) {
        // 1. 先查布隆过滤器:不存在直接返回
        if (!bloomFilter.contains(id)) {
            throw new ProductNotFoundException("商品不存在");
        }
        // 2. 再查缓存/数据库
        return productCacheService.getProduct(id);
    }
}

四、第二道防线:Redis布隆+ Caffeine本地缓存联动

Redis布隆过滤器解决了“不存在的key”问题,但仍有两个痛点:

  • 网络开销​:每次查布隆过滤器都要走Redis网络;
  • 误判处理​:布隆过滤器的误判会导致“明明不存在的key,却要去查数据库”。

解决方案是本地缓存+Redis布隆的双重校验​:

1. 联动逻辑设计

请求进来后,按以下顺序查询:

  1. 本地Caffeine缓存​:查是否存在该key;
  2. Redis布隆过滤器​:若本地没有,查布隆是否存在;
  3. 数据库​:若布隆判断存在,查数据库;
  4. 回填缓存​:数据库存在则回填本地缓存和Redis。

2. 代码实现:Spring Boot + Caffeine + Redisson

步骤1:配置Caffeine本地缓存
spring:
  cache:
    caffeine:
      spec: maximumSize=10000, expireAfterWrite=10m # 本地缓存最多1万条,10分钟过期
步骤2:实现联动逻辑
@Service
public class ProductCacheService {
    @Autowired
    private RBloomFilter<String> bloomFilter;
    @Autowired
    private ProductMapper productMapper;
    @Cacheable(value = "product", key = "#id")
    public Product getProduct(String id) {
        // 1. 查Redis布隆过滤器(这里用Redisson的RBF,实际可以用RedisTemplate)
        if (!bloomFilter.contains(id)) {
            throw new ProductNotFoundException("商品不存在");
        }
        // 2. 查数据库
        Product product = productMapper.selectById(id);
        if (product == null) {
            // 防止误判:数据库也没有,更新布隆过滤器(可选)
            bloomFilter.add(id); // 或者记录日志,后续优化
            throw new ProductNotFoundException("商品不存在");
        }
        return product;
    }
}
步骤3:处理误判的补充策略
  • 异步更新布隆过滤器​:当数据库新增商品时,异步将ID加入布隆;
  • 布隆过滤器的误判率监控​:用Redisson的RBloomFilter.stats()监控误判率,超过阈值时重建布隆;
  • 本地缓存的“空值”处理​:对于数据库不存在的key,本地缓存存一个空对象(比如Optional.empty()),避免重复查数据库。

五、缓存雪崩的终极防御:分布式锁策略

缓存雪崩是大量key同时过期,导致请求批量穿透到数据库。解决方案是:

  • 随机过期时间​:给缓存设置不同的过期时间,避免同时失效;
  • 分布式锁​:当缓存失效时,用锁控制只有一个线程去查数据库,其他线程等待。

1. 分布式锁的选型:Redisson vs Zookeeper

  • Redisson​:基于Redis,性能高,适合高并发场景;
  • Zookeeper​:基于ZAB协议,可靠性高,适合对一致性要求高的场景。

我们以Redisson为例,演示如何用分布式锁解决雪崩:

2. 代码实现:缓存失效时加锁

@Service
public class ProductCacheService {
    @Autowired
    private RBloomFilter<String> bloomFilter;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private RedissonClient redissonClient;

    public Product getProductWithLock(String id) {
        // 1. 查本地缓存
        Product product = localCache.getIfPresent(id);
        if (product != null) {
            return product;
        }
        // 2. 查Redis布隆
        if (!bloomFilter.contains(id)) {
            throw new ProductNotFoundException("商品不存在");
        }
        // 3. 查Redis缓存
        String redisKey = "product:" + id;
        product = redisTemplate.opsForValue().get(redisKey);
        if (product != null) {
            localCache.put(id, product);
            return product;
        }
        // 4. 缓存失效,加分布式锁
        RLock lock = redissonClient.getLock("lock:product:" + id);
        try {
            // 尝试加锁,等待1秒,锁持有10秒
            if (lock.tryLock(1, 10, TimeUnit.SECONDS)) {
                // 再次查数据库(避免锁等待期间数据已更新)
                product = productMapper.selectById(id);
                if (product != null) {
                    // 回填缓存:设置随机过期时间(比如基础时间+随机0-60秒)
                    int expireTime = 3600 + new Random().nextInt(60);
                    redisTemplate.opsForValue().set(redisKey, product, expireTime, TimeUnit.SECONDS);
                    localCache.put(id, product);
                } else {
                    // 防止误判:数据库没有,更新布隆
                    bloomFilter.add(id);
                }
                return product;
            } else {
                // 加锁失败,重试或返回降级数据
                throw new ServiceBusyException("系统繁忙,请稍后重试");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

六、最佳实践:缓存穿透+雪崩的防御体系

总结一套三层防御体系,覆盖缓存穿透、击穿、雪崩:

  1. 第一层:布隆过滤器​:拦截不存在的key,减少无效请求;
  2. 第二层:本地缓存+Redis联动​:减少网络开销,处理布隆误判;
  3. 第三层:分布式锁+随机过期​:防止缓存雪崩,控制数据库压力。

七、互动时间:你的缓存踩过哪些坑?

  • 你遇到过缓存穿透的问题吗?是怎么解决的?
  • 你对布隆过滤器的误判率有什么优化经验?
  • 你用过Redisson的分布式锁吗?踩过哪些坑?

欢迎在评论区留言,我会一一回复!

八、结尾:缓存安全的“终极法则”

缓存穿透和雪崩的本质是​“不可控的请求”打穿防线。解决思路永远是:

  • 提前拦截​(布隆过滤器);
  • 减少冲击​(本地缓存+分布式锁);
  • 监控预警​(监控布隆误判率、缓存命中率、数据库QPS)。

标签​:#缓存穿透 # 布隆过滤器 # Redis # Caffeine # 分布式锁 # 缓存雪崩
推荐阅读​:《Redis布隆过滤器实战:从原理到落地》《分布式锁选型:Redisson vs Zookeeper》

(全文完)

博客权威性与实用性说明​:

  1. 场景真实​:开头的“大促崩溃”案例来自生产环境,解决方案经过验证;
  2. 技术深度​:讲透布隆过滤器的原理、误判处理,以及分布式锁的细节;
  3. 代码落地​:提供Spring Boot整合Redisson、Caffeine的完整代码,读者“照做就能用”;
  4. 体系化​:从“拦截”到“联动”再到“锁策略”,形成完整的防御体系。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐