快速入门 Quartz 单机

2024-10-29 14:28

image-20241029143113484

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

在最早开始实习的时候,公司使用 Quartz 作为任务调度中间件。考虑到我们要实现定时任务的高可用,需要部署多个 JVM 进程。比较舒服的是,Quartz 自带了集群方案。它通过将作业信息存储到关系数据库中,并使用关系数据库的行锁来实现执行作业的竞争,从而保证多个进程下,同一个任务在相同时刻,不能重复执行。

可能很多胖友对 Quartz 还不是很了解,我们先来看一段简介:

FROM https://www.oschina.net/p/quartz

Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。

它有很多特征,如:数据库支持,集群,插件,EJB 作业预构建,JavaMail 及其它,支持 cron-like 表达式等等。

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

  • Scheduler :调度器

  • Trigger :触发器

  • Job :任务

不了解的胖友,可以直接看看 《Quartz 入门详解》 文章。这里,就不重复赘述。

FROM https://medium.com/@ChamithKodikara/spring-boot-2-quartz-2-scheduler-integration-a8eaaf850805 Quartz 整体架构图

Quartz 分成单机模式和集群模式。

  • 本小节,我们先来学习下 Quartz 的单机模式,入门比较快。

  • 下一下「5. 再次入门 Quartz 集群」 ,我们再来学习下 Quartz 的集群模式。在生产环境下,一定一定一定要使用 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.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>lab-28-task-quartz-memory</artifactId>
    <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>
</project>

具体每个依赖的作用,胖友自己认真看下添加的所有注释噢。

示例 Job

在 cn.iocoder.springboot.lab28.task.config.job 包路径下,我们来创建示例 Job 。

创建 DemoJob01 类,示例定时任务 01 类。代码如下:

// DemoJob01.java

public class DemoJob01 extends QuartzJobBean {

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

    private final AtomicInteger counts = new AtomicInteger();
 
    @Autowired
    private DemoService demoService;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        logger.info("[executeInternal][定时第 ({}) 次执行, demoService 为 ({})]", 			            counts.incrementAndGet(),demoService);
    }
}
  • 继承 QuartzJobBean 抽象类,实现 #executeInternal(JobExecutionContext context) 方法,执行自定义的定时任务的逻辑。

  • QuartzJobBean 实现了 org.quartz.Job 接口,提供了 Quartz 每次创建 Job 执行定时逻辑时,将该 Job Bean 的依赖属性注入。例如说,DemoJob01 需要 @Autowired 注入的 demoService 属性。核心代码如下:

// QuartzJobBean.java

public final void execute(JobExecutionContext context) throws JobExecutionException {
    try {
        // 将当前对象,包装成 BeanWrapper 对象
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
        // 设置属性到 bw 中
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.addPropertyValues(context.getScheduler().getContext());
        pvs.addPropertyValues(context.getMergedJobDataMap());
        bw.setPropertyValues(pvs, true);
     } catch (SchedulerException ex) {
      throw new JobExecutionException(ex);
	}
    // 执行提供给子类实现的抽象方法
    this.executeInternal(context);
}

protected abstract void executeInternal(JobExecutionContext context) throws JobExecutionException;
  • 这样一看,是不是清晰很多。不要惧怕中间件的源码,好奇哪个类或者方法,就点进去看看。反正,又不花钱。

  • counts 属性,计数器。用于我们后面我们展示,每次 DemoJob01 都会被 Quartz 创建出一个新的 Job 对象,执行任务。这个很重要,也要非常小心。

    创建 DemoJob02 类,示例定时任务 02 类。代码如下:

// DemoJob02.java

public class DemoJob02 extends QuartzJobBean {

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

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

}
  • 比较简单,为了后面演示案例之用。

ScheduleConfiguration

在 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() {
            // 基于 Quartz Cron 表达式的调度计划的构造器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 				* * * * ? *");
            // Trigger 构造器
            return TriggerBuilder.newTrigger()
                    .forJob(demoJob02()) // 对应 Job 为 demoJob02
                    .withIdentity("demoJob02Trigger") // 名字为 demoJob02Trigger
                    .withSchedule(scheduleBuilder) // 对应 Schedule 为 scheduleBuilder
                    .build();
        }

    }
}
  • 内部创建了 DemoJob01Configuration 和 DemoJob02Configuration 两个配置类,分别配置 DemoJob01 和 DemoJob02 两个 Quartz Job 。

  • DemoJob01Configuration

  • #demoJob01() 方法,创建 DemoJob01 的 JobDetail Bean 对象。

  • #demoJob01Trigger() 方法,创建 DemoJob01 的 Trigger Bean 对象。其中,我们使用 SimpleScheduleBuilder 简单的调度计划的构造器,创建了每 5 秒执行一次,无限重复的调度计划。

  • DemoJob2Configuration

  • #demoJob2() 方法,创建 DemoJob02 的 JobDetail Bean 对象。

  • #demoJob02Trigger() 方法,创建 DemoJob02 的 Trigger Bean 对象。其中,我们使用 CronScheduleBuilder 基于 Quartz Cron 表达式的调度计划的构造器,创建了每第 10 秒执行一次的调度计划。这里,推荐一个 Quartz/Cron/Crontab 表达式在线生成工具 ,方便帮我们生成 Quartz Cron 表达式,并计算出最近 5 次运行时间。

😈 因为 JobDetail 和 Trigger 一般是成双成对出现,所以习惯配置成一个 Configuration 配置类。

Application

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

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

运行 Application 类,启动示例项目。输出日志精简如下:

 创建了 Quartz QuartzScheduler 并启动
2019-11-30 23:40:05.123  INFO 92812 --- [           main] org.quartz.impl.StdSchedulerFactory      : Using default implementation for ThreadExecutor
2019-11-30 23:40:05.130  INFO 92812 --- [           main] org.quartz.core.SchedulerSignalerImpl    : Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
2019-11-30 23:40:05.130  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : Quartz Scheduler v.2.3.2 created.
2019-11-30 23:40:05.131  INFO 92812 --- [           main] org.quartz.simpl.RAMJobStore             : RAMJobStore initialized.
2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler meta-data: Quartz Scheduler (v2.3.2) 'quartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler 'quartzScheduler' initialized from an externally provided properties instance.
2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.impl.StdSchedulerFactory      : Quartz scheduler version: 2.3.2
2019-11-30 23:40:05.132  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : JobFactory set to: org.springframework.scheduling.quartz.SpringBeanJobFactory@203dd56b
2019-11-30 23:40:05.158  INFO 92812 --- [           main] o.s.s.quartz.SchedulerFactoryBean        : Starting Quartz Scheduler now
2019-11-30 23:40:05.158  INFO 92812 --- [           main] org.quartz.core.QuartzScheduler          : Scheduler quartzScheduler_$_NON_CLUSTERED started.

 DemoJob01
2019-11-30 23:40:05.164  INFO 92812 --- [eduler_Worker-1] c.i.springboot.lab28.task.job.DemoJob01  : [executeInternal][定时第 (1) 次执行, demoService 为 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)]
2019-11-30 23:40:09.866  INFO 92812 --- [eduler_Worker-2] c.i.springboot.lab28.task.job.DemoJob01  : [executeInternal][定时第 (1) 次执行, demoService 为 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)]
2019-11-30 23:40:14.865  INFO 92812 --- [eduler_Worker-4] c.i.springboot.lab28.task.job.DemoJob01  : [executeInternal][定时第 (1) 次执行, demoService 为 (cn.iocoder.springboot.lab28.task.service.DemoService@23d75d74)]

 DemoJob02
2019-11-30 23:40:10.004  INFO 92812 --- [eduler_Worker-3] c.i.springboot.lab28.task.job.DemoJob02  : [executeInternal][我开始的执行了]
2019-11-30 23:40:20.001  INFO 92812 --- [eduler_Worker-6] c.i.springboot.lab28.task.job.DemoJob02  : [executeInternal][我开始的执行了]
2019-11-30 23:40:30.002  INFO 92812 --- [eduler_Worker-9] c.i.springboot.lab28.task.job.DemoJob02  : [executeInternal][我开始的执行了]
  • 项目启动时,会创建了 Quartz QuartzScheduler 并启动。

  • 考虑到阅读日志方便,这里把 DemoJob01 和 DemoJob02 的日志分开来了。

  • 对于 DemoJob01 ,每 5 秒左右执行一次。同时我们可以看到,demoService 成功注入,而 counts 每次都是 1 ,说明每次 DemoJob01 都是新创建的。

  • 对于 DemoJob02 ,每第 10 秒执行一次。

下面「3.5 应用配置文件」两个小节,是补充知识,建议看看。

应用配置文件

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

spring:
 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 的时候,才需要配置
  • 在 spring.quartz 配置项,Quartz 的配置,对应 QuartzProperties 配置类。

  • Spring Boot QuartzAutoConfiguration 自动化配置类,实现 Quartz 的自动配置,创建 Quartz Scheduler(调度器) Bean 。

注意,spring.quartz.wait-for-jobs-to-complete-on-shutdown 配置项,是为了实现 Quartz 的优雅关闭,建议开启。关于这块,和我们在 Spring Task 的「2.6 应用配置文件」 提到的是一致的。

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

在最早开始实习的时候,公司使用 Quartz 作为任务调度中间件。考虑到我们要实现定时任务的高可用,需要部署多个 JVM 进程。比较舒服的是,Quartz 自带了集群方案。它通过将作业信息存储到关系数据库中,并使用关系数据库的行锁来实现执行作业的竞争,从而保证多个进程下,同一个任务在相同时刻,不能重复执行。

可能很多胖友对 Quartz 还不是很了解,我们先来看一段简介:

FROM https://www.oschina.net/p/quartz

Quartz 是一个开源的作业调度框架,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。它提供了巨大的灵活性而不牺牲简单性。你能够用它来为执行一个作业而创建简单的或复杂的调度。

它有很多特征,如:数据库支持,集群,插件,EJB 作业预构建,JavaMail 及其它,支持 cron-like 表达式等等。

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

  • Scheduler :调度器

  • Trigger :触发器

  • Job :任务

不了解的胖友,可以直接看看 《Quartz 入门详解》 文章。这里,就不重复赘述。

FROM https://medium.com/@ChamithKodikara/spring-boot-2-quartz-2-scheduler-integration-a8eaaf850805 Quartz 整体架构图

Quartz 分成单机模式和集群模式。

  • 本小节,我们先来学习下 Quartz 的单机模式,入门比较快。

  • 下一下「5. 再次入门 Quartz 集群」 ,我们再来学习下 Quartz 的集群模式。在生产环境下,一定一定一定要使用 Quartz 的集群模式,保证定时任务的高可用。

😈 下面,让我们开始遨游~

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

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