布隆过滤器原理与应用
一句话总结
布隆过滤器是一个”节省到变态”的判空神器——它用位数组 + 多个哈希函数来回答一个问题:“这个东西肯定不存在?还是可能存在?“说不在就是不在,说在可能不在(误判)。用极小的内存挡住绝大多数无效查询,专门对付”缓存穿透”。
🌰 先搞懂它解决什么问题
场景:缓存穿透
你的系统查一个商品详情:
请求来 → 查 Redis → 有就返回 ✅
→ 没有就查 MySQL → 有就写回 Redis
→ 没有就返回 null
正常情况:
100 个请求查商品 1001 → Redis 没有 → 查 MySQL → 没有 → 返回 null
↓
也就一次 MySQL 查询,能接受
恶意攻击(缓存穿透):
攻击者写了个脚本,每秒发 10000 个请求
每个请求的商品 ID 都是随机的(10001, 10002, 10003...)
Redis:没有
MySQL 扛不住了 → 每秒 10000 次查询 → 数据库被打死 ❌
问题在哪?
查一个不存在的商品,每次都要穿透到数据库。
明明不存在,但你要去数据库证实"不存在"。
布隆过滤器要解决的就是:用一个超省内存的方式,提前告诉你”别查了,这个肯定不存在”。
另一个类比:机场安检
正常流程:
每个旅客都要过安检(查数据库)
10000 个旅客,查 10000 次
布隆过滤器 = 在安检入口放一个"黑名单"小黑板:
张三 → 航空黑名单 → 标记过
李四 → 不在上面 → 可以进安检
如果名字在黑名单上 → 绝对不放行
如果名字不在上面 → 放行(但可能看走眼,极小概率)
关键在于:这个黑名单只需要很小的黑板就记下了几万人。
布隆过滤器的工作原理
核心结构:一个位数组 + 三个哈希函数
假设我们有一个 16 位的数组,全部初始化为 0:
位数组: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
↑
第 0 位到第 15 位
有三个哈希函数(hash1, hash2, hash3):
分别把一个输入映射到 0~15 之间的位置。
第一步:添加元素
添加 "apple" 到布隆过滤器:
hash1("apple") = 3
hash2("apple") = 7
hash3("apple") = 11
把位数组的第 3、7、11 位设为 1:
位置: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
0 0 0 **1** 0 0 0 **1** 0 0 0 **1** 0 0 0 0
再添加 "banana":
hash1("banana") = 7
hash2("banana") = 12
hash3("banana") = 2
位置 7 已经是 1(apple 设的),不动它
位置 12 设为 1
位置 2 设为 1
结果:
位置: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
0 0 **1** **1** 0 0 0 **1** 0 0 0 **1** **1** 0 0 0
第二步:查询元素
查询 "apple" 是否存在:
hash1("apple") = 3 → 位数组[3] = 1 ✅
hash2("apple") = 7 → 位数组[7] = 1 ✅
hash3("apple") = 11 → 位数组[11] = 1 ✅
所有位置都是 1 → "apple 可能存在" ✅
查询 "grape"(没添加过):
hash1("grape") = 5 → 位数组[5] = 0 ❌
有一个位置是 0 → "grape 肯定不存在" ✅
再查询 "watermelon"(没添加过):
hash1("watermelon") = 3 → 位数组[3] = 1
hash2("watermelon") = 11 → 位数组[11] = 1
hash3("watermelon") = 2 → 位数组[2] = 1
三个位置都是 1 → "watermelon 可能存在"
但 watermelon 确实没添加过!→ 误判 ❌
为什么误判?
位置 3、11、2 被 apple、banana 和其他元素"凑巧"全部设成了 1。
虽然 watermelon 没来过,但它的哈希位置被其他元素占满了。
三个关键结论(直接背)
结论 1:说"不在"就是 100% 不在
只要有一个哈希位置是 0 → 元素绝对没出现过
因为如果它来过,那几个位置一定都是 1
结论 2:说"在"可能是误判(假阳性)
可能这 3 个位置是被其他元素"碰巧"全部设成 1 的
结论 3:布隆过滤器永远不说"不在"但实际在(无假阴性)
如果它说不在,那就是真的不在
这刚好是缓存穿透场景需要的特性!
三个核心参数
布隆过滤器的表现取决于三个参数:
1. n = 预期要存的元素数量(比如 100 万条数据)
2. p = 你能接受的误判率(比如 1%)
3. m = 位数组的长度(越多越省误判)
4. k = 哈希函数的个数
公式:
位数组长度 m = - n * ln(p) / (ln2)^2
哈希函数数量 k = m/n * ln2
实际意义:
100 万个元素,1% 误判率
→ 位数组需要约 119 万字节 ≈ 1.19 MB
→ 需要的哈希函数 ≈ 7 个
用 1.2MB 的内存,就能挡住 100 万条不存在的查询穿透到数据库。
Redis 布隆过滤器怎么用
Redis 4.0+ 提供了布隆过滤器插件(redisbloom)。
命令只有三个:
1. BF.ADD filter_name key
添加一个元素到布隆过滤器
2. BF.EXISTS filter_name key
判断 key 是否存在(返回 0 或 1)
3. BF.RESERVE filter_name error_rate capacity
提前初始化,指定误判率和容量
不设的话默认 1% 误判率
例子:
BF.RESERVE product_cache 0.01 1000000
→ 创建一个布隆过滤器,存 100 万商品,1% 误判率
BF.ADD product_cache 1001
BF.ADD product_cache 1002
BF.EXISTS product_cache 1001 → 1(可能存在)
BF.EXISTS product_cache 9999 → 0(肯定不存在)
布隆过滤器防缓存穿透完整流程
graph TD REQ["请求商品 10086"] --> BF["布隆过滤器<br/>(1.2MB)"] BF -->|"BF.EXISTS"| CHECK{"返回 0 还是 1"} CHECK -->|"0:肯定不存在"| NULL1["直接返回 null"] CHECK -->|"1:可能存在"| REDIS["查 Redis"] REDIS -->|"有 ✅"| RET1["返回"] REDIS -->|"没有"| MYSQL["查 MySQL"] MYSQL -->|"有"| WR["写回 Redis"] MYSQL -->|"没有"| NULL2["直接返回 null"]
改造后的流程:
用户请求商品 10086
① 查布隆过滤器(BF.EXISTS)
→ 0(肯定不在)→ 直接返回"商品不存在"
↓
→ 1(可能存在)→ 继续
② 查 Redis
→ 有 → 返回
→ 没有 → 继续
③ 查 MySQL
→ 有 → 写回 Redis → 返回
→ 没有 → 返回 null
好处:
99.99% 的不存在请求在第一步就被拦住了
几十万条恶意请求 → 布隆过滤器几十微秒搞定
数据库完全不受影响 ✅
布隆过滤器在别的地方也用
不止 Redis,布隆过滤器在很多地方都有应用:
① Chrome 浏览器
用户访问一个 URL → 查布隆过滤器
判断这个 URL 是不是恶意网站
几十亿恶意 URL 的数据库 → 只要几 MB 内存
② 数据库(Cassandra / HBase)
查询一行数据之前,先查布隆过滤器
"这一行存在吗?"
如果说不存在 → 不用去磁盘找了
减少 90% 以上的无效磁盘读取
③ 推荐系统
给用户推荐内容时
先查布隆过滤器:"这个内容用户看过了吗?"
看过了就不再推荐
④ 爬虫去重
爬过的 URL 存布隆过滤器
来一个新的 URL → 先问布隆过滤器爬过没
说爬过(可能误判)→ 跳过
说没爬过 → 肯定没爬过 → 爬它
布隆过滤器的局限
① 不能删除元素
假设我们把 "apple" 的三个 1 改回 0
但位置 7 也是 "banana" 的哈希位
把位置 7 清 0 → "banana" 也被删了 → 下次查 banana 说不在 ❌
解法:用布谷鸟过滤器(Cuckoo Filter)支持删除
② 误判率会随着数据增多而变高
存 100 万时误判率 1%
你硬塞了 1000 万 → 位数组大量位置被设为 1
误判率飙升到 20% 以上
解法:提前估算好容量,超了就重建
③ 不能知道元素存了多少
你知道位数组里很多位置是 1
但不知道这 1 是哪个元素留下的
布隆过滤器没有"遍历"功能
一句话讲清
延伸提问:"布隆过滤器是什么?"
你:
"布隆过滤器是一个概率型数据结构。
用一个位数组 + 多个哈希函数来判空。
核心特性:
说不在 → 100% 不在(无假阴性)
说在 → 可能在,也可能误判(有假阳性)
误判率可控制,一般在 1% 以内。
原理就是:
添加时用 k 个哈希函数算出 k 个位置,全部标 1。
查询时算 k 个位置,只要有一个是 0 → 肯定不在。
全 1 → 可能在(可能被其他元素碰巧填满了)。
最常用在缓存穿透场景:
请求来的时候先查布隆过滤器。
它说不在 → 直接返回,数据库都不用碰。
延伸提问:"误判率怎么控制?"
你:
"通过调整三个参数:预期元素数量 n、位数组长度 m、哈希函数数量 k。
比如 100 万数据、1% 误判率,大概需要 1.2MB 位数组和 7 个哈希函数。
要记住的是:布隆过滤器不能删除元素。
因为多个元素共用的哈希位,你删一个会把别的也删了。
要支持删除用布谷鸟过滤器。"
记忆口诀
位数组 + k 个哈希函数,存在标 1,查询看位。
说不在 → 绝对不在,说在 → 可能在(无假阴性,有假阳性)。
极省内存——100 万数据只要 1.2MB,挡住 99.99% 无效查询。
不能删——哈希位是共用的,删一个会误伤另一个。
最常用防缓存穿透:先问布隆再查 DB,不存在直接返回。
速记卡(面试闪卡)
Q1:一句话讲清「布隆过滤器原理与应用」到底是什么?
A:布隆过滤器是用位数组和多个哈希函数判空的概率型数据结构,专治缓存穿透。
Q2:一句话总结 —— 怎么理解?
A:像机场安检口的黑名单小黑板——名字不在上面一定不放行(无假阴性),在上面的才过检(可能极小误判)。Bloom Filter(布隆过滤器)用极小内存提前告诉你”这肯定不存在”,专治缓存穿透。
Q3:🌰 先搞懂它解决什么问题 —— 怎么理解?
A:像恶意脚本每秒刷 1 万个随机商品 ID,全穿透到 MySQL 把库打死;布隆过滤器在门口先挡掉”肯定不存在”的请求。Cache Penetration(缓存穿透)就是它被克制的头号场景。
Q4:布隆过滤器的工作原理 —— 怎么理解?
A:像给一块 16 格小黑板,存东西时让 3 个哈希函数各选 1 格打勾;查的时候只要有一格没勾,就 100% 没来过。Bit Array(位数组)+ Hash Functions(哈希函数)配合:全勾=可能存在,有空格=必不存在。
Q5:三个关键结论(直接背) —— 怎么理解?
A:像三条铁律:①说不在=绝对不在(无假阴性),正是缓存穿透最需要的;②说在可能误判(假阳性);③永远不漏报,所以敢放心挡库。False Positive(假阳性)/ False Negative(假阴性)是面试必问对比点。
Q6:核心速记主线有哪些?
-
结构:位数组 + k 个哈希函数,存时全标 1,查时看位
-
特性:无假阴性(说不在必不在)、有假阳性(说在可能误判)
-
应用:防缓存穿透,1.2MB 挡百万查询,99.99% 直接返回
-
局限:不能删(哈希位共用)、误判率随数据增多而升高
口诀
A:位数组加多哈希,存时打勾查看位;
说不在就必不在,说在可能遇误判;
极省内存挡穿透,百万查询一点头;
不能删因位共用,缓存穿透第一关。