定時調度quartz實戰代碼示例

定時任務是日常開發中非常常見的功能。

對於簡單的任務處理Spring的@Scheduled非常好用。

如果處理更復雜的情況,比如需要宕機恢復或者集羣調度,那麼Quartz是個不錯的輕量級方案。

一些重量級的第三方任務調度系統也是基於Quartz擴展的,比如XXL-JOB,本文直說quartz的實現原理,文末會附上實踐代碼。

Quartz的模塊

quartz流程圖1

  • 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章