SpringBoot+Vue 后端异步多线程加前端实时更新的实现

Spring Boot在一些场景下需要用户发起请求服务器执行耗时任务(例如批量发送邮件),此时如果让用户一直等待任务执行,明显不合理,而且会很容易超时,这个时候正确的做法是后端采取异步多线程的方式执行任务,并保持任务执行的进度状态,前端使用定时器获取任务执行状态进行持续更新直到任务完成。具体的实现方法

1、后端异步多线程实现

一、springboot开启异步支持
  1. 在application入口类添加@EnableAsync注解

  2. 使用配置类为springboot配置一个线程池,此处必须实现AsyncConfigurer接口,并使用@Configuration进行注解

    @Configuration
    @EnableAsync
    public class AsyncTaskConfig implements AsyncConfigurer {
        /**
         * 核心线程数
         */
    //    @Value("${threadPool.corePoolSize}")
        private int corePoolSize = 10;
        /**
         * 最大线程数
         */
        private int maxPoolSize = 50;
        /**
         * 线程池缓冲队列容量
         */
        private int queueCapacity = 10;
        /**
         * 空闲线程销毁前等待时长
         */
        private int awaitTerminationSeconds = 10;
        /**
         * 线程名前缀
         */
        private String threadNamePrefix = "txzmap-tile-download";
    
        /**
         * ThreadPoolTaskExcutor运行原理
         * 当线程池的线程数小于corePoolSize,则新建线程入池处理请求
         * 当线程池的线程数等于corePoolSize,则将任务放入Queue中,线程池中的空闲线程会从Queue中获取任务并处理
         * 当Queue中的任务数超过queueCapacity,则新建线程入池处理请求,但如果线程池线程数达到maxPoolSize,将会通过RejectedExecutionHandler做拒绝处理
         * 当线程池的线程数大于corePoolSize时,空闲线程会等待keepAliveTime长时间,如果无请求可处理就自行销毁
         */
    
        @Override
        @Bean
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
            threadPool.setCorePoolSize(corePoolSize);
            threadPool.setMaxPoolSize(maxPoolSize);
            threadPool.setQueueCapacity(queueCapacity);
            threadPool.setAwaitTerminationSeconds(awaitTerminationSeconds);
            threadPool.setThreadNamePrefix(threadNamePrefix);
    
            //关机时,是否等待任务执行完
            threadPool.setWaitForTasksToCompleteOnShutdown(true);
            //设置拒绝策略
            //CALLER_RUNS:由调用者所在的线程执行该任务
            threadPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    
            //初始化线程
            threadPool.initialize();
            return threadPool.getThreadPoolExecutor();
        }
    
        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return null;
        }
    }
    
二、编写具体的任务方法

在具体的任务方法上使用@Async进行注解,表示该方法需要异步执行,这里没有指定线程池则为上面已经配置的默认线程池,如果有多个线程池可以通过@Async的value属性指定线程池

@Service
@Scope("prototype")
public class MailSendService {
    @Autowired
    TaskInfoList taskInfoList;
    @Async
    public void sendMail(String[] mails) throws InterruptedException {
        System.out.println("任务开始");        
        TaskInfoList.TaskInfo task = new TaskInfoList.TaskInfo();
        int size = taskInfoList.addNewTask(task);
        int process=0;
        int total=mails.length;
        for (int i = 0; i <= total; i++) {
           //发送邮件
                process++;
                doSendMail();
                System.out.println(Thread.currentThread().getName() + "Mission is running at " + process + "/" + total);
                task.setProcess(process);
                task.setStatus(TaskInfoList.TaskStatus.RUNNING);
                taskInfoList.update(task);            
        }
        System.out.println("mission completed!");
        //更新任务状态信息
        task.setStatus(TaskInfoList.TaskStatus.FINISHED);
        task.setEndTime(System.currentTimeMillis());
        task.setUrl("finished");
        taskInfoList.update(task);
    }
}
三、任务状态保持

为了记录当前任务的执行状态(耗时,完成进度,状态)等信息,就需要一个全局的方法对任务的状态进行维护,这里可以选用redis或者是一个简单的管理类来实现。由于项目较小,就没有必要为了这个功能而上Redis,如果项目已经有Redis了那么可考虑使用Redis,这里我们采用后者,编写一个简单的任务状态维护类。上代码

/**
 * 任务信息管理
 */
@Component
public class TaskInfoList {
    public enum TaskStatus {
        START, RUNNING, FINISHED
    }
    //任务信息类
    public static class TaskInfo {
        String uuid;
        String name = "";
        Long startTime;
        Long endTime;
        TaskStatus status;
        Integer total = 0;
        Integer process = 0;
        Integer size = 0;
        //最后生成的文件存放的地址
        String url = "";
        //省略getter setter
    }

    public static Map<String, TaskInfo> taskList = new HashMap<>();

    /**
     * 添加新任务并返回当前所有任务的总数
     *
     * @param task
     * @return
     */
    public Integer addNewTask(TaskInfo task) {
        taskList.put(task.getUuid(), task);
        return taskList.size();
    }

    /**
     * 更新任务状态
     *
     * @param task
     */
    public void update(TaskInfo task) {
        taskList.put(task.getUuid(), task);
    }
    /**
     * 根据uuid移除相关的任务信息
     *
     * @param task
     */
    public void remove(TaskInfo task) {
        taskList.remove(task.getUuid());
    }
    /**
     * 根据uuid查询
     *
     * @param uuid
     * @return
     */
    public TaskInfo search(String uuid) {
        return taskList.get(uuid);
    }

    //系统初始化的时候
    @PostConstruct
    private void init() {
    }

    //系统结束运行的时候
    @PreDestroy
    public void destroy() {
        //系统运行结束
        taskList.clear();
    }
}

具体状态维护业务代码可见上文中的业务代码。

四、Controller业务实现

这里需要实现两个接口

  1. 用于用户提交任务信息的接口

        @PostMapping("/sendMail")
        public MyResult sendMail(Integer cid) {
            //生成taskId
            String taskId = UUID.randomUUID().toString();
            String[] mails=mailService.getByCid(cid);
            try {
                MailSendService.sendMail(String[] mails);
            } catch (InterruptedException e) {
                e.printStackTrace();
                return new MyResult(0, "创建失败" + e.getMessage());
            }
            //返回rediskey 然后用户可以循环查询
            return new MyResult(1, "任务添加成功!", taskId);
        }
    

    最后将该任务的uuid返回给用户便于用户查询

  2. 提供任务状态查询的接口

    @GetMapping("/ps")
        public TaskInfoList.TaskInfo updateProcessStatus(@RequestParam(value = "id", required = true) String taskId) {
            return taskInfoList.search(taskId);
        }
    

2、vue前端进度更新的实现

  • 用一个map来维护当前的多任务状态

     data: {taskMap:new Map()}
    
  • 任务添加提交

     this.post(url, p, data => {
                        if (data.code == 1) {
                            this.$message.success('任务添加成功!');
                            //获取到任务id
                            let id = data.data;
                            this.taskMap.set('id',data);
               }
       });
    
  • 开启定时器持续获取所有任务的状态直到任务完成

    this.timer = setInterval(() => {
                            this.taskMap.forEach((value, key, map) => {
                                //如果该任务还在进行中 则继续获取状态 
                                if (value.status != 'FINISHED') {
                                    this.get(url + key, data => {
                                        console.log(data);
                                        //更新
                                        map.set(key, data);
                                        this.$forceUpdate();
                                    });
                                }
                            });
                        }, 1000);
    

    剩下的就是相关的view层的更新,具体实现很简单,大家根据项目情况进行修改就行,这里不赘述。

整个方案是本人在实际项目中的实践过程记录,由于项目较小不涉及高并发等复杂情况,仅供参考,欢迎交流,大佬勿喷!

Logo

前往低代码交流专区

更多推荐