java中執行定時任務的6種姿式

定時任務的場景

所謂定時任務實際上有兩種狀況, 一種是在某個特定的時間點觸發執行某個任務, 例如天天凌晨, 每週六下午2點等等. 另一種是以特定的間隔或頻率觸發某個任務,例如每小時觸發一次等.html

在咱們實際的工做中,用到定時任務的場景是很是的多的,例如:java

  1. 天天統計某些業務數據, 作報表展現
  2. 用戶下單後, 30分鐘未支付則取消訂單
  3. 特定的時間點給用戶發送消息(祝福短信等)
  4. 補償機制, 按期掃描數據庫和日誌,對比差別的數據並進行補償
  5. 等等...

正是由於應用場景很是的廣, 因此前輩程序員們也是絞盡腦汁, 爲咱們創造了不少實用用的工具和框架, 咱們今天才得以站在巨人的肩膀上,看得更遠,走得更快.linux

下面咱們就來列舉一下這些方法和工具.程序員

crontab

crontab嚴格來講並非屬於java內的. 它是linux自帶的一個工具, 能夠週期性地執行某個shell腳本或命令.shell

可是因爲crontab在實際開發中應用比較多, 並且crontab表達式跟咱們後面介紹的其餘定時任務框架的cron表達式是相似的, 因此這裏仍是最早介紹crontab數據庫

crontab的用法是:json

crontabExpression command
複製代碼

首先, command能夠是一個linux命令(例如echo 123), 或一個shell腳本(例如 test.sh), 也能夠是二者結合(例如: cd /tmp; sh test.sh)api

crontabExpression大概是長下面這樣子bash

# 每小時的第5分鐘執行一次命令
5 * * * * Command 
# 指定天天下午的 6:30 執行一次命令
30 18 * * * Command 
# 指定每個月8號的7:30分執行一次命令
30 7 8 * * Command
# 指定每一年的6月8日5:30執行一次命令
30 5 8 6 * Command 
# 指定每星期日的6:30執行一次命令
30 6 * * 0 Command 
複製代碼

其中crontabExpression一共有5列, 含義以下:服務器

  1. 第一列表示是分鐘, 取值爲0-59
  2. 第二列表示是時, 取值爲0-59
  3. 第三列表示是日
  4. 第四列表示是月, 取值是0-12
  5. 第5列表示是星期

此外, 每列還能夠是* ? -等等特殊字符, 具體含義能夠參考這篇文章, 裏面總結得比較好, 我這裏就再也不多說了

timer

即jdk裏面提供的java.util.Timer和java.util.TimerTask兩個類.

其中TimerTask表示具體的任務,而Timer調度任務.

簡單的例子以下:

import java.util.Timer;
import java.util.TimerTask;

public class TimerTest extends TimerTask {

    private String jobName = "";

    public TimerTest(String jobName) {
        super();
        this.jobName = jobName;
    }

    @Override
    public void run() {
        System.out.println("execute " + jobName);
    }

    public static void main(String[] args) {
        Timer timer = new Timer();
        long delay1 = 1 * 1000;
        long period1 = 1000;
        // 從如今開始 1 秒鐘以後,每隔 1 秒鐘執行一次 job1
        timer.schedule(new TimerTest("job1"), delay1, period1);
        long delay2 = 2 * 1000;
        long period2 = 2000;
        // 從如今開始 2 秒鐘以後,每隔 2 秒鐘執行一次 job2
        timer.schedule(new TimerTest("job2"), delay2, period2);
    }
}
複製代碼

固然在生產環境中Timer是不建議使用了的. 它在多線程的環境下, 會存在必定的問題:

1. 當一個線程拋出異常時,整個timer都會中止運行.例如上面的job1拋出異常的話, 
job2也不會再跑了.
2. 當一個線程裏面處理的時間很是長的話, 會影響其餘job的調度. 
例如, 若是job1處理的時間要60秒的話, 那麼job2就變成了60秒跑一次了.
複製代碼

基於上面的緣由, timer如今通常都不會再使用了.

ScheduledExecutorService

ScheduledExecutorService 就是JDK裏面自定義的幾種線程池中的一種.

從API上看, 感受它就是用來替代Timer的,並且徹底能夠替代的. 只是不知道爲什麼Timer仍是沒有被標記爲過時, 想必是還有一些應用的場景吧

首先, Timer能作到的事情ScheduledExecutorService都能作到;

其次, ScheduledExecutorService能夠完美的解決上面所說的Timer存在的兩個問題:

1. 拋異常時, 即便異常沒有被捕獲, 線程池也還會新建線程, 因此定時任務不會中止

2. 因爲ScheduledExecutorService是不一樣線程處理不一樣的任務, 所以,無論一個線程的運行時間有多長, 都不會影響到另一個線程的運行.
複製代碼

固然, ScheduledExecutorService也不是萬能的. 例如若是我想實現"在每週六下午2點"執行某行代碼這個需求時, ScheduledExecutorService實現起來就有點麻煩了.

ScheduledExecutorService更適合調度這些簡單的以特定頻率執行的任務.其餘的, 就要輪到咱們大名鼎鼎的quartz上場了.

quartz

在java的世界裏, quartz絕對是總統山級別的王者的存在. 市面上大多數的開源的調度框架也基本都是直接或間接基於這個框架來開發的.

先來看經過一個最簡單的quartz的例子, 來簡單地認識一下它.

使用cron表達式來讓quartz每10秒鐘執行一個任務:

先引入maven依賴:

<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
複製代碼

編寫代碼:

import com.alibaba.fastjson.JSON;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import static org.quartz.CronScheduleBuilder.cronSchedule;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

public class QuartzTest implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("這裏是你的定時任務: " + JSON.toJSONString( jobExecutionContext.getJobDetail()));
    }


    public static void main(String[] args) {
        try {
            // 獲取到一個StdScheduler, StdScheduler實際上是QuartzScheduler的一個代理
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 啓動Scheduler
            scheduler.start();
            // 新建一個Job, 指定執行類是QuartzTest(需實現Job), 指定一個K/V類型的數據, 指定job的name和group
            JobDetail job = newJob(QuartzTest.class)
                    .usingJobData("jobData", "test")
                    .withIdentity("myJob", "group1")
                    .build();
            // 新建一個Trigger, 表示JobDetail的調度計劃, 這裏的cron表達式是 每10秒執行一次
            Trigger trigger = newTrigger()
                    .withIdentity("myTrigger", "group1")
                    .startNow()
                    .withSchedule(cronSchedule("0/10 * * * * ?"))
                    .build();


            // 讓scheduler開始調度這個job, 按trigger指定的計劃
            scheduler.scheduleJob(job, trigger);


            // 保持進程不被銷燬
           //  scheduler.shutdown();
            Thread.sleep(10000000);

        } catch (SchedulerException se) {
            se.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

複製代碼

上面這個簡單的例子已經包含了quartz的幾個核心組件:

Scheduler - 能夠理解爲是一個調度的實例,用來調度任務
Job - 這個是一個接口, 表示調度要執行的任務. 相似TimerTask.
JobDetail - 用於定義做業的實例。進一步封裝和拓展Job的具體實例
Trigger(即觸發器) - 定義JobDetail的調度計劃。例如多久執行一次, 何時執行, 以什麼頻率執行等等
JobBuilder - 用於定義/構建JobDetail實例。
TriggerBuilder - 用於定義/構建觸發器實例。
複製代碼
1. Scheduler

Scheduler是一個接口, 它一共有4個實現:

JBoss4RMIRemoteMBeanScheduler
RemoteMBeanScheduler
RemoteScheduler
StdScheduler
複製代碼

咱們上面的例子使用的是StdScheduler, 表示的直接在本地進行調度(其餘的都帶有remote字樣, 明顯是跟遠程調用有關).

來看一下StdScheduler的註釋和構造方法

/**
 * <p>
 * An implementation of the <code>Scheduler</code> interface that directly
 * proxies all method calls to the equivalent call on a given <code>QuartzScheduler</code>
 * instance.
 * </p>
 * 
 * @see org.quartz.Scheduler
 * @see org.quartz.core.QuartzScheduler
 *
 * @author James House
 */
public class StdScheduler implements Scheduler {

    /**
     * <p>
     * Construct a <code>StdScheduler</code> instance to proxy the given
     * <code>QuartzScheduler</code> instance, and with the given <code>SchedulingContext</code>.
     * </p>
     */
    public StdScheduler(QuartzScheduler sched) {
        this.sched = sched;
    }
}
複製代碼

原來StdScheduler只不過是一個代理而已, 它最終都是調用org.quartz.core.QuartzScheduler類的方法.

查看RemoteScheduler等另外三個的實現, 也都是代理QuartzScheduler而已.

因此很明顯, quartz的核心是QuartzScheduler類.

因此來看一下QuartzScheduler的javadoc註釋:

/**
 * <p>
 * This is the heart of Quartz, an indirect implementation of the <code>{@link org.quartz.Scheduler}</code>
 * interface, containing methods to schedule <code>{@link org.quartz.Job}</code>s,
 * register <code>{@link org.quartz.JobListener}</code> instances, etc.
 * </p>
 * 
 * @see org.quartz.Scheduler
 * @see org.quartz.core.QuartzSchedulerThread
 * @see org.quartz.spi.JobStore
 * @see org.quartz.spi.ThreadPool
 * 
 * @author James House
 */
public class QuartzScheduler implements RemotableQuartzScheduler {
	...
}
複製代碼

大概意思就是說: QuartzScheduler是quartz的心臟, 間接實現了org.quartz.Scheduler接口, 包含了調度Job和註冊JobListener的方法等等

說是間接實現說Scheduler接口,可是來看一下它的繼承圖, 你會發現它跟Scheduler接口沒有半毛錢關係(果真夠間接的), 徹底是本身獨立搞了一套, 基本全部調度相關的邏輯都在裏面實現了

image

另外從這個繼承圖中的RemotableQuartzScheduler也能夠看出, QuartzScheduler是天生就能夠支持遠程調度的(經過rmi遠程觸發調度, 調度的管理和調度的執行能夠分離).

固然, 實際應用中也大多數都是這麼用, 只是咱們這個最簡單的例子是本地觸發調度,本地執行任務而已.

2. Job, JobDetail

Job是一個接口, 它只定義了一個execute方法, 表明任務執行的邏輯.

public interface Job {
    void execute(JobExecutionContext context)
        throws JobExecutionException;
}
複製代碼

JobDetail其實也是一個接口, 它的默認實現是JobDetailImpl.JobDetail內部指定了JobDetail的實現類, 另外還新增了一些參數:

1. name和group, 會組合成一個JobKey對象, 做爲這個JobDetail的惟一標識ID
2. jobDataMap, 能夠給Job傳遞一些額外參數
3. durability, 是否須要持久化.這就是quartz跟通常的Timer之流不同的地方了. 他的job是能夠持久化到數據庫的
複製代碼

能夠看的出來, JobDetail實際上是對Job類的一種加強. Job用來表示任務的執行邏輯, 而JobDetail更多的是跟Job管理相關.

3. Trigger

Trigger接口能夠說纔是quartz的核心功能. 由於quartz是一個定時任務調度框架, 而定時任務的調度邏輯, 就是在Trigger中實現的.

來看一下Trigger的實現類, 乍一看還挺多. 可是實際就圖中紅圈圈出來的那幾個是真正的實現類, 其餘的都是接口或實現類:

image

而實際上, 咱們用得最多的也只是SimpleTriggerImpl和CronTriggerImpl, 前者表示簡單的調度邏輯,例如每1分鐘執行一次. 後者可使用cron表達式來 指定更復雜的調度邏輯.

很明顯, 上面簡單的例子咱們用的是CronTriggerImp

不過須要注意的是, quartz的cron表達式和linux下crontab的cron表達式是有必定區別的, 它能夠直接到秒級別:

1. Seconds
2. Minutes
3. Hours
4. Day-of-Month
5. Month
6. Day-of-Week
7. Year (optional field)

例如: "0 0 12?* WED" - 這意味着"每一個星期三下午12:00"複製代碼

使用CronTrigger的時候, 直接寫cron表達式是比較容易出錯的, 因此最好有個工具驗證一下本身的cron表達式是否寫正確, 以及驗證觸發的時間是不是咱們期待的.

這個工做已經有人幫咱們作好了, 例以下面這個網站:

tool.lu/crontab/

實際效果以下:

iamge

以上就算是quartz的一個入門教程了. 可是確實也只是一個入門教程而已.實際上quartz遠比這個例子表現出來的複雜, 也同時也遠比這個例子體現出來的強大.

例如:

1. quartz能夠配置成集羣模式,能夠提供失敗轉移,負載均衡等功能, 在提高計算能力的同時,也提高了系統的可用性
2. quartz還支持JTA事務, 能夠將一些job運行在一個事務中
3. 只要服務器資源上能支持, quartz理論上能運行成千上萬的job
4. 等等等...
複製代碼

固然, quartz也不是沒有缺點; 整個框架的重點都是在於"調度"上,而忽略了一些其餘的方面, 例如交互和性能.

  1. 交互上, quartz只是提供了"scheduler.scheduleJob(job, trigger)" 這種api的方式. 沒有提供任何的管理界面,這是很是的不人性化的.

  2. quartz並無原生地支持分片的功能.這會致使運行一個大的任務時, 運行時間會很是的長. 例如要跑一億個會員的數據時, 有可能一天都跑不完.若是是支持分片的那就好辦不少了.能夠把一億會員拆分到多個實例上跑, 性能更高.

在這兩點上, 一些其餘的框架作得就更好了.

elastic-job 和 xxlJob

elastic-job和xxl-job是兩個很是優秀的分佈式任務調度框架, 在我使用過的全部分佈調度框架中, 這兩個框架起碼能排前2位(由於我就用過這兩個, 哈哈哈)

這兩個框架各有各的特色, 其中共同點都有: 分佈式, 輕量級, 交互人性化

elastic-job

elastic-job是噹噹基於quartz二次開發而開源的一個分佈式框架, 功能十分強大. 但在我使用的經驗來看, elastic-job最大的亮點有兩個: 1是做業分片, 2是彈性擴容縮容

1. 做業分片就是上面所說的, 把一個大的任務拆分紅多個子任務, 而後由多個做業節點去處理這些子任務, 以此縮短做業的時間.
2. 彈性擴容縮容實際上是跟做業分片息息相關的, 簡單的理解就是增長或減小一個做業節點, 都能保證每個分片都有節點處理, 每一個節點都有分片可處理.
複製代碼

更多elastic-job的知識和原理請參考官網, 相信我再怎麼總結也沒有官網總結得清晰和完善了.

image

xxl-job

xxl-job是被普遍使用的另一款使用的分佈式任務調度框架. 早起的xxljob也是基於quartz開發的, 不過如今慢慢去quartz化了, 改爲自研的調度模塊.

相對於elastic-job, 我更加喜歡使用xxl-job, 其優勢以下:

1. 功能更強大. elastic-job支持的功能, xxl-job基本都支持. 原本我想截一下圖的, 結果發現一屏根本截不過來. 你們仍是去官網本身看一下吧.
2. 真正實現調度和執行分離, 相對而言, elastic-job的調度和執行其實糅雜在一塊兒的,都是嵌入到業務系統中, 這一點我就不太喜歡了
3. xxl-job的管理後臺更加豐富和靈活, 還有我最喜歡的一個點, 就是能夠在控制檯裏面看到任務執行的日誌.
複製代碼

一樣, 因爲官方的文檔很是詳細, 因此我這裏再怎麼介紹也比不過官網的. 因此更多的特性和原理, 你們能夠移步官網

image

總結

本文一共從簡單到複雜, 一共介紹了6種調度任務的處理的方案. 固然生產環境中通常都是建議使用elastic-job和xxl-job. 可是若是是簡單的任務的話, 使用簡單crontab等也不是不可, 我以前就常用crontab作業務相關的定時任務.

固然, 在數據量愈來愈大, 大數據技術發展得也愈來愈快的今天, 像Hadoop,Spark等生態中也出現了很多優秀的定時調度框架.但那就不在本文中的討論範疇中了.

相關文章
相關標籤/搜索