Redis缓存常见错误:过期时间须传time.Duration类型;需调用client.Close()防fd耗尽;JSON序列化字段须导出并加json tag;并发场景应用SetNX避免缓存击穿。

redis.Client.Set() 的过期时间参数容易传错类型
Go 用 redis.Client.Set() 写缓存时,第三个参数是过期时间(expiration),但很多人直接传 60 或 "60s",结果缓存永不过期或立刻失效。这是因为 Redis 客户端要求它必须是 time.Duration 类型,不是整数秒也不是字符串。
- ✅ 正确写法:
client.Set(ctx, "user:123", data, 60*time.Second) - ❌ 常见错误:
client.Set(ctx, "user:123", data, 60)(被当作纳秒,相当于 60ns) - ❌ 另一个错误:
client.Set(ctx, "user:123", data, "60s")(编译不通过,类型不匹配) - ⚠️ 注意:如果传
0,表示“不过期”;传-1在部分旧版客户端会 panic,新版通常转为永不过期
用 redis.NewClient() 连接池没调 client.Close() 会导致 fd 耗尽
Go 的 redis.NewClient() 默认创建带连接池的客户端,但很多人只在初始化时调一次,进程退出前忘了关。连接池不会自动释放底层 TCP 连接,长时间运行的服务(比如 HTTP server)会慢慢耗尽文件描述符,报错 dial tcp: lookup xxx: no such host 或 too many open files。
- ✅ 启动时初始化:
client := redis.NewClient(&redis.Options{Addr: "localhost:6379"}) - ✅ 关闭时机:在
main()函数退出前、或 HTTP serverShutdown()后调用client.Close() - ✅ 更稳妥做法:把
client包进自定义结构体,实现io.Closer接口,方便统一管理 - ⚠️ 不要每次请求都
NewClient():连接池复用才有意义,频繁新建等于放弃连接池
JSON 序列化后存 Redis,反序列化时 struct 字段没加 json: tag 会丢失数据
缓存对象常用 json.Marshal() 存字符串,再用 json.Unmarshal() 读回来。但如果目标 struct 字段没加导出标签(首字母大写)或显式 json: tag,反序列化会静默失败——字段保持零值,也不报错。
- ✅ 正确示例:
type User struct { ID int `json:"id"` Name string `json:"name"` } - ❌ 错误示例:
type User { id int; name string }(小写字段不可导出,json包跳过) - ⚠️ 注意:Redis 里存的是纯字符串,
json.Unmarshal()失败时返回error,但很多人忽略这个返回值 - ? 小技巧:存之前先
json.Marshal()一下,检查是否报错,能提前发现 struct 定义问题
并发读写同一个 key 时,GET + SET 不是原子操作,得用 SET NX EX
想实现“缓存未命中就查 DB 并写入”,很多人写成先 GET,再 if nil { ... SET ... }。这在并发场景下会触发多次 DB 查询,击穿缓存。Go 客户端的 client.Get().Result() 和 client.Set() 是两个独立命令,中间有竞态窗口。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 正确姿势:用
client.SetNX()(对应 Redis 的SET key value NX EX 60) - ✅ 示例:
ok, err := client.SetNX(ctx, "user:123", data, 60*time.Second).Result(),只有 key 不存在时才设置成功 - ⚠️ 注意:
SetNX()返回bool表示是否设成功,不是 error;失败时ok==false,不代表出错 - ? 衍生场景:需要“查 DB + 缓存穿透防护”,就得结合布隆过滤器或空值缓存,单靠
SET NX解决不了空结果重复查 DB
Set() 或 Get(),而是想清楚「谁负责更新」「过期时间怎么定」「并发时以谁为准」——这些没法靠库函数自动解决,得在业务路径里卡死。


















