布隆过滤器必须放在 Gin handler 第一行,因为只有最先执行才能100%拦截根本不存在的 key;若延迟执行,网络开销、序列化及 DB 查询准备已发生,防护失效。

布隆过滤器必须在 Gin handler 最开头执行,filter.Test 返回 false 就该直接 c.AbortWithStatusJSON,任何延迟(比如先查 Redis 再判)都会让防护失效。
为什么 Bloom Filter 一定要放在 Gin handler 第一行?
缓存穿透的本质是「大量请求携带根本不存在的 key」,防御目标不是提速,而是拦截。布隆过滤器说「不存在」就 100% 不存在——这个结论只在它最先被执行时才有效。
- 如果先走
cache.Get,哪怕 Redis 返回redis.Nil,请求已经消耗了网络往返和序列化开销,且没挡住 DB 查询准备动作 - 如果写在
singleflight.Do后面,等于给恶意 key 做请求合并,goroutine 卡在等待一个永远不会有结果的 DB 查询上 - Gin 中间件里放也行,但必须确保它不依赖后续中间件注入的上下文字段(比如 auth token 解析后的 user ID),否则 key 格式对不上
filter.Test 返回 false 却还是查了 DB?检查这三处
常见现象:日志里看到 bloom.Test("user:999999999") = false,但紧接着又打出 DB query: SELECT * FROM users WHERE id = 999999999。说明拦截逻辑被绕过了。
- key 格式不一致:
filter.Add([]byte("999999999"))但查询时用了filter.Test([]byte("user:999999999"))—— 前缀、大小写、编码(UTF-8 vs GBK)必须完全相同 - filter 变量不是全局常驻:
var filter *bloom.Filter在 handler 里重新bloom.New,每次请求都是空过滤器,Test永远返回true - 误判率设太高导致
Test返回true:比如预估总量 1000 万却用bloom.New(1e4, 0.01),位图太小,实际误判率可能超 20%,大量非法 key 被放行
怎么让 Bloom Filter 和数据写入强同步?
布隆过滤器不能动态删除,所以「数据落库成功 → 立即 filter.Add」是唯一可靠路径。异步写入(比如发 MQ)必然有窗口期,这期间新 ID 的查询会穿透。
- 用户注册成功后,在同一个事务 commit 后立刻调用
filter.Add([]byte(strconv.Itoa(user.ID))),不要等 defer 或 goroutine - 批量导入场景:用
filter.Grow(如果库支持)或分片重建,别用for range ids { filter.Add(...) }—— 大量 Add 会触发内部扩容,性能骤降 - 重启后加载全量 ID:启动时从 DB
SELECT id FROM users流式读取,每 1000 条 batch 调用一次filter.AddAll(需库支持),避免内存爆掉 - 删用户不用管 Bloom Filter:接受「该 ID 后续短暂误判为存在」,靠短 TTL 空值缓存兜底即可
Gin handler 里怎么写才算安全?
一个典型且可落地的 handler 片段:
func getUserHandler(c *gin.Context) {
idStr := c.Param("id")
// 1. 格式标准化:只允许数字,防 "user:-1" 或 "abc"
if !regexp.MustCompile(`^\d+$`).MatchString(idStr) {
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
key := "user:" + idStr
<pre class="brush:php;toolbar:false;">// 2. 布隆第一道拦截
if !filter.Test([]byte(key)) {
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
// 3. 后续流程:cache.Get → miss → singleflight.Do("db:user:"+idStr, fn)
// ...(此处接标准缓存+击穿防护逻辑)}
最关键的细节是:所有前置校验(正则、长度、范围)必须在 filter.Test 之前;filter.Test 的输入 key 必须和 filter.Add 时完全一致;AbortWithStatusJSON 后绝不执行后续任何 DB 或 cache 操作。漏掉任意一环,防护就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











