你們好,我是艿艿,一個平常在地鐵擼碼的小胖子~html
今天咱們來一塊兒瞅瞅,在 Spring Boot 應用中,有哪些定時任務的技術選型~java
在產品的色彩斑斕的黑的需求中,有存在一類需求,是須要去定時執行的,此時就須要使用到定時任務。例如說,每分鐘掃描超時支付的訂單,每小時清理一第二天志文件,天天統計前一天的數據並生成報表,每月月初的工資單的推送,每一年一次的生日提醒等等。mysql
其中,艿艿最喜歡「每月月初的工資單的推送」,你呢?
在 JDK 中,內置了兩個類,能夠實現定時任務的功能:git
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 來實現定時任務的需求。主要有幾點緣由:github
因此,通常狀況下,咱們會選擇專業的調度任務中間件。web
關於「 任務」的叫法,也有叫「 做業」的。在英文上,有 Task 也有 Job 。本質是同樣的,本文兩種都會用。而後,通常來講是調度任務,定時執行。因此胖友會在本文,或者其它文章中,會看到「調度」或「定時」的字眼兒。面試
在 Spring 體系中,內置了兩種定時任務的解決方案:spring
第二種,Spring Boot 2.0 版本,整合了 Quartz 做業調度框架,提供了功能強大的定時任務的實現。sql
注:Spring Framework 已經內置了 Quartz 的整合。Spring Boot 1.X 版本未提供 Quartz 的自動化配置,而 2.X 版本提供了支持。
在 Java 生態中,還有很是多優秀的開源的調度任務中間件:數據庫
惟品會基於 Elastic-Job 之上,演化出了 Saturn 項目。
目前國內採用 Elastic-Job 和 XXL-JOB 爲主。從艿艿瞭解到的狀況,使用 XXL-JOB 的團隊會更多一些,主要是上手較爲容易,運維功能更爲完善。
本文在提供完整代碼示例,可見 https://github.com/YunaiV/Spr... 的 lab-28 目錄。原創不易,給點個 Star 嘿,一塊兒衝鴨!
示例代碼對應倉庫: lab-28-task-demo 。
考慮到實際場景下,咱們不多使用 Spring Task ,因此本小節會寫的比較簡潔。若是對 Spring Task 比較感興趣的胖友,能夠本身去閱讀 《Spring Framework Documentation —— Task Execution and Scheduling》 文檔,裏面有 Spring Task 相關的詳細文檔。
在本小節,咱們會使用 Spring Task 功能,實現一個每 2 秒打印一行執行日誌的定時任務。
在 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
依賴。
在 cn.iocoder.springboot.lab28.task.config
包路徑下,建立 ScheduleConfiguration 類,配置 Spring Task 。代碼以下:
// ScheduleConfiguration.java @Configuration @EnableScheduling public class ScheduleConfiguration { }
@EnableScheduling
註解,啓動 Spring Task 的定時任務調度的功能。在 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 類,一個定時任務。😈
建立 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) 次執行]
至此,咱們已經完成了 Spring Task 調度任務功能的入門。實際上,Spring Task 還提供了異步任務 ,這個咱們在其它文章中,詳細講解。
下面 「2.5 @Scheduled」和 「2.6 應用配置文件」兩個小節,是補充知識,建議看看。
@Scheduled
註解,設置定時任務的執行計劃。
經常使用屬性以下:
cron
屬性:Spring Cron 表達式。例如說,"0 0 12 * * ?"
表示天天中午執行一次,"11 11 11 11 11 ?"
表示 11 月 11 號 11 點 11 分 11 秒執行一次(哈哈哈)。更多示例和講解,能夠看看 《Spring Cron 表達式》 文章。注意,以調用完成時刻爲開始計時時間。fixedDelay
屬性:固定執行間隔,單位:毫秒。注意,以調用完成時刻爲開始計時時間。fixedRate
屬性:固定執行間隔,單位:毫秒。注意,以調用開始時刻爲開始計時時間。不經常使用屬性以下:
initialDelay
屬性:初始化的定時任務執行延遲,單位:毫秒。zone
屬性:解析 Spring Cron 表達式的所屬的時區。默認狀況下,使用服務器的本地時區。initialDelayString
屬性:initialDelay
的字符串形式。fixedDelayString
屬性:fixedDelay
的字符串形式。fixedRateString
屬性:fixedRate
的字符串形式。在 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.task.scheduling.shutdown
配置項,是爲了實現 Spring Task 定時任務的優雅關閉。咱們想象一下,若是定時任務在執行的過程當中,若是應用開始關閉,把定時任務須要使用到的 Spring Bean 進行銷燬,例如說數據庫鏈接池,那麼此時定時任務還在執行中,一旦須要訪問數據庫,可能會致使報錯。
await-termination = true
,實現應用關閉時,等待定時任務執行完成。這樣,應用在關閉的時,Spring 會優先等待 ThreadPoolTaskScheduler 執行完任務以後,再開始 Spring Bean 的銷燬。await-termination-period = 60
,等待任務完成的最大時長,單位爲秒。具體設置多少的等待時長,能夠根據本身應用的須要。示例代碼對應倉庫: lab-28-task-quartz-memory 。
在艿艿最先開始實習的時候,公司使用 Quartz 做爲任務調度中間件。考慮到咱們要實現定時任務的高可用,須要部署多個 JVM 進程。比較舒服的是,Quartz 自帶了集羣方案。它經過將做業信息存儲到關係數據庫中,並使用關係數據庫的行鎖來實現執行做業的競爭,從而保證多個進程下,同一個任務在相同時刻,不能重複執行。
可能不少胖友對 Quartz 還不是很瞭解,咱們先來看一段簡介:
FROM https://www.oschina.net/p/quartzQuartz 是一個開源的做業調度框架,它徹底由 Java 寫成,並設計用於 J2SE 和 J2EE 應用中。它提供了巨大的靈活性而不犧牲簡單性。你可以用它來爲執行一個做業而建立簡單的或複雜的調度。
它有不少特徵,如:數據庫支持,集羣,插件,EJB 做業預構建,JavaMail 及其它,支持 cron-like 表達式等等。
在 Quartz 體系結構中,有三個組件很是重要:
不瞭解的胖友,能夠直接看看 《Quartz 入門詳解》 文章。這裏,艿艿就不重複贅述。
FROM https://medium.com/@ChamithKo...
Quartz 分紅單機模式和集羣模式。
😈 下面,讓咱們開始遨遊~
在 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>
具體每一個依賴的做用,胖友本身認真看下艿艿添加的全部註釋噢。
在 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); } }
#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][我開始的執行了]"); } }
在 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(); } } }
#demoJob01()
方法,建立 DemoJob01 的 JobDetail Bean 對象。#demoJob01Trigger()
方法,建立 DemoJob01 的 Trigger Bean 對象。其中,咱們使用 SimpleScheduleBuilder 簡單的調度計劃的構造器,建立了每 5 秒執行一次,無限重複的調度計劃。#demoJob2()
方法,建立 DemoJob02 的 JobDetail Bean 對象。#demoJob02Trigger()
方法,建立 DemoJob02 的 Trigger Bean 對象。其中,咱們使用 CronScheduleBuilder 基於 Quartz Cron 表達式的調度計劃的構造器,建立了每第 10 秒執行一次的調度計劃。這裏,推薦一個 Quartz/Cron/Crontab 表達式在線生成工具 ,方便幫咱們生成 Quartz Cron 表達式,並計算出最近 5 次運行時間。😈 由於 JobDetail 和 Trigger 通常是成雙成對出現,因此艿艿習慣配置成一個 Configuration 配置類。
建立 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][我開始的執行了]
demoService
成功注入,而 counts
每次都是 1 ,說明每次 DemoJob01 都是新建立的。下面 「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
配置項,Quartz 的配置,對應 QuartzProperties 配置類。注意,spring.quartz.wait-for-jobs-to-complete-on-shutdown
配置項,是爲了實現 Quartz 的優雅關閉,建議開啓。關於這塊,和咱們在 Spring Task 的「2.6 應用配置文件」 提到的是一致的。
示例代碼對應倉庫: 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 單機」 基本一致。😈 下面,讓咱們開始遨遊~
在 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>
spring-boot-starter-test
依賴,等會會寫兩個單元測試方法。在 cn.iocoder.springboot.lab28.task.config.job
包路徑下,建立 DemoJob01 和 DemoJob02 類。代碼以下:
// 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][我開始的執行了]"); } }
@DisallowConcurrentExecution
註解,保證相同 JobDetail 在多個 JVM 進程中,有且僅有一個節點在執行。注意,不是以 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 進程中,有且僅有一個節點在執行。
在 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 ,咱們手動建立表結構。
在 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
數據源,修改爲本身的便可。
在 Quartz Download 地址,下載對應版本的發佈包。解壓後,咱們能夠在 src/org/quartz/impl/jdbcjobstore/
目錄,看到各類數據庫的 Quartz 表結構的初始化腳本。這裏,由於咱們使用 MySQL ,因此使用 tables_mysql_innodb.sql
腳本。
在數據庫中執行該腳本,完成初始化 Quartz 表結構。以下圖所示:
關於每一個 Quartz 表結構的說明,能夠看看 《Quartz 框架(二)——JobStore 數據庫表字段詳解》 文章。😈 實際上,也能夠不看,哈哈哈哈。
咱們會發現,每一個表都有一個 SCHED_NAME
字段,Quartz Scheduler 名字。這樣,實現每一個 Quartz 集羣,數據層面的拆分。
在 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 的數據源。😈 注意,必定要配置啊,這裏艿艿卡了很久!!!!完成上述的工做以後,咱們須要配置 Quartz 的定時任務。目前,有兩種方式:
在 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 持久化到數據庫。通常狀況下,艿艿推薦使用 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); } }
Scheduler#scheduleJob(JobDetail jobDetail, Trigger trigger)
方法,將 JobDetail 和 Trigger 持久化到數據庫。Scheduler#scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace)
方法,傳入 replace = true
進行覆蓋配置。建立 Application.java
類,配置 @SpringBootApplication
註解便可。代碼以下:
// Application.java @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
若是胖友想要測試集羣下的運行狀況,能夠再建立 建立 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); } }
示例代碼對應倉庫: lab-28-task-xxl-job 。
雖說,Quartz 的功能,已經可以知足咱們對定時任務的訴求,可是距離生產可用、好用,仍是有必定的距離。在艿艿最先開始實習的時候,由於Quartz 只提供了任務調度的功能,不提供管理任務的管理與監控控制檯,須要本身去作二次封裝。當時,由於社區中找不到合適的實現這塊功能的開源項目,因此咱們就本身進行了簡單的封裝,知足咱們的管理與監控的需求。
不過如今呢,開源社區中已經有了不少優秀的調度任務中間件。其中,比較有表明性的就是 XXL-JOB 。其對本身的定義以下:
XXL-JOB 是一個輕量級分佈式任務調度平臺,其核心設計目標是開發迅速、學習簡單、輕量級、易擴展。
對於 XXL-JOB 的入門,艿艿已經在 《芋道 XXL-JOB 極簡入門》 中編寫,胖友先跳轉到該文章閱讀。重點是,要先搭建一個 XXL-JOB 調度中心。😈 由於,本文咱們是來在 Spring Boot 項目中,實現一個 XXL-JOB 執行器。
在 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... 。
在 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 [選填]:非空時啓用;
在 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
註解,配置了啓動和銷燬方法。在 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; } }
#execute(String param)
方法,從而實現定時任務的邏輯。@JobHandler
註解,設置 JobHandler 的名字。後續,咱們在調度中心的控制檯中,新增任務時,須要使用到這個名字。#execute(String param)
方法的返回結果,爲 ReturnT 類型。當返回值符合 「ReturnT.code == ReturnT.SUCCESS_CODE」
時表示任務執行成功,不然表示任務執行失敗,並且能夠經過 「ReturnT.msg」
回調錯誤信息給調度中心;從而,在任務邏輯中能夠方便的控制任務執行結果。
#execute(String param)
方法的方法參數,爲調度中心的控制檯中,新增任務時,配置的「任務參數」。通常狀況下,不會使用到。
建立 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 調度中心進行相應的配置。
瀏覽器打開 http://127.0.0.1:8080/xxl-job... 地址,即「執行器管理」菜單。以下圖:
點擊「新增執行器」按鈕,彈出「新增執行器」界面。以下圖:
填寫完 "lab-28-executor"
執行器的信息,點擊「保存」按鈕,進行保存。耐心等待一會,執行器會自動註冊上來。以下圖:
相同執行器,有且僅需配置一次便可。
瀏覽器打開 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 執行器的入門。
可能不少胖友不瞭解 Elastic-Job 這個中間件。咱們看一段其官方文檔的介紹:
Elastic-Job 是一個分佈式調度解決方案,由兩個相互獨立的子項目 Elastic-Job-Lite 和 Elastic-Job-Cloud 組成。Elastic-Job-Lite 定位爲輕量級無中心化解決方案,使用 jar 包的形式提供分佈式任務的協調服務。
Elastic-Job 基本是國內開源最好的調度任務中間件的幾個中間件,可能沒有之一,嘿嘿。目前處於有點「斷更」的狀態,具體可見 https://github.com/elasticjob... 。
因此關於這塊的示例,艿艿暫時先不提供。若是對 Elastic-Job 源碼感興趣的胖友,能夠看看艿艿寫的以下兩個系列:
① 如何選擇?
可能胖友但願瞭解下不一樣調度中間件的對比。表格以下:
特性 | 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 屬於中心化的任務調度平臺。目前採用這種方案的還有:
去中心化的任務調度平臺,目前有:
艿艿:若是胖友想要更加的理解,能夠看看艿艿朋友寫的 《中心化 V.S 去中心化調度設計》
③ 任務競爭 V.S 任務預分配
那麼,若是從任務分配的角度來看,能夠分紅兩類:
如此可知 XXL-Job 屬於任務競爭的任務調度平臺。目前採用這種方案的還有:
任務預分配的任務調度平臺,目前有:
通常來講,基於任務預分配的任務調度平臺,都會選擇使用 Zookeeper 來協調分配任務到不一樣的節點上。同時,任務調度平臺必須是去中心化的方案,每一個節點便是調度器又是執行器。這樣,任務在預分配在每一個節點以後,後續就本身調度給本身執行。
相比較而言,隨着節點愈來愈多,基於任務競爭的方案會由於任務競爭,致使存在性能下滑的問題。而基於任務預分配的方案,則不會存在這個問題。而且,基於任務預分配的方案,性能會優於基於任務競爭的方案。
這裏在推薦一篇 Elastic Job 開發者張亮的文章 《詳解當當網的分佈式做業框架 elastic-job》 ,灰常給力!
④ Quartz 是個優秀的調度內核
絕大多數狀況下,咱們不會直接使用 Quartz 做爲咱們的調度中間件的選擇。可是,基本全部的分佈式調度中間件,都將 Quartz 做爲調度內核,由於 Quartz 在單純任務調度自己提供了很強的功能。
不過呢,隨着一個分佈式調度中間件的逐步完善,又會逐步考慮拋棄 Quartz 做爲調度內核,轉而自研。例如說 XXL-JOB 在 2.1.0 RELEASE 的版本中,就已經更換成自研的調度模塊。其替換的理由以下:
XXL-JOB 最終選擇自研調度組件(早期調度組件基於 Quartz);
- 一方面,是爲了精簡系統下降冗餘依賴。
- 另外一方面,是爲了提供系統的可控度與穩定性。
在 Elastic-Job 3.X 的開發計劃中,也有一項計劃,就是自研調度組件,替換掉 Quartz 。