加锁必须用SET key value NX PX原子命令,禁止单独调用SetNX再设过期时间,否则存在竞态导致死锁;解锁须用Lua脚本校验value后删除,避免误删他人锁。

加锁必须用 SET key value NX PX,别拆成 SetNX + Expire
直接调用两步命令是高频出错点:SetNX 成功后网络抖动或 Redis 连接中断,导致 Expire 没发出去,锁就永久存在。Go 客户端(如 github.com/go-redis/redis/v9)的 client.SetNX(ctx, key, value, ttl) 底层会拼成原子命令 SET key value NX PX <ms></ms>,这才是安全起点。
常见错误现象:redis: nil 错误后请求卡住、后续所有请求都因锁残留而排队、日志里反复出现“failed to acquire lock”但没释放痕迹。
-
NX是强制要求——XX在首次加锁时永远失败,因为 key 一开始不存在 -
PX(毫秒级)比EX(秒级)更精确,尤其业务耗时在 200–800ms 区间时,设EX 1会导致锁残留整整 1 秒 - value 必须全局唯一,推荐
uuid.NewString();拼接时间戳或 PID 容易重复,不满足安全性要求
解锁必须走 Lua 脚本,DEL 单独调用等于埋雷
用 DEL 直接删 key 是最典型的误删行为:A 拿到锁,执行超时被踢出;B 此时成功加锁;A 的 defer 里执行 DEL,就把 B 的锁干掉了。Redis 单线程救不了三步逻辑(GET → 判断 → DEL),中间必有竞态窗口。
正确脚本只有这 5 行,必须原样使用:
立即学习“go语言免费学习笔记(深入)”;
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
endGo 中调用方式:client.Eval(ctx, script, []string{key}, value).Result(),返回值需判断是否为 int64(1),不是 nil 就代表删除成功。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 别用
redis.call("EXISTS")替代GET—— 多一次 call,且语义不如 GET 明确 - 如果用
redsync等封装库,务必确认其Unlock方法底层是否真调了这个 Lua 脚本(有些旧版只是简单Del) - value 校验必须严格相等,不能用模糊匹配或子串判断
Gin 中锁要绑定单次请求上下文,别塞进全局变量或结构体字段
把 lockKey 或 token 声明成全局变量、handler 外部的 struct 字段,会导致并发请求互相覆盖 value,A 的请求可能删掉 B 的锁,或者 B 加锁时因 key 已存在被拒——这不是锁机制问题,是作用域污染。
真实业务中,锁粒度往往和请求参数强相关,比如按 user_id 或 order_id 构造 key:
lockKey := fmt.Sprintf("order:lock:%s", c.Param("id"))
- 所有锁操作(加、续、解)必须使用同一个
ctx,且该ctx来自c.Request.Context(),不能用context.Background() - 不要在 middleware 里提前加锁再传给 handler——锁生命周期应由 handler 自己控制,否则异常 panic 时 defer 不触发
- 若需等待锁(如重试 3 次),每次重试都要生成新 value,不能复用上一轮的 token
redis.NewLock 默认不续期,手动续期要注意锁已丢失的边界
github.com/go-redis/redis/v9 提供的 redis.NewLock 看似开箱即用,但它默认关闭自动续期(auto-renew)。一旦业务处理时间超过 TTL,锁过期,其他 goroutine 就能抢入——而你的代码还傻乎乎地继续执行,造成双写。
启用续期必须显式传参:redis.WithAutoRenew(true) 和 redis.WithExpiration(30 * time.Second)。但注意:续期间隔固定为 TTL 的 1/3,不可调;若业务 P99 耗时波动大(比如 500ms → 2500ms),建议自己用 time.Ticker 配合 EVAL 刷新 TTL。
- 手动 renew 时检查
err == redis.ErrNil—— 这代表锁已被别人删除或过期,此时应立即中止业务,而不是继续 -
Lock()返回成功只表示加锁命令发出去了,不代表你真正进入了临界区;实际业务逻辑前最好再做一次轻量校验(比如查下 key 是否仍存在且 value 匹配) - Redlock 不要轻易上——除非你在跨机房部署且 SLA 要求 99.999%,否则单实例 + 正确实现 + 业务幂等性兜底,比 Redlock 更快更稳
锁本身不是银弹。真正容易被忽略的是:锁过期时间必须基于真实压测的 P99 耗时来设,而不是拍脑袋填 30s;value 的唯一性必须靠密码学随机生成,而不是时间戳拼接;还有,哪怕锁逻辑全对,网络分区时仍可能脑裂,所以关键路径一定要有幂等设计。

















