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