工具篇-- 定时任务quartz
在项目开发中我们经常需要在某个特定的时间做业务处理,如发生生日祝福,除夕拜年短信等,那么就需要使用定时任务框架来解决;Quartz是一个功能丰富的开源任务调度库,用于在Java应用程序中进行任务调度。它提供了一种灵活而强大的方式来定义和安排任务的执行时间,支持周期性任务、延迟任务、固定间隔任务等。Quartz的特点和功能:灵活的任务调度:Quartz可以根据各种调度规则定义任务的执行时间,如固定延
前言
在项目开发中我们经常需要在某个特定的时间做业务处理,如发生生日祝福,除夕拜年短信等,那么就需要使用定时任务框架来解决;
一、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中,JobDetail
和Job
是密切相关的,并且存在父子关系, 在设计上可以将job 包装成各种各样的 jobDetai,一个Job 可以对应为多个jobDetai,但是一个jobDetai 只能对用某一个job,但是通常在业务开发中 一个job 只创建一个jobDetai 。
JobDetail
是描述任务的具体细节的类,它包含了任务的标识、执行类、执行时所需的数据/参数等信息。它是任务的静态信息。
Job
是org.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中,Trigger
和JobDetail
是密切相关的并且有着父子关系。一个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的调度功能,可以灵活地安排和管理任务的执行时间,实现定时任务和定时调度的需求。可以配置多个JobDetail
和Trigger
来实现不同的任务调度策略,并通过调度器进行统一管理和执行。
以下是一个使用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 对象进行业务的处理;
参考:
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)