缓存穿透在Gin+Redis中表现为请求绕过缓存直击数据库,典型现象是查无此ID却反复查询DB;需用空值缓存(带短过期)、布隆过滤器(与Gin中间件联动)和细粒度Redis锁三者分层防护。

缓存穿透在Gin+Redis中长什么样
你写的接口明明加了Redis缓存,压测时数据库连接池却瞬间打满——这不是缓存没生效,而是请求根本绕过了缓存逻辑。典型表现是:GET /user/123456789 返回 404,但日志里 redis.Get("user:123456789") 返回 nil,紧接着就是 db.Query("SELECT * FROM users WHERE id = ?") 执行了上百次。问题不在Redis连不上,而在“查不到就放行”的逻辑被并发击穿。
空值缓存必须带过期时间,且不能用零值判断
很多人写成:if data == nil { dbQuery(); cache.Set(key, data, time.Hour) } ——这会把 nil 写进 Redis,而 Go 的 redis.String() 解包失败直接 panic;更糟的是,如果数据库真返回空结果(比如 user_id 是负数),你缓存的其实是无效的 nil 字节流,下次读出来还是 nil,但无法区分是“查无此 ID”还是“缓存损坏”。
- 必须显式序列化空结果:用
cache.Set(key, "", 5*time.Minute)或cache.Set(key, "null", 5*time.Minute) - 读取时先
cache.Get(key),再判断返回值是否等于空字符串或预设哨兵值,而不是依赖err != nil - 过期时间别设太长:恶意构造的 ID 可能成千上万,
5~10 分钟是平衡内存与防护的常见选择
布隆过滤器不是银弹,得和 Gin 中间件联动
Go 生态里常用 github.com/yourbasic/bloom 或 github.com/AndreasBriese/bbloom,但直接在 handler 里初始化布隆过滤器会吃掉大量内存,且不同进程间不共享。真正可行的做法是把它做成 Gin 中间件,并配合 Redis 持久化布隆位图。
- 启动时从 Redis 加载已知合法 key 的布隆过滤器(如用户 ID、商品 SKU),用
redis.Get("bloom:user")反序列化 - 中间件中调用
bloom.Test([]byte(id)),返回false就直接c.AbortWithStatusJSON(404, nil) - 新增合法 ID 时(比如注册新用户),同步调用
bloom.Add([]byte(newID))并redis.Set("bloom:user", bloomBytes, 0) - 注意误判率:设成
0.01时,百万级 key 约占用 1.2MB 内存,别盲目调低
Gin handler 里锁不是加在 DB 查询前,而是加在缓存写入前
很多人以为“加个 sync.Mutex 就行”,但在 Gin 多协程场景下,全局锁会拖垮吞吐量;用 Redis 分布式锁又容易写错超时逻辑。最简稳方案是:只对「缓存未命中 + 数据库查询完成」这个窗口加锁,且锁 Key 必须包含业务标识。
立即学习“go语言免费学习笔记(深入)”;
- 不要锁整个 handler,而是锁具体 key:
lockKey := "lock:" + key - 用
redis.Set(lockKey, "1", &redis.Options{Expire: 3 * time.Second})实现租约锁,避免死锁 - 只有第一个抢到锁的 goroutine 查 DB 并
cache.Set(key, result, ttl);其余等待者循环cache.Get(key)直到非空或超时 - 别忘了 defer
redis.Del(lockKey),但要在 Set 成功后才删,否则可能删掉别人刚设的锁
空值缓存和布隆过滤器能挡住 90% 的穿透流量,剩下那 10% 的合法但冷门请求,靠锁保底。三者不是替代关系,而是分层拦截——漏掉一层,下一层接住。真正容易被忽略的,是布隆过滤器的更新时机和锁的释放位置,这两个点一错,整个防护就形同虚设。


















