JAVA面试题分享三百七十九:Quartz讲解

目录

基础实用示例

触发器Trigger

SimpleTrigger

CronTrigger

常见操作

挂起任务

关闭scheduler

监听器

job监听器

TriggerListener和SchedulerListener

一些注意事项

key值覆盖问题

job无状态问题

存在问题

Quartz横向扩展带来的问题

任务分片问题

横向对比其他方案


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

实用Quartz的目的就是为了让认读调度更加丰富、高效且安全,只需调用几个接口进行一些简单配置,即可快速实现一个任务调度程序。

图片

 

基础实用示例

首先自然是引入「Quartz」的依赖:

 <!--quartz-->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>

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

 public void run() {
        qs.addInternalSchedulerListener(this);

        try {
            OperableTrigger trigger = (OperableTrigger) jec.getTrigger();
            JobDetail jobDetail = jec.getJobDetail();

            do {

                JobExecutionException jobExEx = null;
                Job job = jec.getJobInstance();

               //略
                // execute the job
                try {
                    log.debug("Calling execute on job " + jobDetail.getKey());
                    job.execute(jec);
                    endTime = System.currentTimeMillis();
                } catch (JobExecutionException jee) {
                  //略
                } catch (Throwable e) {
                   //略
                }

                //略
                break;
            } while (true);

        } finally {
            qs.removeInternalSchedulerListener(this);
        }
    }

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

@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」分组中,执行计划为1s1次。

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

 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();
    }

对应的输出结果如下所示,任务不间断1s执行1次:

22:48:30.361 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
22:48:30.361 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
22:48:30.370 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.

22:48:30.416 [DefaultQuartzScheduler_Worker-1] INFO com.sharkchili.quartzExample.MyJob - 任务执行时间:2024-01-04 22:48:30
22:48:31.383 [DefaultQuartzScheduler_Worker-2] INFO com.sharkchili.quartzExample.MyJob - 任务执行时间:2024-01-04 22:48:31

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

图片

 

触发器Trigger

SimpleTrigger

默认情况下我们使用的都是「SimpleTrigger」,它支持设置任务触发起止时间,例如我们现在要求任务从现在开始执行,5s后直接停止,我们就可以通过startAt、endAt设置任务执行的时间区间,如此一来任务在执行5s后就不再触发了:

DateTime startTime = DateUtil.date();
        DateTime endTime = DateUtil.offsetSecond(startTime, 5);


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

CronTrigger

如果我们希望使用Cron表达式调度任务,那么就可以使用「CronTrigger」,使用示例如下,笔者这里采用1s执行1次的表达式,更多cron表达式建议到这个工具网站生成:在线Cron表达式生成器

public static void cronTriggerExample() throws SchedulerException {
        // 获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

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

        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("testTrigger", "testTriggerGroup")
                .withSchedule(CronScheduleBuilder.cronSchedule("* * * * * ?"))
                .build();

        // 使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

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

常见操作

挂起任务

将任务调度挂起,即将调度任务全部暂停。

scheduler.standby();

关闭scheduler

如果我们希望关闭「scheduler」,可以通过「shutdown」进行,注意:true表示等待所有正在执行的「job」执行完毕之后,再关闭「Scheduler」,而「false」则是强制关闭。

        scheduler.shutdown(false);

监听器

job监听器

对于上述三者Quartz也为我们提供了监听器,通过监听器我们可以非常方便的官渡到事件的发生和一些调度意外的逻辑响应。 如果我们希望对job进行监听则可以通过继承「JobListener」 实现一个监听器,该监听器为我们提供了4个方法:

  1. 「getName」:获取监听器名称。

  2. 「jobToBeExecuted」「JobDetail」被执行时调用该方法。

  3. 「jobExecutionVetoed」:JobDetail即将被执行,但又被「TriggerListerner」否决时会调用该方法。

  4. 「jobWasExecuted」「JobDetail」执行之后会调用该方法。

@Slf4j
public class MyJobListener implements JobListener {
    @Override
    public String getName() {
        String name = getClass().getSimpleName();
        log.info("监听器的名称是:" + name);
        return name;
    }

    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        log.info("Job的名称是:" + jobName + " Scheduler在JobDetail将要被执行时调用这个方法");
    }

    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        String jobName = context.getJobDetail().getKey().getName();
        log.info("Job的名称是:" + jobName + " Scheduler在JobDetail即将被执行,但又被TriggerListerner否决时会调用该方法");
    }

    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        String jobName = context.getJobDetail().getKey().getName();
        log.info("Job的名称是:" + jobName + " Scheduler在JobDetail被执行之后调用这个方法");
    }
}

对应的对于Job的监听器,我们可以按照下面这段示例进行添加:

 // 创建并注册一个全局的Job Listener
        scheduler.getListenerManager()
                .addJobListener(new MyJobListener(),
                        EverythingMatcher.allJobs());

输出结果:

23:50:05.009 [DefaultQuartzScheduler_Worker-2] INFO com.sharkchili.quartzExample.MyJobListener - 监听器的名称是:MyJobListener
23:50:05.010 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
23:50:05.010 [DefaultQuartzScheduler_Worker-2] INFO com.sharkchili.quartzExample.MyJobListener - Job的名称是:myJob Scheduler在JobDetail将要被执行时调用这个方法
23:50:05.010 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job myJobGroup.myJob
23:50:05.010 [DefaultQuartzScheduler_Worker-2] INFO com.sharkchili.quartzExample.MyJob - 任务执行时间:2024-01-04 23:50:05 ,JobDataMap:{"count":3} name:null
23:50:05.010 [DefaultQuartzScheduler_Worker-2] INFO com.sharkchili.quartzExample.MyJobListener - 监听器的名称是:MyJobListener
23:50:05.010 [DefaultQuartzScheduler_Worker-2] INFO com.sharkchili.quartzExample.MyJobListener - Job的名称是:myJob Scheduler在JobDetail被执行之后调用这个方法

TriggerListener和SchedulerListener

对应的「TriggerListener」「SchedulerListener」同理,这里就不多做解释了。

触发器监听器「MyTriggerListener」 示例代码如下,读者可参考日志的注释了解其每个监听回调的执行时间点:

@Slf4j
public class MyTriggerListener implements TriggerListener {
    private String name;

    public MyTriggerListener(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        String triggerName = trigger.getKey().getName();
        log.info(" 触发器:{} 被触发", triggerName);
    }

    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        String triggerName = trigger.getKey().getName();
        log.info(" {} 返回false,表示触发器并没有否决,该任务将被触发", triggerName);
        return false; // true:表示不会执行Job的方法
    }

    @Override
    public void triggerMisfired(Trigger trigger) {
        String triggerName = trigger.getKey().getName();
        log.info(" {}:错过触发", triggerName);
    }

    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext jobExecutionContext, Trigger.CompletedExecutionInstruction completedExecutionInstruction) {
        String triggerName = trigger.getKey().getName();
        log.info("{}: 完成之后触发", triggerName);
    }

}

对应的这里也给出监听器的设置示例:

 // 创建并注册一个全局的Trigger Listener
        scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), EverythingMatcher.allTriggers());

「SchedulerListener」 示例如下,读者可以参照注释调试理解:

@Slf4j
public class MySchedulerListener implements SchedulerListener {
    @Override
    public void jobScheduled(Trigger trigger) {
        String jobName = trigger.getJobKey().getName();
       log.info(jobName + " 完成部署");
    }

    @Override
    public void jobUnscheduled(TriggerKey triggerKey) {
       log.info(triggerKey + " 完成卸载");
    }

    @Override
    public void triggerFinalized(Trigger trigger) {
       log.info("触发器被移除 " + trigger.getJobKey().getName());
    }

    @Override
    public void triggerPaused(TriggerKey triggerKey) {
       log.info(triggerKey + " 正在被暂停");
    }

    @Override
    public void triggersPaused(String triggerGroup) {
       log.info("触发器组 " + triggerGroup + " 正在被暂停");
    }

    @Override
    public void triggerResumed(TriggerKey triggerKey) {
       log.info(triggerKey + " 正在从暂停中恢复");
    }

    @Override
    public void triggersResumed(String triggerGroup) {
       log.info("触发器组 " + triggerGroup + " 正在从暂停中恢复");
    }

    @Override
    public void jobAdded(JobDetail jobDetail) {
       log.info(jobDetail.getKey() + " 添加工作任务");
    }

    @Override
    public void jobDeleted(JobKey jobKey) {
       log.info(jobKey + " 删除工作任务");
    }

    @Override
    public void jobPaused(JobKey jobKey) {
       log.info(jobKey + " 工作任务正在被暂停");
    }

    @Override
    public void jobsPaused(String jobGroup) {
       log.info("工作任务组 " + jobGroup + " 正在被暂停");
    }

    @Override
    public void jobResumed(JobKey jobKey) {
       log.info(jobKey + " 正在从暂停中恢复");
    }

    @Override
    public void jobsResumed(String jobGroup) {
       log.info("工作任务组 " + jobGroup + " 正在从暂停中恢复");
    }

    @Override
    public void schedulerError(String msg, SchedulerException cause) {
       log.info("产生严重错误时调用:   " + msg + "  " + cause.getUnderlyingException());
    }

    @Override
    public void schedulerInStandbyMode() {
       log.info("调度器在挂起模式下调用");
    }

    @Override
    public void schedulerStarted() {
       log.info("调度器 开启时调用");
    }

    @Override
    public void schedulerStarting() {
       log.info("调度器 正在开启时调用");
    }

    @Override
    public void schedulerShutdown() {
       log.info("调度器 已经被关闭 时调用");
    }

    @Override
    public void schedulerShuttingdown() {
       log.info("调度器 正在被关闭 时调用");
    }

    @Override
    public void schedulingDataCleared() {
       log.info("调度器的数据被清除时调用");
    }
}

对应的监听配置:

  // 创建并注册一个全局的Trigger Listener
        scheduler.getListenerManager().addTriggerListener(new MyTriggerListener("simpleTrigger"), EverythingMatcher.allTriggers());

        // 创建SchedulerListener
        scheduler.getListenerManager().addSchedulerListener(new MySchedulerListener());

一些注意事项

通过对上述基础示例的调试我们已经对「Quartz」的使用有了基本的了解。接下来我们就来了解一下「Quartz」使用的注意事项。

key值覆盖问题

当我们需要对当前任务传入一些定制化参数时,我们可以通过「usingJobData」方法将参数以键值对的形式存到这个任务的「jobDataMap」中:

public JobBuilder usingJobData(String dataKey, String value) {
        jobDataMap.put(dataKey, value);
        return this;
    }

然后框架会将这个键值对通过反射的方式,设置到对应的成员变量中,就像下面这段代码,我们设置了「name」这个键值对,也就意味着「Quartz」启动后,对应「name」的键值对就会存到「MyJob」「name」成员变量中:

@Slf4j
public class MyJob implements Job {

    private String name;

    public void setName(String name) {
        this.name = name;
    }


    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        log.info("任务执行时间:{} ,JobDataMap:{} name:{}", dateTime, JSONUtil.toJsonStr(jobExecutionContext.getJobDetail().getJobDataMap()), name);
    }
}

但需要注意的是如果「job」和触发器「trigger」存在相同key时 「(如下所示的name)」 ,执行时就会以后设置的为主。

 /**
     * job和trigger设置同一个key的情况下,反射后的值会以后者为主
     */
    @SneakyThrows
    public static void errorExamlme() {
        // 获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

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

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

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

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

所以本次的输出「name」值为「trigger」设置的「name」值:

23:06:20.048 [DefaultQuartzScheduler_Worker-1] INFO com.sharkchili.quartzExample.MyJob - 任务执行时间:2024-01-04 23:06:20 ,JobDataMap:{"name":"JobDetail"} name:trigger

job无状态问题

有时候我们希望记录「job」的执行状态,例如没执行一次,「job」「count」自增一下:

public class MyJob implements Job {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    private Integer count;


    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        ++count;
        jobExecutionContext.getJobDetail().getJobDataMap().put("count", count);
        log.info("任务执行时间:{} ,JobDataMap:{} name:{}", dateTime, JSONUtil.toJsonStr(jobExecutionContext.getJobDetail().getJobDataMap()), name);
    }
}

但是这个任务执行之后却发现每次输出的「count」结果都是2,原因就是因为默认情况下「job」是无状态的,这也就意味每次执行的「job」都是一个全新的「job」实例。

 @SneakyThrows
    public static void errorExamlme() {
        // 获取任务调度的实例
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

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

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

        // 使用触发器调度任务的执行
        scheduler.scheduleJob(job, trigger);

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

要想解决这个问题,我们只需在job上加一个 「@PersistJobDataAfterExecution」 注解:

@Slf4j
@PersistJobDataAfterExecution
public class MyJob implements Job {

    private String name;

    public void setName(String name) {
        this.name = name;
    }

    private Integer count;


    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        ++count;
        jobExecutionContext.getJobDetail().getJobDataMap().put("count", count);
        log.info("任务执行时间:{} ,JobDataMap:{} name:{}", dateTime, JSONUtil.toJsonStr(jobExecutionContext.getJobDetail().getJobDataMap()), name);
    }
}

如此一来执行结果便是有效递增的:

23:22:42.952 [DefaultQuartzScheduler_Worker-1] INFO com.sharkchili.quartzExample.MyJob - 任务执行时间:2024-01-04 23:22:42 ,JobDataMap:{"name":"JobDetail","count":2} name:trigger

23:22:47.876 [DefaultQuartzScheduler_Worker-2] INFO com.sharkchili.quartzExample.MyJob - 任务执行时间:2024-01-04 23:22:47 ,JobDataMap:{"name":"JobDetail","count":3} name:trigger

存在问题

Quartz横向扩展带来的问题

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

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

图片

 

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

图片

 

任务分片问题

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

图片

 

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

横向对比其他方案

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

之乎者也·

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值