开启掘金成长之旅!这是我参与「掘金日新计划 · 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);
}
}
}
以下为执行结果:
调用创建任务方法:
查询任务进度:
成功19个,失败的那个是手动设置为失败的
当任务从过期队列中删除后,再查询任务进度报错,这个具体可以根据业务来修改。