Redis SETNX不能直接当分布式锁用,因其无法原子性设置key和过期时间,易导致死锁;应使用SET key value EX seconds NX命令,并配合唯一value、Lua校验解锁、连接复用控制等机制保障可靠性。

直接用 sync.Mutex 或裸调 rdb.SetNX 在多实例部署下根本没锁住——所有“加锁成功”日志都是假象,性能提升无从谈起。真正能压测不掉帧、线上不双写、故障不雪崩的分布式锁,必须满足四个硬条件:原子加锁、value 校验解锁、TTL 自动兜底、续期防误删。
Go 框架里 rdb.SetNX 为什么不能直接用
很多框架封装了 rdb.SetNX(ctx, key, val, ttl),看起来一行搞定,但底层是否真走原子 SET key val NX PX ms 完全取决于 Redis 版本和客户端实现:
- Redis 2.6.12 以下不支持
PX参数,go-redis/v9 会 fallback 成SETNX+EXPIRE两步,中间若 panic 或网络断开,key 就永久卡住 - Redigo 的
Do("SETNX", ...)从不带过期时间,必须手动补EXPIRE,竞态无法避免 - 即使 v9 默认走原子命令,若你显式调用
rdb.SetNX而非rdb.Set,某些自定义 pipeline 或 mock 测试环境仍可能拆解执行
实操建议:一律改用 rdb.Set(ctx, key, value, ttl),它在 v9 中强制走 SET ... NX PX 原子命令;value 必须是 uuid.NewString(),别用固定字符串或时间戳。
解锁必须用 Lua 脚本,GET + DEL 是高危操作
HTTP handler 里先 rdb.Get(ctx, key) 判断值相等,再 rdb.Del(ctx, key),看似逻辑清晰,实则必然出错:
立即学习“go语言免费学习笔记(深入)”;
- A 拿着锁执行中,B 查到 key 过期(或刚好 TTL 到),B
GET返回空或旧值,B 立即SET新锁,A 接着DEL—— 删的是 B 刚设的锁 - Redis 主从异步复制下,A 在 master 上
GET到值,但 slave 还没同步,DEL发到 slave 就变成无效操作
正确做法是把校验和删除收进 Lua:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end调用时用 redis.NewScript(unlockScript).Run(ctx, rdb, []string{key}, clientID),返回 int64,非 1 就得立刻记录告警,不能重试——锁已不属于你。
长任务必须配看门狗续期,但续期本身要防“幽灵续期”
TTL 设为业务 P99 × 2(比如导出接口 P99 是 1.2s,就设 2 * time.Second),但遇到耗时不可控的任务(如第三方回调等待),光靠 TTL 不够,必须续期:
- 续期脚本也必须校验 value:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("EXPIRE", KEYS[1], ARGV[2]) else return 0 end - 续期间隔设为
TTL / 3(TTL=2s 就每 600ms 续一次),太密打满 Redis,太疏容易掉锁 - 业务函数
return前必须显式stopCh <- struct{}{}关闭续期 goroutine,否则锁释放了,后台还在不停续——变成“幽灵续期”,Redis key 永远不会过期
注意:所有续期请求必须复用同一个 context.WithTimeout,超时时间要比 TTL 短至少 200ms,避免续期命令卡在 Redis 队列里没发出,TTL 却已过期。
连接池与上下文超时是性能瓶颈的隐形推手
压测 QPS 上不去、大量 redis: connection pool timeout 报错,90% 是因为连接池和 context 配置反模式:
-
MaxIdle设太小(默认是 2)→ 高并发下排队;设太大(>100)→ 打满 Redismaxclients;建议 20–50 -
MinIdleConns必须 ≥5,否则 SLB/NAT 静默断连后,第一个请求必重连失败 - 所有 Redis 操作必须带
context.WithTimeout(r.Context(), 300*time.Millisecond),禁用context.Background();handler 卡 5 秒,不是 Redis 慢,是你没设超时 - 批量读写必须用
Send+Flush+Receive,循环里反复Do("GET", key)会让 RTT 放大百倍
最易被忽略的一点:加锁、续期、解锁三个操作必须走同一个 Redis 连接池实例。如果加锁用 poolA,续期误用了 poolB,网络抖动时命令乱序,续期可能作用在旧 key 上,导致锁提前失效。



















