必须用 client.Set 原子设置带超时的互斥锁,因其单命令实现 SET key value NX EX,避免 SETNX+EXPIRE 两步导致的死锁;value 必须为唯一 UUID,ttl 设为业务最大耗时的 2–3 倍,解锁须用 Lua 脚本校验 value 后删除。

用 client.Set 原子设置带超时的互斥锁
不能拆成 SETNX + EXPIRE 两步,否则进程崩溃在中间就会留下无过期时间的死锁 key。Go-redis/v9 的 Set 方法支持原子写入值、条件(NX)、过期时间三合一:client.Set(ctx, key, value, ttl) 内部实际发的是 SET key value NX EX 30 这类单条 Redis 命令。
关键点:
-
value必须全局唯一,推荐用uuid.NewString(),不能写死为"1"或"locked" -
ttl建议设为业务最大耗时的 2–3 倍,比如下单逻辑最长 5 秒,就设15 * time.Second - 不要用
SetNX单独方法——它不支持同时传过期时间,容易误用 - 返回
ok == false表示锁已被占用,不是错误,别直接return err
用 Lua 脚本安全释放锁,防止误删
直接调 client.Del(ctx, key) 是危险操作:A 拿到锁后执行慢,锁已过期被 B 重新获取,A 最后一拍脑门 DEL,就把 B 的锁干掉了。必须用 Lua 脚本做「读-判-删」原子操作。
标准脚本长这样:
立即学习“go语言免费学习笔记(深入)”;
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
endGo 里封装成 redis.NewScript(...),执行时传入 key 和当初生成的 value(即 clientID):
- 脚本返回
1表示删除成功 - 返回
0表示 key 不存在,或值不匹配——此时不应报错,而是静默忽略 - 别自己拼
GET+DEL,网络往返+判断间隙就是竞态根源
加锁失败后要不要重试?怎么控制频率
业务场景决定是否重试。比如支付回调这种强一致性操作,通常要等锁;而日志上报这类可丢数据,直接放弃更合理。如果选择重试,注意别压垮 Redis。
实操建议:
- 用指数退避,比如首次等
10ms,第二次30ms,第三次100ms,最多不超过 5 次 - 每次重试前检查上下文是否已取消(
ctx.Err() != nil),避免 goroutine 泄漏 - 别用固定间隔轮询,比如每
50ms问一次——高并发下 Redis QPS 会飙升 - 超时时间应略大于锁的
ttl,否则可能刚拿到锁就因 context 超时被 cancel
单实例 Redis 锁够不够用?什么情况下必须上 Redlock
绝大多数业务用单节点 Redis + 正确实现的锁就足够了。Redlock 不是银弹,它解决的是「主节点宕机、从节点升主后锁丢失」这个 CAP 下的固有风险,但代价是复杂度陡增、性能下降、且仍不能 100% 避免问题。
你该考虑 Redlock 的真实信号:
- 业务对「绝对不可重入」有金融级要求(如账户扣款),且已确认单节点故障率无法接受 Redis 集群部署了 5 个完全独立的实例(非主从),并能容忍其中 2 个不可用
- 团队已稳定维护 Redlock 客户端多年,且监控覆盖所有锁操作成功率与延迟
- 别因为「听说 Redlock 更高级」就上——它带来的运维负担和 bug 风险远超收益
真正容易被忽略的是锁续期:长任务(如导出大文件)必须在 ttl 过半时主动刷新过期时间,否则锁提前释放会导致并发。这需要额外心跳机制,不是基础锁 API 自带的。


















