布隆过滤器:判断“可能存在“和“一定不存在“

缓存穿透这个问题,大家应该都听过。就是恶意请求大量不存在的数据,直接打到数据库上。布隆过滤器就是解决这个问题的利器。

先说个生活里的例子

想象你是一个图书馆管理员。有人问:"你有没有这本书?"

你可以:

  • 搜遍整个图书馆(太慢)
  • 内存里放一份所有书名的列表(太占内存)

或者你可以用布隆过滤器------一种特殊的数据结构,它会告诉你:

  • "一定没有" ------ 如果过滤器说没有,那这本书确实不存在
  • "可能有" ------ 如果过滤器说有,这本书可能存在(但也可能是误判)

这就是布隆过滤器的核心特点:它绝不会把不存在的东西说成"一定有",但可能把存在的东西误判为"可能有"。

布隆过滤器怎么工作的

本质上就是一个 bit 数组 + 多个哈希函数:

复制代码
假设我们有 10 个 bit 位置的数组, 初始都是 0:

index:  0  1  2  3  4  5  6  7  8  9
value:  0  0  0  0  0  0  0  0  0  0

我们用 3 个哈希函数: h1, h2, h3

现在要添加 "user:100" 这个 key:
- h1("user:100") = 2  → bit[2] = 1
- h2("user:100") = 5  → bit[5] = 1
- h3("user:100") = 7  → bit[7] = 1

添加 "order:200":
- h1("order:200") = 1  → bit[1] = 1
- h2("order:200") = 5  → bit[5] 已经 = 1 了
- h3("order:200") = 7  → bit[7] 已经 = 1 了

现在的数组:
index:  0  1  2  3  4  5  6  7  8  9
value:  0  1  1  0  0  1  0  1  0  0

现在查询 "user:100" 是否存在:
- h1 → bit[2] = 1 ✓
- h2 → bit[5] = 1 ✓
- h3 → bit[7] = 1 ✓
→ 三次都命中,返回 "可能存在" (true)

查询 "user:999" 是否存在:
- h1 → bit[?] = ?
- 如果任何一次不命中,直接返回 "一定不存在" (false)

Redis 里的布隆过滤器

Redis 4.0 之后支持了 Bloom Filter 模块,用起来很简单:

bash 复制代码
# 添加元素到 bloom filter
BF.ADD user:bloom user:100
BF.ADD user:bloom user:200

# 判断元素是否存在
BF.EXISTS user:bloom user:100
# 返回 1,表示可能存在

BF.EXISTS user:bloom user:999
# 返回 0,表示一定不存在

Java 里用 Redisson:

java 复制代码
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("user:bloom");

// 初始化,预期插入 100000 个元素,期望误判率 1%
bloomFilter.tryInit(100000, 0.01);

// 添加
bloomFilter.add("user:100");
bloomFilter.add("user:200");

// 判断
boolean mightExist = bloomFilter.contains("user:100");  // true
boolean definitelyNot = bloomFilter.contains("user:999"); // false

常见使用场景

1. 缓存穿透

java 复制代码
public User getUser(Long userId) {
    String cacheKey = "user:" + userId;

    // 先查布隆过滤器
    if (!bloomFilter.contains(cacheKey)) {
        // 布隆过滤器说一定不存在,直接返回 null
        return null;
    }

    // 过滤器说可能存在,再查缓存
    User user = redis.get(cacheKey);
    if (user != null) {
        return user;
    }

    // 查数据库
    user = userMapper.selectById(userId);
    if (user != null) {
        redis.set(cacheKey, user);
        bloomFilter.add(cacheKey);
    }
    return user;
}

2. 用户点赞/浏览记录去重

java 复制代码
// 判断用户是否点赞过
public boolean hasUserLiked(Long userId, Long articleId) {
    String key = "liked:" + userId;
    return bloomFilter.contains(key + ":" + articleId);
}

// 用户点赞时记录
public void userLike(Long userId, Long articleId) {
    String key = "liked:" + userId + ":" + articleId;
    if (!bloomFilter.contains(key)) {
        // 实际写入数据库
        likeMapper.insert(userId, articleId);
        bloomFilter.add(key);
    }
}

3. 邮箱/手机号去重注册

java 复制代码
// 注册前检查
public Result register(String email) {
    if (bloomFilter.contains("email:" + email)) {
        // 可能重复,但需要再查数据库确认
        if (userMapper.existsByEmail(email)) {
            return Result.fail("邮箱已被注册");
        }
    }
    // 执行注册逻辑...
}

4. 商品 SKU 过滤

电商秒杀场景,滤掉无效的 SKU 请求:

java 复制代码
public Result seckill(Long userId, Long skuId) {
    // 有效 SKU 在布隆过滤器里,不存在直接拒绝
    if (!bloomFilter.contains("sku:" + skuId)) {
        return Result.fail("无效商品");
    }
    // 继续处理秒杀逻辑...
}

误判率与空间占用

布隆过滤器的误判率和两个因素有关:

元素数量 bit 位数 哈希函数数量 误判率
10000 100000 7 ~1%
10000 100000 3 ~1%
10000 200000 7 ~0.1%

空间占用计算:

复制代码
假设 10 亿条数据,误判率 1%:
- 每个元素需要 ≈ 10 bit
- 总空间 ≈ 10 * 10亿 / 8 ≈ 1.25GB

对比用 HashSet 存同样的数据:
- 每个元素需要 ≈ 16 bytes
- 总空间 ≈ 16 * 10亿 ≈ 16GB

布隆过滤器空间占用只有 HashSet 的 1/10 左右。

Guava 版布隆过滤器

不需要 Redis,直接用 Guava:

xml 复制代码
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>
java 复制代码
// 创建布隆过滤器,期望 1000000 个元素,误判率 1%
BloomFilter<String> filter = BloomFilter.create(
    Funnels.stringFunnel(StandardCharsets.UTF_8),
    1000000,
    0.01
);

// 添加
filter.put("user:100");

// 判断
if (filter.mightContain("user:100")) {
    // 可能存在,再查数据库确认
    User user = userMapper.selectById(100);
    if (user != null) {
        return user;
    }
}

注意事项

  1. 布隆过滤器不支持删除 - 因为删除一个元素可能会影响其他元素的判断。如果需要删除,可以用布隆过滤器的变体:Counting Bloom Filter 或者直接把删除的元素也加入一个 "已删除" 的过滤器里

  2. 误判率不能为 0 - 布隆过滤器的数学原理决定了它一定有误判率,只能通过增加位数来降低

  3. 适用于"查多为写少"的场景 - 读请求远多于写请求,或者数据量特别大的场景

总结

特点 说明
空间效率 比 HashSet 节省约 10 倍空间
查询效率 O(k),k = 哈希函数数量,通常 3-10 个
不支持删除 删除会影响其他元素
误判不可避免 只能说"可能存在",不能说"一定存在"
零漏报 说"不存在"就一定不存在

布隆过滤器不是什么场景都能用,但如果你的场景恰好满足"允许少量误判"+"查询远多于写入",用它准没错。

相关推荐
兔小盈1 小时前
多线程篇-(二)线程创建、中断与终止
java·开发语言·多线程
gQ85v10Db1 小时前
Redis分布式锁进阶第十八篇:本地缓存+分布式锁双锁架构 + 高并发削峰兜底 + 极致性能无损优化实战
redis·分布式·缓存
jnrjian1 小时前
Library Cache Load Lock library cache pins are replaced by mutexes
java·后端·spring
abcnull2 小时前
传统的JavaWeb项目Demo快速学习!
java·servlet·elementui·vue·javaweb
risc1234562 小时前
【lucene】PostingsEnum跟TermsEnum 的区别是啥?
java·lucene
小江的记录本2 小时前
【Kafka核心】Kafka高性能的四大核心支柱:零拷贝、批量发送、页缓存、压缩
java·数据库·分布式·后端·缓存·kafka·rabbitmq
gQ85v10Db2 小时前
Redis分布式锁进阶第十四篇:全系列终局架构复盘 + 锁体系统一规范 + 线上全年零事故收官方案
redis·分布式·架构
SamDeepThinking2 小时前
程序员过35岁之前,应该完成的三件事
java·后端·程序员