redis的setnx命令只接受key和value两个参数,若用do方法多传参数(如ex),会导致协议解析失败而返回nil;应改用set命令的nx+ex选项或go-redis/v9的setnx方法。

redis.Client.Do 调用 SETNX 为什么总是返回 nil?
Go 用 redis.Client.Do 直接发 SETNX 命令,结果拿到 nil,不是 1 或 0 —— 这大概率是命令参数传错了。Redis 的 SETNX 只接受两个参数:key 和 value,但 Go 的 Do 方法会把每个参数单独序列化,如果你写了 c.Do("SETNX", "mylock", "abc", "EX", "10"),Redis 实际收到的是 5 个参数,它不认识 EX,直接忽略后续,只执行 SETNX mylock abc,然后返回 1 或 0;但如果你多传了(比如误加了 nil 或空字符串),底层协议解析失败,就可能返回 nil。
更稳妥的做法是改用 SETEX 或(推荐)SET 命令的原子选项:
-
c.Do("SET", "mylock", "abc", "NX", "EX", "10")—— 注意顺序:NX和EX是 flag,不是值,必须紧挨在 value 后面 - 用官方
github.com/go-redis/redis/v9的SetNX方法,自动处理类型和错误,返回bool, error - 别自己拼
Do,除非你明确需要绕过 client 封装做特殊协议操作
用 redigo 还是 go-redis/v9?锁超时后自动续期怎么做?
选 go-redis/v9。redigo 是低层封装,所有命令都要自己处理连接、重试、类型转换;而分布式锁的核心难点不在“设个 key”,而在“持有期间续期”和“释放时校验所有权”。v9 提供 Lock / Unlock 接口,背后用 Lua 脚本保证删除操作的原子性(只删自己设的 value),redigo 没这层能力,得自己写脚本、自己 EVAL、自己比对 value,极易出错。
自动续期(renew)不是开个 goroutine 定时 EXPIRE —— 那不原子,且无法判断锁是否已被别人抢走。正确做法是:
- 初始化锁时传入
WithContext+WithExpiration,并启用WithAutoRefresh - 锁对象内部起一个 ticker,在剩余 TTL 的 1/3 时间点尝试刷新(需服务端支持,v9 默认开启)
- 刷新失败(比如锁已丢失)会触发回调,你可以选择 panic 或降级逻辑
- 注意:auto-refresh 不是无限续,它只在锁仍被当前客户端持有时才生效
解锁时报 “ERR no such key” 或 “attempt to unlock unlocked mutex”
这不是 Redis 报错,是客户端库(如 go-redis/v9 的 mutex 包)抛的 panic。根本原因是:你调用了 Unlock,但此时 key 已过期、被别的客户端覆盖、或压根没成功加锁(比如 SetNX 返回 false 却没检查)。
典型错误链:
- 加锁后没检查返回值,直接往下走,以为锁拿到了
- 业务逻辑 panic 或提前 return,忘了 defer Unlock
- 用同一个
*redis.Mutex实例在多个 goroutine 里并发 Unlock(mutex 不是线程安全的) - 锁 key 被其他系统手动删了(比如运维清缓存),但你的代码还在 try unlock
解决办法很简单:每次 Unlock 前,先确认锁还属于你。用 v9 的标准用法:
if ok, err := mu.Lock(ctx); !ok || err != nil {
// 加锁失败,别碰 Unlock
return
}
defer func() {
if err := mu.Unlock(ctx); err != nil {
// log but don't panic —— 错误可能是锁已失效,不是程序 bug
}
}()
锁 key 冲突、误删、或被扫描清理怎么办?
Redis 没命名空间,KEYS lock:* 或 SCAN 扫描可能误伤业务锁 key。更危险的是,如果多个服务共用一个 Redis 实例,又都用 mylock 这种裸 key,A 服务删了 B 服务的锁,B 就会静默越权执行。
必须做隔离:
- key 名强制带前缀,比如
svc:order:lock:pay_123,前缀体现服务名+业务域 - value 必须是唯一标识(如 UUID 或进程+goroutine ID 组合),不能用固定字符串,否则无法区分谁持锁
- 禁止任何人工
DEL或FLUSHDB操作锁 key;如果要用 scan 清理,正则必须精确匹配前缀,且加人工确认环节 - 线上环境建议为分布式锁单独部署 Redis DB(
redis://host:port/3),避免混用
锁不是银弹,它解决的是“同一时刻最多一个实例执行某段逻辑”,但没法防止网络分区、时钟漂移、或客户端崩溃后残留锁。真正的健壮性来自:合理的超时设置、幂等设计、以及锁失效后的兜底补偿逻辑——这些比怎么写 SET NX EX 重要得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











