redis分布式锁应使用set key value ex seconds nx保证原子性,value须为uuid,解锁用lua脚本校验;etcd更适强一致性场景,依赖租约和compareandswap。

Redis 实现分布式锁为什么常踩 SET 命令的坑
直接用 SET key value EX seconds NX 是最简方式,但很多人漏掉原子性校验和续期逻辑。Redis 官方推荐的 Redlock 已被证明在某些网络分区下不可靠,实际项目中更常用单节点 Redis + 正确的 SET 参数组合。
-
NX必须加,否则覆盖旧锁导致并发冲突 -
EX(而非PX)足够,除非需要毫秒级精度;但要注意 Go 的time.Second传给EX时别误转成纳秒 - value 必须是唯一标识(比如 UUID),不能写死字符串,否则解锁时无法判断归属
- 解锁必须用 Lua 脚本:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 key value,否则存在「误删他人锁」风险
ETCD 的 CompareAndSwap 比 Redis 更适合强一致性场景
ETCD 天然支持租约(lease)和带版本的原子操作,CompareAndSwap(即 txn)能避免 Redis 那种「过期但未及时删除」的残留锁问题。但它吞吐低、延迟高,不适合高频短锁场景。
- 锁 key 应带租约 ID,比如
/locks/order_12345,租约 TTL 设为锁预期持有时间的 2–3 倍 - 获取锁必须用
clientv3.Txn()做条件写入:Compare: []clientv3.Cmp{{Key: key, Version: 0, Result: clientv3.CompareVersion}},确保只在 key 不存在时写入 - 续约靠
clientv3.KeepAlive()维持租约,别手动调Grant(),否则易触发租约重置 - 注意 ETCD v3 的 key 是字节数组,
string(key)不等于原始字符串,比较时统一用string(kvs[0].Key)
Go 客户端选型:redis-go vs etcd/clientv3 的关键差异
github.com/go-redis/redis/v9 对 SET 原子命令封装友好,但默认不启用 pipeline,高并发下易打满 Redis 连接;go.etcd.io/etcd/client/v3 自带连接池和重试,但 txn 错误码分散(ErrCompacted、ErrFutureRev 都要单独处理)。
- Redis 客户端务必设置
PoolSize(建议 20–50),并禁用MinIdleConns(v9 默认为 0,够用) - ETCD 客户端初始化时必须传
clientv3.WithRequireLeader(),否则可能读到陈旧值 - 两者都建议包装一层
TryLock(ctx, key, ttl time.Duration) (bool, error)接口,屏蔽底层差异 - 别在 defer 里 unlock —— ETCD 租约一过期 key 自动消失,Redis 则必须显式调 Lua 解锁,defer 可能因 panic 来不及执行
锁失效边界:网络分区、GC 停顿、时钟漂移怎么影响实际行为
分布式锁没有银弹。Redis 单点故障时锁全失效;ETCD 集群脑裂时,follower 可能返回过期数据;而 Go 程序一次 STW 达 10ms 就可能让 100ms 的锁提前过期。
立即学习“go语言免费学习笔记(深入)”;
- Redis 场景下,业务侧必须容忍「锁提前释放」——比如订单支付中,用幂等 key(如
pay_order_12345)兜底重复提交 - ETCD 场景下,租约 TTL 至少设为业务处理耗时的 3 倍,并在关键步骤插入
lease.TimeToLive()检查剩余时间 - 绝对不要依赖系统时钟做锁超时判断——容器环境、VM 迁移都可能导致时钟跳变,所有超时一律以服务端时间(Redis 的
TIME、ETCD 的Revision)为准 - 测试时用
stress-ng --vm 2 --vm-bytes 2G模拟内存压力,观察 GC 是否拖垮锁续约
真正难的不是实现,是搞清你的业务能容忍哪类失败:丢锁?重复执行?还是长等待?选型前先写清楚这三行,比抄十篇示例代码有用。










