缓存穿透防护关键在于布隆过滤器前置拦截,而非仅依赖空值缓存;需预热合法key、严格对齐字段与哈希处理、配合带类型/抖动TTL的空值缓存及singleflight兜底,确保各环严丝合缝。
缓存穿透防护不是加个空值缓存就完事,关键在于「提前拒掉根本不存在的 key」——否则空值缓存本身会因类型错、序列化乱、ttl 短或误更新而失效,请求照样打穿到 db。
用布隆过滤器做第一道拦截
布隆过滤器必须在缓存 Get 之前调用,且只对「确定存在」的 key 生效。它不保证 100% 准确(有极小误判率),但能以极低内存开销挡住 99% 的恶意或错误 key。
- 初始化时预热所有合法 key(比如用户 ID、商品 SKU),不能靠运行时动态写入——布隆过滤器不支持删除
- key 必须和数据库主键/索引字段完全一致(大小写、前缀、编码都要对齐),否则
bloomFilter.Test("user:123")返回 false 就白搭 - 别用
string直接塞进布隆过滤器;如果 key 是 JSON 或带结构体字段,先做哈希(如sha256.Sum256([]byte(key)).Sum(nil))再测 - 选库推荐
gonum.org/v1/gonum/stat/distuv或轻量级github.com/willf/bloom,避免自己实现位图越界
空值缓存必须带类型和显式 TTL
只写 cache.Set("user:9999999999", nil, 5*time.Minute) 是危险的:Go-Cache 对 nil 序列化行为不一致,某些版本返回 nil,某些返回 interface{},导致后续 if val != nil 判断永远为 true。
- 统一用占位结构体,比如
type Null struct{},然后cache.Set(key, Null{}, 5*time.Minute) - TTL 不能固定写死 5 分钟;应按业务容忍度设(如秒级查询可设 60s,配置类可设 10m),并加 1–3s 随机抖动,防集中过期
- 空值缓存要和正常值走同一套序列化逻辑(比如都用
json.Marshal),否则反序列化时类型不匹配,cache.Get()可能 panic - 别把空值缓存当“永久黑名单”——它只是临时兜底,布隆过滤器才是长期防线
singleflight 要 wrap 在布隆 + 空值之后
singleflight.Group.Do 不是用来防穿透的,它是防击穿的。但如果把它放在布隆过滤器之前,等于给非法 key 也做了请求合并——这反而放大了攻击效果(一个恶意 key 触发 N 个 goroutine 等待,全卡在 Do 里)。
- 正确顺序是:
bloomFilter.Test(key) → false ? return nil : cache.Get(key) → nil ? singleflight.Do(...) : return val - Do 的回调函数里,必须先查 DB,再判断结果是否为空;若为空,才写空值缓存,否则跳过(避免把空值覆盖成有效值)
- 别在
Do回调里直接调cache.Set后就返回;要确保 set 成功后再释放等待队列,否则可能并发写入冲突 - 注意
singleflight的 key 是字符串,和缓存 key 不一定相同;比如 DB 查询用"user:123",但 Do key 可设为"db:user:123"避免和其他逻辑冲突
真正难的不是堆砌三个组件,而是让它们咬合严丝合缝:布隆过滤器漏掉的 key,得靠空值缓存兜住;空值缓存过期瞬间的并发,得靠 singleflight 拦住;而任何一环的序列化、TTL、key 构造出错,都会让整条链路失效。上线前务必用伪造的非法 key 压测整条路径,看 DB QPS 是否真的归零。


















