作为一名天天跟 “数据” 打交道的后端博主,我猜你一定踩过这些坑:
手里攥着 100 万条用户日志要去重,数据库查一次卡 3 分钟;缓存穿透把 MySQL 捶得奄奄一息,老板在后面盯着问 “怎么又 502 了”;甚至判断一个 IP 是不是恶意地址,查库慢到用户都跑光了……
如果你也有过这种 “想把数据扔了” 的冲动,那今天要聊的布隆过滤器,绝对是你的 “救命神器”。它不占空间、速度还快,就是偶尔会 “认错人”—— 别急,咱们一步步把它聊透。
一、先搞懂:布隆过滤器是个啥 “黑科技”?
其实布隆过滤器一点不 “黑”,你可以把它理解成小区门口的保安大爷:
大爷记不住所有业主的脸(毕竟人太多),但他会记几个关键特征 —— 比如 “戴黑框眼镜、穿蓝色拖鞋、拎着买菜袋”。只要来人符合这几个特征,大爷就默认是业主(放行);如果一个特征都对不上,那肯定是外人(拦住)。
布隆过滤器的核心逻辑跟这一模一样,只不过它用的是 “bit 数组 + 多个哈希函数”:
-
初始化一个 bit 数组:比如长度为 10 的数组,初始值全是 0(相当于大爷的 “空白特征本”);
-
添加元素时 “打标记” :比如要加 “用户 A”,先用 3 个哈希函数分别计算,得到 3 个位置(比如 2、5、7),然后把这 3 个位置的 bit 从 0 改成 1(相当于大爷记下 “用户 A 的 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 试一下,用了就知道有多香~
你们项目里用布隆过滤器解决过什么问题?评论区聊聊,互相取经~