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