缓存击穿必须用互斥锁而非单纯重试,因其本质是热点key过期瞬间的高并发请求洪峰,单机锁无效,需redis的setnx实现跨进程原子加锁,先占位再查库,避免多请求同时击穿db。

为什么缓存击穿中间件必须用互斥锁而非单纯重试
缓存击穿本质是高并发下热 key 过期瞬间的“请求洪峰”,不是数据不存在(那是穿透),而是重建动作没加锁导致多个 goroutine 同时查库。如果只在业务层做 if cache == nil { loadFromDB(); setCache() },在并发 100+ 时大概率触发 10 次以上数据库查询——Redis 没拦住,DB 直接被打满。
关键点在于:锁必须落在缓存层,且要能跨进程生效。单机 mutex 完全无效;Redis 的 SETNX 是唯一可行的原子入口。
- 不能把锁逻辑写在 handler 里,否则中间件无法复用、易漏加
- 不能等 DB 查询完再写 Redis,得先占位再查库,否则锁就形同虚设
- 锁 key 必须带业务标识(如
lock:article:123),避免不同资源互相阻塞
Go + Gin 封装互斥重建中间件的三步写法
中间件核心逻辑是:查缓存 → 缓存 miss 且无锁 → 尝试加锁 → 加锁成功则查库并回填 → 加锁失败则短暂等待后重查缓存。整个流程必须封装成可配置的函数工厂。
- 用
rdb.SetNX(ctx, lockKey, "1", 3*time.Second)获取分布式锁,过期时间必须短于预期 DB 查询耗时(建议 ≤3s) - 锁获取失败后不要立即返回错误,而应
time.Sleep(50 * time.Millisecond)后c.Next()重走流程(避免雪崩式重试) - DB 查询结果必须用
rdb.Set(ctx, cacheKey, data, ttl)写入,且ttl要随机偏移(如baseTTL + rand.Int63n(300)),防雪崩
示例片段:
func CacheBreakMiddleware(rdb *redis.Client, cacheKeyFunc func(c *gin.Context) string, loadFunc func(c *gin.Context) (interface{}, error), ttl time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
key := cacheKeyFunc(c)
val, err := rdb.Get(c.Request.Context(), key).Result()
if err == redis.Nil {
// 缓存未命中,尝试加锁
lockKey := "lock:" + key
ok, _ := rdb.SetNX(c.Request.Context(), lockKey, "1", 3*time.Second).Result()
if ok {
// 加锁成功:查库、写缓存、释放锁(自动过期)
data, loadErr := loadFunc(c)
if loadErr == nil {
rdb.Set(c.Request.Context(), key, data, ttl+time.Second*time.Duration(rand.Int63n(300)))
}
c.Next() // 继续走业务逻辑(或直接写响应)
return
}
// 加锁失败:小睡后重试
time.Sleep(50 * time.Millisecond)
c.Next()
return
}
if err == nil {
c.Set("cached_data", val) // 注入上下文供 handler 使用
c.Next()
return
}
c.Next()
}
}
容易踩的三个坑:锁失效、空值穿透、中间件顺序错乱
实际部署中最常翻车的不是逻辑,而是细节:
-
SetNX返回false不代表失败,要检查err == nil && !ok才是锁被占用;忽略这点会导致永远走重试分支 - DB 查不到数据时,必须写空值缓存(如
rdb.Set(ctx, key, "", 2*time.Minute)),否则攻击者可用无效 ID 狂刷,演变成缓存穿透 - 该中间件必须注册在鉴权中间件之后、业务 handler 之前;如果放在 JWT 解析前,就拿不到
userID,无法构造带用户维度的 cacheKey
性能与降级边界必须明确写死
互斥锁不是银弹。当 DB 已经慢到 3 秒都查不完,或者 Redis 集群延迟抖动严重时,这个中间件反而会放大问题。必须设置硬性熔断:
- 锁等待总次数上限(如最多重试 3 次),超限直接透传请求(不缓存也不锁)
- DB 查询超时强制 cancel(用
context.WithTimeout包裹loadFunc) - Redis 异常时 fallback 到本地内存缓存(
sync.Map)或直接透传,不能让整个 API 因缓存层故障而不可用
这些边界条件不编码进中间件,等于没做防护。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











