布隆过滤器必须前置在缓存和数据库查询之前,作为第一道硬拦截;filter.Test()需在cache.Get()前调用,输入必须是与Add()完全一致的[]byte序列,且Add()须与DB写入强同步,不可异步或延迟。

布隆过滤器必须前置在缓存和数据库查询之前,否则等于没加——它不是锦上添花,而是第一道硬拦截。
filter.Test() 必须在 cache.Get() 之前调用
很多团队把布隆过滤器当成“补丁”加在缓存逻辑之后,比如先查 Redis,miss 了再 filter.Test(),这完全违背设计初衷。布隆过滤器的价值在于「确定不存在就立刻返回」,不产生任何下游开销。
- 错误链路:
cache.Get()→ miss →filter.Test()→ false → 返回空:此时 Redis 已经被查了一次,DB 可能也已被触发(如果没做二次保护) - 正确链路:
filter.Test([]byte(key))→ false → 直接 return;只有 true 才继续cache.Get() - 注意:
Test()输入必须是[]byte,且和Add()时的字节序列完全一致——"user:123"和"123"是两个 key,大小写、前缀、编码都不能差一丝一毫 - Go 生态推荐用
github.com/yourbasic/bloom,它内部已处理哈希一致性与位图对齐,比手写或用golang.org/x/exp/bloom(无自定义哈希)更稳
filter.Add() 必须与 DB 写入强同步
布隆过滤器不支持删除,也不支持动态扩容,它的可靠性完全依赖「写入即同步」。漏加一个合法 key,就会导致真实请求被误判为不存在;多加一个非法 key,就等于主动放行攻击流量。
- DB 事务提交成功后,立刻执行
filter.Add([]byte(strconv.Itoa(user.ID))),不能异步、不能延迟、不能靠定时任务补漏 - 不要在初始化阶段只加载历史数据就完事——新注册用户、新上架商品等增量数据,必须走同一路径写入 filter
- 并发写入
Add()时必须加锁:sync.RWMutex的Lock()包一层;但Test()是无锁的,因为底层读[]byte单字节天然原子 - 别把 filter 存 Redis 或序列化到磁盘——网络 IO 和反序列化开销远超内存访问,QPS 高时会成瓶颈
空值缓存必须带类型、显式序列化、短 TTL
布隆过滤器放行后,仍可能遇到「真实存在但尚未写缓存」的 key(如刚注册用户),这时空值缓存是第二道防线。但它极易因类型混乱失效。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 绝对不要传
nil给redis.Set():Go-Redis v9 会存成空字符串或 panic,后续redis.String()解包失败,业务逻辑直接崩 - 统一用结构体兜底,例如:
type CacheNull struct{ Empty bool `json:"empty"` },查不到时json.Marshal(CacheNull{Empty: true})再写入 - TTL 要短(建议
2 * time.Second到5 * time.Second),且加 1–3s 随机抖动,防集中过期引发击穿 - 读取时必须先
redis.Get(ctx, key).Bytes()拿原始字节,再json.Unmarshal(),不能依赖redis.String()或redis.Any()自动转换
singleflight.Do() 必须放在布隆 + 空值之后
singleflight 是防击穿的,不是防穿透的。把它放在布隆过滤器前面,等于给恶意 key 做请求合并——一个非法 key 触发上千 goroutine 等待,CPU 和内存全卡在 Do() 里。
- 正确顺序:
filter.Test()→ false?return;true?→cache.Get()→ hit?return;miss?→singleflight.Do() -
Do()回调里必须先查 DB,再判断结果:若 DB 返回空,才写空值缓存;若返回有效数据,直接cache.Set(),避免空值覆盖 - 别在回调里写完缓存就 return——要确保
cache.Set()成功后再释放等待,否则可能造成部分 goroutine 拿到旧值或 nil - 布隆过滤器容量预估必须贴合生命周期:填
bloom.New(10_000_000, 0.01)是为了撑住三年 1000 万用户,不是拍脑袋写的数字;一旦总量逼近上限,误判率会非线性飙升,得提前分片或重建
最易被忽略的点是 key 格式的严格对齐——哪怕数据库里存的是 int64,查询时拼的是 "user:" + strconv.FormatInt(id, 10),那 Add() 时就必须用同样方式转字符串,不能用 fmt.Sprintf 或直接 string(rune),否则布隆过滤器就形同虚设。


















