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

布隆过滤器是一种概率型数据结构,用于高效地判断元素是否存在。它比传统数据结构如List、Set、Map更节省空间,但结果是概率性的。本文深入探讨布隆过滤器的实现原理、优缺点及最佳实践。

原文: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>嗯,用户的问题是关于如何用布隆过滤器解决缓存穿透问题,而且已经提供了几个相关引用资料。用户可能正在准备面试或者设计高并发系统,需要具体落地方案。 注意到用户引用的资料里提到布隆过滤器的基本原理Redis结合使用场景,特别是引用[3]给出了Guava库的具体代码示例。用户的核心诉求应该是“如何实操”,而不仅仅是理论。 用户的历史提问显示ta对缓存穿透有基本理解,但更关注布隆过滤器的实现细节。需要重点解释这几个关键点: 1. 布隆过滤器如何拦截不存在的数据请求(二进制向量的特性) 2. 具体实现时的初始化步骤(像引用[3]那样配置预期元素数量误判率) 3. 如何与现有缓存流程结合(必须强调“先查布隆过滤器再查缓存”的顺序) 用户提供的引用[2]提到布隆过滤器能提高应用稳定性,这点可以融入总结部分。但要注意避免过度承诺——布隆过滤器有误判率,需要明确说明解决方案(比如允许少量误判时直接穿透到DB,或者用白名单补偿)。 代码示例部分会很有帮助,考虑用Guava的BloomFilter类演示,因为引用[3]已经给出框架。需要补充说明的是: - 初始化后要预热数据(如addToBloomFilter("user123")) - mightContain()方法在业务逻辑中的调用位置 - 误判率0.01的实际影响(10万次查询可能有1000次误放行) 最后要提醒用户,布隆过滤器需要同步更新:当数据库新增数据时,必须调用addToBloomFilter(),否则会出现新数据被拦截的问题。这点在引用资料里没强调,但对生产环境很重要。 相关问题的设计可以延续布隆过滤器的技术深度(比如误判率优化)扩展场景(如垃圾邮件过滤),帮助用户建立系统知识。</think>使用布隆过滤器(Bloom Filter)解决缓存穿透问题,需将其作为前置过滤器拦截无效查询请求。以下是具体实现步骤原理详解: --- ### 一、布隆过滤器核心原理 1. **数据结构** 由长度为 $m$ 的**二进制位数组** $k$ 个**独立哈希函数**组成。 2. **写入操作** 当插入元素 $x$ 时: - 用 $k$ 个哈希函数计算哈希值:$h_1(x), h_2(x), ..., h_k(x)$ - 将位数组中对应位置设为 1: $$ \text{set bit}[h_i(x) \mod m] = 1, \quad i=1,2,...,k $$ 3. **查询操作** 判断元素 $y$ 是否存在: - 若所有 $k$ 个位置均为 1 → **可能存在**(存在误判) - 若任一位置为 0 → **一定不存在**(无漏判)[^4][^5] --- ### 二、解决缓存穿透的流程 ```mermaid graph TD A[用户请求] --> B{布隆过滤器检查} B -- 返回 false --> C[直接返回空结果] B -- 返回 true --> D{查询缓存} D -- 缓存命中 --> E[返回缓存数据] D -- 缓存未命中 --> F[查询数据库] F -- 数据存在 --> G[写入缓存并返回] F -- 数据不存在 --> H[向布隆过滤器添加标记] ``` 1. **请求拦截阶段** - 请求到达时,先用布隆过滤器检查查询键(如商品ID) - 若返回 `false`(键一定不存在),直接返回空结果或错误响应,**避免访问数据库**[^1] 2. **缓存查询阶段** - 布隆过滤器返回 `true` 时,继续查询缓存 - 缓存命中则返回数据 3. **数据库兜底阶段** - 缓存未命中时查询数据库 - 若数据存在:写入缓存 - 若数据不存在:**将键加入布隆过滤器**(防止后续重复穿透)[^3] --- ### 三、代码实现示例(Java + Guava) ```java import com.google.common.hash.BloomFilter; import com.google.common.hash.Funnels; public class CacheService { // 初始化布隆过滤器(预期元素10万,误判率1%) private static BloomFilter<String> bloomFilter = BloomFilter.create( Funnels.stringFunnel(StandardCharsets.UTF_8), 100000, 0.01 ); public Object getData(String key) { // 1. 布隆过滤器拦截 if (!bloomFilter.mightContain(key)) { return null; // 直接返回空 } // 2. 查询缓存 Object data = cache.get(key); if (data != null) { return data; } // 3. 查询数据库 data = database.query(key); if (data == null) { // 3.1 数据不存在:将key加入布隆过滤器 bloomFilter.put(key); // 后续相同请求将被拦截 } else { // 3.2 数据存在:写入缓存 cache.set(key, data); } return data; } } ``` > **关键点**:数据库查询返回空时,需调用 `bloomFilter.put(key)` 将键加入过滤器[^3]。 --- ### 四、优势与注意事项 | **优势** | **注意事项** | |-----------------------------------|--------------------------------------| | ✅ 内存占用极低(1亿元素约114MB) | ⚠️ 误判率存在(可配置但不可消除) | | ✅ 查询时间复杂度 $O(k)$(常数级) | ⚠️ **不支持删除操作**(需用计数布隆过滤器) | | ✅ 完全阻止无效键访问数据库 | ⚠️ 需预热:启动时加载所有合法键到过滤器 | > **误判率优化公式**: > 给定元素数量 $n$ 位数组长度 $m$,最优哈希函数数量 $k$ 为: > $$ k = \frac{m}{n} \ln 2 \approx 0.7 \frac{m}{n} $$ > 误判率 $p$ 近似为: > $$ p \approx \left(1 - e^{-kn/m}\right)^k $$ > 通过调整 $m$ $k$ 可控制误判率[^4][^5]。 --- ### 五、典型应用场景 1. **缓存穿透防护**:拦截非法ID请求[^1][^2] 2. **爬虫URL去重**:避免重复抓取 3. **邮箱垃圾邮件过滤**:识别已知垃圾邮件地址 4. **分布式系统路由**:快速判断节点是否存在 --- 通过布隆过滤器前置拦截,可确保无效请求**永不访问数据库**,从根源解决缓存穿透问题。需注意定期维护布隆过滤器(如数据库新增数据时同步更新)并监控误判率[^3][^5]。 --- **相关问题**: 1. 布隆过滤器的误判率如何影响系统性能?如何优化误判率? 2. 除了 Guava,Redis 中如何实现布隆过滤器? 3. 计数布隆过滤器(Counting Bloom Filter)是如何支持删除操作的? 4. 布隆过滤器在分布式系统中如何保证数据一致性?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值