基本概述

Quartz是一款轻量级且特性丰富的任务调度库,它是基于Java实现的调度框架,本文会针对日常任务调度的使用场景来演示Quartz的使用姿势。

源码角度分析

都说Quartz是任务调度框架,从源码就可以看出其本质也就是工作线程轮询并执行继续的调度任务。

Quartz将任务定义为Job,Job是工作任务调度的接口,该接口定义了execute方法,所以当我们需要提交任务给Quartz时,就需要继承Job接口并在execute方法里告知要执行的任务:

1
2
3
4
5
6
7
8
9
@Slf4j
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) {
String dateTime = LocalDateTime.now()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info("任务执行时间:{}", dateTime);
}
}

有了Job,就需要安排调度计划,在Quartz这个框架中,Trigger就是告知调度器如何进行任务触发的触发器,使用代码如下所示:

  1. 基于JobBuilder创建job,并声称job的名称。
  2. 定义触发器,该触发器立即启动并设置名称为testTrigger,触发器属于testTriggerGroup分组中,执行计划为1秒1次。

最后就是声明scheduler将触发器和任务关联,通过schedulerscheduleJob方法关联,就会形成一个以Job为工作内容,并按照触发器的安排进行任务的调度的任务定时被调度器执行。 注意该方法还会返回第一次执行的时间,一旦调用start,当前方法调度工作就正式开始了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void main(String[] args) throws Exception {
// 获取任务调度的实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

// 定义任务调度实例, 并与TestJob绑定
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "myJobGroup")
.build();

// 定义触发器, 会马上执行一次, 接着1秒执行一次
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("testTrigger", "testTriggerGroup")
.startNow()
.withSchedule(SimpleScheduleBuilder.repeatSecondlyForever(1))
.build();

// 使用触发器调度任务的执行 获取任务调度时间
Date date =scheduler.scheduleJob(job, trigger);


// 开启任务
scheduler.start();
}

可以看出Quartz的工作核心就是,通过Job来指定任务的详情,结合触发器Trigger指定任务的执行时间和间隔还有次数等信息,再让调度器scheduler定期去执行前两者关联而生成的定时任务。

存在问题

Quartz横向扩展带来的问题

虽说Quartz支持集群模式实现横向扩展,也就是我们常说的分布式调度,但需要业务方面通过一些手段实现节点任务执行的互斥和安全,从而避免任务重复执行等一些问题,常见的解决方案分别由数据库锁和分布式锁两种:

在调度进行任务争抢时先对数据库表上锁,只有拿到锁的节点才可以进行获取任务并调度,这种是常规情况下的解决方案,但这种实现方式有着很强的侵入性,且在高并发的场景性能表现也不是很出色,所以大部分情况下,我们不是很推荐通过数据表的形式实现分布式任务调度一致性。

通常情况下,采用redis分布式锁是针对Quartz框架分布式任务调度的较好解决方案,通过在内存中进行任务争抢,大大提分布式调度性能,但还是存在调度空跑问题,即先抢到锁的节点获取仅有的任务,而其他节点随后得锁后却没有执行任务,造成一次空跑。

任务分片问题

试想一个场景,原本一个节点负责调度全国系统的所有任务,随着业务激增我们将Quartz设置为集群模式,希望各个节点负责执行不同省份的任务。其他调度框架例如XXL-JOB,可以通过配置中心决定这个调度规则例如工具任务的编号知晓省份通过hash取模分配给不同的省份。

在这里插入图片描述

Quartz因为没有对应的页面和配置中心,所以实现任务分片需要通过硬编码的形式来实现,有着很强的代码侵入以及实现的复杂性。

横向对比其他方案

所以对于简单且较为轻量的任务调度场景,我们可优先考虑Quartz,若希望在集群环境下实现分布式调度以及任务分片等复杂的需求时,可参照下面酌情考虑这些更高效中心化的任务调度中心xxl-job或者elastic-job

对比项 Quartz elastic-job xxl-job
集群、弹性扩容 多节点,部署,通过竞争数据库锁来保证只有一个节点执行任务 通过zookeeper的注册与发现,可以动态的添加服务器。支持水平扩容 使用Quartz基于数据库的分布式功能,服务器超出一定数量会给数据库造成一定的压力
任务分片
管理界面
高级功能 弹性扩容,多种作业模式,失效转移,运行状态收集,多线程处理数据,幂等性,容错处理,spring命名空间支持 弹性扩容,分片广播,故障转移,Rolling实时日志,GLUE(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化
缺点 没有管理界面,以及不支持任务分片等。不适用于分布式场景 需要引入zookeeper,mesos,增加系统复杂度,学习成本较高 调度中心通过获取DB锁来保证集群中执行任务的唯一性,如果短任务很多,随着调度中心集群数量增加,那么数据库的锁竞争会比较厉害,性能不好。