后端人必懂!布隆过滤器:从 “海量去重头痛” 到 “缓存穿透救星” 的神奇存在✨

158 阅读10分钟

作为一名天天跟 “数据” 打交道的后端博主,我猜你一定踩过这些坑:
手里攥着 100 万条用户日志要去重,数据库查一次卡 3 分钟;缓存穿透把 MySQL 捶得奄奄一息,老板在后面盯着问 “怎么又 502 了”;甚至判断一个 IP 是不是恶意地址,查库慢到用户都跑光了……

如果你也有过这种 “想把数据扔了” 的冲动,那今天要聊的布隆过滤器,绝对是你的 “救命神器”。它不占空间、速度还快,就是偶尔会 “认错人”—— 别急,咱们一步步把它聊透。

一、先搞懂:布隆过滤器是个啥 “黑科技”?

其实布隆过滤器一点不 “黑”,你可以把它理解成小区门口的保安大爷
大爷记不住所有业主的脸(毕竟人太多),但他会记几个关键特征 —— 比如 “戴黑框眼镜、穿蓝色拖鞋、拎着买菜袋”。只要来人符合这几个特征,大爷就默认是业主(放行);如果一个特征都对不上,那肯定是外人(拦住)。

布隆过滤器的核心逻辑跟这一模一样,只不过它用的是 “bit 数组 + 多个哈希函数”:

  1. 初始化一个 bit 数组:比如长度为 10 的数组,初始值全是 0(相当于大爷的 “空白特征本”);

  2. 添加元素时 “打标记” :比如要加 “用户 A”,先用 3 个哈希函数分别计算,得到 3 个位置(比如 2、5、7),然后把这 3 个位置的 bit 从 0 改成 1(相当于大爷记下 “用户 A 的 3 个特征”);

  3. 查询元素时 “验特征” :要查 “用户 A 在不在”,还是用那 3 个哈希函数算位置,看这 3 个位置是不是全为 1—— 全是 1 就 “可能在”,有一个 0 就 “肯定不在”。

划重点:它不存完整数据,只存 “特征标记”,所以特别省空间(bit 级别的存储,100 万数据也才占几十 KB),而且查的时候只算几个哈希,速度快到飞起

二、实战场景:布隆过滤器能帮咱解决哪些 “老大难”?

别光说原理,咱后端聊技术,最终要看 “能不能落地”。布隆过滤器的应用场景,全是后端的 “高频痛点”:

1. 海量数据去重:再也不用 “全量比对” 了

比如日志去重、用户签到记录去重、爬虫去重(避免重复爬同一网页)。
举个例子:你要处理 1 亿条用户行为日志,去重后存到 Hive 里。如果用 HashSet 去重,1 亿条数据至少要占几百 MB 内存(甚至 GB 级),机器可能直接 OOM;但用布隆过滤器,先把所有日志过一遍,标记 “已存在的日志特征”,新日志来了先查过滤器 ——“肯定不在” 就直接存,“可能在” 再细查,内存占用直接降一个数量级。

2. 垃圾邮件过滤:邮件服务器的 “前置门神”

邮箱服务商判断一封邮件是不是垃圾邮件,总不能每次都查 “全量垃圾邮件库”(那得多大啊)。
所以他们会把所有垃圾邮件的发件人 / 关键词,先存到布隆过滤器里。新邮件进来时,先查过滤器:如果 “肯定不是垃圾邮件”,直接放行到收件箱;如果 “可能是”,再走更复杂的过滤逻辑(比如内容检测)。这样既快又能减少资源消耗。

3. 安全领域:恶意 IP / 黑名单的 “快速排查”

比如你的服务经常被恶意 IP 攻击,你维护了一个 10 万条的恶意 IP 黑名单。如果每次请求都去数据库查 “这个 IP 在不在黑名单里”,QPS 一高 DB 就扛不住。
这时布隆过滤器就派上用场了:把所有恶意 IP 存到过滤器里,新请求进来先查过滤器 ——“肯定不是恶意 IP” 就直接放行,“可能是” 再去 DB 确认。相当于给 DB 加了一层 “前置缓冲”,压力直接少一半。

4. 避免缓存穿透:DB 的 “最后一道防线”

这是后端最常遇到的场景之一!先复习下 “缓存穿透”:
用户查一个 “缓存里没有、DB 里也没有” 的 key(比如查 id=-1 的用户),因为缓存没命中,就会一直去查 DB,大量这种请求会把 DB 捶垮。

布隆过滤器怎么解决?
所有 DB 里存在的 key(比如所有用户 id),先存到布隆过滤器里。当有新请求时,先查过滤器:

  • 如果过滤器说 “这个 key 肯定不存在”,直接返回空,不查缓存也不查 DB;
  • 如果过滤器说 “可能存在”,再走正常流程(查缓存→查 DB→更新缓存)。
    这样就把 “无效请求” 拦在了 DB 外面,完美避免缓存穿透。

三、灵魂拷问:布隆过滤器为啥会 “认错人”(误判)?

前面一直说 “可能存在”“肯定不存在”,为啥不能 100% 准确?答案是哈希碰撞

比如有两个不同的元素 A 和 B:

  • A 用哈希函数算出来的位置是 2、5、7;

  • B 用同样的哈希函数算出来的位置,刚好也是 2、5、7(虽然概率低,但架不住数据多)。

这时布隆过滤器里,A 和 B 的 “特征标记” 是一样的。当你查 B 的时候,过滤器会以为 “B 就是 A”,返回 “可能存在”—— 但其实 B 根本没存过,这就是 “误判”。

简单说:布隆过滤器只会把 “不存在的判成存在”(误判),不会把 “存在的判成不存在”(漏判)。而且误判率是可控的,不是随机瞎判。

四、优化方案:怎么让布隆过滤器少 “犯错”?

想降低误判率,关键看两个参数,记住这个口诀:数组越大,误判越低;哈希越多,误判越低(但别太多)

1. 增大 bit 数组的长度

bit 数组越长,每个元素的 “特征位” 就越不容易和其他元素重叠。比如把 10 长度的数组改成 1000,A 和 B 哈希到同一个位置的概率就会大大降低。

但也不能无限大 —— 数组太长会浪费空间,违背布隆过滤器 “省空间” 的初衷。

2. 选择合适数量的哈希函数

哈希函数越多,每个元素会在数组里标记更多的 “特征位”,重叠的概率就越低。比如用 3 个哈希函数比用 1 个,误判率会低很多。

但哈希函数也不是越多越好:太多的话,每个元素要占更多的 bit 位,数组会很快被 “填满”(全是 1),反而会导致误判率上升。

小技巧:实际使用时,我们不用自己算 “数组多大、哈希多少个”—— 主流的布隆过滤器库(比如 Guava)会根据你输入的 “预期插入量” 和 “目标误判率”,自动计算最优的数组长度和哈希函数数量。

五、上手实操:两种常用布隆过滤器用法(Guava + Redisson)

光说不练假把式,咱直接上代码。后端常用的布隆过滤器有两种:本地用 Guava,分布式用 Redisson(基于 Redis 的 bitmap)。

1. Guava:本地单机场景首选(简单到离谱)

Guava 是 Google 的开源库,自带布隆过滤器实现,不用自己写哈希和数组,直接调用 API 就行。

第一步:引入依赖(Maven)

xml

<!-- Guava依赖,版本选最新的稳定版就行 -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.3-jre</version>
</dependency>

第二步:写代码(核心就 3 步:创建→添加→查询)

java

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

public class GuavaBloomFilterDemo {
    public static void main(String[] args) {
        // 1. 定义关键参数
        long expectedInsertions = 100000; // 预期要插入的元素数量(比如10万条用户id)
        double fpp = 0.01; // 目标误判率(1%,越小越准,但占空间越大)

        // 2. 创建布隆过滤器
        // Funnels.integerFunnel():指定元素类型(这里是Integer,也可以是String、Long等)
        BloomFilter<Integer> bloomFilter = BloomFilter.create(
                Funnels.integerFunnel(),
                expectedInsertions,
                fpp
        );

        // 3. 添加元素(比如添加100个用户id)
        for (int i = 1; i <= 100; i++) {
            bloomFilter.put(i);
        }

        // 4. 查询元素
        // 查存在的元素(比如id=50):应该返回true
        System.out.println("查id=50:" + bloomFilter.mightContain(50)); // 输出true

        // 查不存在的元素(比如id=1000):很大概率返回false(误判率1%)
        System.out.println("查id=1000:" + bloomFilter.mightContain(1000)); // 大概率输出false

        // 查可能误判的元素(比如id=200):如果误判,会返回true,但其实没存过
        System.out.println("查id=200:" + bloomFilter.mightContain(200)); // 小概率输出true
    }
}

关键说明:

  • mightContain()方法:返回 “true” 表示 “可能存在”,返回 “false” 表示 “肯定不存在”—— 别写成contains(),Guava 故意用这个方法名提醒你 “有误判风险”;
  • 预期插入量和误判率:这两个参数要根据实际场景设。比如你要存 100 万数据,能接受 0.1% 的误判率,就把expectedInsertions设 1000000,fpp设 0.001。

2. Redisson:分布式场景必备(基于 Redis)

如果你的服务是分布式的(比如多台机器共享一个布隆过滤器),Guava 就不够用了(本地过滤器不能跨机器共享)。这时就需要 Redisson—— 它基于 Redis 的 bitmap 实现了分布式布隆过滤器,所有服务都能通过 Redis 访问同一个过滤器。

第一步:引入依赖(Maven)

xml

<!-- Redisson依赖,版本选最新稳定版 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.23.5</version>
</dependency>

第二步:写代码(核心:连接 Redis→创建过滤器→操作)

java

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonBloomFilterDemo {
    public static void main(String[] args) {
        // 1. 配置Redis连接(这里用单机Redis,集群配置也类似)
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");

        // 2. 创建Redisson客户端
        RedissonClient redissonClient = Redisson.create(config);

        // 3. 创建分布式布隆过滤器
        RBloomFilter<Integer> bloomFilter = redissonClient.getBloomFilter("user:id:bloom");
        // 初始化:预期插入10万条,误判率1%
        bloomFilter.tryInit(100000, 0.01);

        // 4. 添加元素
        bloomFilter.add(1);
        bloomFilter.add(2);
        bloomFilter.add(3);

        // 5. 查询元素
        System.out.println("查id=1:" + bloomFilter.contains(1)); // true
        System.out.println("查id=100:" + bloomFilter.contains(100)); // 大概率false

        // 6. 关闭客户端(实际项目里别随便关,放Spring容器里管理)
        redissonClient.shutdown();
    }
}

关键说明:

  • Redis 的 bitmap:Redisson 底层把布隆过滤器的 bit 数组存在 Redis 的 bitmap 里,所以占用空间小,而且支持分布式访问;
  • tryInit():只能初始化一次,如果过滤器已经存在,再调用会报错 —— 实际项目里可以用exists()判断是否已初始化。

六、总结:布隆过滤器的 “功与过”

最后咱们客观评价下这个神器,避免你用错场景:

优点:

  • 省空间:bit 级存储,100 万数据只占几十 KB;
  • 速度快:添加和查询都是 O (k)(k 是哈希函数数量,一般个位数);
  • 防穿透:完美解决缓存穿透问题,保护 DB。

缺点:

  • 有误判:只能 “肯定不存在”,不能 “肯定存在”;
  • 不能删:普通布隆过滤器不能删除元素(删了会影响其他元素的判断);
  • 需预热:用之前要把所有 “存在的元素” 提前加进去,不然没效果。

最后一句:

如果你在做日志去重、缓存穿透、黑名单过滤这些场景,布隆过滤器绝对是 “花小钱办大事” 的选择。赶紧拿 Guava 试一下,用了就知道有多香~

你们项目里用布隆过滤器解决过什么问题?评论区聊聊,互相取经~