redis setnx 单靠超时会引发死锁,因其不支持原子性设置过期时间,客户端崩溃或两步操作中断会导致永久锁;业务超时释放锁后原节点误删他人锁。

为什么 Redis SETNX 单靠超时会引发死锁
因为 SETNX 本身不带过期时间,直接用它加锁等于“上锁不设表”——客户端崩溃后锁永远留在 Redis 里;哪怕你后续补 EXPIRE,两步非原子操作中间一旦中断(网络断开、panic、GC 暂停),就会留下无主锁。更隐蔽的问题是:业务执行慢于 TTL 时锁自动释放,其他节点趁机抢入,等原节点恢复后继续删锁,实际删的是别人的锁。
加锁必须用原子命令 + 唯一 token
不能分两步写 SET 再 EXPIRE,也不能用固定字符串当 value。正确做法是:
-
SET lock:order:123 uuid.NewString() NX PX 30000—— 一条命令完成写入、过期、互斥 - value 必须是全局唯一随机串(推荐
uuid.NewString()),不是时间戳、进程 ID 或硬编码字符串 - TTL 设为「预估最大耗时 × 2~3」,但上限不超过业务可容忍等待时间(比如订单处理最长等 5 秒,TTL 就别设 60 秒)
- key 命名带业务上下文前缀,如
lock:order-svc:order_123456,避免不同服务误删彼此锁
Lua 脚本续期和释放才是防误删的关键
续期或释放时若先 GET 再判断再操作,中间存在竞态窗口。必须用 Lua 在 Redis 端原子执行:
续期脚本:if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("PEXPIRE", KEYS[1], ARGV[2]) else return 0 end
释放脚本:if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end
- 调用时传入锁 key、当前 token、新 TTL(续期)或仅 token(释放)
- 返回
1表示成功,0表示 token 不匹配(已被覆盖或过期),此时应立即放弃而非重试 - 续期间隔设为 TTL 的 1/3~1/2(例如 TTL=30s,每 10~15s 续一次),留出网络与执行余量
- 不要无限续期:设定最大续期次数(如 20 次)或总存活时间(如 5 分钟),防止任务卡死却持续霸占锁
context 控制生命周期比 defer 更可靠
defer unlock() 在 panic 时可能不执行,goroutine 泄漏会让续期协程一直跑。真正可控的做法是:
- 加锁后立即启动一个带
context.WithCancel的续期 goroutine,并用sync.Once确保只启一次 - 业务函数外层用
defer+recover()捕获 panic,然后强制调用 Lua 释放逻辑(仍需校验 token) - 整个流程绑定同一个
context.Context,超时后主动 cancel 并触发释放 - 上线前用
redis-cli --scan --pattern "lock:*"定期抽检残留锁,观察是否出现长期未更新的 key
最易被忽略的是:锁的初始 TTL 是防死锁的最后一道防线,绝不能依赖续期来“兜底”。哪怕续期失败,锁也必须在设定时间后自动消失。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











