关于redisson处理异步任务 一个需求狂踩数坑!!!
需求:如果当前用户正在执行异步任务时,调接口无法执行,抛出异常,等待其执行结束可继续创建
由于异步任务抛出异常,主线程无法感知,所以这里在 异步任务 外创建锁,采用redisson的分布式锁实现,代码如下
//执行异步任务接口
@PostMapping("...")
void autoCreateUser(@RequestBody AutoCreateUserDTO req) throws InterruptedException {
Long userId = SecurityUtils.getUserId();
RLock lock = redissonClient.getFairLock(String.format(RedisConstants.ASYNC_TASK, userId));
log.info("创建任务接口,线程Id:{}", Thread.currentThread().getId());
if (lock.tryLock(0L, 60L, TimeUnit.SECONDS)) {//不等待立即返回加锁成功与否,持有锁60秒自动释放
CompletableFuture.runAsync(() -> {
log.info("异步任务,线程Id:{}", Thread.currentThread().getId());
try {
userService.autoCreateUser(req);
} catch (Exception e) {
if(lock.isHeldByCurrentThread()){
log.info("释放锁,线程Id:{}",Thread.currentThread().getId());
lock.unlock();
}
}
}).exceptionally(e -> {
log.error("异步任务执行失败", e);
return null;
});
} else {
throw new RException(ResCode.REPEATED_REQUESTS_FROM_USERS, "正在执行创建任务,请稍后再试");
}
}
第一坑:@Async注解失效
这里如果不用CompletableFuture.runAsync() 而是用注解@Async标记异步任务时注意不要放在同一个类
(同一个类中的一个方法调用另一个标记了@Async的方法,会导致注解失效)
原因是:spring在扫描@Async注解时,会动态的生成一个子类代理类,代理类会继承自原来的bean;
该代理类会代为执行异步逻辑,如果@Async标记的方法被同一个类中的其他方法调用,该方法不会走代理类而是直接调用原来的bean,从而导致注解失效。
第二坑:异步任务无法获取应用上下文信息,如需使用,要在参数里进行传递
Long userId = SecurityUtils.getUserId(); //从jwt解析用户id
userService.autoCreateUser(req,userId);
第三坑:可重入锁被同一个线程调用时,tryLock会成功,可在redis中查看其重入次数发生变化 +1
刚开始使用isLocked()时一直返回false,只要键在redis中存在,就返回false,很疑惑,所以尝试使用tryLock获取锁后,再解锁这种笨办法;
但是由于是可重入锁,同一个线程调用tryLock ,就算没unlock,也会tryLock成功;
//获取任务状态接口
@PostMapping("...")
Integer autoCreateUserStatus() {
Long userId = SecurityUtils.getUserId();
RLock lock = redissonClient.getLock(String.format(RedisConstants.ASYNC_TASK, userId));
if (lock.tryLock()) {
try {
log.info("获取锁后线程Id:{}",Thread.currentThread().getId());
return 1;
} finally {
lock.unlock();
}
} else {
return 0;
}
}
这样写,一直无法满足需求,锁状态飘忽不定,莫名被锁,莫名又释放了。
通过输出线程id排查,发现各阶段输出的id值都不同
第四坑:异步任务runAsync 和 whenComplete方法都会开辟新线程执行任务
这里获取到的线程id不同,导致isHeldByCurrentThread()方法一直返回false
(该方法判断锁是否被当前线程持有)
if(lock.isHeldByCurrentThread()){
log.info("释放锁,线程Id:{}",Thread.currentThread().getId());
lock.unlock();
}
如果将判断去掉,则会报错:
java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node
解释:锁不被该线程持有,既runAsync、whenComplete方法都会使用全新的线程而非主线程执行任务 主线程创建的锁,不能被其他线程释放
第五坑:这里的问题就来了,锁是在主线程创建的,异步任务执行完之后,如何在主线程中释放锁呢?
最后发现 lock.unlockAsync() 可以传入线程id
RFuture<Void> unlockAsync(long threadId)
最终解决方案:
将接口线程id传递给异步任务的回调方法whenComplete(这个方法会在runAsync执行完成后回调)
在这里执行lock.unlockAsync(接口线程id),在接口线程中将锁释放,完美实现需求!
@PostMapping("...")
void autoCreateUser(@RequestBody AutoCreateUserDTO req) throws InterruptedException {
Long userId = SecurityUtils.getUserId();
RLock lock = redissonClient.getFairLock(String.format(RedisConstants.ASYNC_TASK, userId));
log.info("创建任务接口,线程Id:{}", Thread.currentThread().getId());
long thisId = Thread.currentThread().getId();
if (lock.tryLock(0L, 90L, TimeUnit.SECONDS)) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
log.info("异步任务,线程Id:{}", Thread.currentThread().getId());
userService.autoCreateUser(req);
}).exceptionally(e -> {
log.error("异步任务执行失败", e);
return null;
});
// 异步任务线程id不同,需在主线程中释放锁
future.whenComplete((result, ex) -> {
try {
log.info("释放锁,线程Id:{}", thisId);
lock.unlockAsync(thisId);
} catch (Exception e) {
log.error("释放锁失败", e);
}
});
} else {
throw new RException(ResCode.REPEATED_REQUESTS_FROM_USERS, "正在执行创建任务,请稍后再试");
}
}
//获取锁状态
@PostMapping("...")
Integer autoCreateUserStatus() {
Long userId = SecurityUtils.getUserId();
RLock lock = redissonClient.getLock(String.format(RedisConstants.ASYNC_TASK, userId));
log.info("获取锁状态,线程Id:{}", Thread.currentThread().getId());
return lock.isLocked() ? 0 : 1; //isLocked()获取锁状态,这个方法很好很单纯,一开始错怪它了,对不起
}