异步并发框架实现批量任务处理

420 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第17天,点击查看活动详情

异步处理,减少任务下载执行时间。

有篇文章看着流程图不错,可以借鉴一番

任务方法返回类型 : TaskResultType.class

public enum TaskResultType {
  // 任务执行成功 - 方法返回了需要的结果
  SUCCESS,
  // 任务执行失败 - 方法返回的是不需要的结果
  FAILURE,
  // 任务执行异常 - 方法执行抛出了异常
  EXCEPTION;
}

任务处理返回结果实体类 : TaskResult.class

public class TaskResult<R> {
    //方法本身是否运行正确的结果类型
    private final  TaskResultType taskResultType;
    // 方法返回业务结果数据
    private final R returnValue;
    // 方法返回失败的原因
    private final String reason;

  // 业务执行失败返回的结果
  public TaskResult(TaskResultType taskResultType, R returnValue, String reason) {
    this.taskResultType = taskResultType;
    this.returnValue = returnValue;
    this.reason = reason;
  }

  // 重载一个构造方法 业务执行成功返回的结果
  public TaskResult(TaskResultType taskResultType, R returnValue) {
    this.taskResultType = taskResultType;
    this.returnValue = returnValue;
    this.reason = TaskResultType.SUCCESS.name();
  }

  public TaskResultType getTaskResultType() {
    return taskResultType;
  }

  public R getReturnValue() {
    return returnValue;
  }

  public String getReason() {
    return reason;
  }

  @Override
  public String toString() {
    return "TaskResult [taskResultType=" + taskResultType
        + ", returnValue=" + returnValue
        + ", reason=" + reason + "]";
  }
}

存放到队列的元素: ItemVo.class

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * 存放到队列的元素
 */
public class ItemVo<T> implements Delayed {
  // 到期时间,单位毫秒
  private final long activeTime;
  private final T data;

  public ItemVo(long activeTime, T data) {
    // 将传入的时长 转为超时的时刻
    this.activeTime = TimeUnit.NANOSECONDS.convert(activeTime,TimeUnit.MILLISECONDS)+System.nanoTime();
    this.data = data;
  }

  public long getActiveTime() {
    return activeTime;
  }

  public T getData() {
    return data;
  }

  // 返回元素的剩余时间
  @Override
  public long getDelay(TimeUnit unit) {
    return unit.convert(this.activeTime - System.nanoTime(),TimeUnit.NANOSECONDS);
  }

  // 按照剩余时间排序
  @Override
  public int compareTo(Delayed o) {
    long d = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
    return (d==0)?0:(d>0?1:-1);
  }
}

提交给任务框架的工作实体类 : JobInfo.class

/**
 * 提交给任务框架的工作实体类:表示本批次需要处理的同性质任务(Task)的一个集合
 */
public class JobInfo<R> {

    // 任务名称 / 区分唯一的任务
    private final String jobName;
    // 工作的任务个数
    private final Integer jobLength;
    // 工作任务的处理器
    private final ITaskProcesser<?,?> taskProcesser;
    // 任务处理成功数量/原子变量负责计数
    private final AtomicInteger successCount;
    // 任务已处理数量 /总进度 /原子变量负责计数
    private final AtomicInteger taskProcessCount;
    // 任务处理返回结果队列 / 是一个阻塞队列/ 拿结果从头拿,放入结果从尾插 /防止阻塞队列发生冲突
    private final LinkedBlockingDeque<TaskResult<R>> taskDetailQueue;
    // 每个任务过期的时间 / 开发人员自己确定 /超过这个时间从缓存中清除掉
    private final long expireTime;

    public JobInfo(String jobName, Integer jobLength, ITaskProcesser<?, ?> taskProcesser,
        long expireTime) {
        this.jobName = jobName;
        this.jobLength = jobLength;
        this.taskProcesser = taskProcesser;
        this.successCount = new AtomicInteger(0);
        this.taskProcessCount = new AtomicInteger(0);
        this.taskDetailQueue = new LinkedBlockingDeque<TaskResult<R>>(jobLength);
        this.expireTime = expireTime;
    }

    // 对于不了解atomic的人员 所以返回int
    // 返回成功处理的结果数
    public int getSuccessCount() {
        return successCount.get();
    }
    // 返回当前已处理的结果数
    public int getTaskProcessCount() {
        return taskProcessCount.get();
    }
    // 返回失败处理的结果数
    public int getFailCount() {
        return taskProcessCount.get() - successCount.get();
    }
    // 任务处理总进度
    public String getTotalProcess(){
        return "Success["+successCount.get()+"]/Current["
            + taskProcessCount.get()+"] Total["+jobLength+"]";
    }

    public ITaskProcesser<?, ?> getTaskProcesser() {
        return taskProcesser;
    }

    // 获得任务详情
    public List<TaskResult<R>> getTaskDetail(){
        List<TaskResult<R>> taskList = new LinkedList<>();
        TaskResult<R> taskResult;
        // 从阻塞队列中拿任务处理结果,循环取,直到为null,说明队列中任务已经执行完了
        while ((taskResult = taskDetailQueue.pollFirst()) != null) {
            taskList.add(taskResult);
        }
//        taskList.add(taskDetailQueue.pollFirst());

        return taskList;
    }


    // 放入结果 - 以下操作是复合操作 队列总数与累计总数有可能会对不上
    // 从业务角度来说,保存最终一致性即可,所以该方法不用加锁,而如果加锁的话 这个方法会成为业务的一个瓶颈
    public void addTaskResult(TaskResult<R>  result,CheckJobProcesser checkJob){
        // 成功 累加成功的数量
        if(TaskResultType.SUCCESS.equals(result.getTaskResultType())){
            successCount.incrementAndGet();
        }
        // 不管成功与失败 从尾部 放入队列中
        taskDetailQueue.addLast(result);
        // 不管成功与失败 处理进度也要进行累加
        taskProcessCount.incrementAndGet();
        // 相等 说明任务已经执行完成
        if(taskProcessCount.get() == jobLength){
            checkJob.putJob(jobName,expireTime);
        }

    }

}

框架使用者要实现的任务接口: ITaskProcesser.class

import java.security.NoSuchAlgorithmException;

/**
 * 框架使用者要实现的任务接口,因为任务的性质再调用的时候才知道,所以传参用泛型,T是传参类型,R是返回类型
 *  通用多线程调度任务框架-业务场景:批量任务执行 并 要查看执行进度 且 对业务开发人员友好
 *  1、提高性能,采用多线程,屏蔽细节,封装线程池和阻塞队列
 *  2、每个批量任务有自己的上下文环境,需要一个并发安全的容器保存每个任务
 *  3、自动清除已完成和过期的任务
 * @param <T> 传入的参数
 * @param <R> 要返回的数据
 */
public interface ITaskProcesser<T,R> {

  /**
   * 任务提交方法
   * @param data 方法需要使用的业务数据
   * @return 方法执行后返回的业务结果
   */
  TaskResult<R> taskExecute(T data) throws InterruptedException, NoSuchAlgorithmException;

}

框架的主体类 : PendJobPool.class

import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 框架的主体类,也是调用者主要使用的类
 */
public class PendJobPool {

  // 线程池数量 = cpu核心(保守数量)
  private static final int THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors();

  // 要执行任务的任务阻塞队列
  private static final BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(5000);

  // 自定义线程池 ,固定大小,有界队列
  private static final ExecutorService taskExecutor = new ThreadPoolExecutor(THREAD_POOL_SIZE,
      THREAD_POOL_SIZE,60, TimeUnit.SECONDS,taskQueue);

  // 工作job的存放容器
  private static final ConcurrentHashMap<String,JobInfo<?>> jobInfoMap = new ConcurrentHashMap<>();

  //
  private static final CheckJobProcesser checkJob =  CheckJobProcesser.getInstance();

  // 对外暴漏的map
  public static Map<String,JobInfo<? >> getMap(){
    return jobInfoMap;
  };


  // 单例模式-----begin-----
  private  PendJobPool(){}
  // 使用延迟加载 实现线程安全
  private static class JobPoolHolder{
    public static PendJobPool pool = new PendJobPool();
  }
  public static PendJobPool getInstance(){
    return JobPoolHolder.pool;
  }
  // 单例模式-----end-----


  // 对工作中的任务进行包装,提交给线程池使用,并处理任务结果,写入缓存以供查询
  private static class PendingTask<T,R> implements Runnable {

    private final JobInfo<R> jobInfo;
    private final T processData;

    public PendingTask(JobInfo<R> jobInfo, T processData) {
      this.jobInfo = jobInfo;
      this.processData = processData;
    }


    @SuppressWarnings("unchecked")
    @Override
    public void run() {
      R r = null;
      ITaskProcesser<T,R> taskProcesser = (ITaskProcesser<T, R>) jobInfo.getTaskProcesser();
      TaskResult<R> result = null;
      try {
        // 调用具体实现方法
        result = taskProcesser.taskExecute(processData);
        // 做检查
        if(null == result){
          result = new TaskResult<>(TaskResultType.EXCEPTION,r,"result is null");
        } else if(null == result.getTaskResultType()){
          if(null == result.getReason()){
            result = new TaskResult<>(TaskResultType.EXCEPTION,r,"reason is null");
          } else {
            result = new TaskResult<>(TaskResultType.EXCEPTION,r,"result type is null ,"
                + "but reason:" + result.getReason());
          }
        }
      }catch (Exception e) {
        e.printStackTrace();
        result = new TaskResult<>(TaskResultType.EXCEPTION,r,e.getMessage());
      }finally {
        jobInfo.addTaskResult(result,checkJob);
      }

    }

  }

  // 根据工作名称检验并检索任务
  @SuppressWarnings("unchecked")
  public <R> JobInfo<R> getJob(String jobName){
    JobInfo<R> jobInfo = (JobInfo<R>) jobInfoMap.get(jobName);
    // 为空表示任务已经结束,而且到期从任务队列中清除掉了
    if(null == jobInfo){
      throw new RuntimeException(jobName+" -已经执行完,非法任务!");
    }
    return jobInfo;
  }

  // 调用者 提交工作中的任务
  public <T,R> void putTask(String jobName,T t){
    JobInfo<R> jobInfo = getJob(jobName);
    PendingTask<T,R> task = new PendingTask<T,R>(jobInfo,t);
    taskExecutor.execute(task);
  }

  // 注册JOB
  public <R> void registerJob(String jobName,int jobLength,ITaskProcesser<?, ?> taskProcesser,
      long expireTime){
    JobInfo<R> jobInfo = new JobInfo<>(jobName,jobLength,taskProcesser,expireTime);
    // 防止任务多次提交
    if(jobInfoMap.putIfAbsent(jobName,jobInfo) != null){
      throw new RuntimeException(jobName+"已经注册过了!");
    }
  }

  // 获得每个任务的任务详情
  public <R> List<TaskResult<R>> getTaskDetail(String jobName){
    JobInfo<R> jobInfo = getJob(jobName);
    return jobInfo.getTaskDetail();
  }

  // 获得工作的整体任务进度
  public <R> String getTaskProcess(String jobName){
    JobInfo<R> jobInfo = getJob(jobName);
    return jobInfo.getTotalProcess();
  }

}

需要定期处理过期的任务 : CheckJobProcesser.class

import java.util.concurrent.DelayQueue;

/**
 * 任务完成后,供再一定时间内查询,之后为之释放资源节约内存,需要定期处理过期的任务
 */
public class CheckJobProcesser {
    // 存放已完成等待过期的任务 队列
    private static final DelayQueue<ItemVo<String>> queue = new DelayQueue<ItemVo<String>>();

  // 单例模式-----begin-----
  private void CheckJobProcesser(){}
  // 使用延迟加载 实现线程安全
  private static class ProcesserHolder{
    public static CheckJobProcesser processer = new CheckJobProcesser();
  }
  public static CheckJobProcesser getInstance(){
    return ProcesserHolder.processer;
  }
  // 单例模式-----end-----

    // 处理队列中过期的任务 线程
    private static class FetchJob implements Runnable {

    // 处理到期任务的实现
      @Override
      public void run() {
        while (true) {
          try{
            // 拿到已经过期的任务
            ItemVo<String> item = queue.take();
            String jobName =  (String) item.getData();
            PendJobPool.getMap().remove(jobName);
            System.out.println(jobName+"is timeout,remove from jobInfoMap");
          }catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
      }
    }

  // 任务完成后,放入队列,经过expireTime后,从队列中移除
  public void putJob(String jobName,long expireTime){
    ItemVo<String> item = new ItemVo<>(expireTime,jobName);
    queue.offer(item);
    System.out.println("Job["+jobName+"已放入过期检查缓存,过期时长:"+expireTime+"]");
  }

  // 静态块,类初始化的时候就进行启动
  static {
    Thread thread = new Thread(new FetchJob());
    thread.setDaemon(true);
    thread.start();
    System.out.println("开启任务过期,检查守护线程............");
  }

}

然后进行测试: 我之前写了篇html 转为 PDF,我基于这个任务进行测试

转换工具类 : HtmlToPdfUtil.class

/**
 * wkhtmltopdf 转换工具类
 */
public class HtmlToPdfUtil implements ITaskProcesser<String,String> {

  @Override
  public TaskResult<String> taskExecute(String data) {
  // data 是传来的单条业务数据,以下写死的数据 应该从这个data中拿

    String destPath = null;
    try {

      // 手动搞一个失败的任务
      if(data.equals("19")){
        throw new RuntimeException("手动抛出异常");
      }


      long startTime = System.currentTimeMillis();
      log.info("generate pdf start ... ");

      // 初始化一个文件夹路径
      Calendar calendar = Calendar.getInstance();//得到日历
      calendar.setTime(new Date());   //设置当前日期
      String yearStr = calendar.get(Calendar.YEAR) + "";//获取当前年
      int month = calendar.get(Calendar.MONTH) + 1;//获取月份
      int day = calendar.get(Calendar.DATE);//获取天

      // 文件最终存放的文件夹路径
      String folderPath = "D:\pdf\" + yearStr + "\" + month + "\" + day;

      //////////////////////////////////以下模拟数据取请求\\\\\\\\\\\\\\\\\\\\\\\\\
      //1.拿取要加载的数据
      Map<String, Object> echartData = new HashMap<>();
      echartData.put("flagVal", 1);// 这个是标识,用于传入html中转换数据格式
      echartData.put("name", "wh");
      echartData.put("text", "Echart 折线图");
      List<String> xAxisList = new ArrayList<>();
      xAxisList.add("衬衫");
      xAxisList.add("羊毛衫");
      xAxisList.add("雪纺衫");
      xAxisList.add("裤子");
      xAxisList.add("高跟鞋");
      xAxisList.add("袜子");
      echartData.put("xAxisData", JSON.toJSONString(xAxisList));

      List<Object> seriesList = new ArrayList<>();
      Map<String, Object> seriesMap = new HashMap<>();
      seriesMap.put("name", "销量");
      seriesMap.put("type", "bar");
      int[] arr = {5, 20, 36, 10, 10, 20};
      seriesMap.put("data", arr);
      seriesList.add(seriesMap);
      echartData.put("series", JSON.toJSONString(seriesList));
      //////////////////////////////////以上模拟数据取请求\\\\\\\\\\\\\\\\\\\\\\\\\

      //2.填充html
      // 要使用的html模板
      String htmlTemplate = "test_template.html";
      // 要使用的html模板存放位置
      String templatePath = "D:\Code\owntest\keep-learn\ruoyi-admin\src\main\resources\templates";

      //临时文件名称
      // 临时 html 文件路径
      String srcPath = saveHtml(echartData, htmlTemplate, templatePath, folderPath, data);

      //3.执行html到pdf的转换
      // pdf文档存储路径,以及名称
      destPath = folderPath + "\"+data+".pdf";
      boolean convert = HtmlToPdfUtil.convert(srcPath, destPath);

      // html转pdf成功,删除临时html
      if (convert) {
        Files.delete(new File(srcPath).toPath());
        log.info("临时 {} DELETE SUCCESS", srcPath);
      }

      log.info("total time(ms)={}", System.currentTimeMillis() - startTime);

      return new TaskResult<String>(TaskResultType.SUCCESS,destPath);

    }catch (Exception e) {
      return new TaskResult<String>(TaskResultType.FAILURE,destPath+"处理失败",e.getMessage());
    }

  }

}

测试调用类:

import com.ruoyi.common.asynThreadTask.PendJobPool;
import com.ruoyi.common.asynThreadTask.TaskResult;
import com.ruoyi.common.pdf.HtmlToPdfUtil;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 异步并发框架生成pdf
 * 
 * @author wh
 */
@RestController
@RequestMapping("/pdf")
public class DownPdfController {
    private static final Logger log = LoggerFactory.getLogger(DownPdfController.class);

    private final static String JOB_NAME = "处理PDF";
    private final static int JOB_LENGTH = 20;

    /**
     * 生成某个班PDF
     * @Params classId 班级ID
     */
    @GetMapping("/create")
    public void createPDF(String classId) {

        // 此处应该有 通过classId 查询数据库,拿到要用的数据

        // JOB_NAME 固定任务名称
        // JOB_LENGTH 任务要处理的数量 = 查询数据库得到的数据长度


        //PDF业务实现类
        HtmlToPdfUtil myTask = new HtmlToPdfUtil();
        // 并发框架实例
        PendJobPool pool = PendJobPool.getInstance();
        // 注册job,传入 任务名称,任务长度,具体实例,过期长度
        pool.registerJob(JOB_NAME,JOB_LENGTH,myTask,1000*60);

        // 循环从数据库中查询到的数据 加入任务队列
        for(int i = 0; i < JOB_LENGTH; i++) {
            // 此处应该是单条数据
            String pdfName =  String.valueOf(i);
            // 依次推入Task
            pool.putTask(JOB_NAME,pdfName);
        }

    }

    /**
     * 查询任务进度详情
     * @Params jobName 任务名称
     */
    @GetMapping("/get")
    public void findTask(String jobName) {
        // 并发框架实例
        PendJobPool pool = PendJobPool.getInstance();
        // 拿取任务详情
        List<TaskResult<String>> taskDetail =  pool.getTaskDetail(JOB_NAME);
        if(!taskDetail.isEmpty()){
            System.err.println(pool.getTaskProcess(JOB_NAME));
            System.err.println(taskDetail);
        }

    }

}

以下为执行结果:

调用创建任务方法: image.png

查询任务进度:

image.png 成功19个,失败的那个是手动设置为失败的

当任务从过期队列中删除后,再查询任务进度报错,这个具体可以根据业务来修改。

image.png