应选用 github.com/yourbasic/bloom 库,因其轻量、API 干净、自动对齐位数组长度为 2 的幂次;高吞吐场景才考虑 github.com/AndreasBriese/bbloom。

选哪个库?直接用 github.com/yourbasic/bloom,别手写
自己实现 NewBloomFilter 看似只要几行位运算,但哈希一致性、位数组边界对齐、并发写入这三关根本过不了。轻则误判率翻倍,重则 goroutine 竞态崩溃或内存越界。生产环境必须用成熟库:github.com/yourbasic/bloom 最轻量、API 干净、自动拉齐位数组长度为 2 的幂次(避开慢的 % 运算);高吞吐场景(如每秒百万级 Add)才考虑 github.com/AndreasBriese/bbloom,它支持哈希种子控制和字节池复用,GC 压力更低。
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+ - 典型参考:
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() 会竞态写同一位——比如两个协程都算出位置 1234,都执行 bits[1234/8] |= 1,但该操作非原子,结果就是某次置位被覆盖,该 bit 永远为 0。现象是:明明 Add 过的 key,Test() 返回 false;或压测时误判率忽高忽低,飘到 5%。
- 正确做法:包一层
sync.RWMutex,Add()用Lock(),Test()用RUnlock() - 别用
sync.Mutex全局锁——Test()是高频只读,没必要阻塞 -
Test()天然无锁:只读 bit,且 byte 读取本身是原子的 - 如果真需要无锁写入,得换布谷鸟过滤器(cuckoo filter),但 Go 生态成熟度低得多,不推荐生产用
Test() 返回 true 后必须查真实存储
布隆过滤器只回答「可能存在」或「一定不存在」,它本身不存原始数据,也不提供精确判断能力。跳过二次校验等于默认接受误判。
立即学习“go语言免费学习笔记(深入)”;
- 典型链路是:
Test()→false就放行;Test()→true则查 Redis 或 DB,存在才返回,不存在再回源并写入缓存 - 常见错误:在风控场景里,
Test()为true就直接拦截请求,结果把新用户当成老用户拒掉——这不是布隆错了,是你没兜住逻辑 - 千万别在
Test()为true时顺手再Add()一次——布隆不负责,也解决不了问题
fmt.Sprintf("%v", x) 依赖 Go 版本和字段顺序;字符串要注意大小写、空格、斜杠结尾等是否提前归一化。统一用 json.Marshal 或 gob.Encode 序列化成确定性字节切片,再喂给 Add/Test。



















