1. 程式人生 > >Quartz任務排程框架初探

Quartz任務排程框架初探

Quartz任務排程框架初探

什麼是Quartz?

Quartz 是一個完全由 Java 編寫的開源作業排程框架,為在 Java 應用程式中進行作業排程提供了簡單卻強大的機制。

Quartz 可以與 J2EE 與 J2SE 應用程式相結合也可以單獨使用。

Quartz 允許程式開發人員根據時間的間隔來排程作業。

Quartz 實現了作業和觸發器的多對多的關係,還能把多個作業與不同的觸發器關聯。

Quartz特點
  1. 強大的排程功能,例如支援豐富多樣的排程方法,可以滿足各種常規及特殊需求;
  2. 靈活的應用方式,例如支援任務和排程的多種組合方式,支援排程資料的多種儲存方式;
  3. 分散式和叢集能力,提供任務持久化以及分佈部署任務排程;
定時任務排程框架對比
定時任務框架 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指令碼檔案即可

需求簡述
  1. 管理資料表中的定時任務記錄(CURD)
  2. 執行、停止定時任務

在這裡我們首先考慮的是需求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>
開始前需要知道的幾點(以下內容來源網路)
  1. Job:是一個介面,只有一個方法void execute(JobExecutionContext context),開發者實現該介面定義執行任務,JobExecutionContext類提供了排程上下文的各種資訊。Job執行時的資訊儲存在JobDataMap例項中;

  2. JobDetail:Quartz在每次執行Job時,都重新建立一個Job例項,所以它不直接接受一個Job的例項,相反它接收一個Job實現類,以便執行時通過newInstance()的反射機制例項化Job。因此需要通過一個類來描述Job的實現類及其它相關的靜態資訊,如Job名字、描述、關聯監聽器等資訊,JobDetail承擔了這一角色。

  3. Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有SimpleTrigger和CronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔週期執行,SimpleTrigger是最適合的選擇;而CronTrigger則可以通過Cron表示式定義出各種複雜時間規則的排程方案:如每早晨9:00執行,週一、週三、週五下午5:00執行等;

  4. 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. 任務停止
}
  1. 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);
     }
    
  2. 任務執行:

     public void startTask(OpsScriptInfo opsScriptInfo) throws SchedulerException {
     	// 全域性schedule工廠獲取scheduler物件,start()方法啟動,並進行任務配置taskConfig
         Scheduler scheduler = schedulerFactory.getScheduler();
         scheduler.start();
         taskConfig(scheduler, opsScriptInfo);
     }
    
  3. 任務停止:

     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,且執行緒池不關閉,併發任務迴圈使用執行緒池中的工作執行緒資源。