在企业应用中,经常会遇到定时任务场景,比如每天定时3点发送一份统计邮件,订票平台10分钟后自动取消未支付的订单,会员权益的定时取消等等。如果你的项目为多线程分布式架构,或者需要对定时任务进行动态管理,例如任务的启动、暂停、恢复、停止和触发时间修改,那么Quartz非常适合你。本文将介绍Quartz的内部工作原理和实例应用,帮助开发者更好的理解和运用这一强大的框架。
Quartz是Java定时领域的一套轻量级任务调度框架,由OpenSymphony(一个开源组织)开发,并对其进行了较为解耦的设计。目前公司使用较多的定时任务框架为XXL-Job,Elastic-Job,也都是基于Quartz进行二次开发。Elastic-Job诞生于2015年,当时业界虽然有Quartz等优秀的定时任务框架,但缺乏分布式方面的应用。Elastic-Job的出现,通过将同一任务分配在不同节点运行,实现分布式调度,有效的填充了作业在分布式调度领域的短板,并且增加了大量的任务监控和管理功能。而XXL-Job可以说是Quartz的一个增强版,其弥补了Quartz不支持并行调度、不支持任务失败处理和任务分片等不足,具有作业管理界面,上手也比较容易。
Quartz作为Java定时任务框架的鼻祖,具有丰富的调度模式和强大的可扩展性,而且很容易集成到Spring中去,用来执行业务任务是一个很好的选择。因此,了解Quartz的内部结构,是研发人员开发定时任务的关键。以下是Quartz体系的结构图:
Quartz主要由三大部分组成:分别是任务(JobDetail)、触发器(Trigger)以及调度器(Scheduler)。
在存储器中,有两种任务数据存储方式,RAMJobStore支持将任务数据存储在内存中,存取速度非常快,但当服务器中断再重启时,无法恢复任务继续执行,而JobStoreSupport是将任务存储于数据库中,当服务器中断了再次重启时,任务还是会接着上次继续执行,能够达到数据持久化的效果。
在我们实际的项目中,当定时任务过多的时候,肯定不能人工去操作,这时候就需要一个任务调度框架,帮我们自动去执行这些程序。那么该如何实现这个功能呢?
Quartz框架的使用方法,也是依据三大组件展开,分别是对任务方法、触发器和调度器的创建。 首先,编写定时任务逻辑,创建一个定时任务类QuartzDemo,打印出当前执行时间:
public class QuartzDemo implements Job{
// 任务触发时所执行的方法
public void execute (JobExecutionContext context) throws JobExecutionException {
//输出当前时间
System.out.println("任务调度" + new Date());
}
}
然后,创建定时任务启动类代码。对三大组件进行封装,设置相关属性,这里Job和Trigger使用用建造者模式封装,通过Scheduler对两者进行整合。代码如下:
public class QuartzShceduleDemo {
public static void main(String[] args) throws Exception{
//1.Job
JobDetail job = JobBuilder.newJob(QuartzDemo.class)
.withIdentity("job1","group1")
.build();
//2.Trigger触发方式,第一种:通过Quartz提供方法完成简单的重复调用
//Trigger trigger = TriggerBuilder.newTrigger().withSchedule(SimpleScheduleBuilder.repeatSecondlyForever()).build();
//第二种:按照cron的表达式来给定触发时间
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/2 * * * * ?"))
.build();
//3.scheduler,将上面的job和trigger进行组装
Scheduler scheduler= StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(job,trigger);
//4.启动
scheduler.start();
}
}
在这里,我们给出了两种触发器的时间触发策略,一种是Simple Trigger实现简单的重复调用,另一种是基于Cron的表达式时间触发。CronTrigger通常比Simple Trigger使用频率更高,如果需要基于日历的概念而不是像Simple Trigger那样固定时间间隔和次数进行定时任务,则推荐使用CronTrigger,例如“每天早上9点”或者“每周五9点至10点每隔五分钟运行一次”。
常用的Cron表达式,例如:
表达式 | 说明 |
0/2 * * * * ? | 表示每2秒执行任务 |
0 0/2 * * * ? | 表示每2分钟执行任务 |
0 0 2 1 * ? | 表示在每月的1日的凌晨2点调整任务 |
0 0 12 ? * WED | 表示每个星期三中午12点 |
0 15 10 * * ? 2024 | 2024年的每天上午10:15触发 |
0 0 10,14,16 * * ? | 每天上午10点,下午2点,4点 |
0 0/5 14,18 * * ? | 每天下午的18点到18点59分,每隔5分触发 |
具体可以使用在线Cron表达式生成器生成自己想要的表达式。
最后,定时任务的运行结果为:
在实际应用中,当多线程并发访问时,遇到一个定时任务没执行结束,另一个定时任务到时间了却无法执行,极有可能产生并发问题,而Quartz框架中的Scheduler每次执行,都会根据JobDetail创建一个新实例,不必等待上一次任务执行完毕,只要间隔时间到就会执行,这样就避免了任务堵塞,并发访问的问题。除此之外,Quartz框架还支持以下场景:
相对于只能单线程运行的Spring-task来说,Quartz框架可支持多线程任务调度,任务之间互不干扰执行,且支持任务的启动、删除等多种调度管理方式。
如下图Quartz的分布式架构所示,在一个包含3个节点的任务调度模块中,各个节点相互独立,互不通信,数据库作为各节点上调度器的枢纽,来确定节点的任务是否执行。当监测到节点1停止运行时,任务会切换至正常运行的服务器节点上继续运行,这就实现了无论集群中有多少应用实例,定时任务只触发一次的效果,且任务A,B,C采用轮询机制执行,在集群之间能够形成负载均衡,提高了节点性能。
由于Quartz集群依赖于数据库,所以项目中要想实现集群模式,必须首先创建quartz数据库表,数据库表的建表语句可以在quartz下载包中找到并执行。作为实现集群模式的核心步骤,需要为每个实例创建相同的quartz.properites配置文件,我们给出了此集群配置文件的示例。其中,org.quartz.jobStore.class属性为JobStoreTX,表示任务持久化到数据库中,这是因为集群中各节点依赖于数据库来传播Scheduler 实例的状态。“org.quartz.jobStore.isClustered”的属性设置为“true”表示启用集群,最后在项目中,可通过创建一个配置类,将配置文件的相关属性值引入进来。
任务持久化:
如果创建了一个定时任务,这时候服务突然崩掉了,再启动时该做的事情没有做(比如某些数据的回退),就会留下脏数据在系统里面。因此在创建定时任务的时候可以对任务进行持久化存储,遇到服务突然崩掉时,能够在系统启动的时候,把之前没删除的定时任务再装载进来继续执行。
而Quartz持久化的实现,通常涉及任务存储的文件配置和数据库表的创建,即在配置文件中添加:“spring.quartz.job-store-type=jdbc”一行,指定使用JDBC JobStore将任务存储在数据库(如MySQL)中,另外在代码中定义任务JobDetail时,则需要将其持久化属性设置为true。
总的来说,不同的的定时任务存在于互联网应用中各个角落,Quartz作为Spring默认的任务调度框架,能够统一将这些作业管理起来,为分布式项目提供强大的任务调度功能。对于希望优化资源分配和扩展应用功能的Java开发者而言,掌握Quartz的使用也将会极大的提升开发效率。