缓存穿透必须用Gin多级中间件拦截,因handler位于请求生命周期最内层,无法阻断恶意null-key请求并发打穿DB;需按洋葱模型外层三级防御:参数校验→布隆过滤→空值缓存。

缓存穿透必须在 Gin 控制层用多级中间件拦截,不能只靠业务 handler 里补查数据库或 fallback。 否则恶意构造的海量 null-key 请求会直接打穿缓存、压垮下游存储,尤其在微服务网关或用户中心这类高并发入口点上,后果立竿见影。
为什么单靠 handler 补查挡不住穿透
缓存穿透本质是大量请求查询根本不存在的 key(如 user_id=9999999999),Redis 返回 nil,若 handler 不加二次校验就直接查 DB,DB 就成了攻击靶心。Gin 的 handler 是请求生命周期最内层,等它执行时,请求已绕过所有前置防护。
- handler 里做“查缓存 → nil → 查 DB → 无结果 → 写空值”逻辑,无法阻止重复的穿透请求并发抵达 DB
- 空值写入 Redis 需设置短过期(如 2 分钟),但恶意请求可高频刷新时间戳绕过
- 没有统一入口控制,不同 handler 实现不一致,漏一个就全盘失效
用 Gin 中间件链实现三级拦截
真正的防御要嵌在洋葱模型外层,按顺序拦截:第一道筛掉明显非法 key,第二道查布隆过滤器(Bloom Filter),第三道兜底空值缓存策略。三者缺一不可。
-
第一级:参数合法性拦截 —— 在
AuthMiddleware后、RateLimitMiddleware前插入,用正则或白名单校验 key 格式(如user_id必须是 1–10 位数字),非法直接c.AbortWithStatusJSON(400, ...) -
第二级:布隆过滤器拦截 —— 独立中间件
BloomCheckMiddleware(),初始化时加载全量有效 key 到内存布隆过滤器(如使用gonum.org/v1/gonum/stat/distuv或轻量github.com/AndreasBriese/bbloom),查出 false negative 概率 c.Abort(),不进缓存也不查 DB -
第三级:空值缓存兜底 —— 在缓存 client 调用前统一包装,例如封装
GetWithNullCache(key, ttl time.Duration)函数,当 Redis 返回 nil 且布隆判定“可能不存在”时,自动写入cache.NullKey(key)并设 TTL,避免后续同 key 请求再穿透
中间件注册顺序决定拦截成败
Gin 的执行顺序就是防御纵深。错放位置会让布隆过滤器形同虚设,或让空值缓存被跳过。
- 正确顺序:
r.Use(LoggerMiddleware())→r.Use(ParamValidateMiddleware())→r.Use(BloomCheckMiddleware())→r.Use(RateLimitMiddleware())→r.Use(CacheMiddleware()) - 绝对禁止把
BloomCheckMiddleware放在RateLimitMiddleware之后——限流是按 IP 统计,而穿透请求往往来自不同 IP,限流根本拦不住 - 也禁止把
CacheMiddleware放在最前——它需要依赖前面中间件校验后的 clean key,否则布隆和参数校验都白做
微服务场景下布隆过滤器的更新陷阱
布隆过滤器一旦加载进内存,就无法自动感知新数据写入。在用户注册、商品上架等写操作发生后,必须同步更新过滤器,否则新 key 会被误判为“不存在”。
- 不要用定时全量重建(延迟高、内存抖动大),改用带分片的增量更新:每写一个新 key,调用
bloom.Add([]byte(key))并广播到所有实例(通过 Redis Pub/Sub 或消息队列) - 避免在中间件里做耗时同步更新操作,更新逻辑应异步化,中间件只做查,不负责写
- 布隆过滤器实例建议用
sync.Map包裹,key 为 service name(如"user"),避免多个微服务共用同一实例导致误判
真正难的不是写三个中间件,而是让布隆过滤器的加载、更新、失效与服务生命周期对齐——它不像日志或鉴权那样静态,而是一个活的数据结构,稍有松懈,穿透就从“理论风险”变成凌晨三点的告警电话。


















