分布式锁必须用原子操作实现:Redis需SET+NX+EX/PX单命令加锁、Lua脚本校验value后解锁续期;etcd需Lease+Txn CAS,且须主动保活与错误兜底。

直接用 sync.Mutex 集成进框架毫无意义——它在多实例部署下完全失效,所有“加锁成功”的日志都是假象。
Redis 加锁必须走原子 SET,别信 SetNX 封装的表象
很多 Go 框架(如 Gin + go-redis/v9)封装了 rdb.SetNX,看起来一行代码搞定,但旧版客户端或自定义 pipeline 可能把它拆成 SETNX + EXPIRE 两条命令。中间若进程 panic、OOM 或网络中断,key 就永远不超时。
必须强制走单条原子命令:SET lock:order:123 "a1b2c3" EX 30 NX:
-
EX是秒级,PX是毫秒级,必须显式指定,否则没自动释放机制 - 返回
"OK"才算抢锁成功,nil表示失败;不能只靠 error 判断 -
value必须是全局唯一字符串(推荐uuid.NewString()),后续所有校验都依赖它 - Go 框架里若用
client.Set(ctx, key, value, ttl),v9 默认走原子 SET,可放心;但若手动调SetNX,得确认底层是否真合并
解锁和续期必须用 Lua 脚本,且 value 校验不可跳过
直接 DEL key 或先 GET 再 DEL 是高危操作:A 加的锁,B 在 A 还没执行完时就删掉,数据就错乱了。
立即学习“go语言免费学习笔记(深入)”;
所有关键动作必须收进 Lua,在 Redis 单线程中原子执行:
- 解锁脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end - 续期脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("EXPIRE", KEYS[1], ARGV[2]) else return 0 end - 调用时传参顺序严格:
keys放 key,argv放 value 和 TTL;TTL 传整数,Lua 里不能当字符串比对 - 返回
int64(0)表示校验失败,业务应立即中止,而不是重试——锁已不属于你
etcd 方案更适合强一致场景,但 lease 生命周期管理极易被忽略
Redis 主从异步复制可能导致脑裂(两个节点同时认为自己持锁),etcd 的 Raft 日志同步能杜绝这点,代价是延迟稍高、API 更重。
etcd 锁不是“设个 key 就行”,核心在 Txn + Lease:
- 加锁前先
client.Grant(ctx, 15)拿 lease ID,再client.Put(ctx, key, clientID, clientv3.WithLease(leaseID)) - 必须用
Txn().If(Compare(Value(key), "=", clientID)).Then(...)做 CAS 校验,裸Put会彻底失效 - lease 不会自动续期,得另起 goroutine 调
client.KeepAliveOnce(ctx, leaseID);失败要主动Revoke并退出 - key 建议加前缀如
/locks/order/12345,避免和业务 key 冲突;context超时设太短,网络抖动时容易提前 cancel 导致 lease 过期
最易被忽略的点是锁的「生命周期管理」:Redis 里 value 不唯一、续期脚本漏校验、etcd 中 lease keepalive 没错误兜底——这些细节不出问题则已,一出就是线上数据错乱。


















