redisson给异步任务加锁

608 阅读3分钟

关于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()获取锁状态,这个方法很好很单纯,一开始错怪它了,对不起
    }