Java中定时任务实现技术选型!定时框架Quartz分析与说明

1,631 阅读11分钟

这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战

基本概念

  • Java中实现定时任务的几种方式:
    • Timer: java.util.Timer, 一个JDK自带的处理简单的定时任务的工具
    • ScheduledExecutorService: java.util.concurrent.ScheduledExecutorService, JDK中的定时任务接口,可以将定时任务与线程池结合使用
    • Sceduled: org.springframework.scheduling.annotation.Scheduled, Spring框架中基于注解来实现定时任务处理方式
    • Quartz: 支持分布式调度任务的开源框架
  • Quartz:
    • Quartz是功能强大的开源作业调度库,可以集成到最小的独立应用程序到最大的电子商务程序中
    • Quartz可以通过创建简单或者复杂的计划来执行成千上万的任务
    • 任何任务标准的Java组件,都可以执行相应的编程操作
    • Quartz Scheduler支持JTA事务和集群
    • Quartz实现了任务和触发器多对多的关系,可以将多个任务和不同的触发器相关联

使用场景

  • 某个时间点执行的任务,或者每隔一段时间重复执行的任务
  • 保持任务调度的定时状态的持久性任务
  • 对调度任务进行有效管理的任务调度
  • 示例:
    • 自动关闭30分钟未支付的订单
    • 定时与第三方公司对账业务,对接接口等
    • 数据统计,比如博客系统统计日粉丝数,日阅读量等
    • 活动开始通知和活动结束通知
    • 在每个月25号自动还款的设置
    • 每周或者每个月的提醒的事项

Quartz特点

  • 强大的任务调度功能,包含丰富的任务调度方法用来满足常规的和复杂的任务调度需求
  • 灵活的应用方式可以支持任务调度和任务多种相结合,支持调度数据的多种存储方式,包括DB,RAM等
  • 支持分布式集群

Quartz

在这里插入图片描述

  • 设计模式:
    • 建造者模式
    • 组合模式
    • 工厂模式
  • Quartz核心元素: Scheduler为实际执行调度的控制器. Trigger,Job和JobDetail为任务调度的元数据
    • 任务调度器Scheduler
    • 触发器Trigger
    • 任务Job
    • 任务调度程序JobDetail 在这里插入图片描述

Scheduler

  • 任务调度器Scheduler:
    • 一个任务调度容器中可以注册多个TriggerJobDetail
    • TriggerJobDetail组合时,就可以被任务调度器Scheduler调度了
    • 一般情况下,一个应用只需要一个Scheduler对象
  • SchedulerFactory用于创建Scheduler:
    • 包括DirectSchedulerFactoryStdSchedulerFactory
    • 因为DirectSchedulerFactory使用需要做很多详细的手工编码设计,所以一般StdSchedulerFactory使用较多
  • Scheduler主要有三种:
    • RemoteMBeanScheduler
    • RemoteScheduler
    • StdScheduler

Trigger

  • 触发器Trigger: 任务调度的时间规则
  • TriggerQuartz的触发器,会通知任务调度器Scheduler执行指定的任务Job
// 指定触发器首次触发的时间
new Trigger().startAt();
// 指定触发器结束触发的时间
new Trigger().endAt();
  • Quartz中提供四种类型的触发器:
    • SimpleTrigger: 可以实现在一个指定时间段内执行一次任务或者在一个时间段内执行多次任务
    • CronTrigger:
      • 基于日历的任务调度,功能非常强大
      • 相比较于SimpleTrigger精准指定间隔 ,CronTrigger是基于Cron表达式的,更加常用
    • DateIntervalTrigger
    • NthIncludedDayTrigger
  • Calendar: 一些日历的特定时间点的集合
    • 一个触发器可以包含多个Calendar, 这样可以用包含或者排除某些时间点

JobDetail

  • 任务调度程序JobDetail:
    • 一个具体的可执行的任务调度程序
    • Job就是这个可执行任务调度程序所要执行的内容
    • JobDetail中还包含了任务调度的方案和策略
  • JobDetail绑定指定的Job, 每次Scheduler调用执行一个Job时,首先会获取对应的Job, 然后创建该Job的实例,再去执行Job中的execute() 的内容,任务执行结束后,关联的Job对象实例会被释放并且会被JVMGC清除
  • JobDetailJob实例提供了许多属性:
    • name
    • group
    • jobClass
    • jobDataMap

Job

  • 任务Job: 表示要执行的具体工作或者被调度的任务
    • 自定义的任务类需要实现该接口,通过重写execute() 方法来定义任务的执行逻辑
  • Job的类型有两种:
    • 无状态的stateless job
    • 有状态的stateful job
      • 对于同一个触发器Trigger来说,有状态的Job不能并行执行,只有上一次触发的任务被执行完成之后,才能触发下一次执行
  • Job的属性有两种:
    • volatility: 表示任务是否被持久化到数据库存储
    • durability: 没有trigger关联的时候任务是否保留
      • 两者都是在值为true时表示任务被持久化或者保留
      • 一个Job可以关联多个Trigger, 一个Trigger只能关联一个Job
  • JobExecutionContext:
    • JobExecutionContext中包含了Quartz运行时的环境以及Job本身的详细数据信息
    • Scheduler调度执行一个Job时,就会将JobExecutionContext传递到当前Jobexecute() 中,当前Job就可以通过JobExecutionContext对象获取信息
  • Quartz设计成JobDetail + Job的原因在于:
    • JobDetail用于定义任务数据, 真正的执行任务的逻辑在Job
    • 因为任务有可能是并发执行的,如果Scheduler直接使用Job, 会存在对同一个Job实例并发访问的问题
    • 通过JobDetail绑定Job的方式 ,Scheduler每次执行,都会根据JobDetail创建一个新的Job实例,可以避免并发访问的问题

Quartz主要线程

  • Quartz包含两类线程:
    • 执行线程
    • 调度线程
  • Quartz主要线程之间的关系:
    在这里插入图片描述
  • 执行线程:
    • 通常情况下,执行任务的线程使用一个线程池Job Thread Pool维护
  • 调度线程:
    • 常规任务调度Regular Scheduler Thread: Regular Thread轮询Trigger, 如果存在将要触发的Trigger, 则会从任务线程池中获取一个空闲线程,然后执行与Trigger相关联的Job
    • 错失任务调度Misfire Scheduler Thread: MisfireThread扫描所有的Trigger, 查看是否存在错失,如果存在错失,则根据指定的策略执行

Quartz数据存储

  • Quartz中的TriggerJob需要存储下来才可以使用
  • Quartz有两种数据存储方式:
    • RAMJobStore
      • TriggerJob存储在内存中
      • RAMJobStore的存取速度非常快,但是在系统停止后所有的数据都会丢失
    • JobStoreSupport
      • 基于JDBCTriggerJob存储在数据库中
      • 在集群应用中,为了避免在系统停止后数据丢失的问题需要使用JobStoreSupport
  • Quartz集群是通过数据库表来感知其余应用的,各个节点之间没有直接的通信.只有使用持久化的JobStore才能完成Quartz集群
  • Quartz中的SQL位置:
    • /docs/dbTables
    • org/quartz/impl/jdbcstore
  • Quartz表结构:
表名描述
QRTZ_CALENDARS存储Quartz的日历信息
QRTZ_CRON_TRIGGERS存储cron类型的Trigger
包括cron表达式和时区信息
QRTZ_FIRED_TRIGGERS存储与已触发的Trigger相关的状态信息以及相关联的Job的执行信息
QRTZ_PAUSED_TRIGGER_GRPS存储已暂停的Trigger组的信息
QRTZ_SCHEDULER_STATE存储Scheduler相关的状态信息
QRTZ_LOCKS如果程序使用了悲观锁,则存储程序的悲观锁信息
QRTZ_JOB_DETAILS存储每一个已经配置的JobDetail信息
QRTZ_SIMPLE_TRIGGERS存储Simple类型的Trigger
包括重复次数,间隔以及已经触发的次数
QRTZ_BLOG_TRIGGERS存储Blog类型的Trigger
QRTZ_TRIGGERS存储已经配置的Trigger的基本信息
QRTZ_TRIGGER_LISTENERS存储Trigger监听器的信息
QRTZ_JOB_LISTENERS存储Job监听器信息
  • 使用cron表达式的Quartz任务需要用到QRTZ_CRON_TRIGGERS, QRTZ_FIRED_TRIGGERS, QRTZ_TRIGGRS, QRTZ_JOB_DETAILS这四张表

Quartz示例

  • Quartz有三个基本组成部分:
    • 任务调度器Scheduler
    • 触发器Trigger : 包括SimpleTriggerCronTrigger
    • 任务Job
  • 首先需要定义一个实现定时功能的接口,也就是Job 在这里插入图片描述
  • 然后定义一个触发任务执行的触发器,也就是Trigger. 触发器的基本功能就是指定Job的执行时间,执行间隔和运行次数等 在这里插入图片描述
  • 最后定义一个任务调度器,也就是Scheduler. 任务调度器的功能就是将JobTrigger结合起来,指定Trigger去执行指定的Job 在这里插入图片描述

Quartz动态管理

  • 对任务Job的动态管理实际上是对任务调度器中Scheduler中的任务调度程序JobDetail做动态操作
    • 首先操作任务调度程序JobDetail
    • 然后根据任务JobKey或者TriggerKey同步更新数据库中的任务Job
  • 要想实现对任务Job的动态管理,任务Job必须要持久化到数据库中
    • Spring容器在启动的时候,从数据库中加载所有的任务Job
      • 需要对任务进行持久化,将基本的信息保存在数据库中
      • 分布式任务
    • 实现CommandLineRunner
    • 定义初始化操作
  • 实现CommandLineRunner接口中的run() 方法:
public void run(Strig... strings) throws Exception {
	// 获取数据库中的任务Job
	List<JobInfo> jobInfoList = jobInfoMapper.selectAll();
	List<JobInfoVo> jobList = BeanUtils.copyList(jobInfoList, JobInfoVo.class);

	// 根据数据库中任务Job的状态同步任务调度器Scheduler的状态
	for (int i = 0; i < jobList.size; i++) {
		JobInfoVo job = jobList.get[i];
		// 获取任务Job
		Class jobClass = Class.forName(job.getJobClass());
		// 获取JobKey
		JobKey jobKey = JobKey.jobKey(job.getJobName(), job,getJobGroup());
		// 获取TriggerKey
		TriggerKey triggerKey = TriggerKey.triggerKey(job.getTriggerName(), job.getTriggerGroup());
		
		// 创建指定的JobDetail实例,并与指定的Job相绑定
		JobDetail jobDetail = JobDetailBuilder.newJob(jobClass)
			.withIdentity(jobKey)
			.storeDurably()
			.build();
		// 创建指定的触发器实例,根据cron表达式实现触发
		Trigger trigger = TriggerBuilder.newTrigger()
			.withIdentity(triggerKey)
			.withSchedule(CronSchedulerBuilder
				.cronSchedule(job.getCronExpression()))
			.builder();

		// 根据任务Job的状态同步任务调度器Scheduler状态
		JobStatus jobStatus = JobStatus.valueOf(job.getJobStatus());
		switch (jobStatus) {
			case RUNNING :
				scheduler.scheduleJob(jobDetail, trigger);
			case PAUSE :
				scheduler.scheduleJob(jobDetail, trigger);
				scheduler.pauseJob(jobKey);
		}
	}
	// 添加任务Job的监听器
	QuartzJobListener jobListener = new QuartzJobListener("jobListener", JobManagerService);
	scheduler.getListenerManager().addJobListener(jobListener, allJobs() );
} 

SpringBoot集成Quartz

  • Quartz使用基本流程:
    • 首先创建任务Job, 这是任务的主体,用于编写任务的业务逻辑
    • 接着创建任务调度器Scheduler, 这是用来对任务进行调度的,主要用于任务的启动,停止,暂停,恢复等操作
    • 接着创建任务明细JobDetail, 这是用来保存任务相关信息,和指定的任务Job相互绑定,用于给调度器执行
    • 然后创建触发器Trigger, 这是用来定义任务的触发规则. 主要使用的有SimpleTriggerCronTrigger两大类触发器
    • 最后就是根据触发器Scheduler来启动任务JobDetail和触发器Trigger

创建任务Job

  • 创建一个类实现任务Job接口,需要重写execute() 方法,方法内容就是具体的业务执行逻辑
  • 如果是动态任务,就需要在创建任务明细JobDetail或者触发器Trigger时动态传入参数,然后通过JobExecutionContext来获取参数进行处理

创建任务调度器Scheduler

  • 如果是普通的,则需要通过任务调度器工厂SchedulerFactory创建
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
  • SpringIOC中,可以通过Spring直接注入即可:
@Autowired
private Scheduler scheduler;
  • 普通模式,通过工厂创建:
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();

创建任务明细JobDetail

/*
 * 通过JobBuilder.newJob()方法获取到当前Job的具体实现,然后通过链式调用添加任务明细
 * 		- 固定的Job实现
 * 		- 如果是动态实现,根据不同的类来创建Job
 * 			- 比如((Job)Class.forName("com.oxford.job.DynamicJob").newInstance()).getClass()
 * 			- 则当前Job的具体实现:JobBuilder.newJob(((Job)Class.forName("com.oxford.job.DynamicJob")).getClass())
 */
 JobDetail jobDetail = JobBuilder.newJob(Job.class)
 	/*
 	 * usingJobData可以以k-v的形式给当前JobDetail添加参数
 	 * 可以通过链式调用usingJobData,传入多个参数
 	 * 在Job实现类中,可以通过JobExecutionContext.getJobDetail().getJobDataMap().get("name")获取参数的值
 	 */
 	.usingJobData("name", "oxford")
 	// 添加认证信息
 	.withIdentity("name","group")
 	// 任务明细创建完成后,执行生效
 	.build()

创建触发器Trigger

  • 通用的触发器包括两类:
    • SimpleTrigger
    • CronTrigger

SimpleTrigger

  • SimpleTrigger: 根据Quartz框架的类中自定义的方法设置定时执行规则
Trigger trigger = TriggerBuilder.newTrigger()
	/*
 	 * usingJobData可以以k-v的形式给当前JobDetail添加参数
 	 * 可以通过链式调用usingJobData,传入多个参数
 	 * 在Job实现类中,可以通过JobExecutionContext.getJobDetail().getJobDataMap().get("name")获取参数的值
 	 */
 	 .usingJobData("name", "oxford")
 	 // 添加认证信息
 	 .withIdentity("name", "group")
 	 // 添加开始执行时间 - 立即执行startNow()
 	 // 开始执行时间
 	 .startAt(start)
 	 // 结束执行时间.如果不添加这个参数表示永久执行
 	 .endAt(end)
 	 // 添加执行规则.使用SimpleScheduleBuilder添加SimpleTrigger触发器
 	 .withSchedule(SimpleSchedulebuilder
 	 	.simpleSchedule()
 	 	// 执行间隔,这里是每隔6秒执行一次
 	 	.withIntervalInSeconds(6)
 	 	.repeatForever())
 	 // 触发器创建完成.执行生效
 	 .build();

CronTrigger

  • CronTrigger: 基于Cron表达式实现触发器
Trigger trigger = TriggerBuilder.newTrigger()
	/*
 	 * usingJobData可以以k-v的形式给当前JobDetail添加参数
 	 * 可以通过链式调用usingJobData,传入多个参数
 	 * 在Job实现类中,可以通过JobExecutionContext.getJobDetail().getJobDataMap().get("name")获取参数的值
 	 */
 	 .usingJobData("name", "oxford")
 	 // 添加认证信息
 	 .withIdentity("name", "group")
 	 // 添加开始执行时间 - 立即执行startNow()
 	 // 开始执行时间
 	 .startAt(start)
 	 // 结束执行时间.如果不添加这个参数表示永久执行
 	 .endAt(end)
 	 // 添加执行规则.使用CronScheduleBuilder添加CronTrigger触发器
 	 withSchedule(CronScheduleBuilder
 	 	.cronSchedule("0 0/2 * * * ?"))
 	 // 触发器创建完成.执行生效
 	 .build();

启动任务

// 根据任务明细和任务触发器添加任务
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isShutdown()) {
	scheduler.start();
}

暂停任务

// 根据触发器中的withIdentity认证信息对任务进行暂停
scheduler.pauseTrigger(Trigger.triggerKey("name", "group"));

恢复任务

// 根据触发器中的withIdentity认证信息对任务进行恢复
scheduler.resumeTrigger(Trigger.triggerKey("name", "group"));

删除任务

// 先对任务进行暂停
scheduler.pauseTrigger(Trigger.triggerKey("name", "group"));
// 然后移除任务
scheduler.unscheduleJob(Trigger.triggerKey("name", "group"));
// 根据任务明细中的withIdentity认证信息对任务进行删除
scheduler.deleteJob(JobKey.jobKey("name", "group"));