Redisson RLock解锁异常排查:IllegalMonitorStateException问题分析
问题现象
业务高峰期偶发抛出异常:
java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: SameId thread id: DifferentThreadId
场景是用RLock实现分布式锁,逻辑为尝试获取锁(等待5秒),成功后执行业务,最后在finally块解锁;未获取锁则执行其他逻辑。
相关代码
核心业务代码
RLock lock = lockService.getLock(TOPICNAME_LOCK_PREFIX + topicPrefix); if (lock.tryLock(5, TimeUnit.SECONDS)) { try { // 业务处理代码 } catch (Exception e) { throw new ServiceException(String.format("error: %s!", e)); } finally { lock.unlock(); } } // 未获取锁时的处理逻辑
LockService实现
private static RedissonClient redissonClient; private static synchronized void initClient(String url) { if (redissonClient == null) { try { URI uri = new URI(url); Config config = new Config(); SingleServerConfig srvConfig = config.useSingleServer() .setAddress(String.format("%s://%s:%s", uri.getScheme(), uri.getHost(), uri.getPort())); if (StringUtils.isNotBlank(uri.getUserInfo())) { srvConfig.setPassword(uri.getUserInfo()); } config.setLockWatchdogTimeout(10000); redissonClient = Redisson.create(config); } catch (Exception e) { log.error(String.format("cannot init redisson client by %s", url), e); } } } @PostConstruct private void init() { initClient(redisProperties.getUrl()); } public RLock getLock(String key) { initClient(redisProperties.getUrl()); return redissonClient.getLock(key); }
依赖版本
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.17.7</version> </dependency>
问题原因分析
这个异常的本质是当前线程尝试解锁一个已经不属于自己的锁,结合高峰期出现的特点,主要由以下因素共同导致:
锁过期与看门狗续期失效
你设置了lockWatchdogTimeout=10000(10秒),调用tryLock(5, TimeUnit.SECONDS)时Redisson会启用看门狗自动续期,但高峰期业务执行时间可能超过10秒,或者Redis与应用间网络波动导致续期请求延迟/失败,锁被Redis自动释放。此时其他线程会成功获取该锁,原线程执行完业务后调用unlock(),就会抛出异常。代码未处理锁已释放的边界情况
finally块中直接调用lock.unlock(),没有先判断当前线程是否仍持有锁。即使锁因过期或其他原因被释放,原线程依然会执行解锁操作,触发异常。
至于你问是网络问题还是代码问题:网络波动是诱因,但代码未处理锁过期后的解锁逻辑是直接触发异常的原因。
优化方案
即使你已经用RPermitExpirableSemaphore解决了问题,针对RLock的场景可以做以下优化避免同类问题:
解锁前判断线程持有状态:在finally块中先检查当前线程是否持有锁,再执行解锁:
finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }显式指定锁持有时间:如果业务执行时间有明确上限,调用
tryLock(waitTime, leaseTime, unit)显式设置锁的持有时间,避免看门狗续期的不确定性:// 例如设置锁持有时间为30秒,根据实际业务调整 if (lock.tryLock(5, 30, TimeUnit.SECONDS)) { // ... }优化业务性能:高峰期业务执行时间变长是根源,优化业务逻辑减少执行耗时,从根本上降低锁过期的概率。
内容的提问来源于stack exchange,提问作者ShawnChen

