直接用 set key value ex seconds nx 不够安全,因其仅保障加锁原子性,未解决解锁原子性、误删他人锁、过期时间不合理及主从切换导致多客户端持锁等问题;正确做法是加锁写入唯一 token,解锁通过 lua 脚本比对 token 后删除。

为什么直接用 SET key value EX seconds NX 不够安全
很多人以为用 Redis 的原子命令就能实现分布式锁,但实际生产中会遇到几个关键问题:锁被别的客户端误删、过期时间设置不合理导致业务没执行完锁就释放、网络分区时主从切换造成多个客户端同时持有锁。最典型的现象是——两个 goroutine 都认为自己拿到了锁,然后并发修改了共享资源。
根本原因在于:SET ... NX 只保证加锁原子性,不保证解锁原子性。如果用 DEL key 删除,而此时锁已被别人续期或重置,就会误删他人持有的锁。
正确做法是:加锁时写入唯一标识(如 UUID),解锁时用 Lua 脚本比对标识再删除,确保“谁加的锁谁删”。
如何用 Lua 脚本保证解锁的原子性
Redis 执行 Lua 脚本是原子的,适合做带条件的删除。核心逻辑就是:只有 key 存在且值等于当前客户端的 token,才删除。
示例脚本:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
在 Go 中调用时注意:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
redis.NewScript(1, script)中的1表示脚本接受 1 个 key 参数 - 调用
script.Eval(ctx, rdb, []string{lockKey}, token),token必须和加锁时一致 - 返回结果为
int64,1表示删除成功,0表示锁不存在或 token 不匹配
加锁失败后要不要重试?怎么设超时和重试间隔
分布式锁本质是竞争资源,重试是常态,但盲目轮询会打爆 Redis 或阻塞 goroutine。关键不是“要不要重试”,而是“怎么退避”。
建议策略:
- 初始等待
50ms,每次失败后指数退避(×1.5),上限设为500ms - 总超时时间必须小于业务操作预期耗时,比如业务最多处理 3s,那锁获取总等待别超过
1.5s - 避免用
time.Sleep硬等,改用time.AfterFunc或带 cancel 的timer,方便上下文取消 - 加锁时的
EX时间不能只看平均耗时,要预留网络抖动 + GC 停顿 + 主从同步延迟,一般设为业务最大耗时的2–3 倍
Redlock 算法在 Go 里有必要实现吗
Redis 官方推荐的 Redlock(向 ≥3 个独立 Redis 实例请求锁)在大多数场景下是过度设计。Go 应用通常部署在单集群内,背后是哨兵或 Cluster 模式,主从自动故障转移已足够可靠。
除非你有跨机房、多云、或强金融级一致性要求,否则:
- 不要自己实现 Redlock —— 它无法解决时钟漂移,且增加了复杂度和延迟
- 优先用单节点 Redis + 正确的加/解/续期逻辑,再配合监控(如记录锁等待时间、冲突次数)
- 如果真需要更高可用,用
redis-go-cluster连接 Cluster,但锁 key 必须用{...}包裹确保落到同一 slot,否则EVAL会失败
真正容易被忽略的是锁续期(renewal):长任务必须在过期前调用 EXPIRE 延长 TTL,且续期操作本身也要防重复 —— 最好由独立 goroutine 负责,用 time.Ticker 触发,并检查锁是否仍属于自己。










