1. 程式人生 > 其它 >Java-定時任務

Java-定時任務

java中執行定時任務

定時任務的場景

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

crontab

crontab嚴格來說並不是屬於java內的. 它是linux自帶的一個工具, 可以週期性地執行某個shell指令碼或命令.

但是由於crontab在實際開發中應用比較多, 而且crontab表示式跟我們後面介紹的其他定時任務框架的cron表示式是類似的, 所以這裡還是最先介紹crontab

crontab的用法是:

crontab Expression command

首先, command可以是一個linux命令(例如echo 123), 或一個shell指令碼(例如 test.sh), 也可以是兩者結合(例如: cd /tmp; sh test.sh)

# 每小時的第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列表示是星期

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介面沒有半毛錢關係(果然夠間接的), 完全是自己獨立搞了一套, 基本所有排程相關的邏輯都在裡面實現了

另外從這個繼承圖中的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的實現類, 乍一看還挺多. 但是實際就圖中紅圈圈出來的那幾個是真正的實現類, 其他的都是介面或實現類:

而實際上, 我們用得最多的也只是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"。

當然, 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. 彈性擴容縮容其實是跟作業分片息息相關的, 簡單的理解就是增加或減少一個作業節點, 都能保證每一個分片都有節點處理, 每個節點都有分片可處理.
xxl-job

xxl-job是被廣泛使用的另外一款使用的分散式任務排程框架. 早起的xxljob也是基於quartz開發的, 不過現在慢慢去quartz化了, 改成自研的排程模組.

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

1. 功能更強大. elastic-job支援的功能, xxl-job基本都支援. 本來我想截一下圖的, 結果發現一屏根本截不過來. 大家還是去官網自己看一下吧.
2. 真正實現排程和執行分離, 相對而言, elastic-job的排程和執行其實糅雜在一起的,都是嵌入到業務系統中, 這一點我就不太喜歡了
3. xxl-job的管理後臺更加豐富和靈活, 還有我最喜歡的一個點, 就是可以在控制檯裡面看到任務執行的日誌.

總結

本文一共從簡單到複雜, 一共介紹了5種排程任務的處理的方案. 當然生產環境中一般都是建議使用elastic-job和xxl-job. 但是如果是簡單的任務的話, 使用簡單crontab等也不是不可, 我之前就經常使用crontab做業務相關的定時任務.

當然, 在資料量越來越大, 大資料技術發展得也越來越快的今天, 像Hadoop,Spark等生態中也出現了不少優秀的定時排程框架.但那就不在本文中的討論範疇中了.