SET命令的NX+EX组合才是加锁正确姿势,原子完成key、value、过期、存在性校验;SETNX不带过期易致死锁;go-redis/v9的SetNX更安全;解锁须用Lua脚本原子校验value;续期需自动、原子、智能;禁用INCR实现锁。

Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
SET 命令的 NX+EX 组合才是加锁的正确姿势,不是 SETNX 单独用 —— 后者不带过期时间,一挂就死锁;前者原子设置 key、value、过期、存在性校验四件事,一步到位。
为什么 redis.Client.Do("SETNX", ...) 总是返回 nil
因为 SETNX 协议只认两个参数:key 和 value。你若多传 "EX", "10",Go 的 Do 方法会把它们全塞进 Redis 协议帧,Redis 解析时发现多余字段直接丢弃,但底层 RESP 协议解析失败,就返回 nil(不是 0 或 1)。
常见错误写法:c.Do("SETNX", "lock:key", "abc", "EX", "10")
正确替代:c.Do("SET", "lock:key", "abc", "NX", "EX", "10") —— 注意 NX 和 EX 是 flag,必须紧跟 value 后,顺序不能错
更推荐:直接用 go-redis/v9 的 SetNX 方法,它内部自动拼成合法 SET 命令,还带 context 和 error 处理
Unlock 报 ERR no such key 或 panic 是谁的锅
这不是 Redis 错,是客户端库(比如 go-redis/v9 的 mutex 包)在做所有权校验时发现 key 已不存在,主动 panic。根本原因只有三个:
• 加锁时 SetNX 返回 false(别人抢到了),你没判断就直接调 Unlock
• 加锁成功,但业务执行太久,key 已过期被自动删掉
• 多个 goroutine 共用同一个锁对象,第二个 Unlock 时第一个早已释放或超时
解锁必须依赖 Lua 脚本做原子校验:GET + DEL 不能拆开,否则可能删掉别人设的 value。脚本长这样:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end
别手写 Del,也别信“先 Get 再 Del”这种两步操作
锁续期不是靠定时 EXPIRE,而是看门狗 ticker
手动起 goroutine 每 5 秒跑一次 EXPIRE 是危险的:它不检查锁是否还在自己手上,也不防并发覆盖。真正的续期必须满足三个条件:
• 只在锁仍属于当前 client 时才刷新 TTL
• 刷新动作本身要原子(用 EVAL 执行 Lua 检查 + 设置)
• 刷新时机得智能——不能等快过期了才动,得在剩余 TTL 的 1/3 时间点触发
go-redis/v9 的 mutex 包默认开启 WithAutoRefresh,内部起 ticker 自动做这事;redigo 没这能力,得自己写脚本、自己管连接、自己处理失败回调
注意:auto-refresh 不是永生锁,它只续当前持有者的锁;一旦锁被别人抢走,下次刷新就会失败并触发回调,这时该降级就降级,该 panic 就 panic
自增锁?不存在的,那是误解
Redis 没有“自增锁”这个概念。你可能想表达的是“用 INCR 实现计数型锁”或者“锁 key 值随请求递增”,但这违背分布式锁基本前提:锁必须可预测、可校验、可释放。用 INCR 当锁值,会导致解锁时无法比对原始 value,Lua 脚本失效,误删风险飙升。
真正需要唯一性标识的场景,应该用随机字符串(如 uuid.NewString())作为 lock value,存进 SET 命令的 value 字段,解锁时拿它去比对。
如果真要支持重入(同一 client 多次加锁),得换数据结构:HSET 存 clientID + 计数,配合 Lua 做原子增减,但这已超出基础锁范畴,属于高级封装

















