布隆过滤器必须在缓存查询前执行且返回false时立即终止流程,否则失去拦截意义;需与空值缓存协同,严格遵循Test→cache.Get→singleflight.Do→DB链路,并确保key格式统一、实例全局复用、预热完成。

布隆过滤器不能单独防穿透,必须搭配空值缓存 + 严格调用顺序,否则空缓存写不进去、恶意 key 全进 DB、误判率再低也白搭。
filter.Test() 必须在 cache.Get() 之前,且 false 时立即返回
很多人把 bloom.Test() 放在 cache.Get() 后面,以为“查不到再过滤”,这完全失去拦截意义。只要 Test() 返回 false,就代表这个 key 确定不存在,必须立刻中断流程。
- 错误写法:
cache.Get(key)→ miss →filter.Test(key)→ false → return;此时已浪费一次 Redis 查询 - 正确写法:
if !filter.Test([]byte(key)) { return nil, errors.New("not found") }→ 后续全跳过 - key 类型必须对齐:DB 存的是
"user:123",Test()和Add()都得传[]byte("user:123"),不能是"123"或int(123) - HTTP 参数带结构(如 JSON)时,先哈希再测:
sha256.Sum256([]byte(raw)).Sum(nil),避免格式差异导致漏判
空值缓存必须用显式占位类型,不能写 nil
cache.Set(key, nil, ttl) 在不同缓存库行为不一致:go-cache 可能序列化成字符串 "<nil>"</nil>,redis-go 可能存成 NULL,读出来后 val == nil 永远为 false,空缓存失效。
- 定义统一空值:
type Null struct{} - 写缓存:
cache.Set(key, Null{}, 3*time.Second) - 读缓存后判断:
if _, ok := val.(Null); ok { return nil, errors.New("not found") } - TTL 要短(1–5 秒),避免合法数据上线后被长期锁死
singleflight.Do 的 key 不能和缓存 key 直接复用
如果直接用原始 key(比如 "user:-1")传给 sg.Do(key, loadFn),所有恶意 key 都会触发 goroutine 合并等待——100 个并发请求打同一个坏 key,结果 100 个 goroutine 一起卡住等 DB 返回 sql.ErrNoRows,反而放大攻击。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法:用哈希或固定前缀隔离,例如
sg.Do("db:user:-1", loadFn),或更稳妥地,只对filter.Test() == true的 key 才进Do -
loadFn内部必须包含完整副作用:查 DB → 判空 → 写空值缓存 → 返回结果;缺一不可 - DB 查询务必带
context.WithTimeout,避免单个慢查询拖垮全部等待者 - 写空值缓存失败不能 panic,要记录日志但继续返回结果
布隆过滤器实例必须全局常驻,不能每次 new
闭包里写 filter := bloom.New(...) 是典型错误:每次调用都新建一个空过滤器,预热白做,Test() 永远返回 false。
- 声明为包级变量:
var filter *bloom.Bloom,启动时初始化一次 - 预热必须在程序启动完成、服务可接受请求前做完:从 DB 或 Redis 全量加载合法 key,统一转
[]byte后批量Add() - 多实例部署时,单机内存 filter 不满足一致性要求;生产环境应改用 RedisBloom(
BF.RESERVE/BF.ADD)或布隆代理服务 - 删数据时 filter 无法删除对应项,靠空值缓存 TTL + 定期全量重建收敛误判
真正起作用的不是某一行代码,而是三段链路的刚性顺序与 key 格式的零误差对齐:Test → cache.Get → singleflight.Do → DB;中间任意一环错位、类型不一致、或实例未复用,整套防护就形同虚设。

















