Go实现Redis分布式锁必须用SET key value EX seconds NX加锁、唯一value防误删、Lua脚本原子解锁、合理设置TTL并启用自动续期,单实例足够,Redlock反而增加复杂度。

Go里实现Redis分布式锁,别直接用 SET 命令裸写
直接用 redis.Client.Set 加锁看似简单,但极易出现死锁或误删锁——比如锁过期时间没设、没校验value唯一性、释放时没用Lua原子操作。真实业务中,SET key value EX seconds NX 是底线,但还不够。
- 必须为每个锁生成唯一value(推荐
uuid.NewString()),避免A线程删掉B线程的锁 - 过期时间(EX)要远大于业务执行时间,但也不能设成几小时——建议按 P99 业务耗时 × 2~3,再加缓冲(如 30s 业务 → 设 90s)
- 释放锁必须用 Lua 脚本:只在
key存在且value匹配时才DEL,否则返回 0
用 github.com/go-redis/redis/v9 的 Lock 方法?小心它默认不带自动续期
v9 官方客户端提供了 redis.NewLock,但它默认不启用 auto-renew(即“看门狗”机制)。一旦业务处理超时、锁过期,其他协程可能趁虚而入,导致并发冲突。
- 启用续期需显式传入
redis.WithAutoRenew(true),且必须配合redis.WithExpiration(30 * time.Second) - 续期间隔默认是过期时间的 1/3,不可改;若业务耗时波动大,建议自己控制续期逻辑(比如用
time.Ticker+EVAL刷新 TTL) -
Lock获取失败时返回redis.ErrNil,不是context.DeadlineExceeded,注意错误判断别写错
为什么不用 Redlock?多数场景它反而增加复杂度和延迟
Redlock(多节点独立加锁)常被误认为“更安全”,但它依赖时钟同步,在容器/K8s 环境下误差常 >50ms,反而削弱一致性。除非你有跨机房、强 SLA 要求的金融级场景,否则单 Redis 实例 + 正确的锁实现已足够。
- 单点 Redis 主从切换时仍可能丢锁(主挂了,从升主前锁丢失),此时应结合业务幂等性兜底,而非迷信 Redlock
- Redlock 的 5 节点要求、时钟漂移校验、多数派投票逻辑,会让锁获取延迟从 ~1ms 升至 ~5–10ms,对高频接口敏感
- 真正关键的是:锁粒度要细(比如按
user_id分片锁,而不是全局限流锁)、业务逻辑要可中断、失败要快速释放
一个最小可行的 Go 分布式锁封装示例
以下不是完整 SDK,而是去掉包装、直击核心的 20 行关键逻辑:
立即学习“go语言免费学习笔记(深入)”;
// 加锁
func TryLock(ctx context.Context, rdb *redis.Client, key, val string, ttl time.Duration) (bool, error) {
status, err := rdb.SetNX(ctx, key, val, ttl).Result()
return status, err
}
<p>// 解锁(必须 Lua)
const unlockScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
func Unlock(ctx context.Context, rdb *redis.Client, key, val string) error {
_, err := rdb.Eval(ctx, unlockScript, []string{key}, val).Result()
return err
}注意:TryLock 返回 true 仅代表 Redis 接受请求,不代表业务能安全执行——你要在 defer 里确保 Unlock 被调用,且 ctx 超时后主动 cancel 续期 goroutine。
最易被忽略的点:锁的 key 命名要有业务上下文(比如 "order:pay:12345"),不能只用 "lock";value 必须全局唯一且不可预测,否则攻击者可伪造 value 提前释放锁。


















