Redis锁适合短时临界操作,需用go-redis/v9的SetNX+Lua解锁;Etcd锁适合强一致长时场景,直接用clientv3.NewMutex——因Redis分步SETNX+EXPIRE易致死锁,且DEL解锁不安全,而Etcd的Txn+Lease天然保障一致性与自动清理。

直接说结论:Redis 锁适合低延迟、高吞吐的短时临界操作(比如缓存预热、秒杀扣减),但必须用 go-redis/v9 的 SetNX + Lua 解锁;Etcd 锁适合强一致、长时持有、跨语言场景(比如订单状态机、配置变更),直接用 clientv3.NewMutex 就行——别自己拼 Put + Get。
为什么 Redis 的 SETNX + EXPIRE 是错的
常见错误是分两步:SETNX key value 成功后,再调 EXPIRE key 30。中间若进程 panic、网络中断或 GC 停顿超长,EXPIRE 永远不执行,key 就变成永不过期的死锁。
-
go-redis/v9的rdb.SetNX(ctx, key, value, ttl)底层走的是SET key value NX PX 30000单命令,原子完成互斥 + 过期 -
value必须是全局唯一标识,比如uuid.NewString(),不能用时间戳或固定字符串 - 过期时间不是越长越好:设为「99% 业务耗时 + 网络抖动余量」,比如业务通常 800ms 完成,就设
2s而非30s - 主从异步复制下,主节点写入后宕机未同步,从节点升主,可能两个客户端同时拿到同一把锁——这是协议层缺陷,无法靠客户端规避
为什么解锁必须用 Lua 脚本而不是 DEL
直接 DEL key 会删掉别人刚加的锁。因为 GET key 和 DEL key 之间存在竞态窗口:A 判断 key 存在且 value 匹配,正要删,B 此时已续期或重抢成功,A 的 DEL 就误删了 B 的锁。
- 正确脚本:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 key value -
go-redis/v9提供script.Load(rdb).Run(ctx, rdb, []string{key}, value).Val()直接执行,返回1表示删除成功,0表示非本人锁 - 别在
defer里调解锁:panic 可能导致 defer 不执行,锁残留;应显式控制释放时机
Etcd 的 Txn + Lease 为什么更可靠
Etcd 天然支持「条件写入 + 自动过期 + 事件通知」闭环,不用手动轮询、也不怕客户端崩溃。
- 加锁本质是
Txn:检查Version(key) == 0(key 不存在)才Put,否则失败,无竞态 - 租约(
Lease)绑定到 key 上,客户端崩溃后租约到期,key 自动删除,无需任何清理逻辑 - 续约不是定时调
Grant,而是监听KeepAlive返回的LeaseResponseChan;收到ErrLeaseExpired必须立刻放弃锁,不能重试 -
clientv3.NewMutex(s, key)已封装全部逻辑,mutex.Lock(ctx)阻塞直到成功或超时,mutex.Unlock(ctx)安全释放
重试和等待逻辑不能混用
Redis 没有服务端事件机制,等锁只能轮询;Etcd 有原生 Watch,锁一释放就能秒级唤醒,但断连必须 resume。
- Redis 等待建议:每次失败后
time.Sleep(50 * time.Millisecond),总等待上限设为5s或最多重试20次,否则网络分区时无限卡住 - Etcd Watch 必须带
WithRev(rev)指定起始 revision,断连重连后才能 resume,否则漏掉 key 删除事件 - 两者都必须限制最大等待时间——
ctx超时应设在Lock调用前,不是包整个业务函数 - 跨语言集成时,
etcd的 gRPC 错误码(如rpc error: code = FailedPrecondition)比 Redis 的文本响应(OK/(nil)/(integer) 1)更容易统一判断
真正难的不是加锁,是定义清楚「这把锁到底保护什么」:如果只是防并发写缓存,Redis 够用;如果涉及资金、库存、状态迁移,Etcd 的租约自动回收 + revision 严格序才是底线。别让锁本身成为单点故障源。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











