Redis分布式锁死锁主因是锁身份失效后误删他人锁:SET加锁用唯一client_id,但业务超时导致锁过期,原线程仍用旧id解锁,删掉新持有者锁;必须用Lua原子解锁、独立守护线程续期、显式校验client_id。

Redis分布式锁为什么一加锁就死锁?
根本原因不是锁没释放,而是加锁和续期逻辑没对齐——SET 命令设了过期时间,但业务执行超时后锁自动过期,其他节点抢锁成功,而原线程还不知道,继续执行并尝试用错误的锁标识去 DEL,结果删掉别人刚持有的锁,导致后续多个节点同时写同一资源。这不是“锁没释放”,是“锁身份失效了还在用”。
常见错误现象:redis.clients.jedis.exceptions.JedisDataException: ERR Error running script (call to f_...): @user_script:1: user_script:1: attempt to access a non local key(Lua 脚本里用了非 EVAL 传入的 key)、或日志里反复出现「获取锁成功但业务未执行完」、「同一时刻两个服务都进了临界区」。
- 必须用原子命令加锁:
SET key random_value EX seconds NX,其中random_value是客户端唯一标识(如 UUID),不能用固定字符串 - 续期不能靠后台线程盲等:要用独立的守护线程,定期检查业务是否还活着,并只对「自己加的锁」调用
EVAL脚本续期 - 解锁必须用 Lua 脚本保证原子性:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock_key client_id
Redlock 真的能防止单点故障引发的死锁吗?
不能。Redlock 的设计目标是提升「锁服务可用性」,不是解决「锁误删」或「业务超时」问题。它在 Redis 集群部分节点失联时仍可能返回加锁成功,但此时已无法保证强一致性;更关键的是,它没改变单个锁的生命周期管理逻辑,续期、解锁、异常中断这些环节的坑一个都没少。
使用场景:只有当你有 5 个完全独立部署的 Redis 实例(不同物理机、不同网络分区),且容忍 TTFB(Time To Fail Back)在毫秒级波动时,Redlock 才比单实例略稳;绝大多数微服务架构下,它反而增加运维复杂度和时钟漂移风险。
- Redlock 的
clock drift补偿值(通常设为总耗时的 2%)极易被低估,尤其在容器化环境里,time()调用可能因 CPU 节流跳变 - 客户端需向 ≥3 个节点发起
SET请求,任意一个失败就重试,这会放大网络抖动影响,反而提高锁延迟 - 主流 SDK(如 Lettuce、Jedis)默认不内置 Redlock 实现,手写容易漏掉
quorum判断或过期时间归一化
用 Redisson 的 RLock 就万事大吉?
不是。Redisson 的 RLock 确实封装了自动续期(watchdog)、可重入、公平锁等能力,但它默认的看门狗续期周期是 30 秒,而这个值写死在 Config.lockWatchdogTimeout 里——如果你的业务方法平均耗时 45 秒,watchdog 在第 30 秒续一次,第 60 秒才续第二次,中间那 15 秒锁就空窗了。
性能影响:watchdog 是通过 Netty 定时任务驱动的,每 10 秒扫一次待续期锁(可配),如果并发持有锁的客户端多,这个定时任务本身会成为瓶颈;另外,RLock.lockInterruptibly() 在中断时不会自动解锁,得手动 unlock(),否则锁残留。
- 务必显式设置
Config.setLockWatchdogTimeout(60_000),值应 > 最长可能业务耗时 × 1.5 - 避免在
try块外写unlock():必须用try-finally或try-with-resources(配合RLock的autoUnlock模式) - 不要把
RLock当作跨服务事务协调器:它不保证多个 key 的原子性操作,比如「扣库存 + 写订单」仍需靠业务层补偿或本地消息表
真正该盯住的三个脆弱点
所有分布式锁方案都会在「锁标识一致性」「时间窗口对齐」「异常路径覆盖」这三处漏风。最容易被忽略的是第三个:90% 的死锁瘫痪不是发生在正常流程,而是在 catch 块里忘了 unlock(),或在 finally 里调用了解锁却没判断锁是否属于自己加的。
比如 Spring AOP 切面加锁时,如果切点抛出 RuntimeException,而切面没做 try-catch,unlock() 根本不会执行;又比如用 Future.get(timeout, TimeUnit) 做超时控制,timeout 触发后线程被中断,但锁对象还握在原线程手里,没人通知它该释放。
- 所有解锁操作前,必须校验当前锁的 value 是否等于本客户端的
client_id - 加锁时传入的
leaseTime不要依赖默认值,明确写成lock.tryLock(3, 30, TimeUnit.SECONDS)这类带等待+持有时间的组合 - 监控项不能只看「加锁成功率」,要加一条:「持有锁但未触发续期的锁数量」,这个指标突增就是 watchdog 失效的早期信号










