Quartz是一個任務調度框架,最近在項目中有用到,所以做個記錄總結。
- Scheduler:調度器,控制任務的調度,將JobDetail和Trigger注冊到Scheduler加以控制。
- Job:任務,是一個接口且只有一個方法void execute(JobExecutionContext context),實現該接口定義任務的執行邏輯。
- JobDetail:Job實例,一個Job可以創建多個Job實例,每一個實例有自己的屬性。
- Trigger:觸發器,定義觸發規則。
我是在Spring Boot項目中使用的,這個Demo也是基於Spring Boot,實際上還可以更簡潔。Quartz版本為2.3.0。
- 增加pom依賴
<dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.0</version> </dependency>
- 編寫配置文件
# quartz.properties org.quartz.scheduler.instanceName=TaskScheduler org.quartz.threadPool.threadCount=5 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX # 數據保存到數據庫,使用JobStoreTX作為JobStore來管理事務 org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 數據庫代理 org.quartz.jobStore.tablePrefix=QRTZ_ # 表前綴,默認為QRTZ_。主要可用於多個服務數據存儲到同一數據庫,可以創建多組不同的表供不同服務使用。 org.quartz.jobStore.dataSource=quartzDataSource # 數據源,在下面定義數據源信息的時候需要用到 org.quartz.dataSource.quartzDataSource.driver=com.mysql.jdbc.Driver org.quartz.dataSource.quartzDataSource.URL=jdbc:mysql://127.0.0.1:3306/test?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&allowMultiQueries=true&characterEncoding=UTF-8&useSSL=false org.quartz.dataSource.quartzDataSource.user=root org.quartz.dataSource.quartzDataSource.password=123456 org.quartz.dataSource.quartzDataSource.maxConnections=5 org.quartz.dataSource.quartzDataSource.validationQuery=select 1
- 定義配置類
@Configuration public class QuartzConfig { @Autowired private SpringJobFactory springJobFactory; // 配置文件,在application.yml文件中配置 @Value("${quartz.config}") private String quartzConfig; @Bean(name = "schedulerFactory") public SchedulerFactoryBean schedulerFactoryBean() throws IOException { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setAutoStartup(true); // 延時5秒啟動 factory.setStartupDelay(5);
// 設置配置信息 factory.setQuartzProperties(quartzProperties()); factory.setJobFactory(springJobFactory); return factory; } @Bean public Properties quartzProperties() throws IOException { PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); propertiesFactoryBean.setLocation(new ClassPathResource(quartzConfig)); propertiesFactoryBean.afterPropertiesSet(); return propertiesFactoryBean.getObject(); } /** * Quartz初始化監聽器 * * @return */ @Bean public QuartzInitializerListener executorListener() { return new QuartzInitializerListener(); } /** * 將Scheduler添加到Spring管理 * * @return * @throws IOException */ @Bean(name = "scheduler") public Scheduler scheduler() throws IOException { return schedulerFactoryBean().getScheduler(); } } - 數據庫增加對應表,可以到Quartz發行版的“docs / dbTables”目錄中找到表創建SQL腳本。
- 定義Job
public class TestJob implements Job, Serializable { private static final Logger LOGGER = LoggerFactory.getLogger(TestJob.class); private static final long serialVersionUID = 1L; @Override public void execute(JobExecutionContext arg0) throws JobExecutionException { LOGGER.info("-------------- 執行Quartz測試任務 --------------"); // Do something ... } }
- 創建JobDetail和Trigger並加入調度器(這部分建議寫成接口)
Class cls = Class.forName("com.xiaoliu.job.TestJob"); cls.newInstance(); // 創建JobDetail JobDetail job = JobBuilder.newJob(cls).withIdentity("test1", "test") .withDescription("測試任務1").build(); // 創建觸發器 CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? ") // 立即觸發一次,然后按照正常的規則執行下一個周期的任務。 .withMisfireHandlingInstructionFireAndProceed(); Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger" + "test1", "test").startNow().withSchedule(cronScheduleBuilder).build(); // 注冊到scheduler scheduler.scheduleJob(job, trigger);
最后運行項目,查看效果。
使用Quartz的過程需要了解清楚Trigger,它關系到任務觸發規則的定義,以及觸發過程可能遇到的問題處理。在這里只涉及最常用的兩種Trigger:SimpleTrigger和CronTrigger。
先看一下TriggerBuilder這個構造類,里面有各種Trigger的一些公共屬性,主要列舉幾個說明:
- jobKey:trigger觸發時被執行的job的key。
- startTime:trigger生效的時間點。
- endTime:trigger失效的時間點。trigger只在startTime和endTime之間才會被觸發。
- priority:優先級,默認為5,priority的值可以是任意整數。假設同時執行的trigger有很多,但是Quartz線程池的工作線程很少(沒有足夠的資源同時觸發這些trigger),這個時候會按照優先級高的先觸發。
- misfire Instructions:錯過觸發策略,trigger定義了一個觸發閥值,在閥值時間范圍內會重新觸發,超過閥值范圍則認為是misfire。因為某種原因,trigger在應該觸發的時候未觸發且錯過了觸發的時機,就需要一定策略來處理misfire,不同的trigger有不同的策略集。所有trigger的默認觸發策略都是MISFIRE_INSTRUCTION_SMART_POLICY,值為0。
① SimpleTrigger
可以滿足的調度需求:在具體的時間點執行一次,或者在具體的時間點執行,並且以指定的間隔重復執行若干次。簡單的調度需求可以使用SimpleTrigger。
SimpleTrigger的主要屬性有:
-
- repeatCount:重復間隔
- repeatInterval:重復次數
例:
SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) // 10s執行一次 .repeatForever() // 次數不限
SimpleScheduleBuilder.simpleSchedule() .withIntervalInMinutes(1) // 1分鍾執行一次 .withRepeatCount(10) // 次數為10次
Misfire策略
SimpleTrigger的misfire策略有以下幾種:
MISFIRE_INSTRUCTION_SMART_POLICY:0,默認策略。會根據實例的配置及狀態,在所有MISFIRE策略中動態選擇一種Misfire策略。
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY:-1,忽略所有的超時狀態,按照觸發器的策略執行。
MISFIRE_INSTRUCTION_FIRE_NOW:1,立即執行
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT:2,立即執行,並重復到指定的次數。
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT:3,立即執行,且超時期內錯過的執行機會作廢。
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT:4,以現在為基准,以repeatInterval為周期,延時到下一個激活點執行。
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT:5,在下一個激活點執行,並重復到指定的次數。
Misfire策略設置方式如下:
SimpleScheduleBuilder.simpleSchedule() .withIntervalInSeconds(10) .repeatForever() .withMisfireHandlingInstructionFireNow()
② CronTrigger
CronTrigger能支持更復雜的調度需求,通常比SimpleTrigger更有用,CronTrigger的屬性只有Cron Expressions,但Cron表達式的功能非常強大。Cron Expressions是由七個子表達式組成的字符串,用於描述日程表的各個細節。這些子表達式用空格分隔,分別是秒/分/時/日/月/周/年,年不是必須的。
表達式 | 是否必須 | 允許值 | 允許的特殊字符 |
---|---|---|---|
秒 | 是 | 0-59 | , - * / |
分 | 是 | 0-59 | , - * / |
時 | 是 | 0-23 | , - * / |
日 | 是 | 1-31 | , - * ? / L W C |
月 | 是 | 1-12 或 JAN-DEC | , - * / |
周 | 是 | 1-7 或 SUN-SAT | , - * ? / L C # |
年 | 否 | 空 或 1970-2099 | , - * / |
比如上面例子中的表達式:0/10 * * * * ? ,表示每10s執行一次。cron的具體規則網上很多,不是本文的重點。
Misfire策略
CronTrigger的misfire策略有以下幾種:
MISFIRE_INSTRUCTION_SMART_POLICY:0,默認策略。在CronTrigger中解釋為MISFIRE_INSTRUCTION_FIRE_ONCE_NOW。
MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY:-1,忽略所有的超時狀態,按照觸發器的策略執行。
MISFIRE_INSTRUCTION_DO_NOTHING:2,什么都不做,然后就按照正常的計划執行。
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:1,立即觸發一次,觸發后恢復正常的頻率。
Misfire策略設置方式如下:
CronScheduleBuilder.cronSchedule("0/10 * * * * ? ")
.withMisfireHandlingInstructionFireAndProceed();
在項目中使用時遇到了一個問題,也是我后面進一步了解Quartz的原因,在此記錄。
Job本地運行正常,但部署到Linux服務器后,觀察業務發現Job未執行,也找不到相關錯誤日志。
最開始的思考問題的原因可能是:Linux和Windows系統或其他環境問題,調度策略或者其他配置問題。通過接口手動觸發Job是正常的,修改觸發策略等等配置后依然無法解決。第二天發現測試系統部分任務成功執行了,查看日志發現有ClassNotFoundException,原因是執行了一個不屬於當前服務的Job。問題好像有點苗頭了,查看qrtz_triggers表的記錄,失敗Job對應的trigger記錄trigger_state的值為ERROR,那么原因找到了:多個服務中的Quartz使用同一組表,維護同一組數據。當服務觸發了一個不屬於本服務的Job后(ClassNotFoundException,業務處理失敗),會修改觸發記錄,其他服務就不會重復觸發該任務,也不會產生錯誤日志。
trigger_state的值有:
- WAITING:等待
- PAUSED:暫停
- ACQUIRED:正常執行
- BLOCKED:阻塞
- ERROR:錯誤
解決方案:
- 任務調度整合在一個服務里,其他服務開放業務處理接口。(服務有多節點時不可行)
- 每個服務創建一組數據庫表,通過在配置文件配置org.quartz.jobStore.tablePrefix屬性指定到對應的表。