应直接使用成熟布隆过滤器库而非手写:日常选 github.com/yourbasic/bloom,高吞吐选 github.com/andreasbriese/bbloom;n 为预估最大插入数,p 为可接受误判率;并发 add() 必加锁,test() 可不加;test() 为 true 后必须二次查真实存储。

别手写,直接用成熟库。自己实现布隆过滤器在 Go 里看似几行位运算就能搞定,实则哈希一致性、位对齐、并发写入这三关一关都过不了,轻则误判率翻倍,重则内存越界或 goroutine 竞态崩溃。
用哪个库?github.com/willf/bloom 还是 github.com/yourbasic/bloom
github.com/willf/bloom 更老但压测充分,github.com/yourbasic/bloom 更轻量、API 更干净(比如自动把位数组长度拉齐到 2 的幂次,避免慢的 % 运算),日常去重选后者足够;高吞吐场景(如每秒百万级 Add)可考虑 github.com/AndreasBriese/bbloom,它支持哈希种子控制和字节池复用,GC 压力更低。
绝对不要用 map[string]struct{} 替代——1000 万个 URL,前者约 12MB 内存,后者轻松破 1.5GB。
bloom.New(n, p) 两个参数到底怎么填
n 是你「预估最大插入数」,不是当前数量;p 是你「能接受的误判率」,比如 0.001 表示千分之一。这两个值直接决定内存用量,不是拍脑袋来的:
- n 填小了:后期插入超限,误判率会指数级飙升,远超你设的 p
- n 填大了:内存浪费,但至少行为可控;比如爬虫每天新增 500 万 URL,计划撑 3 天,n 至少设为
20_000_000 - p 每降一档(比如从 0.01 → 0.001),位数组大小几乎翻倍;
p=1e-6会让 1 亿条目的内存从 ~12MB 涨到 200MB+
典型参考:
爬虫去重 1000 万 URL → bloom.New(10_000_000, 0.01)
风控拦截需更严 → bloom.New(10_000_000, 0.001)
局域网设备白名单几百个 → bloom.New(1000, 0.0001)
并发 Add() 必须加锁,但 Test() 可以不加
bloom.Filter 底层是 []byte,多个 goroutine 同时 Add() 同一个 key,可能同时对同一 byte 的不同 bit 执行 |= 1 —— 这个操作非原子,结果就是某次置位被覆盖,该 bit 永远为 0。
现象:Add() 过的 key,Test() 返回 false;或者压测时误判率忽高忽低,飘到 5%。
正确做法:
• 包一层 sync.RWMutex,Add() 用 Lock(),Test() 用 RUnlock()
• 别用 sync.Mutex 全局锁——Test() 是高频只读,没必要阻塞
• 如果真需要无锁写入,得换布谷鸟过滤器(cuckoo filter),但 Go 生态成熟度低得多,不推荐生产用
Test() 返回 true 后必须查真实存储
布隆过滤器只回答「可能存在」或「一定不存在」,它本身不存原始数据,也不提供精确判断能力。跳过二次校验等于默认接受误判。
典型链路:
• 缓存穿透防护:先 filter.Test(key) → false 就直接回源;true 则必须查 Redis 或 DB,确认存在才返回,否则仍要回源
• 风控拦截:Test() 为 true 就直接拒掉请求?错——新用户可能被当成老用户误拦,这是业务逻辑没兜住,不是布隆错了
• 别在 Test() 为 true 时顺手再 Add() 一次——它不负责去重,只负责快速过滤;重复 Add() 不影响结果,但纯属浪费 CPU
最常被忽略的一点:布隆过滤器一旦实际插入数明显超过初始化时设的 n,就别硬撑了——重建过滤器并批量重放历史 key,否则误判率会失控。这个阈值没有 magic number,得靠监控 Test() 返回 true 后真实查不到的比例来发现。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











