Quartz任務排程框架初探
Quartz任務排程框架初探
什麼是Quartz?
Quartz 是一個完全由 Java 編寫的開源作業排程框架,為在 Java 應用程式中進行作業排程提供了簡單卻強大的機制。
Quartz 可以與 J2EE 與 J2SE 應用程式相結合也可以單獨使用。
Quartz 允許程式開發人員根據時間的間隔來排程作業。
Quartz 實現了作業和觸發器的多對多的關係,還能把多個作業與不同的觸發器關聯。
Quartz特點
- 強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種常規及特殊需求;
- 靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種儲存方式;
- 分散式和叢集能力,提供任務持久化以及分佈部署任務排程;
定時任務排程框架對比
定時任務框架 | Cron表示式 | 固定間隔執行 | 固定頻率執行 | 任務持久化 |
---|---|---|---|---|
JDK TimerTask | 不支援 | 支援 | 支援 | 不支援 |
Spring Schedule | 支援 | 支援 | 支援 | 不支援 |
Quartz | 支援 | 支援 | 支援 | 支援 |
關於任務持久化,當應用程式停止執行時,所有排程資訊不被丟失,當你重新啟動時,排程資訊還存在,這就是持久化任務。個人理解是對任務執行日誌等相關資訊進行持久化儲存。任務持久化不一定需要通過框架進行操作,只是Quartz整合度較高。
Quartz分散式叢集配置(瞭解)
Quartz使用持久的JobStore才能完成Quartz叢集配置,關於JobStore是基於JDBC的,需要對任務排程Scheduler資訊進行持久化。
由此Quartz自帶的11張表就是做此事的。
關於這11張表:例項化採用資料庫儲存,基於資料庫引擎及 High-Available 的策略自動協調每個節點。
表下載地址:http://www.quartz-scheduler.org/downloads/
解壓縮之後:quartz-2.2.3\docs\dbTables\ 根據資料庫型別選擇SQL指令碼檔案即可
需求簡述
- 管理資料表中的定時任務記錄(CURD)
- 執行、停止定時任務
在這裡我們首先考慮的是需求2:定時任務的執行和停止
依賴匯入
資料庫驅動:業務資料庫為SqlServer
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<version>4.0</version>
<!--<scope>test</scope>-->
</dependency>
Quartz:定時任務框架依賴
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.0</version>
</dependency>
context-support:注意,該依賴必須匯入,Spring提供對schedule的支援
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
開始前需要知道的幾點(以下內容來源網路)
-
Job:是一個介面,只有一個方法void execute(JobExecutionContext context),開發者實現該介面定義執行任務,JobExecutionContext類提供了排程上下文的各種資訊。Job執行時的資訊儲存在JobDataMap例項中;
-
JobDetail:Quartz在每次執行Job時,都重新建立一個Job例項,所以它不直接接受一個Job的例項,相反它接收一個Job實現類,以便執行時通過newInstance()的反射機制例項化Job。因此需要通過一個類來描述Job的實現類及其它相關的靜態資訊,如Job名字、描述、關聯監聽器等資訊,JobDetail承擔了這一角色。
-
Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有SimpleTrigger和CronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔週期執行,SimpleTrigger是最適合的選擇;而CronTrigger則可以通過Cron表示式定義出各種複雜時間規則的排程方案:如每早晨9:00執行,週一、週三、週五下午5:00執行等;
-
Scheduler:代表一個Quartz的獨立執行容器,Trigger和JobDetail可以註冊到Scheduler中,兩者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查詢定位容器中某一物件的依據,Trigger的組及名稱必須唯一,JobDetail的組和名稱也必須唯一(但可以和Trigger的組和名稱相同,因為它們是不同型別的)。Scheduler定義了多個介面方法,允許外部通過組及名稱訪問和控制容器中Trigger和JobDetail。
核心成員組織圖:
JobDetail是任務的定義,而Job是任務的執行邏輯。在JobDetail裡會引用一個Job Class定義。
關於SpringBoot與Quartz整合
目前專案中採用的是SpringBoot 1.0 版本,並未整合Quartz。在SpringBoot 2.0版本中,Quartz已經被整合進來了。
由於底層框架採用SpringBoot,因此XML的配置方式被捨棄,可以通過配置類的方式進行配置。
QuartzConfigration.java:通過SchedulerFactoryBean,生成Schedule。
@Configuration
public class QuartzConfigration {
@Autowired
private JobFactory jobFactory;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setJobFactory(jobFactory);
// 用於quartz叢集,QuartzScheduler 啟動時更新己存在的Job
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setStartupDelay(1);
return schedulerFactoryBean;
}
@Bean
public Scheduler scheduler() {
return schedulerFactoryBean().getScheduler();
}
}
AutowireCapableBeanFactory,通過看到的部落格得知這個類會限制繼承Job類的任務類中進行依賴注入。
這個並沒有實測,文章 https://blog.csdn.net/qq_28483283/article/details/80623417 的作者構造瞭如下類進行該問題的解決:
@Component
public class JobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
// 呼叫父類的方法
Object jobInstance = super.createJobInstance(bundle);
// 進行注入
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
具體實施:DynamicScheduledTask + TaskManager
DynamicScheduledTask.java:動態定時任務
/**
* description: 動態定時任務
* author:jiangyanfei
* date:2018/11/6
* time:17:34
*/
public class DynamicScheduledTask implements Job {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicScheduledTask.class);
// 上述繼承Job類的任務類依賴注入問題指的就是這裡,配置上述類後,可以使用@Autowired註解進行依賴注入
@Override
@DataSource(name = DSEnum.DATA_SOURCE_OPS)
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// 通過Job上下文物件進行JobDetail、Trigger獲取相關資訊,下面是獲取了Trigger中的引數URL
String url = (String) jobExecutionContext.getTrigger().getJobDataMap().get("url");
// 定時任務請求業務邏輯
LOGGER.info("當前時間:" + dateFormat.format(new Date()) + " 任務資訊:" +
jobExecutionContext.getJobDetail().getKey().getName() + " URL:" + url);
}
}
TaskManager:工作管理員,負責任務的執行、停止邏輯,以及任務排程器Schedule的組裝。
/**
* description: 工作管理員
* author:jiangyanfei
* date:2018/11/7
* time:15:26
*/
@Component
public class TaskManager {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
private static final Logger LOGGER = LoggerFactory.getLogger(TaskManager.class);
// 這裡需要說明,採用標準的定時器工廠類進行schedule的生成,作為全域性物件等待被呼叫
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
// 1. taskConfig方法:組裝Schedule
// 2. 任務執行
// 3. 任務停止
}
-
taskConfig方法:Schedule的組裝,這個方法是改造過的,傳入了相關的業務引數OpsScriptInfo。
private void taskConfig(Scheduler scheduler, OpsScriptInfo opsScriptInfo) throws SchedulerException { // 後續業務請求的URL測試拼裝,結合傳遞過來的OpsScriptInfo物件 String url = "/" + opsScriptInfo.getId() + opsScriptInfo.getPath() + "/" + opsScriptInfo.getFileName(); // Scheduler元件1: JobDetail // 構建新任務,指定動態定時任務類:DynamicScheduledTask JobDetail jobDetail = JobBuilder.newJob(DynamicScheduledTask.class) .withIdentity("Timing-task" + opsScriptInfo.getId(), "TimingTask") .build(); // Scheduler元件2: Trigger(SimpleTrigger、CronTrigger) CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("Trigger-task-trigger" + opsScriptInfo.getId(), "TimingTaskTrigger") // 傳入OpsScriptInfo物件攜帶的Cron表示式 .withSchedule(CronScheduleBuilder.cronSchedule(opsScriptInfo.getJobCron())) .usingJobData("url", url) // 在觸發器中攜帶引數傳遞 .build(); // 將JobDetail、CronTrigger進行Scheduler進行組裝 scheduler.scheduleJob(jobDetail, cronTrigger); }
-
任務執行:
public void startTask(OpsScriptInfo opsScriptInfo) throws SchedulerException { // 全域性schedule工廠獲取scheduler物件,start()方法啟動,並進行任務配置taskConfig Scheduler scheduler = schedulerFactory.getScheduler(); scheduler.start(); taskConfig(scheduler, opsScriptInfo); }
-
任務停止:
public void stopTask(Long scriptId) throws SchedulerException { Scheduler scheduler = schedulerFactory.getScheduler(); // 通過傳遞引數任務ID,結合JobKey拼裝Key,從JobKey中獲取對應Id的Key JobKey key = JobKey.jobKey("Timing-task" + scriptId, "TimingTask"); // 判斷該Key下,任務排程器中任務是否存在,若存在,將該Key對應任務從排程器中移除 if (scheduler.checkExists(key)) { scheduler.deleteJob(key); LOGGER.info("當前時間:" + dateFormat.format(new Date()) + " 定時任務:Timing-task" + scriptId + " 已停止"); } }
控制層中注入工作管理員,在對應的請求處理方法中進行呼叫即可
@Autowired
private TaskManager taskManager;
quartz.properties
#===============================================================
#Configure Main Scheduler Properties 排程器屬性
#===============================================================
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
#===============================================================
#Configure ThreadPool 執行緒池屬性
#===============================================================
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#===============================================================
#Configure JobStore 作業儲存設定
#===============================================================
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#===============================================================
#Configure Plugins 外掛配置
#===============================================================
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin
org.quartz.plugin.jobInitializer.overWriteExistingJobs = true
org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.jobInitializer.validating=false
主要內容為上述,quartz的預設配置檔案中主要指定了執行緒池屬性,預設的起了一個擁有10個執行緒的執行緒池。
org.quartz.threadPool.class 是要使用的ThreadPool實現的名稱。Quartz附帶的執行緒池是"org.quartz.simpl.SimpleThreadPool",並且應該能夠滿足幾乎每個使用者的需求。它有非常簡單的行為,並經過很好的測試。它提供了一個固定大小的執行緒池,可以計劃程式的生命週期。
org.quartz.threadPool.threadCount 可以是任何正整數,這是可用於併發執行作業的執行緒數。如果你只有幾個工作每天執行幾次,那麼1個執行緒已經足夠。如果你有成千上萬的工作,每分鐘都有很多工作,那麼你可能希望一個執行緒數可能更多的是50或100(這很重要,取決於你的工作所執行的工作的性質,以及你的系統資源!)。
org.quartz.threadPool.threadPriority 可以是Thread.MIN_PRIORITY(即1)和Thread.MAX_PRIORITY(即10)之間的任何int。預設值為Thread.NORM_PRIORITY(5)。
請求測試:12個定時任務,Cron表示式均為:0/10 * * * * ?
可以看出,併發的定時任務中在執行時間是還是存在差異的,毫秒級的執行時間間隔,可以忽略。
執行緒池中的工作執行緒數預設為10,且執行緒池不關閉,併發任務迴圈使用執行緒池中的工作執行緒資源。