定時任務是日常開發中非常常見的功能。
對於簡單的任務處理Spring的@Scheduled非常好用。
如果處理更復雜的情況,比如需要宕機恢復或者集羣調度,那麼Quartz是個不錯的輕量級方案。
一些重量級的第三方任務調度系統也是基於Quartz擴展的,比如XXL-JOB,本文直說quartz的實現原理,文末會附上實踐代碼。
Quartz的模塊
- Trigger定義了何時觸發任務,可以說是一個觸發器,主要是兩種SimpleTrigger和CronTigger,其他Tigger基本都可以通過這兩種實現。Trigger還可以定義錯過的任務如何處理。下表是說明:
[外鏈圖片轉存失敗(img-xBshAxIv-1562638860054)(https://producted.github.io/images/posts/java/quartz/流程2.jpeg)]
- Calendar與Trigger相反,Calendar定義哪些時間是特例,不能執行任務。Calendar的優先級高於Trigger。HolidayCalendar比較常用,定義了哪些節日是特殊情況。
[外鏈圖片轉存失敗(img-bwpYoGEN-1562638860055)(https://producted.github.io/images/posts/java/quartz/流程3.png)]
- Job 負責定義任務所處理的邏輯,實現類需要實現org.quartz.Job接口
public interface Job {
void execute(JobExecutionContext context) throws JobExecutionException;
}
需要留意的是QuartzJobBean抽象類已經默認替我們實現了Job接口,並對工廠進行了一些自動化配置,源碼如下:
public abstract class QuartzJobBean implements Job {
public QuartzJobBean() {
}
// execute
public final void execute(JobExecutionContext context) throws JobExecutionException {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValues(context.getScheduler().getContext());
pvs.addPropertyValues(context.getMergedJobDataMap());
bw.setPropertyValues(pvs, true);
} catch (SchedulerException var4) {
throw new JobExecutionException(var4);
}
this.executeInternal(context);
}
protected abstract void executeInternal(JobExecutionContext var1) throws JobExecutionException;
}
如上,我們應用時可以通過繼承QuartzJobBean來並重寫executeInternal就可以實現任務配置和記錄等操作:
public class ScheduleJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) {
// ...
}
}
具體示例代碼在文末給出。
- 通過拋出JobExecutionException可以強制控制任務後續處理
public class JobExecutionException extends SchedulerException {
private boolean refire = false;//true: 重新執行任務(不會觸發下一次)
private boolean unscheduleTrigg = false;//true: 直接標記Trigger完成
private boolean unscheduleAllTriggs = false;//true: 直接標記所有和Job相關的Trigger都已經完成
}
- Stateful Job。不同於一般的無狀態任務,可以同時執行。有狀態任務不能同時執行,而且需要保存狀態。根據需要給可以Job類添加@PersistJobDataAfterExecution 或 @DisallowConcurrentExecution
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public interface StatefulJob extends Job {
}
-
JobDetail 保存Job的元信息,包括類定義和設置。
-
SchedulerFactory負責初始化,讀取配置文件,然後創建Scheduler
-
Scheduler是中樞調度器,負責管理Trigger/JobDetail和3個調度線程
springboot集成quartz
本示例技術棧:
-
springboot2.1.6
-
quartz2.3.0
-
tk.mybatis 實現自動化data操作
上面可以說是說了一堆零件,那麼如何使用呢?簡單說一下需要留意的地方,springboot1.x和springboot2.x在配置上相比,2.x版本將quartz提供了starter依賴,所以可以通過application內直接配置相關參數即可,具體官網有可以去參考,本文講的是java代碼實現配置,當然依賴和自動配置依賴有所不同,請讀者自行區分,不貼一大堆了,quartz核心依賴如下:
<!--quartz 定時框架-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
application.properties配置參考:
server.port=8081
# 數據源配置
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://dev.51haohuo.net:3306/quartz?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=hc
# mybstis配置
mybatis.mapper-locations=classpath:mapper/*/*.xml
mybatis.type-aliases-package=com.zhangpk.bean
mybatis.configuration.callSettersOnNulls=true
核心配置類,註釋都在代碼內了:
@Configuration
public class ScheduleConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
// quartz參數
Properties prop = new Properties();
// 任意值 集羣節點中必須相同。
prop.put("org.quartz.scheduler.instanceName", "MyScheduler");
prop.put("org.quartz.scheduler.instanceId", "AUTO");
// 線程池配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "20");
// 優先級
prop.put("org.quartz.threadPool.threadPriority", "5");
// JobStore配置
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
// 集羣配置
prop.put("org.quartz.jobStore.isClustered", "true");
// 屬性定義了Scheduler 實例檢入到數據庫中的頻率(單位:毫秒)。
// Scheduler 檢查是否其他的實例到了它們應當檢入的時候未檢入;這能指出一個失敗的 Scheduler 實例,且當前 Scheduler 會以此來接管任何執行失敗並可恢復的 Job。
//通過檢入操作,Scheduler 也會更新自身的狀態記錄。clusterChedkinInterval 越小,Scheduler 節點檢查失敗的 Scheduler 實例就越頻繁。默認值是 15000 (即15 秒)。
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1");
prop.put("org.quartz.jobStore.misfireThreshold", "12000");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
factory.setQuartzProperties(prop);
factory.setSchedulerName("MyScheduler");
// 延時啓動
factory.setStartupDelay(1);
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
// 可選,QuartzScheduler
// 啓動時更新己存在的Job,這樣就不用每次修改targetObject後刪除qrtz_job_details表對應記錄了
factory.setOverwriteExistingJobs(true);
// 設置自動啓動,默認爲true
factory.setAutoStartup(true);
return factory;
}
}
完成這些基礎配置之後,我們需要繼承上面提到的QuartzJobBean來實現動態任務調用,如下:
public class ScheduleJob extends QuartzJobBean {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private ExecutorService service = Executors.newSingleThreadExecutor();
@Override
protected void executeInternal(JobExecutionContext context) {
Job scheduleJob = (Job) context.getMergedJobDataMap().get(Job.JOB_PARAM_KEY);
long startTime = System.currentTimeMillis();
try {
// 執行任務
logger.info("任務準備執行,任務ID:{}", scheduleJob.getJobId());
ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(), scheduleJob.getMethodName(),
scheduleJob.getParams());
Future<?> future = service.submit(task);
future.get();
long times = System.currentTimeMillis() - startTime;
logger.info("任務執行完畢,任務ID:{} 總共耗時:{} 毫秒", scheduleJob.getJobId(), times);
} catch (Exception e) {
logger.error("任務執行失敗,任務ID:" + scheduleJob.getJobId(), e);
long times = System.currentTimeMillis() - startTime;
}
}
}
這裏注意一點,我們是基於數據庫來進行對定時任務的記錄,重啓恢復的操作的,具體在項目的resource目錄下的sql腳本內有建表語句。
另外,由於沒有任何頁面,所以可以通過接口測試工具來進行調用實現,後期會先把swagger2整合進去,前端方面-畢竟前端很弱,有時間的話會補上。
[外鏈圖片轉存失敗(img-1o9YQQDf-1562638860055)(https://producted.github.io/images/posts/java/quartz/調用圖.jpg)]
最後我們可以通過線程和quartz提供的刪除、暫停、恢復等操作來實現任務調度,通過spring注入要執行的任務類,任務類中寫我們要執行的任務就ok了。
本文參考自:簡書作者黃大海
本文代碼地址:https://github.com/producted/spring-learning/tree/master/scheduler