Spring Boot 定時任務的技術選型對比

芋道原始碼發表於2020-06-09

大家好,我是艿艿,一個日常在地鐵擼碼的小胖子~

今天我們來一起瞅瞅,在 Spring Boot 應用中,有哪些定時任務的技術選型~

1. 概述

在產品的色彩斑斕的黑的需求中,有存在一類需求,是需要去定時執行的,此時就需要使用到定時任務。例如說,每分鐘掃描超時支付的訂單,每小時清理一次日誌檔案,每天統計前一天的資料並生成報表,每個月月初的工資單的推送,每年一次的生日提醒等等。

其中,艿艿最喜歡“每個月月初的工資單的推送”,你呢?

在 JDK 中,內建了兩個類,可以實現定時任務的功能:

  • java.util.Timer :可以通過建立 java.util.TimerTask 排程任務,在同一個執行緒中序列執行,相互影響。也就是說,對於同一個 Timer 裡的多個 TimerTask 任務,如果一個 TimerTask 任務在執行中,其它 TimerTask 即使到達執行的時間,也只能排隊等待。因為 Timer 是序列的,同時存在 坑坑 ,所以後來 JDK 又推出了 ScheduledExecutorService ,Timer 也基本不再使用。
  • java.util.concurrent.ScheduledExecutorService :在 JDK 1.5 新增,基於執行緒池設計的定時任務類,每個排程任務都會被分配到執行緒池中併發執行,互不影響。這樣,ScheduledExecutorService 就解決了 Timer 序列的問題。

在日常開發中,我們很少直接使用 Timer 或 ScheduledExecutorService 來實現定時任務的需求。主要有幾點原因:

  • 它們僅支援按照指定頻率,不直接支援指定時間的定時排程,需要我們結合 Calendar 自行計算,才能實現複雜時間的排程。例如說,每天、每週五、2019-11-11 等等。
  • 它們是程式級別,而我們為了實現定時任務的高可用,需要部署多個程式。此時需要等多考慮,多個程式下,同一個任務在相同時刻,不能重複執行。
  • 專案可能存在定時任務較多,需要統一的管理,此時不得不進行二次封裝。

所以,一般情況下,我們會選擇專業的排程任務中介軟體

關於“任務”的叫法,也有叫“作業”的。在英文上,有 Task 也有 Job 。本質是一樣的,本文兩種都會用。

然後,一般來說是排程任務,定時執行。所以胖友會在本文,或者其它文章中,會看到“排程”或“定時”的字眼兒。

在 Spring 體系中,內建了兩種定時任務的解決方案:

  • 第一種,Spring FrameworkSpring Task 模組,提供了輕量級的定時任務的實現。
  • 第二種,Spring Boot 2.0 版本,整合了 Quartz 作業排程框架,提供了功能強大的定時任務的實現。

    注:Spring Framework 已經內建了 Quartz 的整合。Spring Boot 1.X 版本未提供 Quartz 的自動化配置,而 2.X 版本提供了支援。

在 Java 生態中,還有非常多優秀的開源的排程任務中介軟體:

目前國內採用 Elastic-Job 和 XXL-JOB 為主。從艿艿瞭解到的情況,使用 XXL-JOB 的團隊會更多一些,主要是上手較為容易,運維功能更為完善。

本文在提供完整程式碼示例,可見 https://github.com/YunaiV/Spr...lab-28 目錄。

原創不易,給點個 Star 嘿,一起衝鴨!

2. 快速入門 Spring Task

示例程式碼對應倉庫:lab-28-task-demo

考慮到實際場景下,我們很少使用 Spring Task ,所以本小節會寫的比較簡潔。如果對 Spring Task 比較感興趣的胖友,可以自己去閱讀 《Spring Framework Documentation —— Task Execution and Scheduling》 文件,裡面有 Spring Task 相關的詳細文件。

在本小節,我們會使用 Spring Task 功能,實現一個每 2 秒列印一行執行日誌的定時任務。

2.1 引入依賴

pom.xml 檔案中,引入相關依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-28-task-demo</artifactId>

    <dependencies>
        <!-- 實現對 Spring MVC 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

因為 Spring Task 是 Spring Framework 的模組,所以在我們引入 spring-boot-starter-web 依賴後,無需特別引入它。

同時,考慮到我們希望讓專案啟動時,不自動結束 JVM 程式,所以我們引入了 spring-boot-starter-web 依賴。

2.2 ScheduleConfiguration

cn.iocoder.springboot.lab28.task.config 包路徑下,建立 ScheduleConfiguration 類,配置 Spring Task 。程式碼如下:

// ScheduleConfiguration.java

@Configuration
@EnableScheduling
public class ScheduleConfiguration {
}
  • 在類上,新增 @EnableScheduling 註解,啟動 Spring Task 的定時任務排程的功能。

2.3 DemoJob

cn.iocoder.springboot.lab28.task.job 包路徑下,建立 DemoJob 類,示例定時任務類。程式碼如下:

// DemoJob.java

@Component
public class DemoJob {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private final AtomicInteger counts = new AtomicInteger();

    @Scheduled(fixedRate = 2000)
    public void execute() {
        logger.info("[execute][定時第 ({}) 次執行]", counts.incrementAndGet());
    }

}
  • 在類上,新增 @Component 註解,建立 DemoJob Bean 物件。
  • 建立 #execute() 方法,實現列印日誌。同時,在該方法上,新增 @Scheduled 註解,設定每 2 秒執行該方法。

雖然說,@Scheduled 註解,可以新增在一個類上的多個方法上,但是艿艿的個人習慣上,還是一個 Job 類,一個定時任務。?

2.4 Application

建立 Application.java 類,配置 @SpringBootApplication 註解即可。程式碼如下:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

執行 Application 類,啟動示例專案。輸出日誌精簡如下:

# 初始化一個 ThreadPoolTaskScheduler 任務排程器
2019-11-30 18:02:58.415  INFO 83730 --- [           main] o.s.s.c.ThreadPoolTaskScheduler          : Initializing ExecutorService 'taskScheduler'

# 每 2 秒,執行一次 DemoJob 的任務
2019-11-30 18:02:58.449  INFO 83730 --- [ pikaqiu-demo-1] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (1) 次執行]
2019-11-30 18:03:00.438  INFO 83730 --- [ pikaqiu-demo-1] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (2) 次執行]
2019-11-30 18:03:02.442  INFO 83730 --- [ pikaqiu-demo-2] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (3) 次執行]
  • 通過日誌,我們可以看到,初始化一個 ThreadPoolTaskScheduler 任務排程器。之後,每 2 秒,執行一次 DemoJob 的任務。

至此,我們已經完成了 Spring Task 排程任務功能的入門。實際上,Spring Task 還提供了非同步任務 ,這個我們在其它文章中,詳細講解。

下面「2.5 @Scheduled」「2.6 應用配置檔案」兩個小節,是補充知識,建議看看。

2.5 @Scheduled

@Scheduled 註解,設定定時任務的執行計劃。

常用屬性如下:

  • cron 屬性:Spring Cron 表示式。例如說,"0 0 12 * * ?" 表示每天中午執行一次,"11 11 11 11 11 ?" 表示 11 月 11 號 11 點 11 分 11 秒執行一次(哈哈哈)。更多示例和講解,可以看看 《Spring Cron 表示式》 文章。注意,以呼叫完成時刻為開始計時時間。
  • fixedDelay 屬性:固定執行間隔,單位:毫秒。注意,以呼叫完成時刻為開始計時時間。
  • fixedRate 屬性:固定執行間隔,單位:毫秒。注意,以呼叫開始時刻為開始計時時間。
  • 這三個屬性,有點雷同,可以看看 《@Scheduled 定時任務的fixedRate、fixedDelay、cron 的區別》 ,一定要分清楚差異。

不常用屬性如下:

  • initialDelay 屬性:初始化的定時任務執行延遲,單位:毫秒。
  • zone 屬性:解析 Spring Cron 表示式的所屬的時區。預設情況下,使用伺服器的本地時區。
  • initialDelayString 屬性:initialDelay 的字串形式。
  • fixedDelayString 屬性:fixedDelay 的字串形式。
  • fixedRateString 屬性:fixedRate 的字串形式。

2.6 應用配置檔案

application.yml 中,新增 Spring Task 定時任務的配置,如下:

spring:
  task:
    # Spring Task 排程任務的配置,對應 TaskSchedulingProperties 配置類
    scheduling:
      thread-name-prefix: pikaqiu-demo- # 執行緒池的執行緒名的字首。預設為 scheduling- ,建議根據自己應用來設定
      pool:
        size: 10 # 執行緒池大小。預設為 1 ,根據自己應用來設定
      shutdown:
        await-termination: true # 應用關閉時,是否等待定時任務執行完成。預設為 false ,建議設定為 true
        await-termination-period: 60 # 等待任務完成的最大時長,單位為秒。預設為 0 ,根據自己應用來設定
  • spring.task.scheduling 配置項,Spring Task 排程任務的配置,對應 TaskSchedulingProperties 配置類。
  • Spring Boot TaskSchedulingAutoConfiguration 自動化配置類,實現 Spring Task 的自動配置,建立 ThreadPoolTaskScheduler 基於執行緒池的任務排程器。本質上,ThreadPoolTaskScheduler 是基於 ScheduledExecutorService 的封裝,增強在排程時間上的功能。

注意spring.task.scheduling.shutdown 配置項,是為了實現 Spring Task 定時任務的優雅關閉。我們想象一下,如果定時任務在執行的過程中,如果應用開始關閉,把定時任務需要使用到的 Spring Bean 進行銷燬,例如說資料庫連線池,那麼此時定時任務還在執行中,一旦需要訪問資料庫,可能會導致報錯。

  • 所以,通過配置 await-termination = true ,實現應用關閉時,等待定時任務執行完成。這樣,應用在關閉的時,Spring 會優先等待 ThreadPoolTaskScheduler 執行完任務之後,再開始 Spring Bean 的銷燬。
  • 同時,又考慮到我們不可能無限等待定時任務全部執行結束,因此可以配置 await-termination-period = 60 ,等待任務完成的最大時長,單位為秒。具體設定多少的等待時長,可以根據自己應用的需要。

3. 快速入門 Quartz 單機

示例程式碼對應倉庫:lab-28-task-quartz-memory

在艿艿最早開始實習的時候,公司使用 Quartz 作為任務排程中介軟體。考慮到我們要實現定時任務的高可用,需要部署多個 JVM 程式。比較舒服的是,Quartz 自帶了叢集方案。它通過將作業資訊儲存到關聯式資料庫中,並使用關聯式資料庫的行鎖來實現執行作業的競爭,從而保證多個程式下,同一個任務在相同時刻,不能重複執行。

可能很多胖友對 Quartz 還不是很瞭解,我們先來看一段簡介:

FROM https://www.oschina.net/p/quartz

Quartz 是一個開源的作業排程框架,它完全由 Java 寫成,並設計用於 J2SE 和 J2EE 應用中。它提供了巨大的靈活性而不犧牲簡單性。你能夠用它來為執行一個作業而建立簡單的或複雜的排程。

它有很多特徵,如:資料庫支援,叢集,外掛,EJB 作業預構建,JavaMail 及其它,支援 cron-like 表示式等等。

在 Quartz 體系結構中,有三個元件非常重要:

  • Scheduler :排程器
  • Trigger :觸發器
  • Job :任務

不瞭解的胖友,可以直接看看 《Quartz 入門詳解》 文章。這裡,艿艿就不重複贅述。

FROM https://medium.com/@ChamithKo...

Quartz 整體架構圖

Quartz 分成單機模式和叢集模式。

  • 本小節,我們先來學習下 Quartz 的單機模式,入門比較快。
  • 下一下「5. 再次入門 Quartz 叢集」 ,我們再來學習下 Quartz 的叢集模式。在生產環境下,一定一定一定要使用 Quartz 的叢集模式,保證定時任務的高可用。

? 下面,讓我們開始遨遊~

3.1 引入依賴

pom.xml 檔案中,引入相關依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-28-task-quartz-memory</artifactId>

    <dependencies>
        <!-- 實現對 Spring MVC 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 實現對 Quartz 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
    </dependencies>

</project>

具體每個依賴的作用,胖友自己認真看下艿艿新增的所有註釋噢。

3.2 示例 Job

cn.iocoder.springboot.lab28.task.config.job 包路徑下,我們來建立示例 Job 。

建立 DemoJob01 類,示例定時任務 01 類。程式碼如下:

// DemoJob01.java

public class DemoJob01 extends QuartzJobBean {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private final AtomicInteger counts = new AtomicInteger();

    @Autowired
    private DemoService demoService;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("[executeInternal][定時第 ({}) 次執行, demoService 為 ({})]", counts.incrementAndGet(),
                demoService);
    }

}
  • 繼承 QuartzJobBean 抽象類,實現 #executeInternal(JobExecutionContext context) 方法,執行自定義的定時任務的邏輯。
  • QuartzJobBean 實現了 org.quartz.Job 介面,提供了 Quartz 每次建立 Job 執行定時邏輯時,將該 Job Bean 的依賴屬性注入。例如說,DemoJob01 需要 @Autowired 注入的 demoService 屬性。核心程式碼如下:

    // QuartzJobBean.java
    
    public final void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            // 將當前物件,包裝成 BeanWrapper 物件
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            // 設定屬性到 bw 中
            MutablePropertyValues pvs = new MutablePropertyValues();
            pvs.addPropertyValues(context.getScheduler().getContext());
            pvs.addPropertyValues(context.getMergedJobDataMap());
            bw.setPropertyValues(pvs, true);
        } catch (SchedulerException ex) {
            throw new JobExecutionException(ex);
        }
    
        // 執行提供給子類實現的抽象方法
        this.executeInternal(context);
    }
    
    protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
    • 這樣一看,是不是清晰很多。不要懼怕中介軟體的原始碼,好奇哪個類或者方法,就點進去看看。反正,又不花錢。
  • counts 屬性,計數器。用於我們後面我們展示,每次 DemoJob01 都會被 Quartz 建立出一個新的 Job 物件,執行任務。這個很重要,也要非常小心。

建立 DemoJob02 類,示例定時任務 02 類。程式碼如下:

// DemoJob02.java

public class DemoJob02 extends QuartzJobBean {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("[executeInternal][我開始的執行了]");
    }

}
  • 比較簡單,為了後面演示案例之用。

3.3 ScheduleConfiguration

cn.iocoder.springboot.lab28.task.config 包路徑下,建立 ScheduleConfiguration 類,配置上述的兩個示例 Job 。程式碼如下:

// ScheduleConfiguration.java

@Configuration
public class ScheduleConfiguration {

    public static class DemoJob01Configuration {

        @Bean
        public JobDetail demoJob01() {
            return JobBuilder.newJob(DemoJob01.class)
                    .withIdentity("demoJob01") // 名字為 demoJob01
                    .storeDurably() // 沒有 Trigger 關聯的時候任務是否被保留。因為建立 JobDetail 時,還沒 Trigger 指向它,所以需要設定為 true ,表示保留。
                    .build();
        }

        @Bean
        public Trigger demoJob01Trigger() {
            // 簡單的排程計劃的構造器
            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(5) // 頻率。
                    .repeatForever(); // 次數。
            // Trigger 構造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob01()) // 對應 Job 為 demoJob01
                    .withIdentity("demoJob01Trigger") // 名字為 demoJob01Trigger
                    .withSchedule(scheduleBuilder) // 對應 Schedule 為 scheduleBuilder
                    .build();
        }

    }

    public static class DemoJob02Configuration {

        @Bean
        public JobDetail demoJob02() {
            return JobBuilder.newJob(DemoJob02.class)
                    .withIdentity("demoJob02") // 名字為 demoJob02
                    .storeDurably() // 沒有 Trigger 關聯的時候任務是否被保留。因為建立 JobDetail 時,還沒 Trigger 指向它,所以需要設定為 true ,表示保留。
                    .build();
        }

        @Bean
        public Trigger demoJob02Trigger() {
            // 基於 Quartz Cron 表示式的排程計劃的構造器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
            // Trigger 構造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob02()) // 對應 Job 為 demoJob02
                    .withIdentity("demoJob02Trigger") // 名字為 demoJob02Trigger
                    .withSchedule(scheduleBuilder) // 對應 Schedule 為 scheduleBuilder
                    .build();
        }

    }

}
  • 內部建立了 DemoJob01Configuration 和 DemoJob02Configuration 兩個配置類,分別配置 DemoJob01 和 DemoJob02 兩個 Quartz Job 。
  • ========== DemoJob01Configuration ==========
  • #demoJob01() 方法,建立 DemoJob01 的 JobDetail Bean 物件。
  • #demoJob01Trigger() 方法,建立 DemoJob01 的 Trigger Bean 物件。其中,我們使用 SimpleScheduleBuilder 簡單的排程計劃的構造器,建立了每 5 秒執行一次,無限重複的排程計劃。
  • ========== DemoJob2Configuration ==========
  • #demoJob2() 方法,建立 DemoJob02 的 JobDetail Bean 物件。
  • #demoJob02Trigger() 方法,建立 DemoJob02 的 Trigger Bean 物件。其中,我們使用 CronScheduleBuilder 基於 Quartz Cron 表示式的排程計劃的構造器,建立了每 10 秒執行一次的排程計劃。這裡,推薦一個 Quartz/Cron/Crontab 表示式線上生成工具 ,方便幫我們生成 Quartz Cron 表示式,並計算出最近 5 次執行時間。

? 因為 JobDetail 和 Trigger 一般是成雙成對出現,所以艿艿習慣配置成一個 Configuration 配置類。

3.4 Application

建立 Application.java 類,配置 @SpringBootApplication 註解即可。程式碼如下:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

執行 Application 類,啟動示例專案。輸出日誌精簡如下:

# 建立了 Quartz QuartzScheduler 並啟動
2019-11-30 23:40:05.123  INFO 92812 --- [           main] org.quartz.impl.StdSchedulerFactory      : Using default implementation for ThreadExecutor
2019-11-30 23:40:05.130  INFO 92812 --- [           main] org.quartz.core.SchedulerSignalerImpl    : Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
2019-11-30 23:40:05.130  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : Quartz Scheduler v.2.3.2 created.
2019-11-30 23:40:05.131  INFO 92812 --- [           main] org.quartz.simpl.RAMJobStore             : RAMJobStore initialized.
2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'quartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler 'quartzScheduler' initialized from an externally provided properties instance.
2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : JobFactory set to: org.springframework.scheduling.quartz.SpringBeanJobFactory@203dd56b
2019-11-30 23:40:05.158  INFO 92812 --- [           main] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now
2019-11-30 23:40:05.158  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler quartzScheduler_$_NON_CLUSTERED started.

# DemoJob01
2019-11-30 23:40:05.164  INFO 92812 --- [eduler_Worker-1] c.i.springboot.lab28.task.job.DemoJob01  : [executeInternal][定時第 (1) 次執行, demoService 為 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)]
2019-11-30 23:40:09.866  INFO 92812 --- [eduler_Worker-2] c.i.springboot.lab28.task.job.DemoJob01  : [executeInternal][定時第 (1) 次執行, demoService 為 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)]
2019-11-30 23:40:14.865  INFO 92812 --- [eduler_Worker-4] c.i.springboot.lab28.task.job.DemoJob01  : [executeInternal][定時第 (1) 次執行, demoService 為 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)]

# DemoJob02
2019-11-30 23:40:10.004  INFO 92812 --- [eduler_Worker-3] c.i.springboot.lab28.task.job.DemoJob02  : [executeInternal][我開始的執行了]
2019-11-30 23:40:20.001  INFO 92812 --- [eduler_Worker-6] c.i.springboot.lab28.task.job.DemoJob02  : [executeInternal][我開始的執行了]
2019-11-30 23:40:30.002  INFO 92812 --- [eduler_Worker-9] c.i.springboot.lab28.task.job.DemoJob02  : [executeInternal][我開始的執行了]
  • 專案啟動時,會建立了 Quartz QuartzScheduler 並啟動。
  • 考慮到閱讀日誌方便,艿艿這裡把 DemoJob01 和 DemoJob02 的日誌分開來了。
  • 對於 DemoJob01 ,每 5 秒左右執行一次。同時我們可以看到,demoService 成功注入,而 counts 每次都是 1 ,說明每次 DemoJob01 都是新建立的。
  • 對於 DemoJob02 ,每 10 秒執行一次。
下面「3.5 應用配置檔案」兩個小節,是補充知識,建議看看。

3.5 應用配置檔案

application.yml 中,新增 Quartz 的配置,如下:

spring:
  # Quartz 的配置,對應 QuartzProperties 配置類
  quartz:
    job-store-type: memory # Job 儲存器型別。預設為 memory 表示記憶體,可選 jdbc 使用資料庫。
    auto-startup: true # Quartz 是否自動啟動
    startup-delay: 0 # 延遲 N 秒啟動
    wait-for-jobs-to-complete-on-shutdown: true # 應用關閉時,是否等待定時任務執行完成。預設為 false ,建議設定為 true
    overwrite-existing-jobs: false # 是否覆蓋已有 Job 的配置
    properties: # 新增 Quartz Scheduler 附加屬性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文件
      org:
        quartz:
          threadPool:
            threadCount: 25 # 執行緒池大小。預設為 10 。
            threadPriority: 5 # 執行緒優先順序
            class: org.quartz.simpl.SimpleThreadPool # 執行緒池型別
#    jdbc: # 這裡暫時不說明,使用 JDBC 的 JobStore 的時候,才需要配置

注意spring.quartz.wait-for-jobs-to-complete-on-shutdown 配置項,是為了實現 Quartz 的優雅關閉,建議開啟。關於這塊,和我們在 Spring Task 的「2.6 應用配置檔案」 提到的是一致的。

4. 再次入門 Quartz 叢集

示例程式碼對應倉庫:lab-28-task-quartz-memory

實際場景下,我們必然需要考慮定時任務的高可用,所以基本上,肯定使用 Quartz 的叢集方案。因此本小節,我們使用 Quartz 的 JDBC 儲存器 JobStoreTX ,並是使用 MySQL 作為資料庫。

如下是 Quartz 兩種儲存器的對比:

FROM https://blog.csdn.net/Evankak...
型別 優點 缺點
RAMJobStore 不要外部資料庫,配置容易,執行速度快 因為排程程式資訊是儲存在被分配給 JVM 的記憶體裡面,所以,當應用程式停止執行時,所有排程資訊將被丟失。另外因為儲存到JVM記憶體裡面,所以可以儲存多少個 Job 和 Trigger 將會受到限制
JDBC 作業儲存 支援叢集,因為所有的任務資訊都會儲存到資料庫中,可以控制事物,還有就是如果應用伺服器關閉或者重啟,任務資訊都不會丟失,並且可以恢復因伺服器關閉或者重啟而導致執行失敗的任務 執行速度的快慢取決與連線資料庫的快慢
艿艿:實際上,有方案可以實現兼具這兩種方式的優點,我們在 「666. 彩蛋」 中來說。

另外,本小節提供的示例和 「3. 快速入門 Quartz 單機」 基本一致。? 下面,讓我們開始遨遊~

4.1 引入依賴

pom.xml 檔案中,引入相關依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-28-task-quartz-jdbc</artifactId>

    <dependencies>
        <!-- 實現對資料庫連線池的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我們使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- 實現對 Spring MVC 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 實現對 Quartz 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>

        <!-- 方便等會寫單元測試 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>
  • 「3.1 引入依賴」 基本一致,只是額外引入 spring-boot-starter-test 依賴,等會會寫兩個單元測試方法。

4.2 示例 Job

cn.iocoder.springboot.lab28.task.config.job 包路徑下,建立 DemoJob01DemoJob02 類。程式碼如下:

// DemoJob01.java

@DisallowConcurrentExecution
public class DemoJob01 extends QuartzJobBean {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DemoService demoService;

    @Override
    protected void executeInternal(JobExecutionContext context) {
        logger.info("[executeInternal][我開始的執行了, demoService 為 ({})]", demoService);
    }

}

// DemoJob02.java

@DisallowConcurrentExecution
public class DemoJob02 extends QuartzJobBean {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected void executeInternal(JobExecutionContext context) {
        logger.info("[executeInternal][我開始的執行了]");
    }

}

注意,不是以 Quartz Job 為維度,保證在多個 JVM 程式中,有且僅有一個節點在執行,而是以 JobDetail 為維度。雖然說,絕大多數情況下,我們會保證一個 Job 和 JobDetail 是一一對應。? 所以,搞不清楚這個概念的胖友,最好搞清楚這個概念。實在有點懵逼,保證一個 Job 和 JobDetail 是一一對應就對了。

而 JobDetail 的唯一標識JobKey ,使用 name + group 兩個屬性。一般情況下,我們只需要設定 name 即可,而 Quartz 會預設 group = DEFAULT

不過這裡還有一點要補充,也是需要注意的,在 Quartz 中,相同 Scheduler 名字的節點,形成一個 Quartz 叢集。在下文中,我們可以通過 spring.quartz.scheduler-name 配置項,設定 Scheduler 的名字。

【重要】為什麼要說這個呢?因為我們要完善一下上面的說法:通過在 Job 實現類上新增 @DisallowConcurrentExecution 註解,實現在相同 Quartz Scheduler 叢集中,相同 JobKey 的 JobDetail ,保證在多個 JVM 程式中,有且僅有一個節點在執行。

4.3 應用配置檔案

application.yml 中,新增 Quartz 的配置,如下:

spring:
  datasource:
    user:
      url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-user?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:
    quartz:
      url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-quartz?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:

  # Quartz 的配置,對應 QuartzProperties 配置類
  quartz:
    scheduler-name: clusteredScheduler # Scheduler 名字。預設為 schedulerName
    job-store-type: jdbc # Job 儲存器型別。預設為 memory 表示記憶體,可選 jdbc 使用資料庫。
    auto-startup: true # Quartz 是否自動啟動
    startup-delay: 0 # 延遲 N 秒啟動
    wait-for-jobs-to-complete-on-shutdown: true # 應用關閉時,是否等待定時任務執行完成。預設為 false ,建議設定為 true
    overwrite-existing-jobs: false # 是否覆蓋已有 Job 的配置
    properties: # 新增 Quartz Scheduler 附加屬性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文件
      org:
        quartz:
          # JobStore 相關配置
          jobStore:
            # 資料來源名稱
            dataSource: quartzDataSource # 使用的資料來源
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 實現類
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_ # Quartz 表字首
            isClustered: true # 是叢集模式
            clusterCheckinInterval: 1000
            useProperties: false
          # 執行緒池相關配置
          threadPool:
            threadCount: 25 # 執行緒池大小。預設為 10 。
            threadPriority: 5 # 執行緒優先順序
            class: org.quartz.simpl.SimpleThreadPool # 執行緒池型別
    jdbc: # 使用 JDBC 的 JobStore 的時候,JDBC 的配置
      initialize-schema: never # 是否自動使用 SQL 初始化 Quartz 表結構。這裡設定成 never ,我們手動建立表結構。
  • 配置項比較多,我們主要對比 「3.5 應用配置檔案」 來看看。
  • spring.datasource 配置項下,用於建立多個資料來源的配置。

    • user 配置,連線 lab-28-quartz-jdbc-user 庫。目的是,為了模擬我們一般專案,使用到的業務資料庫。
    • quartz 配置,連線 lab-28-quartz-jdbc-quartz 庫。目的是,Quartz 會使用單獨的資料庫。? 如果我們有多個專案需要使用到 Quartz 資料庫的話,可以統一使用一個,但是要注意配置 spring.quartz.scheduler-name 設定不同的 Scheduler 名字,形成不同的 Quartz 叢集。
  • spring.quartz 配置項下,額外增加了一些配置項,我們逐個來看看。

    • scheduler-name 配置,Scheduler 名字。這個我們在上文解釋了很多次了,如果還不明白,請拍死自己。
    • job-store-type 配置,設定了使用 "jdbc" 的 Job 儲存器。
    • properties.org.quartz.jobStore 配置,增加了 JobStore 相關配置。重點是,通過 dataSource 配置項,設定了使用名字為 "quartzDataSource" 的 DataSource 為資料來源。? 在 「4.4 DataSourceConfiguration」 中,我們會使用 spring.datasource.quartz 配置,來建立該資料來源。
    • jdbc 配置項,雖然名字叫這個,主要是為了設定使用 SQL 初始化 Quartz 表結構。這裡,我們設定 initialize-schema = never ,我們手動建立表結構。

咳咳咳,配置項確實有點多。如果暫時搞不明白的胖友,可以先簡單把 spring.datasource 資料來源,修改成自己的即可。

4.4 初始化 Quartz 表結構

Quartz Download 地址,下載對應版本的釋出包。解壓後,我們可以在 src/org/quartz/impl/jdbcjobstore/ 目錄,看到各種資料庫的 Quartz 表結構的初始化指令碼。這裡,因為我們使用 MySQL ,所以使用 tables_mysql_innodb.sql 指令碼。

在資料庫中執行該指令碼,完成初始化 Quartz 表結構。如下圖所示:Quartz 表結構

關於每個 Quartz 表結構的說明,可以看看 《Quartz 框架(二)——JobStore 資料庫表欄位詳解》 文章。? 實際上,也可以不看,哈哈哈哈。

我們會發現,每個表都有一個 SCHED_NAME 欄位,Quartz Scheduler 名字。這樣,實現每個 Quartz 叢集,資料層面的拆分。

4.5 DataSourceConfiguration

cn.iocoder.springboot.lab28.task.config 包路徑下,建立 DataSourceConfiguration 類,配置資料來源。程式碼如下:

// DataSourceConfiguration.java

@Configuration
public class DataSourceConfiguration {

    /**
     * 建立 user 資料來源的配置物件
     */
    @Primary
    @Bean(name = "userDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.user") // 讀取 spring.datasource.user 配置到 DataSourceProperties 物件
    public DataSourceProperties userDataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 建立 user 資料來源
     */
    @Primary
    @Bean(name = "userDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.user.hikari") // 讀取 spring.datasource.user 配置到 HikariDataSource 物件
    public DataSource userDataSource() {
        // 獲得 DataSourceProperties 物件
        DataSourceProperties properties =  this.userDataSourceProperties();
        // 建立 HikariDataSource 物件
        return createHikariDataSource(properties);
    }

    /**
     * 建立 quartz 資料來源的配置物件
     */
    @Bean(name = "quartzDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.quartz") // 讀取 spring.datasource.quartz 配置到 DataSourceProperties 物件
    public DataSourceProperties quartzDataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 建立 quartz 資料來源
     */
    @Bean(name = "quartzDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
    @QuartzDataSource
    public DataSource quartzDataSource() {
        // 獲得 DataSourceProperties 物件
        DataSourceProperties properties =  this.quartzDataSourceProperties();
        // 建立 HikariDataSource 物件
        return createHikariDataSource(properties);
    }

    private static HikariDataSource createHikariDataSource(DataSourceProperties properties) {
        // 建立 HikariDataSource 物件
        HikariDataSource dataSource = properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        // 設定執行緒池名
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }

}
  • 基於 spring.datasource.user 配置項,建立了名字為 "userDataSource" 的 DataSource Bean 。並且,在其上我們新增了 @Primay 註解,表示其是資料來源。
  • 基於 spring.datasource.quartz 配置項,建立了名字為 "quartzDataSource" 的 DataSource Bean 。並且,在其上我們新增了 @QuartzDataSource 註解,表示其是 Quartz 的資料來源。? 注意,一定要配置啊,這裡艿艿卡了好久!!!!

4.6 定時任務配置

完成上述的工作之後,我們需要配置 Quartz 的定時任務。目前,有兩種方式:

4.6.1 Bean 自動設定

cn.iocoder.springboot.lab28.task.config 包路徑下,建立 ScheduleConfiguration 類,配置上述的兩個示例 Job 。程式碼如下:

// ScheduleConfiguration.java

@Configuration
public class ScheduleConfiguration {

    public static class DemoJob01Configuration {

        @Bean
        public JobDetail demoJob01() {
            return JobBuilder.newJob(DemoJob01.class)
                    .withIdentity("demoJob01") // 名字為 demoJob01
                    .storeDurably() // 沒有 Trigger 關聯的時候任務是否被保留。因為建立 JobDetail 時,還沒 Trigger 指向它,所以需要設定為 true ,表示保留。
                    .build();
        }

        @Bean
        public Trigger demoJob01Trigger() {
            // 簡單的排程計劃的構造器
            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(5) // 頻率。
                    .repeatForever(); // 次數。
            // Trigger 構造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob01()) // 對應 Job 為 demoJob01
                    .withIdentity("demoJob01Trigger") // 名字為 demoJob01Trigger
                    .withSchedule(scheduleBuilder) // 對應 Schedule 為 scheduleBuilder
                    .build();
        }

    }

    public static class DemoJob02Configuration {

        @Bean
        public JobDetail demoJob02() {
            return JobBuilder.newJob(DemoJob02.class)
                    .withIdentity("demoJob02") // 名字為 demoJob02
                    .storeDurably() // 沒有 Trigger 關聯的時候任務是否被保留。因為建立 JobDetail 時,還沒 Trigger 指向它,所以需要設定為 true ,表示保留。
                    .build();
        }

        @Bean
        public Trigger demoJob02Trigger() {
            // 簡單的排程計劃的構造器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
            // Trigger 構造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob02()) // 對應 Job 為 demoJob02
                    .withIdentity("demoJob02Trigger") // 名字為 demoJob02Trigger
                    .withSchedule(scheduleBuilder) // 對應 Schedule 為 scheduleBuilder
                    .build();
        }

    }

}

在 Quartz 排程器啟動的時候,會根據該配置,自動呼叫如下方法:

  • Scheduler#addJob(JobDetail jobDetail, boolean replace) 方法,將 JobDetail 持久化到資料庫。
  • Scheduler#scheduleJob(Trigger trigger) 方法,將 Trigger 持久化到資料庫。

4.6.2 Scheduler 手動設定

一般情況下,艿艿推薦使用 Scheduler 手動設定。

建立 QuartzSchedulerTest 類,建立分別新增 DemoJob01 和 DemoJob02 的 Quartz 定時任務配置。程式碼如下:

// QuartzSchedulerTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class QuartzSchedulerTest {

    @Autowired
    private Scheduler scheduler;

    @Test
    public void addDemoJob01Config() throws SchedulerException {
        // 建立 JobDetail
        JobDetail jobDetail = JobBuilder.newJob(DemoJob01.class)
                .withIdentity("demoJob01") // 名字為 demoJob01
                .storeDurably() // 沒有 Trigger 關聯的時候任務是否被保留。因為建立 JobDetail 時,還沒 Trigger 指向它,所以需要設定為 true ,表示保留。
                .build();
        // 建立 Trigger
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(5) // 頻率。
                .repeatForever(); // 次數。
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail) // 對應 Job 為 demoJob01
                .withIdentity("demoJob01Trigger") // 名字為 demoJob01Trigger
                .withSchedule(scheduleBuilder) // 對應 Schedule 為 scheduleBuilder
                .build();
        // 新增排程任務
        scheduler.scheduleJob(jobDetail, trigger);
//        scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
    }

    @Test
    public void addDemoJob02Config() throws SchedulerException {
        // 建立 JobDetail
        JobDetail jobDetail = JobBuilder.newJob(DemoJob02.class)
                .withIdentity("demoJob02") // 名字為 demoJob02
                .storeDurably() // 沒有 Trigger 關聯的時候任務是否被保留。因為建立 JobDetail 時,還沒 Trigger 指向它,所以需要設定為 true ,表示保留。
                .build();
        // 建立 Trigger
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail) // 對應 Job 為 demoJob01
                .withIdentity("demoJob02Trigger") // 名字為 demoJob01Trigger
                .withSchedule(scheduleBuilder) // 對應 Schedule 為 scheduleBuilder
                .build();
        // 新增排程任務
        scheduler.scheduleJob(jobDetail, trigger);
//        scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
    }

}
  • 建立 JobDetail 和 Trigger 的程式碼,其實和 「4.6.1 Bean 自動設定」 是一致的。
  • 在每個單元測試方法的最後,呼叫 Scheduler#scheduleJob(JobDetail jobDetail, Trigger trigger) 方法,將 JobDetail 和 Trigger 持久化到資料庫。
  • 如果想要覆蓋資料庫中的 Quartz 定時任務的配置,可以呼叫 Scheduler#scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace) 方法,傳入 replace = true 進行覆蓋配置。

4.7 Application

建立 Application.java 類,配置 @SpringBootApplication 註解即可。程式碼如下:

// Application.java

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}
  • 執行 Application 類,啟動示例專案。具體的執行日誌,和 「3.4 Application」 基本一致,艿艿這裡就不重複羅列了。

如果胖友想要測試叢集下的執行情況,可以再建立 建立 Application02.java 類,配置 @SpringBootApplication 註解即可。程式碼如下:

// Application02.java

@SpringBootApplication
public class Application02 {

    public static void main(String[] args) {
        // 設定 Tomcat 隨機埠
        System.setProperty("server.port", "0");

        // 啟動 Spring Boot 應用
        SpringApplication.run(Application.class, args);
    }

}
  • 執行 Application02 類,再次啟動一個示例專案。然後,觀察輸出的日誌,可以看到啟動的兩個示例專案,都會有 DemoJob01 和 DemoJob02 的執行日誌。

5. 快速入門 XXL-JOB

示例程式碼對應倉庫:lab-28-task-xxl-job

雖然說,Quartz 的功能,已經能夠滿足我們對定時任務的訴求,但是距離生產可用、好用,還是有一定的距離。在艿艿最早開始實習的時候,因為Quartz 只提供了任務排程的功能,不提供管理任務的管理與監控控制檯,需要自己去做二次封裝。當時,因為社群中找不到合適的實現這塊功能的開源專案,所以我們就自己進行了簡單的封裝,滿足我們的管理與監控的需求。

不過現在呢,開源社群中已經有了很多優秀的排程任務中介軟體。其中,比較有代表性的就是 XXL-JOB 。其對自己的定義如下:

XXL-JOB 是一個輕量級分散式任務排程平臺,其核心設計目標是開發迅速、學習簡單、輕量級、易擴充套件。

對於 XXL-JOB 的入門,艿艿已經在 《芋道 XXL-JOB 極簡入門》 中編寫,胖友先跳轉到該文章閱讀。重點是,要先搭建一個 XXL-JOB 排程中心。? 因為,本文我們是來在 Spring Boot 專案中,實現一個 XXL-JOB 執行器。

5.1 引入依賴

pom.xml 檔案中,引入相關依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-28-task-xxl-job</artifactId>

    <dependencies>
        <!-- 實現對 Spring MVC 的自動化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- XXL-JOB 相關依賴 -->
        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
            <version>2.1.1</version>
        </dependency>
    </dependencies>

</project>

具體每個依賴的作用,胖友自己認真看下艿艿新增的所有註釋噢。比較可惜的是,目前 XXL-JOB 官方並未提供 Spring Boot Starter 包,略微有點尷尬。不過,社群已經有人在提交 Pull Request 了,詳細可見 https://github.com/xuxueli/xx...

5.2 應用配置檔案

application.yml 中,新增 Quartz 的配置,如下:

server:
  port: 9090 #指定一個埠,避免和 XXL-JOB 排程中心的埠衝突。僅僅測試之用

# xxl-job
xxl:
  job:
    admin:
      addresses: http://127.0.0.1:8080/xxl-job-admin # 排程中心部署跟地址 [選填]:如排程中心叢集部署存在多個地址則用逗號分隔。執行器將會使用該地址進行"執行器心跳註冊"和"任務結果回撥";為空則關閉自動註冊;
    executor:
      appname: lab-28-executor # 執行器 AppName [選填]:執行器心跳註冊分組依據;為空則關閉自動註冊
      ip: # 執行器IP [選填]:預設為空表示自動獲取IP,多網路卡時可手動設定指定IP,該IP不會繫結Host僅作為通訊實用;地址資訊用於 "執行器註冊" 和 "排程中心請求並觸發任務";
      port: 6666 # ### 執行器埠號 [選填]:小於等於0則自動獲取;預設埠為9999,單機部署多個執行器時,注意要配置不同執行器埠;
      logpath: /Users/yunai/logs/xxl-job/lab-28-executor # 執行器執行日誌檔案儲存磁碟路徑 [選填] :需要對該路徑擁有讀寫許可權;為空則使用預設路徑;
      logretentiondays: 30 # 執行器日誌檔案儲存天數 [選填] : 過期日誌自動清理, 限制值大於等於3時生效; 否則, 如-1, 關閉自動清理功能;
    accessToken: yudaoyuanma # 執行器通訊TOKEN [選填]:非空時啟用;
  • 具體每個引數的作用,胖友自己看下詳細的註釋哈。

5.3 XxlJobConfiguration

cn.iocoder.springboot.lab28.task.config 包路徑下,建立 DataSourceConfiguration 類,配置 XXL-JOB 執行器。程式碼如下:

// XxlJobConfiguration.java

@Configuration
public class XxlJobConfiguration {

    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;
    @Value("${xxl.job.executor.appname}")
    private String appName;
    @Value("${xxl.job.executor.ip}")
    private String ip;
    @Value("${xxl.job.executor.port}")
    private int port;
    @Value("${xxl.job.accessToken}")
    private String accessToken;
    @Value("${xxl.job.executor.logpath}")
    private String logPath;
    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;

    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        // 建立 XxlJobSpringExecutor 執行器
        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
        xxlJobSpringExecutor.setAppName(appName);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
        // 返回
        return xxlJobSpringExecutor;
    }

}
  • #xxlJobExecutor() 方法,建立了 Spring 容器下的 XXL-JOB 執行器 Bean 物件。要注意,方法上新增了的 @Bean 註解,配置了啟動和銷燬方法。

5.4 DemoJob

cn.iocoder.springboot.lab28.task.job 包路徑下,建立 DemoJob 類,示例定時任務類。程式碼如下:

// DemoJob.java

@Component
@JobHandler("demoJob")
public class DemoJob extends IJobHandler {

    private Logger logger = LoggerFactory.getLogger(getClass());

    private final AtomicInteger counts = new AtomicInteger();

    @Override
    public ReturnT<String> execute(String param) throws Exception {
        // 列印日誌
        logger.info("[execute][定時第 ({}) 次執行]", counts.incrementAndGet());
        // 返回執行成功
        return ReturnT.SUCCESS;
    }

}
  • 繼承 XXL-JOB IJobHandler 抽象類,通過實現 #execute(String param) 方法,從而實現定時任務的邏輯。
  • 在方法上,新增 @JobHandler 註解,設定 JobHandler 的名字。後續,我們在排程中心的控制檯中,新增任務時,需要使用到這個名字。

#execute(String param) 方法的返回結果,為 ReturnT 型別。當返回值符合 “ReturnT.code == ReturnT.SUCCESS_CODE” 時表示任務執行成功,否則表示任務執行失敗,而且可以通過 “ReturnT.msg” 回撥錯誤資訊給排程中心;從而,在任務邏輯中可以方便的控制任務執行結果。

#execute(String param) 方法的方法引數,為排程中心的控制檯中,新增任務時,配置的“任務引數”。一般情況下,不會使用到。

5.5 Application

建立 Application.java 類,配置 @SpringBootApplication 註解即可。程式碼如下:

// Application.java

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

執行 Application 類,啟動示例專案。輸出日誌精簡如下:

# XXL-JOB 啟動日誌
2019-11-29 00:58:42.429  INFO 46957 --- [           main] c.xxl.job.core.executor.XxlJobExecutor   : >>>>>>>>>>> xxl-job register jobhandler success, name:demoJob, jobHandler:cn.iocoder.springboot.lab28.task.job.DemoJob@3af9aa66
2019-11-29 00:58:42.451  INFO 46957 --- [           main] c.x.r.r.provider.XxlRpcProviderFactory   : >>>>>>>>>>> xxl-rpc, provider factory add service success. serviceKey = com.xxl.job.core.biz.ExecutorBiz, serviceBean = class com.xxl.job.core.biz.impl.ExecutorBizImpl
2019-11-29 00:58:42.454  INFO 46957 --- [           main] c.x.r.r.provider.XxlRpcProviderFactory   : >>>>>>>>>>> xxl-rpc, provider factory add service success. serviceKey = com.xxl.job.core.biz.ExecutorBiz, serviceBean = class com.xxl.job.core.biz.impl.ExecutorBizImpl
2019-11-29 00:58:42.565  INFO 46957 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2019-11-29 00:58:42.629  INFO 46957 --- [       Thread-7] com.xxl.rpc.remoting.net.Server          : >>>>>>>>>>> xxl-rpc remoting server start success, nettype = com.xxl.rpc.remoting.net.impl.netty_http.server.NettyHttpServer, port = 6666

此時,因為我們並未在 XXL-JOB 排程中心進行相關的配置,所以 DemoJob 並不會執行。下面,讓我們在 XXL-JOB 排程中心進行相應的配置。

5.6 新增執行器

瀏覽器開啟 http://127.0.0.1:8080/xxl-job... 地址,即「執行器管理」選單。如下圖:執行器管理

點選「新增執行器」按鈕,彈出「新增執行器」介面。如下圖:新增執行器

填寫完 "lab-28-executor" 執行器的資訊,點選「儲存」按鈕,進行儲存。耐心等待一會,執行器會自動註冊上來。如下圖:執行器管理

  • 執行器列表中顯示線上的執行器列表, 可通過 "OnLine 機器" 檢視對應執行器的叢集機器。

相同執行器,有且僅需配置一次即可。

5.7 新建任務

瀏覽器開啟 http://127.0.0.1:8080/xxl-job... 地址,即「任務管理」選單。如下圖:任務管理

點選最右邊的「新增」按鈕,彈出「新增」介面。如下圖:新增

填寫完 "demoJob" 任務的資訊,點選「儲存」按鈕,進行儲存。如下圖:任務管理

點選 "demoJob" 任務的「操作」按鈕,選擇「啟動」,確認後,該 "demoJob" 任務的狀態就變成了 RUNNING 。如下圖:任務管理

此時,我們開啟執行器的 IDE 介面,可以看到 DemoJob 已經在每分鐘執行一次了。日誌如下:

2019-11-29 01:30:00.161  INFO 48374 --- [      Thread-18] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (1) 次執行]
2019-11-29 01:31:00.012  INFO 48374 --- [      Thread-18] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (2) 次執行]
2019-11-29 01:32:00.009  INFO 48374 --- [      Thread-18] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (3) 次執行]
2019-11-29 01:33:00.010  INFO 48374 --- [      Thread-18] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (4) 次執行]
2019-11-29 01:34:00.005  INFO 48374 --- [      Thread-18] c.i.springboot.lab28.task.job.DemoJob    : [execute][定時第 (5) 次執行]

並且,我們在排程中心的介面上,點選 "demoJob" 任務的「操作」按鈕,選擇「查詢日誌」,可以看到相應的排程日誌。如下圖:查詢日誌

至此,我們已經完成了 XXL-JOB 執行器的入門。

6. 快速入門 Elastic-Job

可能很多胖友不瞭解 Elastic-Job 這個中介軟體。我們看一段其官方文件的介紹:

Elastic-Job 是一個分散式排程解決方案,由兩個相互獨立的子專案 Elastic-Job-Lite 和 Elastic-Job-Cloud 組成。

Elastic-Job-Lite 定位為輕量級無中心化解決方案,使用 jar 包的形式提供分散式任務的協調服務。

Elastic-Job 基本是國內開源最好的排程任務中介軟體的幾個中介軟體,可能沒有之一,嘿嘿。目前處於有點“斷更”的狀態,具體可見 https://github.com/elasticjob...

所以關於這塊的示例,艿艿暫時先不提供。如果對 Elastic-Job 原始碼感興趣的胖友,可以看看艿艿寫的如下兩個系列:

666. 彩蛋

① 如何選擇?

可能胖友希望瞭解下不同排程中介軟體的對比。表格如下:

特性 quartz elastic-job-lite xxl-job LTS
依賴 MySQL、jdk jdk、zookeeper mysql、jdk jdk、zookeeper、maven
高可用 多節點部署,通過競爭資料庫鎖來保證只有一個節點執行任務 通過zookeeper的註冊與發現,可以動態的新增伺服器 基於競爭資料庫鎖保證只有一個節點執行任務,支援水平擴容。可以手動增加定時任務,啟動和暫停任務,有監控 叢集部署,可以動態的新增伺服器。可以手動增加定時任務,啟動和暫停任務。有監控
任務分片 ×
管理介面 ×
難易程度 簡單 簡單 簡單 略複雜
高階功能 - 彈性擴容,多種作業模式,失效轉移,執行狀態收集,多執行緒處理資料,冪等性,容錯處理,spring名稱空間支援 彈性擴容,分片廣播,故障轉移,Rolling實時日誌,GLUE(支援線上編輯程式碼,免釋出),任務進度監控,任務依賴,資料加密,郵件報警,執行報表,國際化 支援spring,spring boot,業務日誌記錄器,SPI擴充套件支援,故障轉移,節點監控,多樣化任務執行結果支援,FailStore容錯,動態擴容。
版本更新 半年沒更新 2年沒更新 最近有更新 1年沒更新

也推薦看看如下文章:

目前的狀況,如果真的不知道怎麼選擇,可以先嚐試下 XXL-JOB

② 中心化 V.S 去中心化

下面,讓我們一起來簡單聊聊分散式排程中介軟體的實現方式的分類。一個分散式的排程中介軟體,會存在兩種角色:

  • 排程器:負責排程任務,下發給執行器。
  • 執行器:負責接收任務,執行具體任務。

那麼,如果從排程系統的角度來看,可以分成兩類:

  • 中心化: 排程中心和執行器分離,排程中心統一排程,通知某個執行器處理任務。
  • 去中心化:排程中心和執行器一體化,自己排程自己執行處理任務。

如此可知 XXL-Job 屬於中心化的任務排程平臺。目前採用這種方案的還有:

  • 鏈家的 kob
  • 美團的 Crane(暫未開源)

去中心化的任務排程平臺,目前有:

艿艿:如果胖友想要更加的理解,可以看看艿艿朋友寫的 《中心化 V.S 去中心化排程設計》

③ 任務競爭 V.S 任務預分配

那麼,如果從任務分配的角度來看,可以分成兩類:

  • 任務競爭:排程器會通過競爭任務,下發任務給執行器。
  • 任務預分配:排程器預先分配任務給不同的執行器,無需進行競爭。

如此可知 XXL-Job 屬於任務競爭的任務排程平臺。目前採用這種方案的還有:

  • 鏈家的 kob
  • 美團的 Crane(暫未開源)
  • Quartz 基於資料庫的叢集方案

任務預分配的任務排程平臺,目前有:

一般來說,基於任務預分配的任務排程平臺,都會選擇使用 Zookeeper 來協調分配任務到不同的節點上。同時,任務排程平臺必須是去中心化的方案,每個節點即是排程器又是執行器。這樣,任務在預分配在每個節點之後,後續就自己排程給自己執行。

相比較而言,隨著節點越來越多,基於任務競爭的方案會因為任務競爭,導致存在效能下滑的問題。而基於任務預分配的方案,則不會存在這個問題。並且,基於任務預分配的方案,效能會優於基於任務競爭的方案。

這裡在推薦一篇 Elastic Job 開發者張亮的文章 《詳解當當網的分散式作業框架 elastic-job》 ,灰常給力!

④ Quartz 是個優秀的排程核心

絕大多數情況下,我們不會直接使用 Quartz 作為我們的排程中介軟體的選擇。但是,基本所有的分散式排程中介軟體,都將 Quartz 作為排程核心,因為 Quartz 在單純任務排程本身提供了很強的功能。

不過呢,隨著一個分散式排程中介軟體的逐步完善,又會逐步考慮拋棄 Quartz 作為排程核心,轉而自研。例如說 XXL-JOB 在 2.1.0 RELEASE 的版本中,就已經更換成自研的排程模組。其替換的理由如下:

XXL-JOB 最終選擇自研排程元件(早期排程元件基於 Quartz);

  • 一方面,是為了精簡系統降低冗餘依賴。
  • 另一方面,是為了提供系統的可控度與穩定性。

在 Elastic-Job 3.X 的開發計劃中,也有一項計劃,就是自研排程元件,替換掉 Quartz 。

推薦閱讀

相關文章