布隆过滤器必须前置为热路径第一道硬闸,即filter.Test()在cache.Get()之前调用;key字节序列须与Add时完全一致;应使用github.com/yourbasic/bloom初始化并预留30%余量;Add需DB事务强同步且加锁;URL请求须归一化后再哈希。

filter.Test() 必须在 cache.Get() 之前调用
布隆过滤器不是补丁,是热路径第一道硬闸。放在缓存查询之后,等于让所有无效请求先打一次 Redis、再触发一次 DB 查询逻辑(哪怕最终没查),完全失去拦截意义。
常见错误链路:cache.Get() → miss → filter.Test() → false → 返回空:此时 Redis 已被访问,DB 查询可能已被调度(若没加二次保护)。
正确做法是短路前置:
-
filter.Test([]byte(key))返回 false → 立即c.AbortWithStatus(403)或返回空响应,不碰任何下游 - 仅当返回 true 时,才继续走
cache.Get()→db.Query()流程 - key 字节序列必须和
Add()时完全一致:“user:123” 和 “123” 是两个 key,大小写、前缀、URL 编码都不能差
用 github.com/yourbasic/bloom 初始化并预估容量
gobloom 不适合热路径:位图固定、不可 resize、哈希轮数硬编码,超容后误判率会从 1% 直接跳到 8%+,且重建期间拦截空白。
立即学习“go语言免费学习笔记(深入)”;
生产热路径必须用 github.com/yourbasic/bloom,它能按生命周期最大唯一项预估位图大小:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
bloom.New(10_000_000, 0.01):预估最多 1000 万合法 key,接受 1% 误判率,内部自动算出最优m(位数组长度)和k(哈希轮数) - 务必预留 30% 余量:比如日增 5 万用户,生命周期 2 年 ≈ 3650 万,直接设
n = 50_000_000 - 初始化必须在
main()中完成,全局单例复用;禁止在 handler 里new或从 Redis 反序列化
Add() 必须与 DB 写入强同步,且并发加锁
布隆过滤器不支持删除,可靠性全靠「写即同步」。漏加一个合法 key,真实请求就被误拒;多加一个非法 key,等于主动放行攻击流量。
实操要点:
- DB 事务提交成功后,立刻执行
filter.Add([]byte("user:" + strconv.Itoa(user.ID))),不能异步、不能延迟、不能靠定时任务补漏 -
Add()不是并发安全的:底层bitArray[i/8] |= mask是读-改-写操作,多个 goroutine 同时命中同一字节会导致位未置 1(漏判) - 最简方案:用
sync.RWMutex包一层,Add()走Lock(),Test()走RUnlock()
URL 类请求要先归一化再哈希
直接把原始 c.Request.URL.String() 喂给布隆器,会因参数扰动(如 utm_source=ad、ts=1729335180)导致相同资源被反复插入,位数组快速饱和,误判率几天内崩到 50%+。
必须做轻量归一化:
- 统一小写:
strings.ToLower() - 去掉末尾
/:strings.TrimSuffix(path, "/") - 剥离跟踪参数:用
url.Parse()解析后,对Values.Del("utm_*", "ref", "ts") - 关键标识符拼接:推荐
method + ":" + host + path,而非完整 URL —— 避免 CDN 重定向或协议切换造成冗余条目
真正难的不是代码怎么写,而是归一化规则是否覆盖了所有线上实际出现的变体。上线后一定要抽样比对 filter.Test() 返回 true 的请求中,有多少比例是归一化后重复的 —— 这个数字高了,说明清洗逻辑有缺口。

















