必须带NX和EX,因为NX确保键不存在才加锁(防覆盖他人锁),EX防止进程崩溃导致死锁,二者结合实现原子性加锁;value须唯一(如UUID),释放需Lua脚本校验value后删除,避免误删。

Go 用 SET 命令加锁时,为什么必须带 NX 和 EX?
因为单靠 SETNX 或 SETEX 都不安全:SETNX 没有过期机制,进程崩溃后锁永远卡住;SETEX 虽设了过期,但无法保证“键不存在才设置”,可能覆盖别人正在持有的锁。
正确做法是用一条原子命令完成三件事:检查键不存在、写入唯一值、设置过期时间。Go 客户端(如 go-redis/v8)的 SetNX(ctx, key, value, ttl) 内部就是封装了 SET key value EX seconds NX。
-
value必须全局唯一,推荐用uuid.NewString(),不能用固定字符串(比如"1"),否则解锁校验失效 -
ttl要略大于业务最大执行时间,但别设成 1 小时——过长会拖慢故障恢复;常见取值是 5–30 秒 - 如果用
redigo,得手写Do("SET", key, value, "NX", "EX", seconds),注意参数顺序和类型(seconds是 int,不是 time.Duration)
释放锁时,为什么不能直接调用 Del?
直接 rdb.Del(ctx, key) 是最常见错误:你删的可能不是自己设的锁。比如 A 获取锁后执行慢,超时自动释放;B 进来重设了锁;A 结束时仍调 Del,就把 B 的锁干掉了。
必须用 Lua 脚本做原子校验:GET 键值 → 比对是否等于自己的 value → 是则 DEL,否则返回 0。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 脚本内容固定,就这 4 行:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end - 调用时传两个参数:
[]string{key}和clientID(即加锁时生成的 value) - 返回
int64(0)表示不是持有者,此时应记录 warn 日志,而不是 panic 或重试
业务耗时不确定时,要不要自动续期?
要,但只在明确可能超时的场景下开启——比如调外部 HTTP、批量写数据库、等待第三方回调。普通 CRUD 通常不用。
续期不是简单地延长 TTL,而是先确认锁还在自己手里,再更新过期时间。否则可能出现“锁已转移,你还续着”的脏状态。
- 启动一个独立 goroutine,在锁剩余时间约 1/3 时触发续期(例如 ttl=15s,则每 5s 检查一次)
- 续期用
PEXPIRE(毫秒级)或EXPIRE(秒级),但必须搭配 Lua 校验:先GET再比对value,匹配才PEXPIRE - 续期失败(Lua 返回 0)说明锁已丢失,必须立刻中止当前业务逻辑,避免后续操作污染数据
- 用
context.WithCancel控制续期 goroutine 生命周期,主流程结束时 cancel,防止 goroutine 泄漏
线上偶尔出现多个实例同时进入临界区,大概率是什么问题?
不是锁逻辑写错了,而是 Redis 连接没管好——特别是用 redigo 时连接池配置不当,或 go-redis 没设 MaxConnAge / MinIdleConns。
现象是本地跑 100 次都正常,上线后偶发失效。根本原因是:连接复用导致命令乱序、Pipeline 中断、或连接被服务端主动断开后未清理,使得 SET 成功但 EXPIRE 没发出去(虽然现在都用原子 SET ... NX EX,但旧代码或中间件代理可能拆解命令)。
- 检查客户端是否复用同一个
*redis.Client实例,不要每次操作都 new 一个 -
go-redis必须配PoolSize(建议 10–30)、MinIdleConns(至少 5)、MaxConnAge(如 30m) - 如果用
redigo,确保Pool.Get()后调Close(),且TestOnBorrow开启 ping 检测 - 加锁失败日志里如果频繁出现
connection reset或i/o timeout,优先排查网络和连接池

















