跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

Golang 实现一个简单的高性能分布式锁框架

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

相关文章