redis分布式锁必须用set key value nx px原子命令,禁止单独调用setnx再设过期时间,否则存在竞态导致死锁;解锁须用lua脚本校验value后删除,避免误删他人锁。

Redis分布式锁必须用 SETNX + PX 原子命令
直接调用 SetNX() 再单独设过期时间是错的——中间存在竞态窗口:如果 SetNX 成功但 Expire 失败,锁就永远不超时。Go 客户端(如 github.com/go-redis/redis/v8)提供原子写法:client.SetNX(ctx, key, value, ttl) 底层实际发的是 SET key value NX PX <ms></ms>。别手写两步逻辑。
常见错误现象:redis: nil 错误后锁没释放、某次请求卡住后整个系统逐渐堆积锁、偶发双写。
- 务必传入
context.WithTimeout,防止网络卡死阻塞 goroutine -
value必须唯一(推荐用 UUID 或os.Getpid()+goroutine ID 拼接),否则释放锁时可能误删别人持有的锁 - TTL 要明显大于业务执行最大耗时(建议至少 ×2),但不宜过长(如超过 30s),否则故障恢复慢
释放锁必须用 Lua 脚本保证原子性
只用 Del(key) 是危险的:A 拿到锁,执行超时被踢出,B 接着拿到锁;此时 A 的 Del 会把 B 的锁干掉。正确做法是用 Lua 脚本校验 value 再删:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
Go 中调用方式:client.Eval(ctx, script, []string{key}, value).Result()。注意返回值要判断是否为 int64(1),不是 nil 就代表删除成功。
- 不要用
GET + if + DEL三步模拟——Redis 单线程也救不了你,这三步之间可能被其他客户端插队 - 脚本里别用
redis.call("EXISTS"),GET更快且语义等价 - 若用 redsync 等封装库,确认它底层是否真用了 Lua(有些旧版本只是简单 Del)
框架集成时别让锁生命周期脱离 HTTP 请求上下文
在 Gin/echo 等框架中,常见错误是把锁变量声明成全局或结构体字段,导致并发请求互相覆盖 value 或 leaseID。锁必须绑定到单次请求的 context.Context 上。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
正确姿势:在 handler 里生成唯一 value,调用 acquireLock(),成功后用 defer 触发 releaseLock()。别试图“复用”锁实例。
- 避免在 middleware 里统一加锁——不同接口锁的 key 和 TTL 需求不同,硬套会锁粒度失控
- 如果业务需重试(如支付回调),锁 key 要包含幂等 ID(如
"pay_lock:" + req.OrderID),而不是固定字符串 - 别在 defer 里直接调用
releaseLock()而不检查是否真的拿到了锁,否则可能删错 key
防重试场景下,锁失败不能直接返回 500
用户重复提交、前端连点、消息队列重投,都会触发锁竞争。此时 acquireLock() 返回 false,不等于服务异常——而是预期中的流量控制信号。
应返回明确业务码(如 HTTP 425 Too Early 或自定义 {"code": 409, "msg": "操作进行中,请勿重复提交"}),并由前端做友好提示或自动轮询。强行重试只会加剧 Redis 压力。
- 别在锁失败时 sleep 后重试——这会让延迟不可控,且可能错过业务 SLA
- 对非关键路径(如日志上报、埋点),可降级为本地
sync.Map+ 时间窗口去重,避免压垮 Redis - 监控必须覆盖锁获取成功率、平均等待时长、超时释放率——这些指标比“锁有没有”更能反映真实风险
真正难的不是写对那几行 Lua 或选对 client 方法,而是把锁的语义嵌进业务流程里:谁负责生成唯一 value,超时后怎么兜底,失败时前端怎么配合,监控告警怎么关联 trace ID。这些地方一漏,再标准的实现也会在线上裸奔。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










