续期不能用普通EXPIRE命令,因其无法校验锁归属,B线程可能误续A线程的锁,破坏互斥性;必须用Lua脚本先检查UUID:threadId再pexpire,确保仅持有者可续期。

续期为什么不能用普通EXPIRE命令
因为普通 EXPIRE 命令无法校验锁归属。如果 A 线程持有锁,B 线程在续期时直接对 key 执行 EXPIRE,就会覆盖掉 A 的锁并延长过期时间——这等于把别人的锁“续”成了自己的,彻底破坏互斥性。
真实场景中,这种误续往往发生在锁已释放、但续期任务尚未取消的窗口期(比如 JVM GC 暂停导致 WatchDog 未及时感知线程退出)。
- Redis 单线程执行 Lua 脚本,天然隔离并发修改
- 续期脚本必须先
hexists检查UUID:threadId是否仍在 hash 中 - 只有匹配才执行
pexpire,否则直接返回 0,WatchDog 收到后会主动取消后续调度
WatchDog 的定时调度为什么必须基于 Netty HashedWheelTimer
不是不能用 ScheduledThreadPoolExecutor,而是它做不到「低开销 + 高精度 + 自动漂移补偿」。
看门狗默认每 lockWatchdogTimeout / 3(即 10 秒)触发一次,但业务线程可能卡在 GC、Full GC 或远程调用里。HashedWheelTimer 的时间轮结构能容忍短暂延迟,且不创建额外线程——所有续期任务共用一个 I/O 线程,避免线程爆炸。
- 若用普通定时器,在高并发锁场景下,每个锁都起一个
ScheduledFuture,极易耗尽线程资源 - HashedWheelTimer 的 tickDuration 默认 10ms,误差可控;而 JDK 定时器在负载高时 drift 可达数百毫秒
- 续期失败时,Redisson 会记录日志
Can't update lock expiration,但不会抛异常——这点容易被监控忽略
Lua 脚本里为什么要用 HINCRBY 而不是 SET
因为 Redisson 锁是可重入的,同一个线程多次 lock(),内部计数要累加;对应地,每次 unlock() 减一,归零才真正删 key。
如果续期时只用 SET 覆盖 value,就丢失了重入计数,导致 unlock() 时误判为「非持有者」而拒绝释放——业务上表现为:锁明明没释放,却再也 unlock 不掉。
-
HINCRBY对应的 field 是UUID:threadId,value 是重入次数(整数) - 续期脚本里
hincrby KEYS[1] ARGV[2] 0是安全的读取计数操作,不改变值但确认存在 - 直接
SET会抹掉整个 hash 结构,让后续解锁脚本里的hexists判定失败
主从架构下续期失败的真实表现
当 Redis 集群发生主从切换,原 master 上的锁 key 还没同步到新 master,WatchDog 发起的续期请求打到旧 master(已降级为 slave),而 slave 默认拒绝写请求,返回 READONLY You can't write against a read only slave。
此时 WatchDog 不会重试,而是静默取消该锁的续期任务——锁将在剩余 TTL 后自动过期,但业务线程仍以为自己持有锁,从而引发数据错乱。
- 这个错误不会抛出异常,也不会打印 ERROR 日志,只有 WARN 级别:
Unable to send unlock message或Failed to renew lock - 必须开启 Redisson 的
setRetryAttempts(0)并配合监控redisson_lock_expired指标才能捕获 - 跨机房部署时,网络分区比主从切换更隐蔽,续期请求超时后同样静默终止
Thread.interrupt() 或容器回收),续期任务立即失效——这点在异步线程池或 Spring @Async 场景下尤其危险。


















