前言

尝试使用SpringBoot整合Quartz实现定时任务持久化到数据库,并配置quartz的集群功能。

定时任务实现方式

首先介绍除了Quartz外实现定时任务的简单方式:

  • Timer。
  • ScheduledThreadPoolExecutor。
  • 以及Spring自带的@Scheduled。

    实现方式1(Timer)

    ```java public class TimerDemo {

    public static void main(String[] args) {

    1. Timer timer = new Timer();
    2. timer.schedule(new TimerTask() {
    3. @Override
    4. public void run() {
    5. System.out.println("TimerTask1 run" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));
    6. }
    7. },1000,5000); // 延时1s,之后每隔5s运行一次

    }

}

  1. 有两点问题需要注意:
  2. 1. scheduleAtFixedRateschedule的区别:scheduleAtFixedRate会尽量减少漏掉调度的情况,如果前一次执行时间过长,导致一个或几个任务漏掉了,那么会补回来,而schedule过去的不会补,直接加上间隔时间执行下一次任务。
  3. 1. 同一个Timer下添加多个TimerTask,如果其中一个没有捕获抛出的异常,则全部任务都会终止运行。但是多个Timer是互不影响。
  4. ![image.png](https://cdn.nlark.com/yuque/0/2020/png/788484/1604029975366-849bcbca-c13b-47f8-ab16-8502c44b6ebc.png#align=left&display=inline&height=199&margin=%5Bobject%20Object%5D&name=image.png&originHeight=317&originWidth=1188&size=203796&status=done&style=none&width=746)<br />会提示使用ScheduledThreadPoolExecutor代替Timer方式。
  5. <a name="NJA2P"></a>
  6. ## 实现方式2(ScheduledThreadPoolExecutor)
  7. ```java
  8. public class SchedulerDemo {
  9. public static void main(String[] args) {
  10. ScheduledExecutorService executorService = new ScheduledThreadPoolExecutor(5);
  11. executorService.scheduleWithFixedDelay(new Runnable() {
  12. @Override
  13. public void run() {
  14. String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
  15. System.out.println("ScheduledThreadPoolExecutor1 run:"+now);
  16. }
  17. },1,2,TimeUnit.SECONDS);
  18. }
  19. }

scheduleWithFixedDelay跟schedule类似,而scheduleAtFixedRate与scheduleAtFixedRate一样会尽量减少漏掉调度的情况。

实现方式3(SpringBoot::@Scheduled)

  1. 启动类添加@EnableScheduling。
  2. 定时任务方法上添加@Scheduled。 ```java @Component public class springScheduledDemo {

    @Scheduled(cron = “1/5 ?”) public void testScheduled(){

    1. System.out.println("springScheduled run:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss")));

    }

}

  1. 这里的cron表达式我们可以参考:[https://www.jianshu.com/p/e9ce1a7e1ed1](https://www.jianshu.com/p/e9ce1a7e1ed1)。但是Spring的@Scheduled只支持6位,年份是不支持的,带年份的7位格式会报错:Cron expression must consist of 6 fields (found 7 in "1/5 * * * * ? 2018")。
  2. <a name="INWFd"></a>
  3. # Quartz定时任务框架
  4. <a name="mmNIU"></a>
  5. ## 1. 简单使用Quartz
  6. Quartz API关键接口:
  7. - **Scheduler**:与调度程序交互的主要API
  8. - **Job**:由希望用调度程序执行的组件实现的接口。
  9. - **JobDetail**:用于定义作业的实例。
  10. - **Trigger(即触发器)**:定义执行给定作业的计划的组件。
  11. - **JobBuilder**:用于定义/构建JobDetail实例,用于定义作业的实例。
  12. - **TriggerBuilder**:用于定义/构建触发器实例。
  13. 1. 添加依赖。<br />
  14. ```xml
  15. <dependency>
  16. <groupId>org.quartz-scheduler</groupId>
  17. <artifactId>quartz</artifactId>
  18. <version>2.3.0</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>org.quartz-scheduler</groupId>
  22. <artifactId>quartz-jobs</artifactId>
  23. <version>2.3.0</version>
  24. </dependency>
  1. 实现Job接口并且在execute方法中实现自己的业务逻辑。
    ```java public class HelloworldJob implements Job {

    @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {

    1. System.out.println("Hello world!:" + jobExecutionContext.getJobDetail().getKey());

    }

}

  1. 3. 创建JobDetail实例并定义Trigger注册到scheduler,启动scheduler开启调度。<br />
  2. ```java
  3. public class QuartzDemo {
  4. public static void main(String[] args) throws Exception {
  5. SchedulerFactory schedulerFactory = new StdSchedulerFactory();
  6. Scheduler scheduler = schedulerFactory.getScheduler();
  7. // 启动scheduler
  8. scheduler.start();
  9. // 创建HelloworldJob的JobDetail实例,并设置name/group
  10. JobDetail jobDetail = JobBuilder.newJob(HelloworldJob.class)
  11. .withIdentity("myJob","myJobGroup1")
  12. // JobDataMap可以给任务传递参数
  13. .usingJobData("job_param","job_param1")
  14. .build();
  15. // 创建Trigger触发器设置使用cronSchedule方式调度
  16. Trigger trigger = TriggerBuilder.newTrigger()
  17. .withIdentity("myTrigger","myTriggerGroup1")
  18. .usingJobData("job_trigger_param","job_trigger_param1")
  19. .startNow()
  20. //.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
  21. .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ? 2018"))
  22. .build();
  23. // 注册JobDetail实例到scheduler以及使用对应的Trigger触发时机
  24. scheduler.scheduleJob(jobDetail,trigger);
  25. }
  26. }

SimpleTrigger和CronTrigger的区别:SimpleTrigger在具体的时间点执行一次或按指定时间间隔执行多次,CronTrigger按Cron表达式的方式去执行更常用。

2. 配置Quartz的持久化方式

Quartz保存工作数据默认是使用内存的方式,上面的简单例子启动时可以在控制台日志中看到JobStore是RAMJobStore使用内存的模式(默认使用RAMJobStore),然后是not clustered表示不是集群中的节点。
image.png

  1. 持久化则需要配置(JDBCJobStore)。

首先到官网下载Quartz压缩包,解压后在“docs\dbTables”目录下看到很多对应不同数据库的SQL脚本,我这里选择mysql数据库且使用innodb引擎对应是“tablesmysql_innodb.sql”,打开可以看到需要添加11个“QRTZ”开头的表。
image.png

  1. 在classpath路径下也就是项目resources根目录下添加quartz.properties配置文件。

    1. org.quartz.scheduler.instanceName = MyScheduler
    2. # 开启集群,多个Quartz实例使用同一组数据库表
    3. org.quartz.jobStore.isClustered = true
    4. # 分布式节点ID自动生成
    5. org.quartz.scheduler.instanceId = AUTO
    6. # 分布式节点有效性检查时间间隔,单位:毫秒
    7. org.quartz.jobStore.clusterCheckinInterval = 10000
    8. # 配置线程池线程数量,默认10个
    9. org.quartz.threadPool.threadCount = 10
    10. org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
    11. org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    12. # 使用QRTZ_前缀
    13. org.quartz.jobStore.tablePrefix = QRTZ_
    14. # dataSource名称
    15. org.quartz.jobStore.dataSource = myDS
    16. # dataSource具体参数配置
    17. org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
    18. org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/testquartz?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
    19. org.quartz.dataSource.myDS.user = root
    20. org.quartz.dataSource.myDS.password = 7777777
    21. org.quartz.dataSource.myDS.maxConnections = 5
  2. 默认使用C3P0连接池,添加依赖。

    1. <dependency>
    2. <groupId>c3p0</groupId>
    3. <artifactId>c3p0</artifactId>
    4. <version>0.9.1.2</version>
    5. </dependency>

    修改自定义连接池则需要实现org.quartz.utils.ConnectionProvider接口quartz.properties添加配置
    org.quartz.dataSource.myDS(数据源名).connectionProvider.class=XXX(自定义ConnectionProvider全限定名)。启动后可以发现控制台输出信息:java JobStoreTX 以及数据库中也添加了相关记录。

image.png
至此,Quartz配置持久化成功。

3. SpringBoot2.0集成Quartz

  1. 添加依赖。

    <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
    
  2. 继承QuartzJobBean并重写executeInternal方法,与之前的实现Job接口类似。
    ```java public class HiJob extends QuartzJobBean {

    @Autowired HelloworldService myService; @Override protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {

     myService.printHelloWorld();
     System.out.println("    Hi! :" + jobExecutionContext.getJobDetail().getKey());
    

    }

}

这里HelloworldService打印一条helloworld模拟调用service的场景。

3. 添加配置类。<br />
```kotlin
@Configuration
public class QuartzConfig {

    @Bean
    public JobDetail myJobDetail(){
        JobDetail jobDetail = JobBuilder.newJob(HiJob.class)
                .withIdentity("myJob1","myJobGroup1")
                // JobDataMap可以给任务execute传递参数
                .usingJobData("job_param","job_param1")
                .storeDurably()
                .build();
        return jobDetail;
    }

    @Bean
    public Trigger myTrigger(){
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(myJobDetail())
                .withIdentity("myTrigger1","myTriggerGroup1")
                .usingJobData("job_trigger_param","job_trigger_param1")
                .startNow()
                //.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
                .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ? 2018"))
                .build();
        return trigger;
    }

}
  1. 配置文件(application.yml)添加Quartz相关配置。

    spring:
    # 配置数据源
    datasource:
     driver-class-name: com.mysql.jdbc.Driver
     url: jdbc:mysql://localhost:3306/testquartz?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
     username: root
     password: password
    quartz:
     # 持久化到数据库方式
     job-store-type: jdbc
     initialize-schema: embedded
     properties:
       org:
         quartz:
           scheduler:
             instanceName: MyScheduler
             instanceId: AUTO
           jobStore:
             class: org.quartz.impl.jdbcjobstore.JobStoreTX
             driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
             tablePrefix: QRTZ_
             isClustered: true
             clusterCheckinInterval: 10000
             useProperties: false
           threadPool:
             class: org.quartz.simpl.SimpleThreadPool
             threadCount: 10
             threadPriority: 5
             threadsInheritContextClassLoaderOfInitializingThread: true
    

    参考配置(https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-quartz,截取自springboot文档配置示例):
    image.png
    启动后可以发现使用的是项目统一的数据源:(LocalDataSourceJobStore extends JobStoreCMT)
    image.png

  2. Quartz使用同一组数据库表作集群只需要配置相同的instanceName实例名称,以及设置“org.quartz.jobStore.isClustered = true”,启动两个节点后关闭其中正在跑任务的节点,另一个节点会自动检测继续运行定时任务(自动切换)。
    image.png

  3. 多任务的问题,多个JobDetail使用同一个Trigger报错:“Trigger does not reference given job!”,这样的话估计只能创建多组trigger和JobDetail配对?
    scheduler.scheduleJob(jobDetail,trigger);
    // 一个Job可以对应多个Trigger,但多个Job绑定一个Trigger报错
    scheduler.scheduleJob(jobDetail2,trigger);
    

    参考

    博客园:Java并发编程:Timer和TimerTask(转载)
    https://www.cnblogs.com/dolphin0520/p/3938991.html
    博客园:简单理解java中timer的schedule和scheduleAtFixedRate方法的区别
    https://www.cnblogs.com/snailmanlilin/p/6873802.html
    W3Cschool:使用Quartz
    https://www.w3cschool.cn/quartz_doc/quartz_doc-1xbu2clr.html