# Quartz教程 - 8 持久化与集群

为什么要持久化?

默认情况下 Quartz 是保存在内存中的,如果不做持久化,系统停止或重启,任务将丢失;

另外如果做了持久化,可以在数据库中查看到有多少 JobDetail 和 Trigger。


为什么需要集群?

因为每台服务器运行的代码是一样的,那么一个定时任务如果被多个服务器运行,就会存在重复执行,另外集群可以保证系统的高可用性,一台服务器发生故障,还能保证系统的稳定运行。


集群是必须利用数据库将 Quartz 的信息保存到数据库中的,每台服务器通过读取数据库的数据抢占执行权限,因此将Quartz数据持久化到数据库是集群的基础。因此先讲解 Quartz 的持久化。


# 8.1 持久化

Quartz 持久化需要使用数据库,并在数据库中建立一些表,我们可以将这些表建在业务数据库中,也可以将这些表建立在独立的数据库中,如果建立在独立的数据库中,那么我们的系统需要配置多个数据源。一般情况下,建立在业务相关的数据库中即可。

# 1 引入依赖

需要在 SpringBoot 项目中,引入数据库连接和数据源的依赖。

在项目的 pom.xml 中添加依赖。

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>

<!-- 引入Spring封装的jdbc,内部默认依赖了 HikariDataSource  数据源-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11

# 2 创建Job

和之前的是一样的 Job 类。

MyJob.java

package com.doubibiji.springbootquartz.job;

import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

@Slf4j
public class MyJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        log.info("执行定时任务");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3 注册Job和Trigger

和之前的操作一样,创建一个 Quartz 的配置类,在这个配置类中,创建 JobDetail 和 Trigger,并将他们添加到Spring 容器中。

Quartz.config

package com.doubibiji.springbootquartz.config;

import com.doubibiji.springbootquartz.job.MyJob;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {

    /**
     * 将Job注册到Spring容器
     */
    @Bean
    public JobDetail myJob(){
        //newJob方法就是在绑定要运行的Job接口实现类,需要实现类的反射做参数
        return JobBuilder.newJob(MyJob.class)
                // 给当前JobDetail对象在调度环境中起名
                .withIdentity("myJob")
                // 将JobDetail存储在 Scheduler中,即使没有触发器绑定当前JobDetail对象,也不会被删除
                .storeDurably()
                .build();
    }

    // 下面是触发器的声明,也会保存到Spring容器中
    @Bean
    public Trigger myTrigger(){
        // 定义Cron表达式
        CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule("*/5 * * * * ?"); // 每5秒执行一次
        return TriggerBuilder.newTrigger()
                // 绑定要运行的JobDetail对象
                .forJob(myJob())
                .startAt(DateBuilder.futureDate(20, DateBuilder.IntervalUnit.SECOND))       // 延迟20秒执行
                // 为触发器起名
                .withIdentity("myTrigger")
                // 绑定cron表达式
                .withSchedule(cron)
                .build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# 4 配置数据源

在项目的 application.yml 中配置数据源。

这里 Quartz 和业务功能使用同一个数据源,所以不用单独为 Quartz 单独配置数据源。

server:
  port: 8080

spring:
	# 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/doubibiji-db?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 3
      maximum-pool-size: 10
      idle-timeout: 10000
      connection-test-query: select 1
      initialization-fail-timeout: 15000
      connection-timeout: 15000

  # quartz 配置
  quartz:
    job-store-type: jdbc				# 存储类型使用数据库
    jdbc:
      initialize-schema: always
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

需要注意:

这里的 initialize-schema 表示初始化 Quartz 数据库表的配置,always 表示每次启动项目都会删除然后重新创建 Quartz 的表,这样是不对的。所以在第一次启动的时候,将 initialize-schema 的值设置为 always,后面再启动项目库,需要将这个值修改为 never

创建表的SQL脚本可以在quartz依赖的jar包中找到。

此时的项目结构:

# 5 运行项目

运行项目,可以看到定时任务每隔5秒运行一次。

同时数据库会生成 Quartz 的运行数据。

# 6 Quartz数据库表

运行项目后,会在数据库中生成一些表,如下:

这里简单介绍一下 Quartz 创建的数据库表。

表名 说明
QRTZ_BLOG_TRIGGERS Trigger 作为 Blob 类型存储(用于 Quartz 用户用 JDBC 创建他们自己定制的 Trigger 类型,JobStore 并不知道如何存储实例的时候)
QRTZ_CALENDARS 以 Blob 类型存储 Quartz 的 Calendar 日历信息
QRTZ_CRON_TRIGGERS 存储 Cron Trigger,包括 Cron 表达式和时区信息
QRTZ_FIRED_TRIGGERS 存储与已触发的 Trigger 相关的状态信息,以及相联 Job 的执行信息
QRTZ_JOB_DETAILS 存储 Job 的详细信息
QRTZ_LOCKS 在集群环境下,用于实现分布式锁的表格,防止多个节点同时执行同一个作业或触发器
QRTZ_PAUSED_TRIGGER_GRPS 存储被暂停的触发器组信息
QRTZ_SCHEDULER_STATE 存储调度程序的状态信息,例如调度器的标识、调度器实例的最近检测时间等。
QRTZ_SIMPLE_TRIGGERS 存储简单触发器信息,如重复次数、重复间隔等
QRTZ_SIMPROP_TRIGGERS 存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种类型的触发器
QRTZ_TRIGGERS 存储触发器的详细信息,包括触发器的名称、组名、触发类型等

# 8.2 集群

集群的前提是必须持久化。

通过集群 Quartz 会将任务分配到不同的机器上,每个任务只会被分配到其中一台机器上。不能一个任务同时在多台机器上同时重复触发。


在上面持久化的基础上,继续完成。

# 1 配置

进行集群,最少要在 application.yaml 中添加 Quartz 集群配置如下:

server:
  port: 8080

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/doubibiji-db?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 3
      maximum-pool-size: 10
      idle-timeout: 10000
      connection-test-query: select 1
      initialization-fail-timeout: 15000
      connection-timeout: 15000

  # quartz 配置
  quartz:
    job-store-type: jdbc             # 使用jdbc的方式持久化
    jdbc:
      initialize-schema: never       # 启动后,不创建数据库表
    # 属性配置
    properties:
      org:
        quartz:
          # 集群配置
          scheduler:
            instanceName: MyScheduler       # 名称自定义,一起集群的各个服务器,名称要一致
            instanceId: AUTO                # 实例ID,各个集群的服务器,不能重复,这里使用自动生成就好了
          jobStore:
            isClustered: true               # 是否进行集群配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 2 运行

为了演示方便,将项目拷贝成两份,修改端口后同时运行,就可以在两个客户端同时运行定时任务了。

运行起来后,你有可能会发现定时任务一直只会在一个项目定时执行,另外一个项目没有动静,这其实是因为另一个项目一直抢不到执行的权限,这个其实是没有问题的,如果关闭调抢到执行权限的项目,你就会发现另一个项目就会执行了。

# 3 其他配置

Quartz 还有一些其他的配置,这里简单举例:

spring:
  # quartz 配置
  quartz:
    job-store-type: jdbc             # 使用jdbc的方式持久化
    jdbc:
      initialize-schema: never       # 启动后,不创建数据库表
    auto-startup: true               # 启动开关为开启状态
    startup-delay: 1s                # 启动延迟1秒
    overwrite-existing-jobs: true    # 启动时更新已存在的任务
    # 属性配置
    properties:
      org:
        quartz:
          # 集群配置
          scheduler:
            instanceName: MyScheduler       # 名称自定义,一起集群的各个服务器,名称要一致
            instanceId: AUTO                # 实例ID,各个集群的服务器,不能重复,这里使用自动生成就好了
          jobStore:
            isClustered: true               # 是否进行集群配置
            tablePrefix: QRTZ_              # 数据库表前缀为QRTZ_,数据库表名称前缀可以修改
            clusterCheckinInterval: 10000   # 集群检查间隔为 15000 毫秒
            useProperties: false
            misfireThreshold: 12000         # 设置任务调度器的 Misfire 阈值为 12000 毫秒
          # 线程池配置
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool    # 使用 SimpleThreadPool 线程池
            threadCount: 3                  # 设置线程池中线程的数量为3
            threadPriority: 5               # 设置线程池中线程的优先级为 5
            threadsInheritContextClassLoaderOfInitializingThread: true    # 设置线程是否继承初始化线程的类加载器为 true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29