Kratos不内置缓存击穿防护,需业务层用redis.Client.SetNX实现互斥锁+双重检查,或采用逻辑过期方案;锁key须带业务上下文,TTL需设超时以防残留。

kratos 本身不内置缓存击穿防护逻辑,它只是提供 redis.Client 封装和基础操作能力。真正解决缓存击穿,得靠你在业务层主动加锁或改写缓存策略——不是框架帮你兜底,而是你用框架提供的工具去实现。
setnx + 双重检查是 Kratos 下最直接的落地方式
Kratos 的 redis.Client 支持原生命令,SETNX(即 SetNX 方法)能原子性地抢锁,配合 Get 和 SetEX 就构成经典互斥锁流程。
- 错误做法:查不到缓存就直接查 DB,再写回 Redis —— 这就是击穿温床
- 正确顺序必须是:查缓存 → 没命中 → 抢锁 → 抢到才查 DB → 写缓存 → 释放锁;抢不到就轮询/等待/直接返回旧值
redis.Client.SetNX 的第三个参数是过期时间(秒),务必设(比如 3~5 秒),否则锁残留会阻塞后续请求。
- 不要依赖
DEL手动删锁:万一进程崩溃,锁永远不释放 - 推荐用带自动过期的锁,或结合 Lua 脚本做原子性释放(但 Kratos 默认 client 不封装 Lua,需自己调
Eval)
为什么不用 redis.Client.Get 后直接 SetEX?
因为这中间存在竞态窗口:两个 goroutine 同时 Get 返回空,都进 DB 查询,都写缓存——相当于没锁。
- 即使 DB 查询很快(10ms),在 QPS 上千时,这个窗口足以让几十个请求同时穿透
-
SetNX是 Redis 服务端原子操作,这才是唯一能打破竞态的手段
示例关键片段(伪代码):
key := "shop:" + strconv.FormatInt(id, 10)
data, _ := rdb.Get(ctx, key).Result()
if data != redis.Nil {
return json.Unmarshal([]byte(data), &shop)
}
// 抢锁
lockKey := "lock:" + key
ok, _ := rdb.SetNX(ctx, lockKey, "1", 3*time.Second).Result()
if !ok {
// 抢锁失败:可选择 sleep 后重试,或直接返回空/旧缓存(若支持逻辑过期)
time.Sleep(20 * time.Millisecond)
return rdb.Get(ctx, key).Result() // 再试一次
}
// 抢锁成功:双重检查 + 查库 + 写缓存
data, _ = rdb.Get(ctx, key).Result()
if data != redis.Nil {
rdb.Del(ctx, lockKey) // 立即释放
return json.Unmarshal([]byte(data), &shop)
}
shop = db.QueryShop(id)
rdb.SetEX(ctx, key, json.Marshal(shop), 300*time.Second)
rdb.Del(ctx, lockKey)
注意:rdb.Del 必须执行,但不能只靠它——锁超时是兜底,Del 是主动清理。
逻辑过期方案在 Kratos 里怎么落地?
Kratos 不限制你往 value 里塞什么结构。你可以把缓存内容包装成:
{"data": {...}, "expireAt": 1734649200}
-
expireAt是 Unix 时间戳(秒级),不是 TTL - 每次
Get后先解析 JSON,再比对time.Now().Unix() > expireAt - 过期了才走抢锁流程,且新写入时更新
expireAt字段,而不是设 Redis TTL
好处:避免物理过期瞬间的请求洪峰;坏处:内存不会自动释放,得靠 Redis 的 maxmemory-policy(如 allkeys-lru)兜底。
- 不要用
Set(无过期)+ 全靠逻辑判断:万一某个 key 永远没人访问,它就永远占着内存 - 建议仍配一个较长的 TTL(比如 24h),作为“保底驱逐机制”
Kratos 的轻量设计意味着它不会替你决定用互斥锁还是逻辑过期,也不会自动注入布隆过滤器。击穿防护的粒度在业务 ID 层,而不在框架层——最容易被忽略的,恰恰是锁 key 的命名是否带业务上下文(比如漏了 tenant_id 或 env 后缀),导致多租户场景下锁错乱。











