redis分布式锁必须补全四件事:加锁带唯一value、原子设置过期时间、lua脚本校验解锁、后台goroutine定期续期;否则无法应对服务崩溃、业务超时、误删锁等生产问题。

直接用 sync.Mutex 或 redis.SetNX 无法构建高可用分布式锁——前者只在单进程内有效,后者缺过期、缺校验、缺续期,线上跑不过三天。
为什么 SET key value NX EX 不是分布式锁的终点
它只解决“没人占着时我能抢到”,不解决“我占着但挂了怎么办”和“我解的是自己的锁吗”。真实系统里,你很快会遇到:
- 服务 panic 后锁没释放,后续所有请求卡死
- 业务耗时 45 秒,但锁设了 30 秒过期,第 31 秒另一个实例闯入写 DB
- A 加锁成功,B 看到锁过期去删,结果 A 还在执行,一删就把 B 刚抢到的锁干掉了
必须补全四件事:加锁带唯一 value(如 hostname:pid:goroutineID)、过期时间用 EX 或 PX 原子设置、解锁走 Lua 脚本校验 value、长任务配后台 goroutine 定期 PEXPIRE 续期。
解锁必须用 Lua 脚本,不能 GET + DEL
这是最常踩的坑。网络往返间隙里,锁归属可能已变更:GET 返回是你加的,DEL 发出去时已被别人续上——删掉的是别人的锁。
固定脚本要这么写:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Go 中调用示例:
res, err := redis.Eval(conn, luaScript, []string{lockKey}, lockValue).Int64()
if err != nil || res != 1 {
// 解锁失败,不是你的锁或已丢失
}
别把脚本拼在代码里,用 embed.FS 或 const 管理;redigo 下记得 conn.Close() 或归还连接池,否则命令发不出去。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
自动续期不是可选项,而是生产必需
锁过期时间 ≠ 业务执行时间,硬设一个值必然两难:设短了频繁丢锁,设长了故障后残留久。
正确做法是启动独立 goroutine 做“看门狗”:
- 续期前先
GET校验锁 still exists && value match,避免续错别人的锁 - 续期间隔设为过期时间的 1/3(比如锁 30 秒,每 10 秒续一次)
- 用
context.WithCancel控制 goroutine 生命周期,Unlock 时 cancel - 续期失败(如 Redis 连接断)要立即释放本地状态并报错,不能静默忽略
别用无脑 EXPIRE,它不检查 value;也别依赖 Redis 主从同步来保一致性——主挂从升,旧锁就丢了,这是 CAP 下的固有代价。
跨机房场景下,别碰 Redis 单集群或 Redlock
跨机房 RTT 高、时钟漂移不可控,Redlock 的“多数派写入+时钟校准”在现实中基本失效。你看到日志里 SET OK,但主节点宕机后从节点没同步,新主上根本没这个 key。
真要跨机房,选 Etcd:
- 用
clientv3.WithRequireLeader()强制读写都走 Leader - 租约 TTL 设为
min(15, P99_RTT × 3)(比如跨机房 P99 RTT 是 20ms,TTL 就设 15 秒) - 抢锁直接用
concurrency.NewMutex(session, "/lock/order/123"),别手写 Put/Get - 三中心部署(3+3+3),Raft 天然强一致,不靠时钟对齐
单机房或同城多活,Redis + 正确实现的 Lua + 续期机制已足够稳;追求强一致又不想运维 ZooKeeper,Etcd 是更现实的选择。锁本身不复杂,复杂的是边界——超时、崩溃、网络分区、时钟漂移,这些地方漏一点,线上就是重复扣款或数据错乱。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










