异常信息:
ttempt to unlock lock, not locked by current thread by node id:*** thread-id: **
场景:
有一个耗时可能很长的业务方法,做了异步处理@Async放入线程池执行。在controller来创建了锁,Rlock作为参数传到异步方法内,异步方法执行完finally内unlock.这个时候controller已经直接返回,异步方法可能执行了10秒钟后进入finally执行lock.unlock(),这时候报错上边的异常信息
贴代码:
controller层
package com.example.study.redisson;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* <h3>study</h3>
* <p></p>
*
* @author : ZhangYuJie
* @date : 2022-05-08 19:43
**/
@Slf4j
@RestController
@RequestMapping("/test")
public class Controller {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedissonService redissonService;
/**
* 解锁异常情况模拟
*/
@SneakyThrows
@PutMapping("/redisson")
public void TestRedisson(){
RLock rlock = redissonClient.getLock("lock1");
// 不设置锁时长,利用redis的看门狗默认30s,10s自动续期到30s,等待异步方法执行结束释放锁
rlock.tryLock(5, TimeUnit.SECONDS);
// 这里是异步方法
redissonService.testRedisson(rlock);
System.err.println("结束");
}
}
service方法
package com.example.study.redisson;
import lombok.SneakyThrows;
import org.redisson.api.RLock;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
/**
* <h3>study</h3>
* <p></p>
*
* @author : ZhangYuJie
* @date : 2022-05-08 19:46
**/
@Service
public class RedissonService {
@SneakyThrows
@Async
public void testRedisson(RLock lock) {
try {
// 任何业务代码
System.err.println("testRedisson");
Thread.sleep(100000);
}finally {
// sleep过后这一行代码unlock报错
lock.unlock();
}
}
}
分析原因有可能是:
A线程直接返回了,Rlock是在A线程创建,B线程虽然拿着锁但是在线程内执行逻辑耗时很长执行到finally释放锁的时候,但是这个时候锁已经不存在了。
那么问题来了,Rlock没有设置过期时间,按理来说redis的看门狗会自动续期。为什么锁直接消失了
经过查看redis的看门狗实现发现:
分析原因有可能是:
A线程直接返回了,Rlock是在A线程创建,B线程虽然拿着锁但是在线程内执行逻辑耗时很长执行到finally释放锁的时候,但是这个时候锁已经不存在了。
那么问题来了,Rlock没有设置过期时间,按理来说redis的看门狗会自动续期。为什么锁直接消失了
经过查看redis的看门狗实现发现:
看门狗机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个 RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔 10 秒 (internalLockLeaseTime / 3) 检查一下,如果客户端 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的存活时间。
看到这里我们就知道为什么会看门狗失效了,因为我们A线程已经执行完结束,看门狗的看到我们的线程ID已经不存在了,自动动释放了锁,导致B线程unlock报错。
解决方案两种:
1、
package com.example.study.redisson;
import lombok.SneakyThrows;
import org.redisson.api.RLock;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* <h3>study</h3>
* <p></p>
*
* @author : ZhangYuJie
* @date : 2022-05-08 19:46
**/
@Service
public class RedissonService {
@SneakyThrows
@Async
public void testRedisson(RLock lock) {
try {
// 任何业务代码
System.err.println("testRedisson");
Thread.sleep(100000);
} finally {
//如果锁被当前线程持有,则释放。多一个判断,减少报错信息的出现
//在解锁之前先判断要解锁的key是否已被锁定并且是否被当前线程保持。如果满足条件时才解锁。
if (Objects.nonNull(lock) && lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
2、
在service的异步方法内加锁而不是在controller传入Rlock。
package com.example.study.redisson;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* <h3>study</h3>
* <p></p>
*
* @author : ZhangYuJie
* @date : 2022-05-08 19:43
**/
@Slf4j
@RestController
@RequestMapping("/test")
public class Controller {
@Autowired
private RedissonService redissonService;
@Autowired
private RedissonClient redissonClient;
/**
* 解锁异常情况模拟
*/
@SneakyThrows
@PutMapping("/redisson")
public void TestRedisson(){
if(redissonClient.getBucket("test:1").isExists()){
throw new Exception("线程在执行过程中,请稍后再试");
};
// 这里是异步方法
redissonService.testRedisson();
System.err.println("结束");
}
}
package com.example.study.redisson;
import lombok.SneakyThrows;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* <h3>study</h3>
* <p></p>
*
* @author : ZhangYuJie
* @date : 2022-05-08 19:46
**/
@Service
public class RedissonService {
@Autowired
private RedissonClient redissonClient;
@SneakyThrows
@Async
public void testRedisson() {
RLock lock = redissonClient.getLock("test:1");
// 不设置锁时长(tryLock如果设置锁时长看门狗会失效),利用redis的看门狗默认30s,10s自动续期到30s,等待异步方法执行结束释放锁
lock.tryLock(5, TimeUnit.SECONDS);
try {
// 任何业务代码
System.err.println("testRedisson");
Thread.sleep(100000);
} finally {
lock.unlock();
}
}
}
另外还遇到一种可能导致看门狗失效:
如果本地调试过程中方法断点了很长时间,会认为服务宕机了,看门狗机制线程也就没有了,也不会延长 key 的过期时间,到了 30s 之后就会自动过期了,其他线程就可以获取到锁