跳转到主内容
趣航编程网 - 趣学编程,启航技术之路!

如何在 Go 中实现基于 Redis 的分布式锁

直接用 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 8.2.3 Redis 8.2.3 是一款安全优先的高性能键值存储系统。该版本紧急修复了可能引发远程代码执行(RCE)的高危漏洞(CVE-2025-62507),并解决了 HyperLogLog 及 Cuckoo Filter 等数据结构在特定场景下的崩溃问题。建议所有用户立即升级,以保障生产环境的系统稳定与数据安全。 下载
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
触发,并检查锁是否仍属于自己。

相关文章