實現一個併發任務執行框架

IT王小二發表於2021-06-23

實現一個併發任務執行框架。

問題參考來自網路

一、需求產生和分析

公司裡有兩個專案組,考試組有批量的離線文件要生成,題庫組則經常有批量的題目進行排重和根據條件批量修改題目的內容。

架構組通過對實際的上線產品進行使用者調查,發現這些功能在實際使用時,使用者都反應速度很慢,而且提交任務後,不知道任務的進行情況,做沒做?做到哪一步了?有哪些成功?哪些失敗了?都一概不知道架構組和實際的開發人員溝通,他們都說,因為前端提交任務到 Web 後臺以後,是一次要處理多個文件和題目,所以速度快不起來。提示用多執行緒進行改進,實際的開發人員表示多執行緒沒有用過,不知道如何使用,也擔心用不好。綜合以上情況,架構組決定在公司的基礎構件庫中提供一個併發任務執行框架,以解決上述使用者和業務開發人員的痛點:

1、對批量型任務提供統一的開發介面。

2、在使用上儘可能的對業務開發人員友好。

3、要求可以查詢批量任務的執行進度。

二、需要怎麼做

1、批量任務,為提高效能,必然的我們要使用 java 裡的多執行緒,為了在使用上儘可能的對業務開發人員友好和簡單,需要遮蔽一些底層 java 併發程式設計中的細節,讓他們不需要去了解併發容器,阻塞佇列,非同步任務,執行緒安全等等方面的知識,只要專心於自的業務處理即可。

2、每個批量任務擁有自己的上下文環境,因為一個專案組裡同時要處理的批量任務可能有多個,比如考試組,可能就會有不同的學校的批量的離線文件生成,而題庫組則會不同的學科都會有老師同時進行工作,因此需要一個併發安全的容器儲存每個任務的屬性資訊。

3、自動清除已完成和過期任務因為要提供進度查詢,系統需要在記憶體中維護每個任務的進度資訊以供查詢,但是這種查詢又是有時間限制的,一個任務完成一段時間後,就不再提供進度查詢了,則就需要我們自動清除已完成和過期任務,用輪詢來處理的話不太優雅,那麼就可以使用一個 DelayQueue 或者 Redis了。

三、實現流程圖

流程圖

四、具體實現程式碼

/**
 * @Author SunnyBear
 * @Description 方法本身執行是否正確的結果型別
 */
public enum TaskResultType {
    /**
     * 方法執行完成,業務結果也正確
     */
    SUCCESS,
    /**
     * 方法執行完成,業務結果錯誤
     */
    FAILURE,
    /**
     * 方法執行丟擲異常
     */
    EXCEPTION
}
/**
 * @Author SunnyBear
 * @Description 任務處理後返回的結果實體類
 */
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 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 + '\'' +
                '}';
    }
}
/**
 * @Author SunnyBear
 * @Description 要求框架使用者實現的任務介面,因為任務的性質在呼叫時才知道,所以傳入的引數(T)和方法(R)的返回值均使用泛型
 */
public interface ITaskProcesser<T, R> {

    TaskResult<R> taskExecute(T data);
}
/**
 * @Author SunnyBear
 * @Description 存放的佇列的元素
 */
public class Item<T> implements Delayed {

    /**
     * 到期時間(秒)
     */
    private long activeTime;
    /**
     * 業務資料
     */
    private T data;

    public Item(long activeTime, T data) {
        this.activeTime = activeTime * 1000 + System.currentTimeMillis();
        this.data = data;
    }

    public long getActiveTime() {
        return activeTime;
    }

    public T getData() {
        return data;
    }

    /**
     * 從這個方法返回到啟用日期的剩餘時間
     */
    @Override
    public long getDelay(TimeUnit unit) {
        long time = unit.convert(this.activeTime - System.currentTimeMillis(), unit);
        return time;
    }

    /**
     * Delayed介面繼承了Comparable介面,按剩餘時間排序,實際計算考慮精度為納秒數
     */
    @Override
    public int compareTo(Delayed o) {
        long d = (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        if (d == 0) {
            return 0;
        } else {
            if (d < 0) {
                return -1;
            } else {
                return 1;
            }
        }
    }
}
/**
 * @Author SunnyBear
 * @Description 提交給框架執行的工作實體類,工作:表示本批次需要處理的同性質任務(Task)的一個集合
 */
public class JobInfo<R> {

    /**
     * 工作名,用以區分框架中唯一的工作
     */
    private final String jobName;
    /**
     * 工作中任務的長度,即一個工作多少個任務
     */
    private final int jobLength;
    /**
     * 處理工作中任務的處理器
     */
    private final ITaskProcesser<?, ?> taskProcesser;
    /**
     * 任務的成功次數
     */
    private AtomicInteger successCount;
    /**
     * 工作中任務目前已經處理的次數
     */
    private AtomicInteger taskProcessCount;
    /**
     * 存放每個任務的處理結果,供查詢用
     */
    private LinkedBlockingDeque<TaskResult<R>> taskDetailQueues;
    /**
     * 保留工作結果資訊供查詢的時長
     */
    private final long expireTime;
    /**
     * checkJobProcesser,用於新增處理的結果到延時佇列
     */
    private CheckJobProcesser checkJobProcesser = CheckJobProcesser.getInstance();

    public JobInfo(String jobName, int 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.taskDetailQueues = new LinkedBlockingDeque<>(jobLength);
        this.expireTime = expireTime;
    }

    public String getJobName() {
        return jobName;
    }

    public int getJobLength() {
        return jobLength;
    }

    public long getExpireTime() {
        return expireTime;
    }

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

    public AtomicInteger getTaskProcessCount() {
        return taskProcessCount;
    }

    public AtomicInteger getSuccessCount() {
        return successCount;
    }

    /**
     * 失敗次數
     */
    public long getFailureCount() {
        return taskProcessCount.get() - successCount.get();
    }

    /**
     * 工作的整體進度資訊
     */
    public String getTotalProcess() {
        return "Success[ " + successCount.get() + " ] / Current[ " + taskProcessCount.get() + " ] Total[ " + jobLength + " ]";
    }

    /**
     * 提供工作中每個任務的處理結果
     */
    public List<TaskResult<R>> getTaskDetail() {
        List<TaskResult<R>> taskResultList = new LinkedList<>();
        TaskResult<R> taskResult;
        while ((taskResult = taskDetailQueues.pollFirst()) != null) {
            taskResultList.add(taskResult);
        }
        return taskResultList;
    }

    /**
     * 個任務處理完成後,記錄任務的處理結果,因為從業務應用的角度來說,
     * 對查詢任務進度資料的一致性要不高
     * 我們保證最終一致性即可,無需對整個方法加鎖
     */
    public void addTaskResult(TaskResult<R> taskResult){
        if(TaskResultType.SUCCESS.equals(taskResult.getTaskResultType())){
            successCount.incrementAndGet();
        }
        taskProcessCount.incrementAndGet();
        taskDetailQueues.addLast(taskResult);
        if(taskProcessCount.get() == jobLength){
            checkJobProcesser.putJob(expireTime, jobName);
        }
    }
}
/**
 * @Author SunnyBear
 * @Description 框架的主題類,也是呼叫者主要使用的類
 */
public class PendingJobPool {

    /**
     * 框架執行時的執行緒數,與機器的CPU數相同
     */
    private static final int THREAD_COUNTS = Runtime.getRuntime().availableProcessors();
    /**
     * 用以存放待處理的任務,供執行緒池使用
     */
    private static BlockingQueue<Runnable> taskQueue = new ArrayBlockingQueue<>(5000);
    /**
     * 執行緒池,固定大小,有界佇列
     */
    private static ExecutorService taskExecutor
            = new ThreadPoolExecutor(THREAD_COUNTS, THREAD_COUNTS, 60, TimeUnit.SECONDS, taskQueue);
    /**
     * 工作資訊的存放容器
     */
    private static ConcurrentHashMap<String, JobInfo<?>> jobInfoMap = new ConcurrentHashMap<>();

    public static Map<String, JobInfo<?>> getMap() {
        return jobInfoMap;
    }

    private PendingJobPool() {

    }

    /**
     * 單例模式
     */
    private static PendingJobPool threadPool = new PendingJobPool();

    public static PendingJobPool getInstance() {
        return threadPool;
    }

    private static class PendingTask<T, R> implements Runnable {

        private JobInfo<R> jobInfo;
        // 任務引數
        private T processData;

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

        @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 (result == null) {
                    result = new TaskResult<R>(TaskResultType.EXCEPTION, r, "結果為空");
                }
                if (result.getTaskResultType() == null) {
                    if (result.getReason() == null) {
                        result = new TaskResult<R>(TaskResultType.EXCEPTION, r, "結果為空");
                    } else {
                        result = new TaskResult<R>(TaskResultType.EXCEPTION, r, "結果為空, 原因:" + result.getReason());
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
                result = new TaskResult<R>(TaskResultType.EXCEPTION, r, e.getMessage());
            } finally {
                // 江結果新增到延時佇列及結果佇列,同時計數
                jobInfo.addTaskResult(result);
            }
        }
    }

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

    /**
     * 根據工作名稱檢索工作
     */
    private <R> JobInfo<R> getJob(String jobName) {
        JobInfo<R> jobInfo = (JobInfo<R>) jobInfoMap.get(jobName);
        if (null == jobInfo) {
            throw new RuntimeException(jobName + "是非法任務!");
        }
        return jobInfo;
    }

    /**
     * 呼叫者註冊工作,如工作名,任務的處理器等等
     */
    public <R> void registerJob(String jobName, int jobLength,
                                ITaskProcesser<?, ?> taskProcesser, long expireTime) {
        JobInfo<R> jobInfo = new JobInfo<R>(jobName, jobLength, taskProcesser, expireTime);
        // putIfAbsent 如果傳入key對應的value已經存在,就返回存在的value,不進行替換。如果不存在,就新增key和value,返回null
        if (jobInfoMap.putIfAbsent(jobName, jobInfo) != null) {
            throw new RuntimeException(jobName + "已經註冊!");
        }
    }

    /**
     * 獲得工作的整體處理進度
     */
    public <R> String getTaskProgess(String jobName) {
        JobInfo<R> jobInfo = getJob(jobName);
        return jobInfo.getTotalProcess();
    }

    /**
     * 獲得每個任務的處理詳情
     */
    public <R> List<TaskResult<R>> getTaskDetail(String jobName) {
        JobInfo<R> jobInfo = getJob(jobName);
        return jobInfo.getTaskDetail();
    }
}
/**
 * @Author SunnyBear
 * @Description 任務完成後, 在一定的時間供查詢結果,之後為釋放資源節約記憶體,需要定期處理過期的任務
 */
public class CheckJobProcesser {

    /**
     * 延時佇列,存放處理好的工作任務結果
     */
    private static DelayQueue<Item<String>> queue = new DelayQueue<>();

    private static CheckJobProcesser processer = new CheckJobProcesser();

    private CheckJobProcesser() {
    }

    /**
     * 使用單例
     */
    public static CheckJobProcesser getInstance() {
        return processer;
    }

    /**
     * 處理佇列中的到期任務
     */
    public static class FetchJob implements Runnable {
        private static DelayQueue<Item<String>> queue = CheckJobProcesser.queue;
        private static Map<String, JobInfo<?>> jobInfoMap = PendingJobPool.getMap();

        @Override
        public void run() {
            while (true) {
                try {
                    // 從延時佇列DelayQueue中獲取任務
                    Item<String> item = queue.take();
                    String jobName = item.getData();
                    jobInfoMap.remove(jobName);
                    System.out.println(jobName + " 過期了,從快取中清除");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 任務完成後,放入佇列,經過expireTime時間後,從整個框架中移除
     */
    public void putJob(long expireTime, String jobName) {
        Item<String> item = new Item<>(expireTime, jobName);
        queue.offer(item);
        System.out.println(jobName + " 已經放入過期檢查快取,時長:" + expireTime);
    }

    /**
     * 處理過期任務設定為守護執行緒
     */
    static {
        Thread thread = new Thread(new FetchJob());
        thread.setDaemon(true);
        thread.start();
        System.out.println("開啟過期檢查的任務執行緒。。。");
    }

}
/**
 * @Author SunnyBear
 * @Description 一個實際任務類(即使用者要執行的任務類),將數值加上一個隨機數,並休眠隨機時間,模擬任務執行過程中的情況
 */
public class MyTask implements ITaskProcesser<Integer, Integer> {

    @Override
    public TaskResult<Integer> taskExecute(Integer data) {
        Random r = new Random();
        int flag = r.nextInt(500);
        try {
            Thread.sleep(flag);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (flag <= 300) {
            // 正常處理的情況
            Integer returnValue = data.intValue() + flag;
            return new TaskResult<Integer>(TaskResultType.SUCCESS, returnValue, "success");
        } else if (flag > 301 && flag <= 400) {
            // 處理失敗的情況
            return new TaskResult<Integer>(TaskResultType.FAILURE, -1, "failure");
        } else {
            // 發生異常的情況
            try {
                throw new RuntimeException("異常發生了!!");
            } catch (Exception e) {
                return new TaskResult<Integer>(TaskResultType.EXCEPTION,
                        -1, e.getMessage());
            }
        }
    }
}
/**
 * @Author SunnyBear
 * @Description 模擬一個應用程式,提交工作和任務,並查詢任務進度
 */
public class AppTest {
    private final static String JOB_NAME = "工作組一";
    /**
     * 一個工作組中的任務數量
     */
    private final static int JOB_LENGTH = 1000;

    /**
     * 查詢任務進度的執行緒
     */
    private static class QueryResult implements Runnable {

        private PendingJobPool pool;

        public QueryResult(PendingJobPool pool) {
            super();
            this.pool = pool;
        }

        @Override
        public void run() {
            int i = 0;
            while (i < 350) {
                List<TaskResult<String>> taskDetail = pool.getTaskDetail(JOB_NAME);
                if (!taskDetail.isEmpty()) {
                    System.out.println("當前進度:" + pool.getTaskProgess(JOB_NAME));
                    System.out.println("任務詳情: " + taskDetail);
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                i++;
            }
        }

    }

    public static void main(String[] args) {
        MyTask myTask = new MyTask();
        PendingJobPool pool = PendingJobPool.getInstance();
        pool.registerJob(JOB_NAME, JOB_LENGTH, myTask, 5);
        Random r = new Random();
        for (int i = 0; i < JOB_LENGTH; i++) {
            pool.putTask(JOB_NAME, r.nextInt(1000));
        }
        Thread t = new Thread(new QueryResult(pool));
        t.start();
    }
}

都讀到這裡了,來個 點贊、評論、關注、收藏 吧!

文章作者:IT王小二
首發地址:https://www.itwxe.com/posts/bbff829d/
版權宣告:文章內容遵循 署名-非商業性使用-禁止演繹 4.0 國際 進行許可,轉載請在文章頁面明顯位置給出作者與原文連結。

相關文章