布隆过滤器必须与数据写入强同步且不可删除,推荐使用github.com/yourbasic/bloom库,按预估总量和0.01误判率建模,配合空值缓存与singleflight防击穿。

布隆过滤器必须和数据写入强同步,否则等于没加
布隆过滤器本身不支持删除,也没法动态感知 DB 新增数据。如果只在服务启动时加载一次全量 ID,后续新增用户、商品等数据就进不了过滤器——请求一来,filter.Test() 返回 false,直接放行打 DB,防护形同虚设。
实操建议:
- 所有写 DB 成功的路径(如
UserService.Create()),必须紧跟着调用filter.Add([]byte("user:" + strconv.Itoa(user.ID))) - 确保写 DB 和 Add 到布隆过滤器在同一个事务或至少是原子性操作:DB 写失败则不 Add;Add 失败要告警,不能静默忽略
- key 格式必须完全一致:写时用
"user:123",查时也必须用"user:123",别一个用 int 转 string,另一个直接拼接 raw int - 不要在 HTTP handler 里临时 new 过滤器,必须作为全局变量或依赖注入的单例存在
别自己手写,优先用 github.com/yourbasic/bloom
手写布隆过滤器容易在哈希函数分布、位图索引计算、字节对齐上出错,导致误判率远高于理论值,甚至出现 panic。Go 官方 golang.org/x/exp/bloom 虽轻量但接口受限(只支持 []byte 输入、不暴露哈希策略),且仍在 x/exp 下,不建议用于生产。
推荐选择 github.com/yourbasic/bloom,它:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 提供
bloom.New(uint64(n), 0.01)直接按预估总量和误判率建模,内部自动算位数组大小和哈希轮数 - 支持
Add()/Test()语义清晰,输入统一为[]byte,避免类型混淆 - 底层用
[]uint64位图,比 bool 切片省内存、位操作更快 - 有 benchmark 对比,实测 1000 万 key、1% 误判下内存约 16MB,吞吐 >5M ops/sec
误判率不是越低越好,0.01 是多数业务的甜点值
把误判率从 0.01 降到 0.001,内存翻倍、初始化变慢,但实际拦截效果提升有限——穿透流量本来就是“大量非法 key + 少量真实但暂无数据的 key”,重点是筛掉前者。
选值依据:
- 预计总量
n = 5_000_000(500 万用户),用bloom.New(5_000_000, 0.01)即可 - 如果业务有明确攻击特征(比如固定前缀的恶意 user_id 构造),可配合前缀白名单 + 布隆过滤器,进一步压低有效 key 总量
- 上线后监控
filter.Test() == false的请求占比,若长期低于 95%,说明过滤器太松;若接近 100% 但 DB 查询量仍高,可能是 key 格式不一致或写入不同步
空值缓存必须和布隆过滤器配合,不能只靠它兜底
布隆过滤器说“可能存在”,你得继续查 Redis;查不到再查 DB;DB 也查不到,就得缓存这个“空”状态。但很多人在这里栽跟头:
-
redis.Set(ctx, key, nil, 2*time.Minute)在 go-redis 中实际存的是字符串"<nil>"</nil>,下次redis.Get().Result()返回非空字符串,逻辑全乱 - 正确做法是定义统一空响应结构体,
json.Marshal(UserResp{NotFound: true})再存,读时json.Unmarshal判断NotFound - 空值 TTL 必须明显短于正常缓存(比如正常缓存 30 分钟,空值只设 2 分钟),否则新用户注册后,旧的空值还在,导致“注册成功却查不到”
- 布隆过滤器放行后,空值缓存未命中时,要用
singleflight.Group拦住并发查询,防止缓存击穿
布隆过滤器不是银弹——它解决的是“根本不存在”,而空值缓存解决的是“当前不存在但未来可能有”。两者漏掉任何一个,都可能让穿透请求漏网。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










