沉梦听雨的编程指南 沉梦听雨的编程指南
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • RPC
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 多媒体框架
  • 虚拟机
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)

沉梦听雨

时间是最好的浸渍剂,而沉淀是最好的提纯器🚀
首页
  • 基础篇
  • 集合篇
  • 并发篇
  • JVM
  • 新特性
  • 计算机网络
  • 操作系统
  • 数据结构与算法
  • 基础篇
  • MySql
  • Redis
  • 达梦数据库
  • Spring
  • SpringBoot
  • Mybatis
  • Shiro
  • 设计须知
  • UML画图
  • 权限校验
  • 设计模式
  • API网关
  • RPC
  • 消息队列
  • SpringCloud
  • 分布式事务
  • 云存储
  • 搜索引擎
  • 多媒体框架
  • 虚拟机
  • 开发工具篇
  • 工具库篇
  • 开发技巧篇
  • 工具类系列
  • 随笔
  • 前端环境搭建
  • HTML与CSS
  • JS学习
  • Vue3入门
  • Vue3进阶
  • 黑马Vue3
  • 脚手架搭建
  • 瑞吉外卖
  • 黑马点评
  • vue-blog
  • 沉梦接口开放平台
  • 用户中心
  • 聚合搜索平台
  • 仿12306项目
  • 壁纸小程序项目
  • RuoYi-Vue
  • 博客搭建
  • 网站收藏箱
  • 断墨寻径摘录
  • 费曼学习法
Github (opens new window)
  • 开发工具篇

  • 工具库篇

    • lombok工具库

    • EasyExcel小记

    • 定时任务相关

      • 基础入门
      • SpringTask学习
      • Quartz学习
        • 基本介绍
        • JobStore 的存储方式
        • Quartz 单机入门示例
          • 引入依赖
          • 应用配置文件
          • 创建任务类
          • 新增配置类
        • Quartz 集群入门示例
          • 引入依赖
          • 应用配置文件
          • 初始化 Quartz 表结构
          • 数据源配置类
          • 创建任务类
          • 定时任务配置
          • Bean 自动设置
          • Schedule 手动设置
          • 启动测试
          • Bug 记录
        • 表结构详解
          • 1、qrtz_job_details(QRTZ_JOB_DETAILS)
          • 2、qrtz_triggers(QRTZ_TRIGGERS)
          • 3、qrtz_corn_triggers(QRTZ_CRON_TRIGGERS)
          • 4、qrtz_simple_triggers(QRTZ_SIMPLE_TRIGGERS)
          • 5、qrtz_simprop_triggers(QRTZ_SIMPROP_TRIGGERS)
          • 6、qrtz_blob_triggers(QRTZ_BLOB_TRIGGERS)
          • 7、qrtz_calendars(QRTZ_CALENDARS)
          • 8、qrtz_fired_triggers(QRTZ_FIRED_TRIGGERS)
          • 9、qrtz_paused_trigger_grps(QRTZ_PAUSED_TRIGGER_GRPS)
          • 10、qrtz_scheduler_state(QRTZ_SCHEDULER_STATE)
          • 11、qrtz_locks(QRTZ_LOCKS)
          • 表之间的关系
        • 学习参考
      • xxl-job学习
      • xxl-job高级参数配置篇
    • Hutool工具库

    • 极光推送学习
    • OkHttp学习
    • BigDecimal类详解
    • PdfBox学习
    • OCR功能实现
  • 开发技巧篇

  • 工具类系列

  • 随笔

  • 开发日常
  • 工具库篇
  • 定时任务相关
沉梦听雨
2024-09-21
目录

Quartz学习

# Quartz 学习

代码仓库地址:

  • GitHub:chenmeng-test-demos/demo8-task at master · cmty256/chenmeng-test-demos (opens new window)
  • Gitee:demo8-task · chenmeng/chenmeng-test-demos - 码云 - 开源中国 (opens new window)

# 基本介绍

Quartz 是一个开源的作业调度框架,它完全由 Java 写成。

Quartz 自带了集群方案。它通过将作业信息存储到关系数据库中,并使用关系数据库的行锁来实现执行作业的竞争,从而保证多个进程下,同一个任务在相同时刻,不能重复执行。

在 Quartz 体系结构中,有三个组件非常重要:

  1. Scheduler :调度器
  2. Trigger :触发器
  3. Job :任务

Quartz 分成【单机模式】和【集群模式】。在生产环境下,一定一定一定要使用 Quartz 的集群模式,保证定时任务的高可用。

# JobStore 的存储方式

有两种:

  • RAMJobStore:将 scheduler 存储在内存中,但是重启服务器信息会丢失。
  • JDBCJobStore:将 scheduler 存储在数据库中。
类型 优点 缺点
RAMJobStore 不要外部数据库,配置容易,运行速度快 因为调度程序信息是存储在被分配给 JVM 的内存里面,所以,当应用程序停止运行时,所有调度信息将被丢失。另外因为存储到 JVM 内存里面,所以可以存储多少个 Job 和 Trigger 将会受到限制
JDBC 作业存储 支持集群,因为所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务 运行速度的快慢取决与连接数据库的快慢

# Quartz 单机入门示例

# 引入依赖

<dependencies>
        <!-- 实现对 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>
    </dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 应用配置文件

spring:
  application:
    name:
      task-quartz-memory
  # Quartz 的配置,对应 QuartzProperties 配置类
  quartz:
    job-store-type: memory # 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:
          threadPool:
            threadCount: 25 # 线程池大小。默认为 10 。
            threadPriority: 5 # 线程优先级
            class: org.quartz.simpl.SimpleThreadPool # 线程池类型
#    jdbc: # 这里暂时不说明,使用 JDBC 的 JobStore 的时候,才需要配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 创建任务类

/**
 * 每次 DemoJob01 都会被 Quartz 创建出一个新的 Job 对象,执行任务。
 *
 * @author chenmeng
 */
public class DemoJob01 extends QuartzJobBean {

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

    // 计数器
    private final AtomicInteger counts = new AtomicInteger();

    @Resource
    private DemoService demoService;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        // counts.incrementAndGet() 的值一直为 1,说明每次 DemoJob01 都是新创建的。
        logger.info("[executeInternal][定时第 ({}) 次执行, demoService 为 ({})]", counts.incrementAndGet(),
                demoService);
    }

}

/**
 * @author chenmeng
 */
public class DemoJob02 extends QuartzJobBean {

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

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("[DemoJob02-executeInternal][我每第10秒执行一次]");
    }

}
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

# 新增配置类

@Configuration
public class ScheduleConfiguration {

    public static class DemoJob01Configuration {

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

        /**
         * 创建 DemoJob01 的 Trigger Bean 对象。
         * 其中,我们使用 SimpleScheduleBuilder 简单的调度计划的构造器,创建了每 5 秒执行一次,无限重复的调度计划
         *
         * @return Trigger
         */
        @Bean
        public Trigger demoJob01Trigger() {
            // 简单的调度计划的构造器
            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(5) // 频率 - 每 5 秒。
                    .repeatForever(); // 次数 - 无限重复。
            // Trigger 构造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob01()) // 对应 Job 为 demoJob01
                    .withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger
                    .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
                    .build();
        }

    }

    public static class DemoJob02Configuration {

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

        /**
         * 创建 DemoJob02 的 Trigger Bean 对象。
         * 其中,我们使用 CronScheduleBuilder 基于 Quartz Cron 表达式的调度计划的构造器,
         * 创建了每第 10 秒执行一次的调度计划。
         *
         * @return Trigger
         */
        @Bean
        public Trigger demoJob02Trigger() {
            //  基于 Quartz Cron 表达式的调度计划的构造器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
            // Trigger 构造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob02()) // 对应 Job 为 demoJob02
                    .withIdentity("demoJob02Trigger") // 名字为 demoJob02Trigger
                    .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
                    .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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

最后启动运行测试。

# Quartz 集群入门示例

# 引入依赖

    <dependencies>
        <!-- 实现对数据库连接池的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- 本示例,我们使用 MySQL -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </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>
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

# 应用配置文件

spring:
  application:
    name:
      task-quartz-jdbc

  datasource:
    user: # 这里主要目的是,为了模拟我们一般项目,使用到的业务数据库。
      url: jdbc:mysql://127.0.0.1:3306/chenmeng_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 
    quartz: # 这里主要目的是,Quartz 会使用单独的数据库。如果我们有多个项目需要使用到 Quartz 数据库的话,可以统一使用一个,但是要注意配置 spring.quartz.scheduler-name 设置不同的 Scheduler 名字,形成不同的 Quartz 集群。
      url: jdbc:mysql://127.0.0.1:3306/chenmeng_quartz?useSSL=false&useUnicode=true&characterEncoding=UTF-8
      driver-class-name: com.mysql.cj.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 # 使用的数据源
            # Spring Boot 2.6.0+ 中,job-store-type: jdbc 会自动选择 JobStoreTX,无需显式指定 org.quartz.jobStore.class。
            # 如果指定了,项目运行会报错。
#            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 ,我们手动创建表结构。
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
41
42
43
44
45
46
47

注意事项:

  • Spring Boot 2.6.0+ 中,job-store-type: jdbc 会自动选择 JobStoreTX,无需显式指定 org.quartz.jobStore.class。
  • 如果手动指定 class: org.quartz.impl.jdbcjobstore.JobStoreTX,需要注意配置冲突,可能需要禁用 Spring Boot 的自动配置。
  • 最佳实践是依赖 Spring Boot 的自动配置,减少手动干预。

# 初始化 Quartz 表结构

Quartz 下载:

在 Quartz Download (opens new window) 地址,下载对应版本的发布包。

  • 解压后,我们可以在 src/org/quartz/impl/jdbcjobstore/ 目录,看到各种数据库的 Quartz 表结构的初始化脚本。
  • 这里,因为我们使用 MySQL,所以使用 tables_mysql_innodb.sql 脚本。

Quartz 表结构的说明:

(详细说明参见下面的 [表结构详解])

  1. qrtz_job_details:记录每个任务的详细信息。
  2. qrtz_triggers:记录每个触发器的详细信息。
  3. qrtz_corn_triggers:记录 cornTrigger 的信息。
  4. qrtz_simple_triggers:存储 SimpleTrigger 类型触发器的时间信息。
  5. qrtz_simprop_triggers:记录带有简单属性的触发器信息(仅在需要时使用)。
  6. qrtz_blob_triggers:存储复杂触发器(如可能包含序列化对象的触发器)。
  7. qrtz_calendars:记录调度器使用的日历信息(用于排除任务触发时间,如节假日)。
  8. qrtz_fired_triggers:记录每个正在执行的触发器。
  9. qrtz_paused_trigger_grps:记录被暂停的触发器分组。
  10. qrtz_scheduler_state:记录 调度器(每个机器节点)的生命状态。
  11. qrtz_locks:记录程序的悲观锁(防止多个节点同时执行同一个定时任务)。

我们会发现,每个表都有一个 SCHED_NAME 字段,Quartz Scheduler 名字。

这样就实现了每个 Quartz 集群,在数据层面的拆分。

# 数据源配置类

@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;
    }

}
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 创建任务类

@DisallowConcurrentExecution // 保证相同 JobDetail 在多个 JVM 进程中,有且仅有一个节点在执行
public class DemoJob01 extends QuartzJobBean {

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

    @Resource
    private DemoService demoService;

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

}

public class DemoJob02 extends QuartzJobBean {

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

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("[DemoJob02-executeInternal][我开始执行了]");
    }

}
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

注意事项:

  1. @DisallowConcurrentExecution 注解 不是以 Quartz Job 为维度,保证在多个 JVM 进程中,有且仅有一个节点在执行,而是以 JobDetail 为维度。
    • 虽然说,绝大多数情况下,我们会保证一个 Job 和 JobDetail 是一一对应。
    • 所以,搞不清楚这个概念的胖友,最好搞清楚这个概念。实在有点懵逼,保证一个 Job 和 JobDetail 是一一对应就对了。
  2. JobDetail 的唯一标识是 JobKey (opens new window) ,使用 name + group 两个属性。
    • 一般情况下,我们只需要设置 name 即可,而 Quartz 会默认 group = DEFAULT。
    • 在 Quartz 中,相同 Scheduler 名字的节点,形成一个 Quartz 集群。
    • 我们可以通过 spring.quartz.scheduler-name 配置项,设置 Scheduler 的名字。
  3. 通过在 Job 实现类上添加 @DisallowConcurrentExecution 注解,实现在相同的 Quartz Scheduler 集群中,相同 JobKey 的 JobDetail,保证在多个 JVM 进程中,有且仅有一个节点在执行。

# 定时任务配置

# Bean 自动设置

/**
 * Bean 自动设置
 *
 * @author chenmeng
 */
@Configuration
public class ScheduleConfiguration {

    public static class DemoJob01Configuration {

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

        /**
         * 创建 DemoJob01 的 Trigger Bean 对象。
         * 其中,我们使用 SimpleScheduleBuilder 简单的调度计划的构造器,创建了每 5 秒执行一次,无限重复的调度计划
         *
         * @return Trigger
         */
        @Bean
        public Trigger demoJob01Trigger() {
            // 简单的调度计划的构造器
            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(5) // 频率 - 每 5 秒。
                    .repeatForever(); // 次数 - 无限重复。
            // Trigger 构造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob01()) // 对应 Job 为 demoJob01
                    .withIdentity("demoJob01Trigger") // 名字为 demoJob01Trigger
                    .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
                    .build();
        }

    }

    public static class DemoJob02Configuration {

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

        /**
         * 创建 DemoJob02 的 Trigger Bean 对象。
         * 其中,我们使用 CronScheduleBuilder 基于 Quartz Cron 表达式的调度计划的构造器,
         * 创建了每第 10 秒执行一次的调度计划。
         *
         * @return Trigger
         */
        @Bean
        public Trigger demoJob02Trigger() {
            //  基于 Quartz Cron 表达式的调度计划的构造器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *");
            // Trigger 构造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob02()) // 对应 Job 为 demoJob02
                    .withIdentity("demoJob02Trigger") // 名字为 demoJob02Trigger
                    .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
                    .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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

# Schedule 手动设置

/**
 * Scheduler 手动设置
 * 执行之前,需要确保项目中只有一个 @SpringBootApplication 注解 或者 指定扫描范围
 *
 * @author chenmeng
 */
@SpringBootTest(classes = QuartzJdbcApplication.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();
        // 添加调度任务(将 JobDetail 和 Trigger 持久化到数据库)
        scheduler.scheduleJob(jobDetail, trigger);
    }

    @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();
        // 添加调度任务(将 JobDetail 和 Trigger 持久化到数据库)
        scheduler.scheduleJob(jobDetail, trigger);

        // 如果想要覆盖数据库中的 Quartz 定时任务的配置,可以调用此方法,传入 replace = true 进行覆盖配置。
        // scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# 启动测试

第一个服务:

@SpringBootApplication
public class QuartzJdbcApplication {

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

}
1
2
3
4
5
6
7
8

第二个服务:

/**
 * 测试集群下的运行情况
 *
 * @author chenmeng
 */
@SpringBootApplication
public class QuartzJdbcApplication02 {

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

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

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Bug 记录

异常信息:

# 找不到数据源
Failed to obtain DB connection from data source 'quartzDataSource': java.sql.SQLException: There is no DataSource named 'quartzDataSource'
1
2

解决方案:

配置文件:

  • 移除或注释掉 class: org.quartz.impl.jdbcjobstore.JobStoreTX 配置项

原因分析

为什么注释掉 class: org.quartz.impl.jdbcjobstore.JobStoreTX 能解决问题?

在 Spring Boot 2.6.0 及以上版本中,Quartz 的配置机制进行了增强,尤其是对于 JobStore 的自动配置:

  1. Spring Boot 自动管理 JobStore 类型:
    • Spring Boot 会根据 spring.quartz.job-store-type 属性自动推断 JobStore的实现类:
      • memory:使用内存存储(RAMJobStore)。
      • jdbc:使用数据库存储(JobStoreTX)。
    • 如果你显式指定了 org.quartz.jobStore.class,则会覆盖 Spring Boot 的默认行为,可能导致配置不兼容或冲突。
  2. Spring Boot Quartz Starter 的改进:
    • Spring Boot 的 spring-boot-starter-quartz 在 2.6.0+ 中已经内置了对 JobStoreTX 的支持,
    • 只需要正确配置 job-store-type: jdbc,无需手动设置 org.quartz.jobStore.class。
  3. 手动配置 class: org.quartz.impl.jdbcjobstore.JobStoreTX 会冲突:
    • 如果手动配置了 org.quartz.jobStore.class,Spring Boot 的自动配置逻辑可能会重复设置 JobStore,从而引发问题,例如无法正确初始化数据源或 JobStore。

# 表结构详解

以下是 Quartz 2.3.0 官方脚本中 11 张表的说明及其功能描述:

# 1、qrtz_job_details(QRTZ_JOB_DETAILS)

  • 功能:记录每个定时任务的详细信息。
  • 主要字段:
    • JOB_NAME 和 JOB_GROUP:任务名称和任务分组,联合主键。
    • DESCRIPTION:任务的描述信息。
    • JOB_CLASS_NAME:执行任务的类的全限定名。
    • IS_DURABLE:是否是持久任务(即任务是否在没有触发器时保留)。
    • JOB_DATA:任务附加的键值对数据。

# 2、qrtz_triggers(QRTZ_TRIGGERS)

  • 功能:记录每个触发器的详细信息。
  • 主要字段:
    • TRIGGER_NAME 和 TRIGGER_GROUP:触发器名称和分组,联合主键。
    • JOB_NAME 和 JOB_GROUP:关联的任务名称和分组。
    • TRIGGER_STATE:触发器当前的状态(如等待、暂停、阻塞等)。
    • TRIGGER_TYPE:触发器的类型(如 Cron、Simple、Blob 等)。
    • START_TIME 和 END_TIME:触发器的起始时间和结束时间。
    • MISFIRE_INSTR:触发器的误操作策略。

# 3、qrtz_corn_triggers(QRTZ_CRON_TRIGGERS)

  • 功能:存储 CronTrigger 类型触发器的时间表达式。
  • 主要字段:
    • TRIGGER_NAME 和 TRIGGER_GROUP:触发器名称和分组(外键关联 QRTZ_TRIGGERS 表)。
    • CRON_EXPRESSION:Cron 表达式。
    • TIME_ZONE_ID:时区 ID。

# 4、qrtz_simple_triggers(QRTZ_SIMPLE_TRIGGERS)

  • 功能:存储 SimpleTrigger 类型触发器的时间信息。
  • 主要字段:
    • TRIGGER_NAME 和 TRIGGER_GROUP:触发器名称和分组(外键关联 QRTZ_TRIGGERS 表)。
    • REPEAT_COUNT:触发次数。
    • REPEAT_INTERVAL:触发间隔。
    • TIMES_TRIGGERED:已触发次数。

# 5、qrtz_simprop_triggers(QRTZ_SIMPROP_TRIGGERS)

  • 功能:记录带有简单属性的触发器信息(仅在需要时使用)。
  • 主要字段:
    • TRIGGER_NAME 和 TRIGGER_GROUP:触发器名称和分组(外键关联 QRTZ_TRIGGERS 表)。
    • STR_PROP_1 ~ STR_PROP_4:自定义字符串属性。
    • INT_PROP_1 ~ INT_PROP_2:自定义整数属性。
    • DEC_PROP_1 ~ DEC_PROP_2:自定义小数属性。
    • BOOL_PROP_1:自定义布尔属性。

# 6、qrtz_blob_triggers(QRTZ_BLOB_TRIGGERS)

  • 功能:存储复杂触发器(如可能包含序列化对象的触发器)。
  • 主要字段:
    • TRIGGER_NAME 和 TRIGGER_GROUP:触发器名称和分组(外键关联 QRTZ_TRIGGERS 表)。
    • BLOB_DATA:二进制大对象数据。

# 7、qrtz_calendars(QRTZ_CALENDARS)

  • 功能:记录调度器使用的日历信息(用于排除任务触发时间,如节假日)。
  • 主要字段:
    • CALENDAR_NAME:日历名称。
    • CALENDAR:序列化后的日历数据。

# 8、qrtz_fired_triggers(QRTZ_FIRED_TRIGGERS)

  • 功能:记录每个正在执行的触发器实例。
  • 主要字段:
    • ENTRY_ID:执行条目的唯一标识。
    • TRIGGER_NAME 和 TRIGGER_GROUP:触发器名称和分组。
    • INSTANCE_NAME:触发器所属的调度器实例。
    • FIRED_TIME:触发时间。
    • PRIORITY:触发器优先级。

# 9、qrtz_paused_trigger_grps(QRTZ_PAUSED_TRIGGER_GRPS)

  • 功能:记录被暂停的触发器分组。
  • 主要字段:
    • TRIGGER_GROUP:被暂停的触发器分组名称。

# 10、qrtz_scheduler_state(QRTZ_SCHEDULER_STATE)

  • 功能:记录调度器(每个机器节点)的运行状态。
  • 主要字段:
    • INSTANCE_NAME:调度器实例的名称。
    • LAST_CHECKIN_TIME:调度器最后一次心跳的时间。
    • CHECKIN_INTERVAL:心跳间隔。

# 11、qrtz_locks(QRTZ_LOCKS)

  • 功能:记录调度器的全局锁(用于悲观锁,防止多节点竞争执行同一个任务)。
  • 主要字段:
    • SCHED_NAME:调度器名称。
    • LOCK_NAME:锁的名称。

# 表之间的关系

  • QRTZ_JOB_DETAILS 是核心任务表,记录任务基本信息。
  • QRTZ_TRIGGERS 与任务相关联,存储触发器的基本信息。
  • QRTZ_CRON_TRIGGERS、QRTZ_SIMPLE_TRIGGERS、QRTZ_BLOB_TRIGGERS 等扩展触发器表,存储不同类型触发器的详细信息。
  • QRTZ_SCHEDULER_STATE 和 QRTZ_LOCKS 用于调度器的分布式协调。

这 11 张表共同支撑了 Quartz 的任务调度功能,既支持单机调度,也支持分布式集群环境。

# 学习参考

  • Quartz 入门详解 (opens new window)
  • 芋道 Spring Boot 定时任务入门 (opens new window)
  • Quartz框架(二)——jobstore数据库表字段详解 - 简书 (opens new window)
  • springboot升级2.6.x,Quartz2.3.2找不到数据源_there is no datasource named 'quartzdatasource-CSDN博客 (opens new window)
上次更新: 2025/1/7 17:52:25
SpringTask学习
xxl-job学习

← SpringTask学习 xxl-job学习→

Theme by Vdoing | Copyright © 2023-2025 沉梦听雨 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式