跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

Go 语言如何实现高并发下的缓存穿透防护

缓存穿透防护关键在于布隆过滤器前置拦截,而非仅依赖空值缓存;需预热合法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 是否真的归零。

相关文章