直接用 set 加锁不够用是因为锁超时后自动失效,导致其他节点抢占;需通过原子 lua 脚本校验 value 后 expire 续期,并严格绑定业务生命周期管理续期任务。

为什么直接用 SET 加锁不够用?
分布式锁最基础的实现是 SET key value NX EX timeout,但一旦业务执行时间超过 timeout,锁自动过期,其他节点就可能趁虚而入——这不是“锁没生效”,而是“锁提前失效”。自动续期的本质,是让持有锁的客户端在业务未完成时,持续延长锁的过期时间,同时确保只有加锁者能续期(防止误删他人锁)。
EVAL 脚本里怎么安全地续期?
续期必须原子执行:先校验当前锁的 value 是否匹配(即“我是原加锁者”),再更新过期时间。不能拆成 GET + EXPIRE,否则有竞态风险。典型 Lua 脚本如下:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end
说明:KEYS[1] 是锁 key,ARGV[1] 是加锁时设置的唯一标识(如 UUID),ARGV[2] 是新过期秒数。返回 1 表示续期成功,0 表示锁已丢失或被覆盖。
- 别用
TTL判断是否快过期再续——TTL 返回值可能已过时,且增加一次网络往返 - 续期间隔建议设为锁超时时间的 1/3~1/2(例如锁 30s,每 10~15s 续一次),避免频繁调用又留出容错窗口
- 脚本里不要用
redis.call("set", ...)替代expire,否则会重置 value,破坏唯一性校验
客户端怎么协调加锁、续期、释放?
自动续期不是开个后台线程无脑刷就行。它必须和业务生命周期绑定,且在锁释放时立即停掉续期任务,否则可能续一个已被删除的 key(虽无害但浪费资源)。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 加锁成功后,启动一个独立 goroutine / thread / Timer,按固定间隔执行上述 Lua 续期脚本
- 释放锁(
EVAL删除 key 并校验 value)后,必须显式取消续期任务——Java 可用ScheduledFuture.cancel(true),Go 可 close channel 控制 ticker - 如果业务异常中断(如 panic、OOM、进程 kill),续期任务可能残留。因此锁本身仍需设置合理
EX时间,作为兜底保障
Redis 连接断了续期脚本还有效吗?
无效。Lua 脚本执行依赖客户端与 Redis 的连接。如果网络闪断或 Redis 主从切换期间连接丢失,续期请求失败,锁会在原定过期时间后自动释放。这不是脚本问题,而是分布式系统固有约束。
应对方式不是拼命重试,而是:业务层做好幂等;锁超时时间预留足够缓冲(比如预估 10s 操作,设 30s 锁);必要时结合看门狗机制(如 ZooKeeper 临时节点 + session timeout)做多活兜底。单纯靠 Lua 脚本无法解决连接可靠性问题。
真正容易被忽略的,是续期任务的生命周期管理——它比脚本本身更难写对。很多线上故障不是锁逻辑错,而是续期线程没及时停,或者锁释放后脚本还在跑,干扰了下一轮加锁判断。










