redisson分布式锁必须用lua脚本,因redis单线程串行执行脚本,天然保证“判断锁是否存在+写入客户端id+设置过期时间”三步原子性;原生命令无法避免中间被其他客户端插入导致的锁失效或死锁。

Redisson 的分布式锁不是靠单条命令,而是靠 Lua 脚本保证加锁、解锁、续期三个动作的原子性。不理解这点,就容易在超时、重入、并发释放等场景踩坑。
加锁脚本为什么必须用 Lua?
Redis 原生命令无法在一个请求里完成「判断锁是否存在 + 写入客户端 ID + 设置过期时间」这三步,而中间任何一步被其他客户端打断,就会导致锁失效或死锁。Lua 脚本在 Redis 服务端串行执行,天然原子。
实际加锁脚本核心逻辑是:
- 若
KEYS[1](锁 key)不存在 → 直接hset写入客户端标识(ARGV[2],格式为UUID:threadId),计数设为 1,并pexpire设置 TTL(默认 30000ms) - 若锁存在且
hexists校验发现是当前客户端 →hincrby重入计数 +1,同时刷新 TTL - 若锁被其他客户端持有 → 返回
pttl剩余毫秒数,触发客户端阻塞等待
解锁脚本如何防止误删锁?
解锁必须严格校验:只有持有者才能删。否则 A 线程锁还没执行完,B 线程过期自动删锁,就会引发并发问题。
解锁 Lua 脚本关键检查点:
- 先
hexists判断KEYS[1]是否存在且持有者是ARGV[1](即当前客户端 ID) - 若不是持有者 → 直接返回 0,不做任何操作
- 若是持有者 →
hincrby计数 -1;若计数归零 →del删除整个 hash 结构,并publish到redisson_lock__channel:{lockName}通知等待线程
注意:publish 这一步不是可选的——它让阻塞中的线程能被唤醒重试,避免空轮询消耗 CPU。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
看门狗续期(renewExpiration)怎么工作?
业务执行时间不确定,但锁不能无限期存在。Redisson 默认启用看门狗,每 10 秒(internalLockLeaseTime / 3,默认 30s 锁租约 → 10s 续期间隔)用 Lua 脚本检查并重置 TTL。
续期脚本只做两件事:
-
hexists确认当前客户端仍是锁持有者 - 如果是 →
pexpire把 TTL 重置为原始值(如 30000ms);如果不是 → 不做任何事(说明锁已被释放或抢走)
这个机制意味着:只要客户端存活且没主动 unlock(),锁就不会因超时被释放;但一旦客户端进程崩溃、网络断开,看门狗停止心跳,锁会在 TTL 后自动过期。
公平锁的 Lua 实现和普通锁有什么区别?
公平锁多了一个有序等待队列,靠 Redis 的 zset 实现。对应 Lua 脚本里多了对 KEYS[2](即 redisson_lock_queue:{lockName})的操作:
- 加锁失败时,不是直接返回 ttl,而是
zadd把当前客户端 ID 和时间戳插入 zset,按分数排序 - 后续重试时,先
zrange KEYS[2] 0 0拿队首元素,只有匹配自己 ID 才允许加锁,否则继续等待 - 加锁成功后,立即
zrem自己的记录,避免重复排队
这个设计让等待线程严格按请求顺序获取锁,但代价是每次加锁/解锁都要读写两个 key(锁 hash + 队列 zset),性能略低于非公平锁,且依赖 Redis 的 zset 功能,集群模式下需确保 key 分布在同一 slot。
真正容易被忽略的是:所有这些 Lua 脚本都运行在 Redis 服务端,客户端只负责传参、解析返回值、控制重试逻辑。如果你手动拼接命令或拆分操作,哪怕只漏掉一次 hexists 校验,就等于把分布式锁退化成“看起来像锁”的竞态条件。










