直接用redis.SetNX不够用,因其仅保证加锁瞬间原子性,无法解决锁过期业务未完成、误删锁、网络分区脑裂等问题;生产级分布式锁需满足互斥性、自动过期、锁续期、防误删四大要求。
为什么直接用
不够用
因为
只能保证「加锁瞬间」的原子性,但无法解决锁过期后业务还没执行完、其他节点误删锁、网络分区导致脑裂等真实问题。你写个
,上线跑两天大概率会遇到任务重复执行或死锁不释放。
真正可用的分布式锁必须同时满足:互斥性、避免死锁(自动过期)、容错性(锁续期)、防误删(只删自己加的锁)。Go 生态里最接近生产级的是
,但它依赖
且不支持 Redis Cluster 模式;自研时得绕开这些坑:
是唯一安全的加锁命令,不能拆成
解锁必须用 Lua 脚本校验
是否匹配,否则 A 加锁、B 过期后 A 删除,B 的锁就被干掉了
锁续期(renew)不能无脑
,得先检查锁是否还存在且归属当前客户端
用
算法还是单 Redis 实例
除非你的服务部署在跨机房、高延迟网络下,否则别碰
。它要求至少 3 个独立 Redis 实例,且每个实例都要做时钟漂移校准,实际落地时因 NTP 同步误差、主从切换延迟等问题,反而比单实例更不可靠。GitHub 上
自己都承认 Redlock 在实践中“没有明显优势”。
对大多数中型业务,用一个 Redis Sentinel 或 Redis Cluster 的主节点(非 proxy 模式)就够了,重点是把单点锁做稳:
立即学习
“
go语言免费学习笔记(深入)
”;
加锁时传入唯一
(比如
),不是固定字符串
锁过期时间设为「预估最大执行时间 × 2」,避免频繁续期
所有操作(加锁、续期、解锁)走同一个 Redis 连接池,避免连接抖动导致命令乱序
如何安全实现
和
关键不在逻辑多复杂,而在边界条件是否全覆盖。下面这段代码能跑通,但线上出问题往往是因为没处理好这几点:
解锁脚本必须严格校验 value:
返回
,超时错误(
)和连接错误要区分对待:前者重试,后者需降级或告警
必须返回结果,如果 Lua 返回 0,说明锁已被别人覆盖,此时记录 warn 日志,但不要 panic
别在 defer 里调用
—— 如果函数 panic,defer 执行时可能已失去上下文,
拿不到
续期机制怎么不拖垮 Redis
每 100ms 轮询一次续期?那锁一多就变成 Redis 的定时炸弹。正确做法是只对「正在执行的锁」做续期,且用最小必要频率:
启动续期协程前,先用
检查剩余 TTL,如果
就跳过本次续期
续期间隔设为锁 TTL 的
(比如锁 30s,每 10s 续一次),避免高频请求
用
缓存当前持有的锁信息(key → {value, expiry}),避免每次续期都查 Redis
续期失败三次后主动释放锁并报错,防止业务卡住不退出
最难缠的其实是锁持有者进程僵死但没被及时发现。这时候光靠续期不够,得配合业务层的心跳上报或超时熔断机制——分布式锁从来不是孤立组件,它只是整个可靠性链条里的一环。
redis.SetNXSetNXif redis.SetNX(key, val, expire) { ... }go-redsyncredigoSET key value NX PX millisecondsEXISTS + SETvalueEXPIRERedlockRedlockantirezclientIDhostname:pid:randTryLockUnlock// 加锁 Lua 脚本
local key = KEYS[1]
local value = ARGV[1]
local expire = ARGV[2]
if redis.call("set", key, value, "nx", "px", expire) then
return 1
else
return 0
end
// 解锁 Lua 脚本
local key = KEYS[1]
local value = ARGV[1]
if redis.call("get", key) == value then
return redis.call("del", key)
else
return 0
end
TryLockbool, errorredis: nilUnlockUnlockclientIDredis.PTTL< 500ms1/3sync.Map