前言


在项目开发中我们经常需要在某个特定的时间做业务处理,如发生生日祝福,除夕拜年短信等,那么就需要使用定时任务框架来解决;

一、quartz 介绍:

Quartz是一个功能丰富的开源任务调度库,用于在Java应用程序中进行任务调度。它提供了一种灵活而强大的方式来定义和安排任务的执行时间,支持周期性任务、延迟任务、固定间隔任务等。

Quartz的特点和功能:

  • 灵活的任务调度:Quartz可以根据各种调度规则定义任务的执行时间,如固定延迟、固定间隔、Cron表达式等。可以轻松地创建简单或复杂的任务调度方案。

  • 分布式调度支持:Quartz支持分布式任务调度,可在多个节点上运行并协调任务的执行。这种分布式架构提供了可靠、高可用的任务调度解决方案。

  • 持久化存储:Quartz支持将任务和触发器的状态信息存储在数据库中,以便在应用程序重启后能够保持任务的持久化和恢复。它提供了与多种数据库的集成,并有内置的任务存储机制。

  • 错误恢复和重试机制:Quartz提供了丰富的错误处理和恢复机制,以确保任务执行的稳定性。如果任务执行失败,Quartz会根据预定义的策略进行错误处理和任务重试。

  • 监控和管理:Quartz提供了一套管理和监控API,可用于手动管理任务调度器、查询任务和触发器的状态、查看执行日志等。这些API可以方便地集成到的应用程序或管理工具中。

  • 插件扩展性:Quartz具有良好的扩展性,允许开发人员编写自定义的任务存储、调度器监听器、触发器监听器等插件,以满足特定的需求和业务逻辑。

二、quartz 的简单使用:

2.1 引入jar:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2.2 定义任务:

(1)任务的执行HelloJob :

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;
import java.util.Date;

public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("\"job 执行\" = " + "job 执行" + sdf.format(new Date()));

    }
}

(2)任务的调度:

import com.example.springmvctest.job.quartz1.HelloJob;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;

public class QuartzTest {
    public static void main(String[] args) {

        try {
            // Grab the Scheduler instance from the Factory
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // and start it off
            scheduler.start();
            // define the job and tie it to our HelloJob class
            JobDetail job = newJob(HelloJob.class)
                    .withIdentity("job1", "group1")
                    .build();

            // Trigger the job to run now, and then repeat every 40 seconds
            Trigger trigger = newTrigger()
                    .withIdentity("trigger1", "group1")
                    .startNow()
                    .withSchedule(simpleSchedule()
                            .withIntervalInSeconds(5)
                            .repeatForever())
                    .build();

            // Tell quartz to schedule the job using our trigger
            scheduler.scheduleJob(job, trigger);
            Thread.sleep(20000);
            scheduler.shutdown();

        } catch (SchedulerException se) {
            se.printStackTrace();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

HelloJob:

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.StringJoiner;

public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//        System.out.println("\"job 执行\" = " + "job 执行" + sdf.format(new Date()));
        StringJoiner outStr = new StringJoiner("")
                .add("job 执行")
                .add(sdf.format(new Date()))
                .add(Thread.currentThread().getName())
                .add(jobExecutionContext.getTrigger().getKey().getName());

        System.out.println(outStr);
    }
}

在这里插入图片描述

三、quartz 核心组件:

在这里插入图片描述

调度器( Scheduler) 通过触发器(trigger) 执行任务(job):

3.1 JobDetail:

3.1.1 JobDetail介绍:

在Quartz中,JobDetail是描述任务(Job)的具体细节的类。它包含了任务的标识、执行类、执行时所需的数据/参数等信息。

以下是JobDetail的一些主要属性:

  • name:任务名称,同一个分组下必须是唯一的。
  • group:任务分组,用于将任务进行分类,方便管理和调度。
  • jobClass:任务执行类,即实现了org.quartz.Job接口的类。它负责实际执行任务任务的逻辑,真正的业务代码执行。
  • jobDataMap:任务的数据/参数,可以传递一些额外的数据给任务类,供其使用。
  • durability:任务的持久性标志,指示任务是否应该存储在调度器中,即使没有与之关联的触发器。
  • requestsRecovery:任务的失败恢复标志,指示任务是否应该在调度器发生故障后恢复执行。

使用JobDetail,可以定义要执行的具体任务的细节,并为任务提供必要的信息。例如,可以指定任务的名称、分组、执行类和参数等。当Quartz调度器触发任务时,将使用JobDetail中定义的信息来执行相应的任务逻辑。

以下是示例代码,展示如何创建一个JobDetail对象:

JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                           .withIdentity("jobName", "jobGroup")
                           .usingJobData("param1", "value1")
                           .build();

在上述示例中,创建了一个JobDetail对象,指定了任务的执行类为MyJob,名称为"jobName",分组为"jobGroup",并使用了一个名为"param1"的参数。

JobDetail是Quartz中重要的概念之一,它定义了要执行的任务的详细信息。调度器使用JobDetail来创建任务的实例,并将其与对应的触发器进行关联,以实现任务的执行调度。

3.1.2 JobDetail 和job 的关系:

在Quartz中,JobDetailJob是密切相关的,并且存在父子关系, 在设计上可以将job 包装成各种各样的 jobDetai,一个Job 可以对应为多个jobDetai,但是一个jobDetai 只能对用某一个job,但是通常在业务开发中 一个job 只创建一个jobDetai 。

JobDetail是描述任务的具体细节的类,它包含了任务的标识、执行类、执行时所需的数据/参数等信息。它是任务的静态信息。

Joborg.quartz.Job接口的实现类,负责实际执行任务任务的逻辑。它是任务的动态逻辑。

在Quartz中,通过将一个JobDetail实例与一个Trigger实例相关联,形成一个任务调度的单元。当触发器触发时,调度器会使用JobDetail中的信息创建一个Job实例,并执行该实例中的任务逻辑。

换句话说,JobDetail是任务的定义,而Job是任务的实例。一个JobDetail可以有多个关联的Job实例,每个实例执行相同的逻辑。这种设计能够使任务实例具有并发执行的能力。

JobDetail是描述任务的静态信息,而Job是实际执行任务任务的动态实例。调度器使用JobDetail定义任务的细节信息,并根据触发器的触发来创建和执行相应的Job实例。

3.2 trigger:

3.2.1 trigger 介绍:

在Quartz中,Trigger是用于定义任务(Job)执行时间的组件。它指定了任务应该何时运行、运行频率和执行规则。

Trigger可以分为以下几种类型:

  • SimpleTrigger:简单触发器,用于指定任务在特定时间点触发一次或多次的执行。可以设置触发时间、重复次数、重复间隔等参数。

  • CronTrigger:Cron触发器,基于Cron表达式定义触发时间规则。Cron表达式可以更灵活地指定任务的触发时间,如每天的特定时间执行、每周的特定日期执行等。

  • CalendarIntervalTrigger:日历间隔触发器,以特定的日历间隔来触发任务的执行。可以指定固定的时间间隔或基于日历规则定义触发时间。

  • DailyTimeIntervalTrigger:每日时间间隔触发器,基于特定的时间间隔和每天的时间窗口来触发任务的执行。

通过使用这些不同类型的Trigger,可以灵活地定义任务的触发时间和执行规则。一旦触发器被触发,Quartz调度器将会调度与之相关联的任务(通过JobDetail),并按照预定义的规则执行任务逻辑。

以下是一个示例代码,展示如何创建一个简单触发器:

Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("triggerName", "triggerGroup")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                        .withIntervalInSeconds(10)
                        .repeatForever())
                .build();

在上述示例中,创建了一个简单触发器,设置了触发器的名称为"triggerName",分组为"triggerGroup",开始时间为当前时间,每隔10秒触发一次任务执行,重复执行无限次。

Trigger是Quartz中重要的组件之一,它定义了任务的触发时间和执行规则。通过使用不同类型的触发器,可以根据需求灵活地调度和执行任务。同时,调度器会管理触发器的调度和执行,并自动触发与之关联的相应任务的执行。

3.2.2 trigger 和jobDetail 的关系:

在Quartz中,TriggerJobDetail是密切相关的并且有着父子关系。一个jobDetail 可以被多个trigger 触发;

  • JobDetail是描述任务的静态信息,包括任务的标识、执行类、执行时所需的数据/参数等信息。
  • Trigger是用于定义任务(Job)执行时间的组件,指定了任务应该何时运行、运行频率和执行规则。

一个JobDetail可以被多个Trigger关联,并且每个Trigger都会触发一个任务实例的执行。这种关联关系使得任务可以在不同的时间点、不同的规则下被调度和执行。

在Quartz调度器中,当一个Trigger被触发时,调度器会使用与之关联的JobDetail信息创建一个Job实例,并执行该实例中定义的任务逻辑。这个Job实例是任务的动态实例。一个JobDetail可以有多个关联的Trigger,每个关联的Trigger都会创建一个新的Job实例并并发执行。

JobDetail是描述任务的静态信息,而Trigger是定义任务触发时间和执行规则的组件。一个JobDetail可以被多个Trigger关联,每个关联的Trigger将创建一个新的Job实例并执行任务逻辑。通过这种关系,Quartz可以实现丰富的任务调度和执行策略。

3.3 schedule:

3.3.1 schedule 介绍:

在Quartz中,调度(schedule)将一个具体的jobDetail 和trigger 关联起来,安排和管理任务的执行时间和频率。

调度过程包括以下步骤:

  • 创建JobDetail对象,描述任务的具体细节,包括任务的标识、执行类、执行时所需的数据/参数等信息。
  • 创建Trigger对象,定义任务的触发时间和执行规则,例如简单触发器(SimpleTrigger)、Cron触发器(CronTrigger)等。
  • JobDetail对象和Trigger对象关联起来,形成任务调度的单元。
  • 将任务调度单元通过调度器(Scheduler)的scheduleJob()方法进行调度,即安排任务的执行。
  • 调度器会根据Trigger定义的触发时间规则,按照预定的时间表执行任务。当触发时间到达时,调度器会创建一个Job实例,执行任务逻辑。
  • 任务执行完毕后,调度器会根据Trigger的定义,继续安排下一次任务的执行,并持续调度任务执行。

通过Quartz的调度功能,可以灵活地安排和管理任务的执行时间,实现定时任务和定时调度的需求。可以配置多个JobDetailTrigger来实现不同的任务调度策略,并通过调度器进行统一管理和执行。

以下是一个使用Quartz进行调度的简单示例代码:

// 创建JobDetail对象
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
        .withIdentity("myJob", "group1")
        .build();

// 创建Trigger对象
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("myTrigger", "group1")
        .withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(10))
        .build();

// 创建调度器对象
Scheduler scheduler = new StdSchedulerFactory().getScheduler();

// 将JobDetail对象和Trigger对象关联起来
scheduler.scheduleJob(jobDetail, trigger);

// 启动调度器开始调度任务
scheduler.start();

在上述示例中,使用Quartz创建了一个任务调度的简单示例。创建了一个JobDetail对象来描述任务细节,并创建一个触发器Trigger来定义任务的触发规则,每10秒执行一次。然后将JobDetail对象和Trigger对象关联起来,并通过调度器调度任务的执行。

调度是Quartz中重要的功能之一,它允许根据需求安排和管理任务的执行时间和频率。通过灵活配置和使用调度器,可以满足定时任务和任务调度的需求。

四、扩展:

4.1 并发执行注解:

@DisallowConcurrentExecution 是否允许并行执行;加上这个注解,同一个任务不能被并行执行:
场景:
定时任务每隔 3分钟,退款订单; 每次任务执行时间假如超过了3分钟;则在第二次任务触发时,则可能发生同一个订单重复退款的情况;

此时需要增加@DisallowConcurrentExecution 让其同一个job 不能并行的执行(是否同一个job 通过JobKey 判断,JobKey 包含了job name 和group )

在这里插入图片描述

4.2 数据持久化:

PersistJobDataAfterExecution 对数据持久化只针对 jobdetail 对trigger 的jobdetail 无效,改注解可以传递数据的变化;
– 示例代码

JobDetail jobDetail = JobBuilder.newJob(QuartzTest.class)
        .usingJobData("job","jobDetail")
        .usingJobData("name","jobDetail")
        .usingJobData("count",0)
        .withIdentity("job","group1").build();

@PersistJobDataAfterExecution
public class MyJob implements Job {
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();

jobDataMap.put("count",jobDataMap.getInt("count")+1);
System.out.println("\"jobcount\" = " + jobDataMap.getInt("count"));
}

1、每次任务被触发,Job都会是一个新的实例默认情况下,相同的任务可以被并发执行
2、@DisallowConcurrentExecution 可以禁止相同任务的并发行为若JobKey相同,则Quartz认为是同一个任务
3.如果任务 需要修改 dataMap,并且下次被触发时 需要用到上次修改的 dataMap;可以使用 @PersistJobDataAfterExecution当使用了@PersistobDataAfterExecution,还应认真考虑是否需要使用 @PersistJobDataAfterExecution (需要考虑并发情况下无法及时获取到被修改后的数据)
在这里插入图片描述

b,c 是任务执行时修改jobDetail 的值,并发执行时,在第三次拿到的数据是b而不是c(此时就需要增加@PersistJobDataAfterExecution 注解);

4.3 任务失火(misfilre) :

misfire,中文意思是“失火”。在 quartz 中的含义是:到了任务触发时间,但是任务却没有被触发失火的原因可能是:

  • 使用了 @DisallowConcurrentExecution 注解,而且任务的执行时间 >任务间隔
  • 线程池满了,没有资源执行任务
  • 机器宕机或者人为停止,过段时间恢复运行

4.3.1 SimpleScheduleBuilder 简单调度任务器失火策略:

当任务发生多次misfire(即多次未能按时执行)时,不同的失火策略会有不同的行为:默认按照启动时间向后正常执行任务

1). withMisfireHandlingInstructionIgnoreMisfires():该策略会忽略所有的misfire,直接跳过错过的触发时间点,等待下一个正常的触发时间。不会补偿misfire的任务。

2). withMisfireHandlingInstructionFireNow():该策略会在发生misfire时立即执行任务,不考虑原定的触发时间。会补偿一次misfire的任务等待下一个正常的触发时间,。

3). withMisfireHandlingInstructionNextWithExistingCount():当发生misfire时,触发器会被重新调度到下一个执行时间点,并保留原有的重复次数。会补偿misfire的任务,继续执行原有的重复次数。
withMisfireHandlingInstructionNextWithExistingCount():当任务发生misfire时,触发器会被重新调度到下一个执行时间点,并保留原有的重复次数。这意味着即使任务错过了之前的触发时间点,它也会继续保持原有的重复次数。例如,如果任务原定要重复10次,在第5次发生misfire后,任务会重新调度到下一个时间点,并继续执行剩余的5次,不会重置为10次。

4). withMisfireHandlingInstructionNextWithRemainingCount():当任务发生misfire时,触发器会被重新调度到下一个执行时间点,并使用剩余的重复次数。会补偿misfire的任务,使用剩余的重复次数。
withMisfireHandlingInstructionNextWithRemainingCount():当任务发生misfire时,触发器会被重新调度到下一个执行时间点,并使用剩余的重复次数。这意味着任务只会继续执行剩余的重复次数,不会考虑原有的总重复次数。例如,如果任务原定要重复10次,在第5次发生misfire后,任务会重新调度到下一个时间点,并仅执行剩余的4次。

5). withMisfireHandlingInstructionNowWithExistingCount():当任务发生misfire时,立即执行任务,并保留现有的重复次数。会补偿misfire的任务,立即执行任务并继续执行原有的重复次数。
withMisfireHandlingInstructionNowWithExistingCount():当任务发生misfire时,立即执行任务,并保留原有的重复次数。这意味着任务会立即执行,但会继续按照原有的重复次数执行,不会重置为当前次数。这样可以确保任务立即得到执行,同时继续执行剩余的重复次数
withMisfireHandlingInstructionNextWithExistingCount()会等待下一个执行时间点继续执行,而withMisfireHandlingInstructionNowWithExistingCount()会立即执行任务。根据具体业务需求和任务执行情况

6). withMisfireHandlingInstructionNowWithRemainingCount():当任务发生misfire时,立即执行任务,并使用剩余的重复次数。会补偿misfire的任务,立即执行任务并使用剩余的重复次数。
withMisfireHandlingInstructionNowWithRemainingCount():当任务发生misfire时,立即执行任务,并使用剩余的重复次数。这意味着任务会立即执行,但会继续执行剩余的重复次数,而不是重置为当前次数。即使任务之前已经重复执行了几次,misfire发生后只会执行剩余的次数。
withMisfireHandlingInstructionNextWithRemainingCount()会等待下一个执行时间点继续执行,而withMisfireHandlingInstructionNowWithRemainingCount()会立即执行任务。

4.3.2 CronScheduleBuilder Cron 调度器失火策略:

在Quartz Scheduler中,CronScheduleBuilder是用于创建基于Cron表达式的触发器的构建器。CronScheduleBuilder提供了三种不同的misfire处理指令,分别是withMisfireHandlingInstructionIgnoreMisfires()withMisfireHandlingInstructionDoNothing()withMisfireHandlingInstructionFireAndProceed(),它们各自具有不同的misfire处理策略,下面详细解释这三种策略:

1). withMisfireHandlingInstructionIgnoreMisfires():当任务发生misfire时,忽略misfire,立即执行任务。这意味着即使触发器错过了触发时间点,也会立即触发执行任务,不会考虑之前未执行的时间点。任务会尽可能快地得到执行,而未执行的触发时间将被忽略。
withMisfireHandlingInstructionIgnoreMisfires()会在任务发生misfire时立即执行一次补偿任务,而不会考虑之前错过的触发时间点。换句话说,即使任务错过了触发时间点,该策略也会立即执行一次任务,无论之前的misfire次数如何。这样可以确保任务尽快地得到执行,而不会等待到下一个预定的触发时间点再执行。

使用withMisfireHandlingInstructionIgnoreMisfires()策略时,会立即触发一次任务,但不会恢复之前未执行的触发时间点。任务将继续按照正常的调度继续执行,而不会受之前的misfire影响。这样可以确保任务尽可能快地得到执行,而不会因misfire而延迟执行。

2). withMisfireHandlingInstructionDoNothing():当任务发生misfire时,不做任何处理,等待下一个触发时间点再触发执行。这意味着如果任务错过了触发时间点,会等待下一个触发时间再次触发执行。之前的misfire不会得到补偿,任务会按照正常的调度继续执行。

3). withMisfireHandlingInstructionFireAndProceed():当任务发生misfire时,立即触发执行任务,并且按照正常的调度继续执行。这意味着任务会立即执行,同时保留原来的调度计划。之后的调度会按照正常的调度继续执行,保留了之前的misfire。

withMisfireHandlingInstructionFireAndProceed()withMisfireHandlingInstructionIgnoreMisfires()都是用于处理任务misfire的策略,它们之间的区别在于处理misfire的具体方式:

1). withMisfireHandlingInstructionFireAndProceed()

  • 当任务发生misfire时,会立即触发执行任务,并且按照正常的调度继续执行。
  • 会执行一次补偿任务,并继续按正常的调度继续执行,保留之前的misfire, 不会丢失任何misfire,可以确保任务得以补偿执行,并且按照正常的调度继续执行,保持任务的连续性。

2). withMisfireHandlingInstructionIgnoreMisfires()

  • 当任务发生misfire时,会忽略misfire,立即执行任务。
  • 会执行一次补偿任务,但会忽略之前的misfire次数,任务会尽可能快地得到执行。

总的来说,withMisfireHandlingInstructionFireAndProceed()会执行一次补偿任务并继续保留原来的调度,而withMisfireHandlingInstructionIgnoreMisfires()会忽略之前的misfire次数,立即执行一次任务但不保留之前的misfire。

4.4 任务执行过程中抛出异常 :

任务执行过程中抛出异常,后续任务正常执行,不影响后续的任务调度;如果是已知的异常,可以在catch 中进行一次处理后,重新发起下一次任务的调用;

4.4.1 任务异常后手动触发补偿本次任务:

第一种方式: 重新构建任务后 通过startNow 启动任务
在这里插入图片描述
第二种方式:
每次 调用JobExecutionContext 都会产生一个新的context:使用同一个context,对job或者trigger中的JobDataMap 进行数据修改;
在这里插入图片描述

4.4.2 任务异常后续改关联任务不在执行:

获取到跟当前job 所有的触发器,进行任务的停止执行;
第一种方式:
在这里插入图片描述
第二种方式:
在这里插入图片描述

4.4 日期排除 :

定时任务的执行,需要把某些时间排除在外;
我们想一下这样的场景,某产业园有家食堂,给 办过会员卡的用户 每天早上10点发一条短信
“xxx您好,本店今日供应午餐xxx,欢迎前来就餐”
此时就需要将节假日排除在外,quartz 中提供了几种类 来处理:
在这里插入图片描述

  • CronCalendar 用来排除 给定CronExpression表示的 时间集
  • AnnualCalendar 用来排除 年 中的 天
  • HolidayCalendar 用来排除 某年 中的 某天 (与 AnnualCalendar 类似,区别是把年考虑进去了)
  • MonthlyCalendar用来排除 月 中的 天
  • WeeklyCalendar 用来排除 星期 中的 天
  • DailyCalendar 用来排除 一天中的 某个时间段 (不能跨天)(可以反转时间段的作用)

用法示例:
在这里插入图片描述


总结:

quartz 中通过定义JobDetail 来对某个业务进行包装,并定义触发器来支持该任务何时被执行,最后通过调度器将jobDetail 和trigger 进行管理,在任务触发时 调度器 通过jobDetail 实例化一个job 对象进行业务的处理;

参考:

1 quartz 快速上手(quartz官网 );

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐