Theory · Data Structure · Bloom Filter

布隆过滤器

用误判率换内存:bit 数组 + k 个哈希 · "可能存在,绝不存在" · 缓存穿透的第一道闸门

结构

m 位数组 + k 个独立哈希函数——插入置 1,查询验 1

语义

返回"可能存在"或"一定不存在"——单向、可误判、不可删除

权衡

1% 误判率只需每元素约 10 bit——比哈希表省 10~20 倍内存

定位:布隆过滤器是"概率数据结构"家族的代言人——空间换精度。一句话语义必须说准:说不存在就一定不存在,说存在只是大概率。

Bit Array + k Hashes

原理:k 个哈希在位数组上盖章

布隆过滤器的插入与查询流程 插入 foo 时用三个哈希函数算出下标 2、7、11 并把位数组这三位置一;查询 bar 时算出下标 2、7、9,其中下标 9 为零,因此 bar 一定不存在。 h₁(x) h₂(x) h₃(x) k=3 个独立哈希 111 012345678910111213 插入 foo:h → {2, 7, 11} 位置一(蓝色) 查询 bar h → {2, 7, 9} 位置 9 = 0 → bar 一定不存在 只要有一个 0 就判负;全 1 才说"可能存在" 全 1 ≠ 一定存在 可能是别的元素盖的章 → 误判
流程图三段:插入盖章(三个哈希位)、查询判负(任一位为 0 → 一定不存在)、判正的保留意见(全 1 可能是别人盖的章)。"一定不存在"是布隆过滤器工程价值的全部来源——负判断零误差。

False Positive · m, k, n Trade-offs

误判率:一个公式管全部参数

误判率公式

p ≈ (1 − e^(−kn/m))^k

m 位数组、k 个哈希、已插 n 个元素。最优哈希个数 k = (m/n)·ln2 ≈ 0.69·(m/n),此时 p ≈ 0.6185^(m/n)

速查表(最优 k)

每元素 8 bit → p≈2.2%;10 bit → 0.8%;16 bit → 0.05%。每元素 10 bit、k=7 是工程默认档位。

参数怎么选

先定可接受的误判率 → 算出每元素 bit 数 → 算出 m 和 k。反过来:内存预算固定时,能容忍的误判率被锁死——空间、精度、容量三角

两个工程陷阱

容量溢出:n 超过设计值,误判率快速恶化——要预估容量或用可扩展布隆(Scalable BF);② 哈希质量:k 个哈希要近似独立(工程常用 double hashing:h₁+h₂·i 模拟 k 个)。

每元素 bit(m/n)最优 k误判率 p
64≈ 5.6%
86≈ 2.2%
107≈ 0.82%
128≈ 0.32%
1611≈ 0.045%
对比账本:存 1 亿个 URL:哈希表(64B 键值+指针开销)≈ 数 GB;布隆过滤器 10 bit/条 ≈ 125 MB——省一个数量级,代价是 0.8% 误判 + 不可删除。cache 场景这点误判无伤大雅。
公式不用背,但"k≈0.69·m/n"和"10 bit 档 ≈ 0.8%"两个数要张口就来。两个陷阱(容量溢出、double hashing)是工程师视角的差异化答案。

Cache Penetration · Standard Fix

主战场:挡住缓存穿透

穿透 vs 击穿 vs 雪崩

穿透:查不存在的 key,缓存与 DB 都没有 → 每次都打到 DB(恶意可放大);击穿:热点 key 过期瞬间;雪崩:大批 key 同时失效。布隆过滤器专治穿透。

标准组合拳

布隆过滤器挡在缓存前:一定不存在的直接拒绝,DB 零压力;② 不存在的 key 缓存空值(短 TTL);③ 限流/参数校验兜底。三层递进,布隆是最省资源的第一道闸。

为什么布隆最合适

穿透流量查的是"海量随机不存在的 key"——布隆的负判断零误差正好匹配:说没有就是没有,放心拒绝;说可能存在才放行到缓存/DB(少量误判会多查一次空,可接受)。

数据一致性

DB 新增 key 后要同步加入布隆过滤器;布隆不支持删除 → 删除的 key 会继续"可能存在"(放行后查 DB 为空,由空值缓存兜底)。详见 缓存模式 deck

场景布隆过滤器的用法
缓存穿透key 是否可能存在,不存在直接拒
爬虫去重 / URL 去重亿级 URL 判"见过没有"
垃圾邮件 / 黑名单可能命中才细查规则库
推荐已读过滤已推荐内容过滤(误判=少推一次,无害)
LevelDB/LSM每个 SSTable 一个 BF,避免无效读盘
HBase / Cassandra同上:读之前先问 BF
面试金句:"误判方向的选取是布隆设计的灵魂——把误判设计成'多做一次便宜操作'(多查一次缓存),而不是'丢数据',它就完美适配。"
穿透三兄弟对比开场(很多候选人背混),然后给"布隆+空值+限流"三层组合拳。右下金句是高级答法:布隆的适用性取决于误判方向的代价——这层想通了,所有场景题都能自洽。

Limitations · Variants

三大局限与对症变体

局限① 不支持删除

一个位被多个元素共享,清零会误伤他人。对症:Counting Bloom Filter——每位换 4 bit 计数器,删除减一;空间 ×4。

局限② 误判率随 n 漂移

n 超设计容量后 p 恶化且不可逆。对症:Scalable BF——满了自动加一层新 BF(查询逐层问)。

局限③ 无法取回元素、无计数

只回答"可能在",不能枚举、不能计数。对症:Cuckoo Filter(布谷鸟)——支持删除、空间更优(同等误判率省 ~25%),且能查"大约多少个"。RedisBloom 模块两者都有。

家族速览

Counting BF(可删)、Scalable BF(自动扩容)、Cuckoo Filter(可删+更强)、Bloomier(存值)、HyperLogLog(基数统计——另一支概率家族)。

结构删除空间场景
Bloom Filter10 bit/元素默认选择
Counting BF~40 bit/元素频繁删除
Scalable BF分层增长容量不可预估
Cuckoo Filter更优 ~25%删除+查询更强
HyperLogLog12KB 计数亿级去重计数(UV)
面试答法:"先承认三局限(删除/容量/不可取回),再按场景给变体——这一套下来,考官就知道你不只会背'误判率公式'。"
局限与变体一一对应:不支持删除→Counting;容量漂移→Scalable;不能取回→Cuckoo。HyperLogLog 点一下"同属概率家族但解决基数问题",为 Redis 话题留钩子。

Go Implementation · LeetCode

手写实现与必刷题

type Bloom struct {
    bits []uint64
    m    uint32 // 位数
    k    uint32 // 哈希个数
}
// double hashing:每元素只算两个独立哈希
func (b *Bloom) Add(x string) {
    h1, h2 := fnv32(x), fnv32(x+"salt")
    for i := uint32(0); i < b.k; i++ {
        b.set((h1 + i*h2) % b.m) // h1+i·h₂
    }
}
func (b *Bloom) MayContain(x string) bool {
    h1, h2 := fnv32(x), fnv32(x+"salt")
    for i := uint32(0); i < b.k; i++ {
        if !b.get((h1 + i*h2) % b.m) {
            return false // 一定不存在
        }
    }
    return true // 可能存在
}

工程要点

① k 个哈希用 double hashing 模拟(h₁+i·h₂),省掉 k 次完整哈希——理论依据:两个独立哈希可生成近似独立的 k 个;② 位操作 set/get 用 uint64 分桶;③ 容量与误判率初始化时锁定。

现成轮子

Redis RedisBloom 模块(BF.ADD / BF.EXISTS,Cuckoo 也有);Go 库 bits-and-blooms/bloom;LevelDB/RocksDB 内建(每个 SSTable 一个)。

LeetCode 题单

705 设计哈希集合 E(对照:真哈希 vs 布隆的语义差);706 设计哈希映射 E;面试官口头题:设计"网页爬虫 URL 去重"系统——10 亿 URL、内存 1GB → 布隆是唯一解(10 bit/URL ≈ 1.25GB?调整到 8bit+可容忍误判 ≈ 1GB)。

系统设计话术

给容量 → 算 bit 数 → 给误判率 → 谈删除需求选变体 → 数据一致性(新增同步写入)。一条链把本 deck 全部知识点串起来。

代码 30 行手写版(double hashing 是亮点);题单重点是口头系统设计题"URL 去重"——布隆 deck 的知识在那一题里全部用上。

Interview QA

高频追问合集

1 · 布隆过滤器为什么会有误判?方向是什么?

位共享只假阳

多个元素的哈希位共享,全 1 可能是别人盖的章 → 假阳性(判为存在但实际没有);假阴性不可能——存在过的元素位永远不会被清零。

2 · 误判率由什么决定?怎么压到 1%?

kn/m

p ≈ (1−e^(−kn/m))^k,只与"每元素平均 bit 数 m/n"和 k 有关。10 bit/元素 + k=7 → ≈0.8%。

3 · k 越多越准吗?

有最优值

不是。k 大了位数组被填满更快、任一位为 0 的概率下降。最优 k* = (m/n)·ln2,此时 p 最低 ≈ 0.6185^(m/n)。

4 · 为什么不能删除?怎么办?

位共享

一个位可能被多个元素共用,清零会误伤。需要删除 → Counting BF(4bit 计数器)或 Cuckoo Filter(空间更优)。

5 · 布隆和哈希表怎么选?

语义差异

需要"确切内容 + 无误判" → 哈希表;只要"存在性判断 + 内存极限" → 布隆。布隆省 10~20 倍空间,代价是误判和不可取回。

6 · 数据库/LSM 里布隆干什么?

免读盘

LevelDB/RocksDB 每个 SSTable 配一个布隆:查 key 先问 BF,"一定不存在"就跳过该文件——把磁盘读次数从"逐文件试探"降到"只读命中的"。

7 · 元素被删后过滤器怎么保持一致?

定期重建

标准布隆会"残留"已删元素(继续判可能存在)。方案:定期全量重建;或 Counting/Cuckoo 支持真删除;缓存场景由空值缓存兜底误判。

8 · 布隆、HyperLogLog、Count-Min Sketch 什么关系?

概率家族

同属概率数据结构:布隆管成员查询、HLL 管基数计数(UV 去重,12KB 计数亿级)、CMS 管频次估计——同一哲学:允许小误差换数量级的空间。

八题覆盖:误判方向、参数、k 最优、删除、与哈希对比、LSM 应用、一致性、概率家族全景。第 8 题把三个概率结构串成一张网,是收束式加分答案。

Related & References

相关知识点与参考

数据结构系列(本分类)

哈希表 →(精确成员查询的对照物)
堆与优先队列 →(TopK 场景的另一个"省内存"答案)

Redis / 算法系列

缓存模式 →(穿透三兄弟与三层防御)
Redis 数据结构 →(RedisBloom 模块载体)
复杂度分析 →(概率结构的"保证"语义辨析)

参考来源(本 deck 结论可溯源至下列一手材料)

B. Bloom (1970) · Space/Time Trade-offs in Hash Coding原始论文:误判率公式与 m/k/n 权衡
LevelDB util/bloom.cc · RocksDB bloom工业实现:double hashing 与 SSTable 集成
Fan et al. (2014) · Cuckoo Filter布谷鸟过滤器:可删除、空间更优
Redis RedisBloom 文档 · BF.ADD/BF.EXISTSRedis 概率模块的命令语义
oi-wiki.org/ds/bloom-filter误判率推导的中文对照
收尾:与哈希表(对照物)、缓存模式(主战场)、Redis(载体)三向链接。Bloom 1970 原始论文 + LevelDB 实现是两个核心出处。总页数 8。