那些年,我們用過的“定時調度”

定時調度

作爲後端開發人員,我們總會遇到這樣的業務場景:每週同步一批數據;每半個小時檢查一遍服務器運行狀況;每天早上八點給用戶發送一份包含今日待辦事項的郵件,等等。

這些場景中都離不開“定時器”,就像一個定好時間規則的鬧鐘,它會在指定時間觸發,執行我們想要定義的調度任務。那麼我們今天就來數一下,那些年我們用過的“定時調度”。

1. job (oracle)

從剛工作就一直使用oracle數據庫,最早接觸的定時任務就是oracle數據庫的job。job有定時執行的功能,可以在指定的時間點或每天的某個時間點自行執行任務。 而且oracle重新啓動後,job會繼續運行,不用重新啓動。

而且job的機制非常完備,可以查詢相關的表或視圖,查詢job的定時規則和執行情況。缺點是作爲oracle數據庫層面的工具,自定義功能擴展,二次開發的難度比較大。

1.1 創建job

DECLARE
  job NUMBER;
BEGIN
    sys.dbms_job.submit(job => job,

    what => 'prc_name;',                          --執行的存儲過程的名字

    next_date => to_date('22-11-2013 09:09:41', 'dd-mm-yyyy hh24:mi:ss'), --下一次執行時間

    interval =>'sysdate+1/24');            --每天24小時,即每小時運行prc_name過程一次
END;

-- job參數是輸出參數,由submit()過程返回的binary_ineger,這個值用來唯一標識一個工作。一般定義一個變量接收,可以去user_jobs視圖查詢job值。
-- what參數是將被執行的PL/SQL代碼塊,存儲過程名稱等。
-- next_date參數指識何時將運行這個工作。
-- interval參數何時這個工作將被重執行

1.2 刪除job

DECLARE
BEGIN
  dbms_job.remove(1093);  -- 1093爲當前需要刪除的 job 值
  COMMIT;
END;

1.3 查詢job

-- 查詢當前用戶的job
select * from user_jobs;
-- 查詢所有job
select * from dba_jobs;
-- 查詢所有運行中的job
select * from dba_jobs_running;

2. crontab (linux)

crond 是linux下用來週期性的執行某種任務或等待處理某些事件的一個守護進程,與windows下的計劃任務類似,當安裝完成操作系統後,默認會安裝此服務 工具,並且會自動啓動crond進程,crond進程每分鐘會定期檢查是否有要執行的任務,如果有要執行的任務,則自動執行該任務。

cron是服務名稱,crond是後臺進程,crontab則是定製好的計劃任務表。大部分linux系統默認都安裝了cron,可以檢查一下。

-- 檢查Crontab工具是否安裝
crontab -l
-- 檢查crond服務是否啓動
service crond status

-- centos安裝
yum install vixie-cron
yum install crontabs

crontab基本操作命令

-- 列出某個用戶cron服務的詳細內容
crontab -l
-- 編輯某個用戶的cron服務
crontab -e 

crontab表達式格式

{minute} {hour} {day-of-month} {month} {day-of-week} {full-path-to-shell-script}

 minute: 區間爲 0 – 59
 hour: 區間爲0 – 23
 day-of-month: 區間爲0 – 31
 month: 區間爲1 – 12. 1 是1月. 12是12月
 Day-of-week: 區間爲0 – 7. 週日可以是0或7.

在以上各個字段中,還可以使用以下特殊字符:
星號(*):代表所有可能的值,例如month字段如果是星號,則表示在滿足其它字段的制約條件後每月都執行該命令操作。
逗號(,):可以用逗號隔開的值指定一個列表範圍,例如,“1,2,5,7,8,9”
中槓(-):可以用整數之間的中槓表示一個整數範圍,例如“2-6”表示“2,3,4,5,6”
正斜線(/):可以用正斜線指定時間的間隔頻率,例如“0-23/2”表示每兩小時執行一次。同時正斜線可以和星號一起使用,例如*/10,如果用在minute字段,表示每十分鐘執行一次。

推薦一個crontab表達式的校驗網站(https://tool.lu/crontab/

3. Timer和ScheduledExecutorService (java)

Timer是jdk中提供的一個定時器工具,使用的時候會在主線程之外起一個單獨的線程執行指定的計劃任務,可以指定執行一次或者反覆執行多次。

//只執行一次
public void schedule(TimerTask task, long delay);
public void schedule(TimerTask task, Date time);
//循環執行
// 在循環執行類別中根據循環時間間隔又可以分爲兩類
public void schedule(TimerTask task, long delay, long period) ;
public void schedule(TimerTask task, Date firstTime, long period) ;
 
public void scheduleAtFixedRate(TimerTask task, long delay, long period)
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)

TimerTask是一個實現了Runnable接口的抽象類,代表一個可以被Timer執行的任務。TimerTask類是一個抽象類,由Timer 安排爲一次執行或重複執行的任務。它有一個抽象方法run()方法,該方法用於執行相應計時器任務要執行的操作。因此每一個具體的任務類都必須繼承TimerTask,然後重寫run()方法。
另外它還有兩個非抽象的方法

-- 取消此計時器任務
boolean cancel()
-- 返回此任務最近實際執行的安排執行時間
long scheduledExecutionTime()

當然,一般使用Timer的比較少,因爲它的缺點比較明顯:

  1. 單線程,當多個timer同時運行時,會等上一個執行完成,再執行下一個。
  2. Timer線程是不會捕獲異常的,如果TimerTask拋出的了未檢查異常則會導致Timer線程終止。

所以一般使用ScheduledExecutorService替代Timer。
ScheduledExecutorService:也是jdk自帶的一個基於線程池設計的定時任務類。其每個調度任務都會分配到線程池中的一個線程執行,所以其任務是併發執行的,互不影響。

4. SpringTask (spring)

Timer和ScheduledExecutorService都是屬於jdk層面上實現定時調度的類,功能還不足以讓我們滿意,那麼現在介紹一個比較完善的定時調度工具 - SpringTask,是Spring提供的,支持註解和配置文件形式,支持crontab表達式,使用簡單但功能強大。我個人非常喜歡SpringTask,僅僅是因爲支持crontab表達式。

在springboot裏面使用方式非常簡單:

  1. 啓動類添加開啓定時調度的註解 @EnableScheduling
  2. 在需要定時執行的方法上,增加註解 @Scheduled(cron ="crontab表達式")

默認的簡單的使用步驟只有以上兩步,但是SpringTask的默認使用方式也有一些不足:

  1. 默認線程池的poolsize爲1,可以理解爲Timer類似的單線程模式。
  2. 無法動態修改crontab表達式,修改完只能重新部署後,才能生效。

問題1的解決方式,可以通過自定義 TaskExecutor來修改當前的線程池。問題2,則可以直接使用 threadPoolTaskScheduler類實現自定義的定時調度規則。

附解決兩個問題的源碼 TaskTimer.class

@Component
public class TaskTimer {

    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
    @Autowired
    private TaskRepo taskRepo;

    /**
     * ***定時引擎***
     *
     * 實例化一個線程池任務調度類
     * 默認 ThreadPoolTaskScheduler 的 poolSize 爲1,類似於newSingleThreadExecutor 單線程模式,只能執行完一個調度,再執行其他調度
     * 需要自定義擴展poolSize,允許一定程度的 多線程並行場景
     *
     * @return
     */
    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(100);
        threadPoolTaskScheduler.setThreadNamePrefix("Thread - ");
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
        return threadPoolTaskScheduler;
    }

    /**
     * 啓動定時調度
     * @param taskCode
     * @param cron
     * @param runnable
     * @return
     */
    public boolean start(String taskCode, String cron,Runnable runnable){
        ScheduledFuture<?> currentFuture=taskRepo.findTask(taskCode);
        //已存在的調度,無法再創建
        if(currentFuture!=null){
            throw  new RuntimeException("調度\""+taskCode+"\"已存在,無法再創建");
        }
        //創建新的調度,並加入 taskMap
        currentFuture = threadPoolTaskScheduler.schedule(runnable,new CronTrigger(cron));
        if (currentFuture!=null){
            this.taskRepo.addTask(taskCode,currentFuture);
            return true;
        }
        throw  new RuntimeException("任務啓動失敗!!!");
    }

    /**
     * 暫停定時調度
     * @param taskCode
     * @return
     */
    public boolean stop(String taskCode) {
        //taskId 不存在的,無法停止,只能修改
        ScheduledFuture<?> currentFuture=this.taskRepo.findTask(taskCode);
        if(currentFuture!=null){
            return currentFuture.cancel(true);
        }
        return true;
    }

    /**
     * 刪除定時調度
     * @param taskCode
     * @return
     */
    public boolean remove(String taskCode){
        ScheduledFuture<?> currentFuture=this.taskRepo.findTask(taskCode);
        if(currentFuture!=null){
             currentFuture.cancel(true);
             taskRepo.removeTask(taskCode);
             return true;
        }
        return false;
    }

    /**
     * 修改定時調度
     * @param taskCode
     * @param cron
     * @param runnable
     * @return
     */
    public boolean update(String taskCode,String cron,Runnable runnable){
        ScheduledFuture<?> currentFuture=this.taskRepo.findTask(taskCode);
        //已存在的定時調度,先停止,再新增,並更新 新的ScheduledFuture
        if(currentFuture!=null) {
            currentFuture.cancel(true);
        }
        currentFuture= threadPoolTaskScheduler.schedule(runnable,new CronTrigger(cron));
        if(currentFuture!=null){
            this.taskRepo.addTask(taskCode,currentFuture);
            return true;
        }
        return false;
    }

}

5. Quartz (其他產品)

Quartz是一個完全由 Java 編寫的開源作業調度框架,爲在 Java 應用程序中進行作業調度提供了簡單卻強大的機制。它是一個功能強大、十分成熟的重量級產品,還支持負載均衡,實現分佈式調度。

不過,對於Quartz的安裝你要多花點功夫了,從數據庫要建哪些表,到應用程序該如何部署。對於這樣一個龐大的產品,本篇文章就不附上它的使用說明書了。

參考文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章