缓存穿透解决方案:布隆过滤器

缓存穿透这个问题,说白了就是请求绕过了缓存,直接打到数据库上。我早年做电商秒杀系统时,就吃过这个亏——恶意用户拿了一堆不存在的商品ID来刷,Redis里查不到,每次都穿透到MySQL,差点把库打挂。

那怎么解决?今天我们就来聊聊布隆过滤器(Bloom Filter)。它不是什么新概念,但对付缓存穿透确实管用。

布隆过滤器原理

布隆过滤器的核心思想很简单:用多个哈希函数,把数据映射到一个很长的二进制向量里。

你想想看,传统做法是把所有合法key存到Set里,但数据量一大,内存就扛不住了。布隆过滤器不一样,它不存原始数据,只存几个“指纹”。

具体怎么做的?

  1. 准备一个长度为m的bit数组,初始全为0。
  2. 准备k个独立的哈希函数。
  3. 插入元素时,用k个哈希函数算出k个位置,把这些位置都置为1。
  4. 查询元素时,同样算出k个位置,如果所有位置都是1,就认为元素可能存在;只要有一个是0,就肯定不存在。

关键特性:布隆过滤器说“不存在”,那一定不存在;说“存在”,只是可能存在(有误判率)。

为什么会误判?因为不同元素的哈希结果可能重叠,把别人的位置占了。但反过来,只要有一个位是0,那这个元素肯定没被插入过——这个特性正好用来拦截不存在的key。

布隆过滤器工作原理 元素 X Hash₁ Hash₂ Hash₃ Bit 数组(m = 16) 1 1 1 说明: • 元素 X 经过 3 个哈希函数,映射到 bit 数组的 3 个位置(索引 2、5、9) • 这些位置被置为 1。查询时同样计算 3 个位置,全部为 1 则可能存在 • 只要有一个位置为 0,则元素一定不存在(用于拦截穿透请求)

实现步骤

在实际项目中,我一般按这几步来落地布隆过滤器:

第一步:确定参数

需要预估两个值:预期数据量n和可接受的误判率p。然后算出bit数组长度m和哈希函数个数k。

参数 含义 经验值
n 预期插入的元素总数 根据业务量估算,留30%余量
p 可接受的误判率 一般取1%~5%
m bit数组长度 m = -n·ln(p) / (ln2)²
k 哈希函数个数 k = (m/n)·ln2

我的经验:n取业务峰值数据的1.3倍,p设1%就够用。m算出来如果太大,可以拆成多个小布隆过滤器分片。

第二步:初始化bit数组

用Redis的bitmap或者Java的BitSet都行。我习惯用Redis,因为可以跨服务共享。

第三步:预热数据

系统启动时,把所有合法key(比如所有商品ID)都插入布隆过滤器。这一步很关键,漏了就等于白做。

第四步:请求拦截

每次查询先过布隆过滤器。如果返回“不存在”,直接返回空结果,不走缓存和数据库。如果返回“可能存在”,才继续查缓存。

代码示例

我用Java + Redis实现一个简单的布隆过滤器。这里用的是Redisson客户端,它内置了布隆过滤器实现。

// 1. 初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("productBloom");
// 预期100万数据,误判率1%
bloomFilter.tryInit(1000000L, 0.01);

// 2. 预热:把所有商品ID插入
List<String> allProductIds = productService.getAllIds();
for (String id : allProductIds) {
    bloomFilter.add(id);
}

// 3. 查询拦截
public Product getProduct(String productId) {
    // 第一步:布隆过滤器拦截
    if (!bloomFilter.contains(productId)) {
        // 一定不存在,直接返回空
        return null;
    }
    
    // 第二步:查缓存
    Product product = cacheService.get(productId);
    if (product != null) {
        return product;
    }
    
    // 第三步:查数据库(此时才真正可能穿透)
    product = databaseService.get(productId);
    if (product != null) {
        cacheService.set(productId, product);
    }
    return product;
}

注意:布隆过滤器不支持删除操作。如果你有删除key的需求,可以考虑用Counting Bloom Filter,或者定期重建过滤器。

适用场景

布隆过滤器不是万能的,它有自己的“舒适区”。我总结了几种典型场景:

场景 是否推荐 原因
缓存穿透防护 ✅ 强烈推荐 拦截不存在的key,效果立竿见影
爬虫URL去重 ✅ 推荐 URL数量巨大,布隆过滤器省内存
垃圾邮件过滤 ✅ 推荐 允许少量误判,但能拦截大部分垃圾
需要精确删除的场景 ❌ 不推荐 标准布隆过滤器不支持删除
数据量极小(几百条) ❌ 不推荐 直接用Set更简单,没必要上布隆

一句话总结:布隆过滤器最适合“数据量大、允许少量误判、只需要判断存在性”的场景。缓存穿透防护是它的经典用法。

避坑指南

我曾经在一个项目里踩过坑,分享出来大家引以为戒:

  • 误判率不是0:布隆过滤器说“存在”时,数据库里可能没有。所以查询数据库的逻辑不能省,只是概率降低了。
  • 数据变化后要重建:如果业务数据频繁增删,布隆过滤器会越来越不准。我一般每天凌晨重建一次。
  • 哈希函数要选好:别用太弱的哈希,容易碰撞。MurmurHash、FNV系列都行。
  • bit数组别太小:m/n比值低于8时,误判率会飙升。算一下再动手。

嗯,布隆过滤器就聊到这儿。它不是什么高深技术,但用好了确实能解决大问题。下次遇到缓存穿透,不妨试试这个方案。