setnx单独调用无法实现可靠分布式锁,因其无过期机制、value不唯一、非原子操作,且主从架构下存在一致性风险。

直接用 go-redis/v9 的 SetNX 加锁,但不配唯一 token、不设 EX、不走 Lua 解锁,等于没锁——它只在单次请求里“看起来成功”,一到并发或异常就漏。
为什么 SetNX 单独调用根本不行
很多人写 rdb.SetNX(ctx, "lock:order", "1", 0),以为设了值就锁住了。错在三处:
-
SetNX本身不带过期逻辑,ttl=0就是永不过期;Redis 实例崩溃或主从切换后,这个 key 可能永远卡住 - value 写死为
"1",多个 goroutine 同时抢锁,谁都删得掉对方的锁 - 老版本 Redis(如 2.4)不支持
SET key val NX EX原子命令,go-redis/v9在 fallback 模式下会拆成SETNX + EXPIRE两步,中间崩溃就死锁
加锁必须一步到位:SET key value NX EX seconds
服务端原子命令才是底线,Go 客户端只是封装。用 go-redis/v9 时要确保:
- Redis 版本 ≥ 2.6.12(推荐 ≥ 6.0),否则
SetNX(ctx, key, val, ttl)会退化为非原子操作 -
val必须是每个 goroutine 独立生成的,例如uuid.NewString(),不能复用或硬编码 -
ttl设为time.Duration类型,别误传纳秒(比如把2 * time.Second错写成2e9) - 实际命令等价于:
SET lock:order abc123 NX EX 2,不是SETNX+EXPIRE
解锁必须用 Lua 脚本,GET+DEL 是竞态温床
下面这段代码看着简洁,实则危险:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
val, _ := rdb.Get(ctx, key).Result()
if val == myToken {
rdb.Del(ctx, key)
}
问题在于:A 判断成功、还没执行 Del,B 已经抢到锁并写入新值,A 一删就把 B 的锁干掉了。正确解法只有 Lua 原子脚本:
const unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
调用时传入 key 和自己的 myToken:
result := redis.NewScript(unlockScript).Run(ctx, rdb, []string{key}, myToken)
ok, _ := result.Result()
if ok != int64(1) {
// 解锁失败,可能锁已过期或被别人覆盖
}
长任务必须配看门狗续期,且续期也得校验 token
业务执行时间不可控,但锁又不能不设 TTL。折中方案是后台起 goroutine 定期续期,但注意:
- 续期命令也必须用 Lua 校验,脚本类似解锁,只是把
DEL换成EXPIRE - 续期间隔建议为
ttl / 3,比如 TTL=2s,就每 600ms 续一次;太密打爆 Redis,太疏容易掉锁 - 业务函数 return 前必须显式 stop 续期 goroutine,否则锁释放了还在续——变成“幽灵续期”
- 别用裸
EXPIRE命令续,它不检查 value,A 可能续了 B 的锁
最易被忽略的一点:Redis 单实例锁在主从架构下天然不安全。主节点写入锁后宕机,从节点升主但没同步该 key,另一个客户端就能绕过锁。这不是实现 bug,是 Redis 架构限制——真要强一致,得换 etcd 或上 Redlock(但运维成本陡增)。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










