Redis SETNX 不足以支撑生产级分布式锁,因其缺乏自动释放、防误删、续期和死锁处理能力;需结合唯一token、Lua原子删除、超时机制及租约管理,或改用Etcd的Lease+CAS实现更可靠的云原生锁。

为什么 Redis SETNX 不足以支撑生产级分布式锁
直接用 SETNX 加锁看似简单,但会遇到锁不释放(进程崩溃)、锁被误删(A 加锁后超时释放,B 获取锁,A 仍执行并删除 B 的锁)、无法续期等问题。Golang 里如果只依赖原生命令封装,大概率在线上出现偶发性任务重复执行或死锁。
真正可用的方案必须包含:唯一锁标识(防止误删)、自动续期(避免业务执行慢导致提前释放)、可重入判断(非必需但高并发下有用)、释放时严格校验所有权。
- 锁 value 必须是客户端生成的随机 UUID(如
uuid.NewString()),不能用时间戳或固定字符串 - 加锁必须使用 Lua 脚本保证原子性:
SET key value NX PX timeout_ms - 续期操作也要用 Lua 校验 value 是否匹配,再更新过期时间
- 释放锁必须用同一段 Lua 脚本:先
GET再比对再DEL,三步不能拆开
Redlock 算法在 Go 中的实际取舍
Redis 官方推荐的 Redlock(向多个独立 Redis 实例请求锁)理论更安全,但实际项目中极少采用——它要求至少 3 个独立 Redis 节点、时钟同步、网络分区容忍等条件,运维成本远高于收益。多数业务场景下,单主 Redis + 合理超时 + 健康检查已足够。
除非你的服务涉及金融级资金操作且能承担多实例延迟与复杂度,否则别硬套 Redlock。Go 生态里 go-redsync 库虽支持 Redlock,但默认配置容易因节点响应不一致返回假阴性(锁未获取成功却误判为失败)。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 优先选单 Redis +
redis-go自行封装,可控性强 - 若用
go-redsync,务必调大quorum和retryDelay,并监控err != nil时的真实原因(常是网络抖动而非锁冲突) - 不要把 Redlock 当成“更高级”选项——它解决的是极小概率的脑裂问题,不是日常并发控制
用 go-redis 封装一个带自动续期的锁
核心难点不在加锁,而在后台续期协程与锁生命周期的绑定。常见错误是启动 goroutine 后忘记回收,导致锁对象被 GC 前续期仍在跑,最终误续其他请求的锁。
正确做法是:锁结构体里保存 ctx 和 cancel,续期 goroutine 监听 ctx.Done();释放锁时显式调用 cancel();加锁失败直接返回,不启续期。
type RedisLock struct {
client *redis.Client
key string
value string
cancel context.CancelFunc
}
func (l *RedisLock) Acquire(ctx context.Context, timeout time.Duration) bool {
l.value = uuid.NewString()
script := redis.NewScript(`
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
`)
ok, err := script.Run(ctx, l.client, []string{l.key}, l.value, int64(timeout.Milliseconds())).Result()
if err != nil || ok != int64(1) {
return false
}
// 启动续期,仅当加锁成功才启动
ctx, l.cancel = context.WithCancel(ctx)
go l.keepAlive(ctx, timeout/3)
return true
}
-
keepAlive每隔timeout/3执行一次 Lua 续期脚本,超时前最后一次续期必须成功 - 续期脚本必须校验当前 value 是否等于本实例的
l.value,否则跳过 - 释放锁时先调
l.cancel(),再执行 Lua 删除逻辑,避免续期 goroutine 干扰
etcd vs Redis 分布式锁怎么选
etcd 的 CompareAndSwap(CAS)天然支持租约(lease)和监听,语义比 Redis 更清晰;但它的 QPS 上限低、延迟高,不适合秒级高频抢锁场景(比如每秒数千次库存扣减)。Redis 在性能和生态成熟度上胜出,但需自行处理脑裂和时钟漂移风险。
选型关键看 SLA:如果你的业务允许 500ms 内完成锁操作且能接受极低概率的双写,用 Redis;如果锁操作本身不频繁(如每分钟一次配置变更)、但必须强一致性,etcd 更稳。
- etcd 客户端推荐用
go.etcd.io/etcd/client/v3,注意 lease TTL 刷新要单独 goroutine 维护 - Redis 方案里,务必在加锁前检查
client.Ping(),避免因连接断开导致静默失败 - 无论哪种,都不要在锁内做 HTTP 调用或数据库长事务——锁持有时间应控制在毫秒级
最常被忽略的一点:锁的 key 设计必须带业务上下文,比如 "order:pay:12345" 而不是 "lock"。否则不同业务线可能互相干扰,排查时连日志都对不上。

















