详解布隆过滤器的原理、使用场景和注意事项

原文:https://www.jianshu.com/p/2104d11ee0a2

在进入正文之前,之前看到的有句话我觉得说得很好:

Data structures are nothing different. They are like the bookshelves
of your application where you can organize your data. Different data
structures will give you different facility and benefits. To properly
use the power and accessibility of the data structures you need to
know the trade-offs of using one.

大意是不同的数据结构有不同的适用场景和优缺点,你需要仔细权衡自己的需求之后妥善适用它们,布隆过滤器就是践行这句话的代表。

什么是布隆过滤器

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。

相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

实现原理

HashMap 的问题

讲述布隆过滤器的原理之前,我们先思考一下,通常你判断某个元素是否存在用的是什么?应该蛮多人回答 HashMap 吧,确实可以将值映射到 HashMap 的 Key,然后可以在 O(1) 的时间复杂度内返回结果,效率奇高。但是 HashMap 的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap 占据的内存大小就变得很可观了。

还比如说你的数据集存储在远程服务器上,本地服务接受输入,而数据集非常大不可能一次性读进内存构建 HashMap 的时候,也会存在问题。

布隆过滤器数据结构

布隆过滤器是一个 bit 向量或者说 bit 数组,长这样:

image

如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置 1,例如针对值 “baidu” 和三个不同的哈希函数分别生成了哈希值 1、4、7,则上图转变为:

image

Ok,我们现在再存一个值 “tencent”,如果哈希函数返回 3、4、8 的话,图继续变为:

image

值得注意的是,4 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了。现在我们如果想查询 “dianping” 这个值是否存在,哈希函数返回了 1、5、8三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “dianping” 这个值不存在。而当我们需要查询 “baidu” 这个值是否存在的话,那么哈希函数必然会返回 1、4、7,然后我们检查发现这三个 bit 位上的值均为 1,那么我们可以说 “baidu” 存在了么?答案是不可以,只能是 “baidu” 这个值可能存在。

这是为什么呢?答案跟简单,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 “taobao” 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断 “taobao” 这个值存在。

支持删除么

传统的布隆过滤器并不支持删除操作。但是名为 Counting Bloom filter 的变种可以用来测试元素计数个数是否绝对小于某个阈值,它支持元素删除。可以参考文章 Counting Bloom Filter 的原理和实现

如何选择哈希函数个数和布隆过滤器长度

很显然,过小的布隆过滤器很快所有的 bit 位均为 1,那么查询任何值都会返回“可能存在”,起不到过滤的目的了。布隆过滤器的长度会直接影响误报率,布隆过滤器越长其误报率越小。

另外,哈希函数的个数也需要权衡,个数越多则布隆过滤器 bit 位置位 1 的速度越快,且布隆过滤器的效率越低;但是如果太少的话,那我们的误报率会变高。

image

如何选择适合业务的 k 和 m 值呢,这里直接贴一个公式:

image.png

k 为哈希函数个数,m 为布隆过滤器长度,n 为插入的元素个数,p 为误报率。
至于如何推导这个公式,我在知乎发布的文章有涉及,感兴趣可以看看,不感兴趣的话记住上面这个公式就行了。

最佳实践

常见的适用常见有,利用布隆过滤器减少磁盘 IO 或者网络请求,因为一旦一个值必定不存在的话,我们可以不用进行后续昂贵的查询请求。

另外,既然你使用布隆过滤器来加速查找和判断是否存在,那么性能很低的哈希函数不是个好选择,推荐 MurmurHash、Fnv 这些。

大Value拆分

Redis 因其支持 setbit 和 getbit 操作,且纯内存性能高等特点,因此天然就可以作为布隆过滤器来使用。但是布隆过滤器的不当使用极易产生大 Value,增加 Redis 阻塞风险,因此生成环境中建议对体积庞大的布隆过滤器进行拆分。

拆分的形式方法多种多样,但是本质是不要将 Hash(Key) 之后的请求分散在多个节点的多个小 bitmap 上,而是应该拆分成多个小 bitmap 之后,对一个 Key 的所有哈希函数都落在这一个小 bitmap 上。

参考资料

probabilistic data structures:bloom filter
bloom filters

<think>好的,用户想了解如何在Java中实现或使用布隆过滤器。首先,我需要回忆布隆过滤器的基本原理,确保理解正确。布隆过滤器是一种概率型数据结构,用于判断一个元素是否可能在集合中,或者一定不在集合中。它的优点是空间效率查询时间都比一般算法好很多,但缺点是有一定的误判率,并且不能删除元素。 接下来,用户提到Java实现,所以我需要整理几种常见的实现方式。根据提供的引用,用户可能希望得到多种方法,包括自己手动实现、使用Guava库、以及通过RedissonRedis实现。需要分别解释每种方法的步骤代码示例。 首先,手动实现的话,需要位数组多个哈希函数。位数组可以用BitSet来表示,哈希函数可以使用现有的算法,比如MD5、SHA1或者不同的种子生成多个哈希值。需要注意的是,哈希函数的数量会影响误判率,所以需要根据预期元素数量误判率来计算合适的哈希函数个数位数组大小。 然后是使用Guava库,这部分需要说明如何引入Guava依赖,如何使用BloomFilter类,设置预期插入数量误判率,以及如何添加判断元素。Guava已经优化了哈希函数位数组的管理,使用起来更方便。 接下来是Redisson的实现,这适用于分布式系统,利用Redis的位图操作。需要引入Redisson客户端,配置布隆过滤器的参数,如预期插入数量误判率,然后通过RBloomFilter对象进行操作。这种方式适合需要跨服务共享布隆过滤器的情况。 用户可能还需要知道布隆过滤器使用场景,比如缓存穿透、爬虫URL去重等。同时,注意事项比如不支持删除、误判率的影响,以及如何根据需求调整参数也需要提及。 检查提供的引用,引用12提到了Java实现、原理、优化方法使用场景,引用3提到了Redisson分片布隆过滤器。需要确保覆盖这些方面,并且正确引用来源。例如,分片布隆过滤器在引用3中提到,可以在优化方法部分提到。 另外,用户的问题可能隐含了性能优化实际应用的问题,所以在回答中应该包含这些内容,比如误判率的控制、哈希函数的选择、分布式环境下的实现等。 最后,确保回答结构清晰,分步骤说明不同的实现方法,每种方法都给出代码示例,并解释关键点。同时,相关问题部分需要生成与布隆过滤器相关的问题,引导用户进一步学习,比如原理、误判率因素、分布式应用等。</think>### Java布隆过滤器实现与用法详解 布隆过滤器是一种高效的概率数据结构,适用于快速判断元素是否存在的场景。以下是Java中三种典型实现方式: --- #### 一、手动实现基础版 ```java import java.util.BitSet; public class SimpleBloomFilter { private BitSet bitSet; private int size; private int[] hashSeeds; public SimpleBloomFilter(int size, int hashFunctions) { this.bitSet = new BitSet(size); this.size = size; this.hashSeeds = new int[hashFunctions]; for (int i = 0; i < hashFunctions; i++) { hashSeeds[i] = i * 31; // 简单哈希种子生成 } } private int hash(String value, int seed) { int hash = seed; for (char c : value.toCharArray()) { hash = hash * 31 + c; } return Math.abs(hash % size); } public void add(String value) { for (int seed : hashSeeds) { bitSet.set(hash(value, seed)); } } public boolean contains(String value) { for (int seed : hashSeeds) { if (!bitSet.get(hash(value, seed))) return false; } return true; } } ``` **关键参数计算**: - 位数组大小$m$与预期元素数量$n$的关系:$m = -\frac{n \ln p}{(\ln 2)^2}$,其中$p$为误判率 - 哈希函数数量$k$的最优值:$k = \frac{m}{n} \ln 2$[^1] --- #### 二、Guava工具实现(推荐) ```java import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class GuavaBloomFilter { public static void main(String[] args) { // 创建过滤器(预期插入量100万,误判率0.1%) BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.unencodedCharsFunnel(), 1_000_000, 0.001 ); // 添加元素 bloomFilter.put("user123"); bloomFilter.put("item456"); // 判断存在性 System.out.println(bloomFilter.mightContain("user123")); // true System.out.println(bloomFilter.mightContain("unknown")); // false } } ``` **优势**: 1. 自动计算最优参数 2. 使用混合哈希算法降低碰撞概率[^2] 3. 支持数据序列化 --- #### 三、Redisson分布式实现 ```java import org.redisson.Redisson; import org.redisson.api.RBloomFilter; import org.redisson.config.Config; public class RedisBloomFilter { public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RBloomFilter<String> bloomFilter = Redisson.create(config) .getBloomFilter("userFilter"); // 初始化(预期元素量100万,误判率0.3%) bloomFilter.tryInit(1_000_000L, 0.003); bloomFilter.add("data1"); System.out.println(bloomFilter.contains("data1")); // true } } ``` **分布式特性**: - 支持多节点共享过滤器状态 - 可通过分片策略提升性能(将大型过滤器拆分为多个子过滤器)[^3] - 数据持久化到Redis --- ### 使用场景建议 | 场景 | 适用性 | 示例 | |-------|--------|------| | 缓存穿透防护 | ★★★★★ | 先查询过滤器再访问数据库 | | URL去重 | ★★★★☆ | 网络爬虫记录已访问链接 | | 垃圾邮件过滤 | ★★★☆☆ | 快速判断疑似垃圾邮件地址 | --- ### 注意事项 1. **不支持删除操作**:传统布隆过滤器修改位数组会影响其他元素判断 2. **误判率控制**:实际元素超出预设量时误判率会上升 3. **哈希函数选择**:应使用独立且均匀分布的哈希算法 4. **容量规划**:初始化后修改参数需要重建过滤器 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值