结论:必须用go-redis/v9的setnx(底层原子set)+lua校验解锁,或etcd/clientv3的txn+lease;禁用非原子的setnx+expire或无compare的put。

直接说结论:用 go-redis/redis/v9 调 SetNX + Lua 脚本做加锁/解锁,或用 clientv3 的 Txn + Lease 做 etcd 锁——别手写 SETNX + EXPIRE,也别裸调 Put 不带比较条件。
Redis 加锁必须用原子 SET 命令,不是两个分开操作
常见错误是先 SETNX 成功,再单独调 EXPIRE。中间若进程崩溃,key 就永远不超时,形成死锁。
- 正确做法是单条命令:
SET key value NX PX 30000,NX保证互斥,PX内置过期,原子完成 - Go 客户端里,
go-redis/v9的rdb.SetNX(ctx, key, value, ttl)底层已封装该语义,可直接用 - 但释放锁不能只
DEL,必须用 Lua 脚本比对value是否匹配,否则可能删掉别人刚拿到的锁 - value 必须全局唯一(比如
uuid.NewString()),不能用固定字符串或时间戳
Etcd 锁必须走 Txn + Compare,不能直接 Put
etcd 不是“设个 key 就行”,关键在“检查当前没锁再写入”,否则多个客户端同时 Put 会都成功,彻底失效。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 加锁必须用事务:
Txn.If(Compare(Version(key), "=", 0)).Then(OpPut(...)).Else(OpGet(...)) -
LeaseID必须由cli.Grant()分配,不能自己拼字符串或用时间戳——否则续期和自动过期都失效 - 续期要监听
KeepAlive返回的LeaseResponseChan,收到ErrLeaseExpired就得立刻放弃锁,不能只靠定时重连 - 解锁也必须 Txn:先
Compare当前 value 是否等于自己的 token,再Delete,缺一不可
Redis 和 etcd 的重试逻辑本质不同
这不是“风格差异”,是协议层决定的:Redis 没有服务端事件通知机制,etcd 有原生 Watch。
- Redis 锁失败后只能轮询:
time.Sleep(50 * time.Millisecond)后重试,需设最大重试次数(如 20 次)或总等待时间(如 5s),否则网络分区时卡死 - etcd 可用
Watch监听锁 key 删除事件,锁一释放就秒级唤醒;但 Watch 可能断连,必须实现reconnect + resume(用WithRev指定起始 revision) - 两者都必须限制等待上限,etcd 的
Watch断连后若没 resume,也可能无限挂起
跨语言或强一致场景优先选 etcd
Redis 协议是文本协议,不同语言 client 对 SET 返回值解析混乱:OK、(nil)、(integer) 1 在 Go/Python/Java 里映射类型不一致,容易漏判失败。
- etcd 是 gRPC 接口,错误码明确(如
rpc error: code = FailedPrecondition),返回结构统一,跨语言集成更稳 - etcd v3.5+ 支持
WithPrevKV,抢锁失败时能直接拿到当前持有者的 value,方便定位谁占着锁不放 - 如果业务允许“最多一次”语义(比如幂等写入),etcd 的 Lease 自动过期 + Watch 更可靠;若要求毫秒级响应且容忍短暂脑裂,Redis 更常见
最易被忽略的一点:无论是 Redis 还是 etcd,锁 key 的命名必须带业务上下文前缀(如 lock:order:123),避免不同模块共用一个 key 导致误删或冲突。这不是可选项,是上线前必须核对的硬规则。










