线上服务卡顿、goroutine堆积、Redis响应慢八成因ctx未设超时;go-redis禁用context.Background(),应优先复用r.Context()并设超时;Set覆盖写,SetNX用于分布式锁;JSON存取需字段导出+json tag;Scan需循环游标、集群不跨slot。

线上服务卡顿、goroutine 堆积、Redis 调用无报错却响应极慢——八成是 ctx 没设超时,而不是缓存逻辑本身有问题。
go-redis 的 ctx 为什么不能用 context.Background()
它没有 deadline、不可取消,一旦 Redis 响应延迟或网络抖动,Get、Set 就会无限阻塞当前 goroutine。连接池耗尽时,请求甚至卡在 pool.Get 阶段,日志里看不到 Redis 错误,只看到 goroutine 数飙升。
- HTTP handler 中优先复用
r.Context(),便于统一超时和取消 - 读操作推荐:
context.WithTimeout(r.Context(), 300*time.Millisecond) - 写操作(尤其是
SetNX、Expire)也必须设超时,否则锁逻辑可能假死 - 别在全局变量里存
ctx = context.Background()—— 它不是“默认上下文”,而是“无约束上下文”
Set 和 SetNX 到底该用哪个
Set 是覆盖写,SetNX 是“仅当 key 不存在时才设值”,语义完全不同。很多人误以为 Set(key, val, time.Hour) 就能防并发写,其实完全不能。
-
Set:不管 key 存不存在,都强行更新值和 TTL;适合普通缓存刷新 -
SetNX:返回bool,true表示抢锁成功,false表示 key 已存在;是分布式锁的基石 - v9+ 支持
client.Set(ctx, key, val, opts).WithArgs("NX", "EX", "30")原子设值 + 过期;老版本必须SetNX+Expire组合,但这两步不原子,有竞态风险 - 多数业务不需要强互斥,盲目用
SetNX反而增加失败率和延迟
JSON 存取总反序列化失败?先看字段导出和 json tag
Redis 只存字节,json.Marshal 出来是空对象 {},大概率是结构体字段没导出或缺 json tag。
立即学习“go语言免费学习笔记(深入)”;
- 结构体字段首字母必须大写(可导出),且显式加 tag:
Name string `json:"name"` - 反序列化前务必检查原始数据:
if len(data) == 0 { return errors.New("empty redis value") } - 更稳妥的做法是封装
GetJSON(ctx, key, &v),内部统一处理redis.Nil、空字节、json.Unmarshal错误 - 别信日志里打印的
val,用redis-cli GET key看原始值,确认是不是真存了 JSON 字符串,还是存成了"&{}"这种 Go 字符串表示
Scan 扫不出 key?不是命令错,是用法和集群限制
Scan 默认每次只返回 10 个 key,扫 10 万个 key 要发上万次请求,延迟高、吞吐低,还容易被 Redis 单线程拖垮。
- 显式加大
Count参数:client.Scan(ctx, cursor, "user:*", 1000).Val() - 游标必须循环使用,不能重置为 0;结束标志是返回的
cursor == 0 - Redis 集群模式下
Scan不跨 slot,只能扫当前节点,别指望它能全量枚举 - 生产环境慎用:会阻塞 Redis 单线程,高并发扫描可能拖慢其他命令
最常被忽略的点是:缓存逻辑本身没问题,问题出在上下文控制、字段导出、游标迭代这些“边缘但致命”的细节上。一个没设超时的 Get,可能让整个服务雪崩;一个没加 json tag 的字段,会让反序列化静默失败。这些地方不踩坑,缓存才能真正起效。


















