Go用Redis实现“自增锁”是错误概念,真正需要的是基于唯一随机token、原子SETNX加锁、Lua脚本解锁、自动续期机制的分布式锁。

直接说结论:Go 用 Redis 实现“自增锁”这个说法本身是错的,Redis 没有原生的“自增锁”,你真正需要的是带唯一标识、防误删、可重入(按需)、支持自动续期的分布式锁。所谓“自增”,大概率是你混淆了 INCR 命令的用途,或误以为锁 token 可以靠自增 ID 保证安全——这在分布式锁里不仅无效,反而引入严重风险。
下面从实操角度拆解关键点,聚焦 Go + go-redis/v8 的真实落地。
为什么不能用 INCR 当锁 token
有人试图这样写:client.Incr(ctx, "lock_counter").Val(),然后把返回值当锁标识去 set key。这是危险的:
-
INCR返回的是全局递增整数,所有客户端共享一个计数器 → 不同业务、不同锁 key 都可能拿到相同值,token 失去唯一性 - 没有绑定客户端上下文,A 拿到 123,B 也拿到 123,A 释放锁时用
DEL或 Lua 判断 value == 123,结果删掉 B 的锁 -
INCR本身不带过期,必须额外EXPIRE,两步操作非原子 → 中间崩溃就留死锁
真正该用的是随机字符串(如 uuid.NewString())作为 token,且全程只用一次:加锁时 set,解锁时比对。
SET key value NX PX ms 是加锁唯一安全起点
这是 Redis 分布式锁的原子基石,go-redis/v8 对应的是 SetNX 方法,但注意参数顺序和单位:
-
SetNX(ctx, key, value, expiration)中expiration是time.Duration,不是毫秒数;若你要设 30000ms,得传30 * time.Second(不是30 * time.Millisecond) - 必须同时满足
NX(不存在才设)+PX(毫秒级过期),缺一不可;只用SET或只用SETEX都无法兼顾原子性和自动释放 - value 必须全局唯一且不可预测(避免被猜出),推荐用
crypto/rand生成 16 字节以上随机字节再 hex 编码
示例:
token := make([]byte, 16) rand.Read(token) lockValue := hex.EncodeToString(token) ok, err := client.SetNX(ctx, "order:123", lockValue, 30*time.Second).Result()
解锁必须用 Lua 脚本,否则必然误删
这是生产环境踩坑最高发点。不能写 GET 判断再 DEL —— 网络延迟或 GC 暂停都可能导致“查到是自己的 token,但刚要删时锁已被别人续期或重抢”。唯一安全方式是 Lua 原子执行:
- 脚本内容固定:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - Go 中调用:
client.Eval(ctx, luaScript, []string{key}, lockValue).Result() - 返回值为
int64:1 表示删除成功,0 表示 token 不匹配(锁已失效或被他人持有) - 别省事写
Del直接删——99% 的线上锁故障源于此
超时时间不是拍脑袋定的,得覆盖最坏执行路径
锁过期时间(lease)不是越长越安全,也不是越短越好。它必须大于「业务逻辑最长可能耗时」+「网络往返 + Redis 响应毛刺」:
- 比如扣库存逻辑通常 200ms 内完成,但下游 DB 偶尔慢查询可能达 8s → lease 至少设 10s
- 如果业务耗时不可控(如调第三方 API),必须配看门狗(watchdog):另起 goroutine 定期用 Lua 续期(
GETSET或重EXPIRE),且仅当自己仍持锁时才续 - 设太短(如 1s)会导致锁提前释放,多个客户端并发进入临界区;设太长(如 1h)则故障后恢复慢,阻塞后续请求
没有银弹,每个锁 key 的超时值应根据对应业务 SLA 单独评估。
实际最难的部分不在加锁或解锁代码本身,而在于:如何确保同一个业务请求在整个生命周期里始终使用同一个 token,并在 panic/defer/ctx.Done() 等各种退出路径下都可靠释放锁。这需要封装成结构体并绑定 context,而不是零散调用函数。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











