再次入门 Quartz 集群

2024-10-29 14:37

示例代码对应仓库:lab-28-task-quartz-memory 。

实际场景下,我们必然需要考虑定时任务的高可用,所以基本上,肯定使用 Quartz 的集群方案。因此本小节,我们使用 Quartz 的 JDBC 存储器 JobStoreTX ,并是使用 MySQL 作为数据库。

如下是 Quartz 两种存储器的对比:

FROM https://blog.csdn.net/Evankaka/article/details/45540885

image-20230211172546226

实际上,有方案可以实现兼具这两种方式的优点

另外,本小节提供的示例和 「3. 快速入门 Quartz 单机」 基本一致。😈 下面,让我们开始遨游~

引入依赖

在 pom.xml 文件中,引入相关依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

<artifactId>lab-28-task-quartz-jdbc</artifactId>
 
<dependencies>
    <!-- 实现对数据库连接池的自动化配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency> <!-- 本示例,我们使用 MySQL -->
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.48</version>
    </dependency>
 
    <!-- 实现对 Spring MVC 的自动化配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
 
    <!-- 实现对 Quartz 的自动化配置 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-quartz</artifactId>
    </dependency>
 
    <!-- 方便等会写单元测试 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

</project>
  • 和 「3.1 引入依赖」 基本一致,只是额外引入 spring-boot-starter-test 依赖,等会会写两个单元测试方法。

示例 Job

在 cn.iocoder.springboot.lab28.task.config.job 包路径下,创建 DemoJob01 和 DemoJob02 类。代码如下:

// DemoJob01.java

@DisallowConcurrentExecution
public class DemoJob01 extends QuartzJobBean {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private DemoService demoService;

    @Override
    protected void executeInternal(JobExecutionContext context) {
        logger.info("[executeInternal][我开始的执行了, demoService 为 ({})]", demoService);
    }

}

// DemoJob02.java

@DisallowConcurrentExecution
public class DemoJob02 extends QuartzJobBean {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    protected void executeInternal(JobExecutionContext context) {
        logger.info("[executeInternal][我开始的执行了]");
    }
}
  • 相比 「3.2 示例 Job」 来说,在类上添加了 Quartz 的 @DisallowConcurrentExecution 注解,保证相同 JobDetail 在多个 JVM 进程中,有且仅有一个节点在执行。

注意,不是以 Quartz Job 为维度,保证在多个 JVM 进程中,有且仅有一个节点在执行,而是以 JobDetail 为维度。虽然说,绝大多数情况下,我们会保证一个 Job 和 JobDetail 是一一对应。😈 所以,搞不清楚这个概念的胖友,最好搞清楚这个概念。实在有点懵逼,保证一个 Job 和 JobDetail 是一一对应就对了。

而 JobDetail 的唯一标识是 JobKey ,使用 name + group 两个属性。一般情况下,我们只需要设置 name 即可,而 Quartz 会默认 group = DEFAULT 。

不过这里还有一点要补充,也是需要注意的,在 Quartz 中,相同 Scheduler 名字的节点,形成一个 Quartz 集群。在下文中,我们可以通过 spring.quartz.scheduler-name 配置项,设置 Scheduler 的名字。

【重要】为什么要说这个呢?因为我们要完善一下上面的说法:通过在 Job 实现类上添加 @DisallowConcurrentExecution 注解,实现在相同 Quartz Scheduler 集群中,相同 JobKey 的 JobDetail ,保证在多个 JVM 进程中,有且仅有一个节点在执行。

应用配置文件

在 application.yml 中,添加 Quartz 的配置,如下:

spring:
  datasource:
    user:
      url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-user?		   			          useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:
    quartz:
      url: jdbc:mysql://127.0.0.1:3306/lab-28-quartz-jdbc-quartz?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.jdbc.Driver
      username: root
      password:

 Quartz 的配置,对应 QuartzProperties 配置类

  quartz:
    scheduler-name: clusteredScheduler # Scheduler 名字。默认为 schedulerName
    job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。
    auto-startup: true # Quartz 是否自动启动
    startup-delay: 0 # 延迟 N 秒启动
    wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 													  false ,建议设置为 true
    overwrite-existing-jobs: false # 是否覆盖已有 Job 的配置
    properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-				    				  scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档
      org:
        quartz:
          # JobStore 相关配置
          jobStore:
            # 数据源名称
            dataSource: quartzDataSource # 使用的数据源
            class: org.quartz.impl.jdbcjobstore.JobStoreTX # JobStore 实现类
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_ # Quartz 表前缀
            isClustered: true # 是集群模式
            clusterCheckinInterval: 1000
            useProperties: false
          # 线程池相关配置
          threadPool:
            threadCount: 25 # 线程池大小。默认为 10 。
            threadPriority: 5 # 线程优先级
            class: org.quartz.simpl.SimpleThreadPool # 线程池类型
    jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置
      initialize-schema: never # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。

配置项比较多,我们主要对比 「3.5 应用配置文件」 来看看。

  • 在 spring.datasource 配置项下,用于创建多个数据源的配置。

    • user 配置,连接 lab-28-quartz-jdbc-user 库。目的是,为了模拟我们一般项目,使用到的业务数据库。

  • quartz 配置,连接 lab-28-quartz-jdbc-quartz 库。目的是,Quartz 会使用单独的数据库。😈 如果我们有多个项目需要使用到 Quartz 数据库的话,可以统一使用一个,但是要注意配置 spring.quartz.scheduler-name 设置不同的 Scheduler 名字,形成不同的 Quartz 集群。

  • 在 spring.quartz 配置项下,额外增加了一些配置项,我们逐个来看看。

    • scheduler-name 配置,Scheduler 名字。这个我们在上文解释了很多次了,如果还不明白,请拍死自己。

  • job-store-type 配置,设置了使用 "jdbc" 的 Job 存储器。

  • properties.org.quartz.jobStore 配置,增加了 JobStore 相关配置。重点是,通过 dataSource 配置项,设置了使用名字为 "quartzDataSource" 的 DataSource 为数据源。😈 在 「4.4 DataSourceConfiguration」 中,我们会使用 spring.datasource.quartz 配置,来创建该数据源。

  • jdbc 配置项,虽然名字叫这个,主要是为了设置使用 SQL 初始化 Quartz 表结构。这里,我们设置 initialize-schema = never ,我们手动创建表结构。

    咳咳咳,配置项确实有点多。如果暂时搞不明白的胖友,可以先简单把 spring.datasource 数据源,修改成自己的即可。

初始化 Quartz 表结构

在 Quartz Download 地址,下载对应版本的发布包。解压后,我们可以在 src/org/quartz/impl/jdbcjobstore/ 目录,看到各种数据库的 Quartz 表结构的初始化脚本。这里,因为我们使用 MySQL ,所以使用 tables_mysql_innodb.sql 脚本。

在数据库中执行该脚本,完成初始化 Quartz 表结构。如下图所示:img

关于每个 Quartz 表结构的说明,可以看看 《Quartz 框架(二)——JobStore 数据库表字段详解》 文章。😈 实际上,也可以不看,哈哈哈哈。

我们会发现,每个表都有一个 SCHED_NAME 字段,Quartz Scheduler 名字。这样,实现每个 Quartz 集群,数据层面的拆分。

DataSourceConfiguration

在 cn.iocoder.springboot.lab28.task.config 包路径下,创建 DataSourceConfiguration 类,配置数据源。代码如下:

// DataSourceConfiguration.java

@Configuration
public class DataSourceConfiguration {
/**
 * 创建 user 数据源的配置对象
 */
@Primary
@Bean(name = "userDataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.user") // 读取 spring.datasource.user 配置到 DataSourceProperties 对象
public DataSourceProperties userDataSourceProperties() {
    return new DataSourceProperties();
}
 
/**
 * 创建 user 数据源
 */
@Primary
@Bean(name = "userDataSource")
@ConfigurationProperties(prefix = "spring.datasource.user.hikari") // 读取 spring.datasource.user 配置到 HikariDataSource 对象
public DataSource userDataSource() {
    // 获得 DataSourceProperties 对象
    DataSourceProperties properties =  this.userDataSourceProperties();
    // 创建 HikariDataSource 对象
    return createHikariDataSource(properties);
}
 
    /**
     * 创建 quartz 数据源的配置对象
     */
    @Bean(name = "quartzDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.quartz") // 读取 spring.datasource.quartz 配置到 DataSourceProperties 对象
    public DataSourceProperties quartzDataSourceProperties() {
        return new DataSourceProperties();
    }
 
    /**
     * 创建 quartz 数据源
     */
    @Bean(name = "quartzDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
    @QuartzDataSource
    public DataSource quartzDataSource() {
        // 获得 DataSourceProperties 对象
        DataSourceProperties properties =  this.quartzDataSourceProperties();
        // 创建 HikariDataSource 对象
        return createHikariDataSource(properties);
    }
 
    private static HikariDataSource createHikariDataSource(DataSourceProperties properties) {
        // 创建 HikariDataSource 对象
        HikariDataSource dataSource = 				              	                properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        // 设置线程池名
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }
}
  • 基于 spring.datasource.user 配置项,创建了名字为 "userDataSource" 的 DataSource Bean 。并且,在其上我们添加了 @Primay 注解,表示其是主数据源。

  • 基于 spring.datasource.quartz 配置项,创建了名字为 "quartzDataSource" 的 DataSource Bean 。并且,在其上我们添加了 @QuartzDataSource 注解,表示其是 Quartz 的数据源。😈 注意,一定要配置啊,这里卡了好久!!!!

定时任务配置

完成上述的工作之后,我们需要配置 Quartz 的定时任务。目前,有两种方式:

  • 方式一,「4.6.1 Bean 自动设置」 。

  • 方式二,「4.6.2 Scheduler 手动设置」 。

Bean 自动设置

在 cn.iocoder.springboot.lab28.task.config 包路径下,创建 ScheduleConfiguration 类,配置上述的两个示例 Job 。代码如下:

// ScheduleConfiguration.java

@Configuration
public class ScheduleConfiguration {
    public static class DemoJob01Configuration {

        @Bean
        public JobDetail demoJob01() {
            return JobBuilder.newJob(DemoJob01.class)
                    .withIdentity("demoJob01") // 名字为 demoJob01
                    .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时,还没 Trigger 指向它,所以需要设置为 true ,表示保留。
                    .build();
        }

        @Bean
        public Trigger demoJob01Trigger() {
            // 简单的调度计划的构造器
            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(5) // 频率。
                    .repeatForever(); // 次数。
            // Trigger 构造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob01()) // 对应 Job 为 demoJob01
                    .withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger
                    .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
                    .build();
        }

    }

    public static class DemoJob02Configuration {

        @Bean
        public JobDetail demoJob02() {
            return JobBuilder.newJob(DemoJob02.class)
                    .withIdentity("demoJob02") // 名字为 demoJob02
                    .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时,还没 Trigger 指向它,所以需要设置为 true ,表示保留。
                    .build();
        }

        @Bean
        public Trigger demoJob02Trigger() {
            // 简单的调度计划的构造器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
            // Trigger 构造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob02()) // 对应 Job 为 demoJob02
                    .withIdentity("demoJob02Trigger") // 名字为 demoJob02Trigger
                    .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
                    .build();
        }
    }
}
  • 和 「3.3 ScheduleConfiguration」 是一模一样的。

    在 Quartz 调度器启动的时候,会根据该配置,自动调用如下方法:

  • Scheduler#addJob(JobDetail jobDetail, boolean replace) 方法,将 JobDetail 持久化到数据库。

  • Scheduler#scheduleJob(Trigger trigger) 方法,将 Trigger 持久化到数据库。

Scheduler 手动设置

一般情况下,推荐使用 Scheduler 手动设置。

创建 QuartzSchedulerTest 类,创建分别添加 DemoJob01 和 DemoJob02 的 Quartz 定时任务配置。代码如下:

// QuartzSchedulerTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class QuartzSchedulerTest {
@Autowired
private Scheduler scheduler;
 
@Test
public void addDemoJob01Config() throws SchedulerException {
    // 创建 JobDetail
    JobDetail jobDetail = JobBuilder.newJob(DemoJob01.class)
            .withIdentity("demoJob01") // 名字为 demoJob01
            .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时,还没 Trigger 指向它,所以需要设置为 true ,表示保留。
            .build();
    // 创建 Trigger
    SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
            .withIntervalInSeconds(5) // 频率。
            .repeatForever(); // 次数。
    Trigger trigger = TriggerBuilder.newTrigger()
            .forJob(jobDetail) // 对应 Job 为 demoJob01
            .withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger
            .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
            .build();
    // 添加调度任务
    scheduler.scheduleJob(jobDetail, trigger);
    //        scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
    }
    @Test
    public void addDemoJob02Config() throws SchedulerException {
        // 创建 JobDetail
        JobDetail jobDetail = JobBuilder.newJob(DemoJob02.class)
                .withIdentity("demoJob02") // 名字为 demoJob02
                .storeDurably() // 没有 Trigger 关联的时候任务是否被保留。因为创建 JobDetail 时,                                    还没 Trigger 指向它,所以需要设置为 true ,表示保留。
                .build();
        // 创建 Trigger
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * *             * * ? *");
        Trigger trigger = TriggerBuilder.newTrigger()
            .forJob(jobDetail) // 对应 Job 为 demoJob01
            .withIdentity("demoJob02Trigger") // 名字为 demoJob01Trigger
            .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
            .build();
        // 添加调度任务
        scheduler.scheduleJob(jobDetail, trigger);
        // scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
    }

}
  • 创建 JobDetail 和 Trigger 的代码,其实和 「4.6.1 Bean 自动设置」 是一致的。

  • 在每个单元测试方法的最后,调用 Scheduler#scheduleJob(JobDetail jobDetail, Trigger trigger) 方法,将 JobDetail 和 Trigger 持久化到数据库。

  • 如果想要覆盖数据库中的 Quartz 定时任务的配置,可以调用 Scheduler#scheduleJob(JobDetail jobDetail, Set<? extends Trigger> triggersForJob, boolean replace) 方法,传入 replace = true 进行覆盖配置。

Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application.java

@SpringBootApplication
public class Application {

public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
}

}
  • 运行 Application 类,启动示例项目。具体的执行日志,和 「3.4 Application」 基本一致,这里就不重复罗列了。

    如果胖友想要测试集群下的运行情况,可以再创建 创建 Application02.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application02.java

@SpringBootApplication
public class Application02 {
    public static void main(String[] args) {
        // 设置 Tomcat 随机端口
        System.setProperty("server.port", "0");

        // 启动 Spring Boot 应用
        SpringApplication.run(Application.class, args);
    }
}

运行 Application02 类,再次启动一个示例项目。然后,观察输出的日志,可以看到启动的两个示例项目,都会有 DemoJob01 和 DemoJob02 的执行日志。

相关文章
热点文章
精彩视频
Tags

站点地图 在线访客: 今日访问量: 昨日访问量: 总访问量: