Quartz从入门到精通(最详细基础-进阶-实战)

Quartz基础

Quartz概述

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由java语言开发,支持分布式、集群部署,且具有丰富的调度方式。

Quartz核心元素

核心元素介绍

Job:
Job是Quartz中的一个函数式接口,其中的execute方法就是我们需要具体实现的业务任务逻辑。

JobDetail:
JobDetail是用于绑定Job,并对Job进行描述,其中提供了很多描述性属性如:
name 任务名称
group 任务组
description 任务描述
jobClass 任务类
jobDataMap 任务自定义参数

Trigger:
Trigger是触发器,用于定义Job的执行时间、执行间隔、执行频率等。
在Quartz中主要有四种类型的Trigger:SimpleTrigger、CronTrigger、DataIntervalTrigger和NthIncludedTrigger。

Scheduler:
Scheduler是调度器,用于实际协调和组织JobDetail与Trigger。
Quartz提供了DirectSchedulerFactory和StdSchedulerFactory等工厂类,用于支持Scheduler相关对象的产生

核心元素关系

Scheduler可看成是一个定时任务调度容器,里面可注入多组任务(JobDetail与Trigger),而每个JobDetail又绑定了一个Job实例。一个JobDetail可对应多个Trigger,一个Trigger只能对应一个JobDetail。
在这里插入图片描述

Quartz线程模型

Quartz中主要存在两类线程:即执行线程和调度线程。执行线程通常由一个线程池维护,主要作用是执行Trigger中即将开始的任务。调度线程又分为Regular Scheduler Thread(执行常规调度)和Misfire Scheduler Thread(执行错失的任务)。其中Regular Thread 轮询Trigger,如果有将要触发的Trigger,则从执行任务线程池中获取一个空闲线程,然后执行与该Trigger关联的job;Misfire Thraed则是扫描所有的trigger,查看是否有错失的,如果有的话,根据一定的策略进行处理。
ClusterManager线程:Quartz集群部署时,则还存在集群线程(ClusterManager线程),主要作用是定时检测集群中各节点健康状态。若发现宕机节点,则将其任务交由其他健康节点继续执行。
在这里插入图片描述

Quartz核心配置文件

Quartz默认加载工程目录下的quartz.properties,如果工程目录下没有,就会去加载quartz.jar包下面的quartz.properties文件,也可自定义配置位置。
配置属性大体可分为:
调度器属性
线程池属性
作业存储设置
插件配置
以整合springboot为例,贴出核心配置:

# 定时任务的表前缀
org.quartz.jobStore.tablePrefix=qrtz_
# 是否是集群的任务
org.quartz.jobStore.isClustered=true
# 检查集群的状态间隔
org.quartz.jobStore.clusterCheckinInterval=5000
# 如果当前的执行周期被错过 任务持有的时长超过此时长则认为任务过期,单位ms
org.quartz.jobStore.misfireThreshold=6000
# 事务的隔离级别 推荐使用默认级别 设置为true容易造成死锁和不可重复读的一些事务问题
org.quartz.jobStore.txIsolationLevelSerializable=false
# 任务存储方式它应当是org.quartz.spi.JobStore的子类
org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
# 保证待执行的任务是锁定的 避免集群任务被其他现场抢断
org.quartz.jobStore.acquireTriggersWithinLock=true
# 数据库系统的方言StdJDBCDelegate标准JDBC方言
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 定时任务实例的id 默认自动
org.quartz.scheduler.instanceId=AUTO
# 定时任务的线程名,相同集群实例名称必须相同
org.quartz.scheduler.instanceName=ClusterJMVCScheduler
# 定时任务线程池
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# 线程池的线程总数 默认10
org.quartz.threadPool.threadCount=10
# 线程池的优先级
org.quartz.threadPool.threadPriority=5
# 自创建父线程
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true

Misfire过失策略

针对CronTrigger和SimpleTrigger过失策略分别如下:

CronTrigger

withMisfireHandlingInstructionDoNothing
不触发立即执行
等待下次Cron触发频率到达时刻开始按照Cron频率依次执行
withMisfireHandlingInstructionIgnoreMisfires
以错过的第一个频率时间立刻开始执行
重做错过的所有频率周期后
当下一次触发频率发生时间大于当前时间后,再按照正常的Cron频率依次执行
withMisfireHandlingInstructionFireAndProceed
以当前时间为触发频率立刻触发一次执行
然后按照Cron频率依次执行

SimpleTrigger

withMisfireHandlingInstructionFireNow
以当前时间为触发频率立即触发执行
执行至FinalTIme的剩余周期次数
以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
调整后的FinalTime会略大于根据starttime计算的到的FinalTime值
withMisfireHandlingInstructionIgnoreMisfires
以错过的第一个频率时间立刻开始执行
重做错过的所有频率周期
当下一次触发频率发生时间大于当前时间以后,按照Interval的依次执行剩下的频率
共执行RepeatCount+1次
withMisfireHandlingInstructionNextWithExistingCount
不触发立即执行
等待下次触发频率周期时刻,执行至FinalTime的剩余周期次数
以startTime为基准计算周期频率,并得到FinalTime
即使中间出现pause,resume以后保持FinalTime时间不变
withMisfireHandlingInstructionNowWithExistingCount
以当前时间为触发频率立即触发执行
执行至FinalTIme的剩余周期次数
以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
调整后的FinalTime会略大于根据starttime计算的到的FinalTime值
withMisfireHandlingInstructionNextWithRemainingCount
不触发立即执行
等待下次触发频率周期时刻,执行至FinalTime的剩余周期次数
以startTime为基准计算周期频率,并得到FinalTime
即使中间出现pause,resume以后保持FinalTime时间不变
withMisfireHandlingInstructionNowWithRemainingCount
以当前时间为触发频率立即触发执行
执行至FinalTIme的剩余周期次数
以调度或恢复调度的时刻为基准的周期频率,FinalTime根据剩余次数和当前时间计算得到
调整后的FinalTime会略大于根据starttime计算的到的FinalTime值

核心策略枚举说明

MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
这个不是忽略已经错失的触发的意思,而是说忽略MisFire策略。它会在资源合适的时候,重新触发所有的MisFire任务,并且不会影响现有的调度时间。
比如,SimpleTrigger每15秒执行一次,而中间有5分钟时间它都MisFire了,一共错失了20个,5分钟后,假设资源充足了,并且任务允许并发,它会被一次性触发。这个属性是所有Trigger都适用。
MISFIRE_INSTRUCTION_FIRE_NOW
忽略已经MisFire的任务,并且立即执行调度。这通常只适用于只执行一次的任务。
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
将startTime设置当前时间,立即重新调度任务,包括MisFire的。
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
类似MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT,区别在于会忽略已经MisFire的任务。
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
在下一次调度时间点,重新开始调度任务,包括MisFire的。
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
类似于MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT,区别在于会忽略已经MisFire的任务。

默认策略

CronTrigger和SimpleTrigger默认采用MISFIRE_INSTRUCTION_SMART_POLICY
大致意思是“把处理逻辑交给聪明的Quartz去决定”。基本策略是,如果是只执行一次的调度,使用MISFIRE_INSTRUCTION_FIRE_NOW
如果是无限次的调度(repeatCount是无限的),使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
否则,使用MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

Quartz进阶

Quartz启动流程

以Quartz和spring整合为例,当spring容器启动时,就会装载相关的bean。SchedulerFactoryBean实现了InitializingBean接口,因此在初始化bean的时候,会执行afterPropertiesSet方法,该方法将会调用SchedulerFactory(DirectSchedulerFactory 或者 StdSchedulerFactory,通常用StdSchedulerFactory)创建Scheduler。SchedulerFactory在创建quartzScheduler的过程中,将会读取配置参数,初始化各个组件,关键组件如下:

ThreadPool:一般是使用SimpleThreadPool,SimpleThreadPool创建了一定数量的WorkerThread实例来使得Job能够在线程中进行处理。WorkerThread是定义在SimpleThreadPool类中的内部类,它实质上就是一个线程。在SimpleThreadPool中有三个list:workers-存放池中所有的线程引用,availWorkers-存放所有空闲的线程,busyWorkers-存放所有工作中的线程;
线程池的配置参数如下所示:

org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=3
org.quartz.threadPool.threadPriority=5

JobStore:分为存储在内存的RAMJobStore和存储在数据库的JobStoreSupport(包括JobStoreTX和JobStoreCMT两种实现,JobStoreCMT是依赖于容器来进行事务的管理,而JobStoreTX是自己管理事务),若要使用集群要使用JobStoreSupport的方式;

QuartzSchedulerThread:用来进行任务调度的线程,在初始化的时候paused=true,halted=false,虽然线程开始运行了,但是paused=true,线程会一直等待,直到start方法将paused置为false;

另外,SchedulerFactoryBean还实现了SmartLifeCycle接口,因此初始化完成后,会执行start()方法,该方法将主要会执行以下的几个动作:

创建ClusterManager线程并启动线程:该线程用来进行集群故障检测和处理,将在下文详细讨论;
创建MisfireHandler线程并启动线程:该线程用来进行misfire任务的处理,只有当QuartzSchedulerThread的paused=false,调度线程才真正开始调度;

Quartz持久化

Quartz持久化即将trigger和job基于jdbc存入数据库。Quartz中有两种存储方式:RAMJobStore,JobStoreSupport,其中RAMJobStore是将trigger和job存储在内存中,而JobStoreSupport是基于jdbc将trigger和job存储到数据库中。RAMJobStore的存取速度非常快,但是由于其在系统被停止后所有的数据都会丢失,所以在集群应用中,必须使用JobStoreSupport。
集成时,执行去Quartz官网下载对应数据库sql文件导入并开启Quartz持久化配置即可:
在这里插入图片描述
在这里插入图片描述

Quartz集群

Quartz集群是基于数据库实现,主要利用了数据库的悲观锁机制。一个Quartz集群中的每个节点是一个独立的Quartz应用,它又管理着其他的节点。这就意味着你必须对每个节点分别启动或停止。Quartz集群中,独立的Quartz节点并不与另一其的节点或是管理节点通信,而是通过相同的数据库表来感知到另一Quartz应用的。
在大型分布式系统中,为了避免Quartz集群表和业务表之间互相影响,导致数据库性能和Quartz集群、业务系统稳定性,建议是Quartz独立出数据库或独立出定时任务系统。
在这里插入图片描述

Quartz坑集盘点

  1. Job无法注入spring容器其他bean
    Quartz中每次执行任务时,会由JobFactory重新创建一个新Job实例,此实例默认采用反射newInstance创建且并未交给spring管理,所以在实例化时也无法注入其他spring bean。
    可通过自定JobFactory方式解决,当然在与springboot整合时,QuartzAutoConfiguration自动配置类已经帮我们处理了。

  2. 集群环境下时间同步问题
    Quartz实际并不关心你是在相同还是不同的机器上运行节点。当集群放置在不同的机器上时,称之为水平集群。节点跑在同一台机器上时,称之为垂直集群。对于垂直集群,存在着单点故障的问题。这对高可用性的应用来说是无法接受的,因为一旦机器崩溃了,所有的节点也就被终止了。对于水平集群,存在着时间同步问题。
    节点用时间戳来通知其他实例它自己的最后检入时间。假如节点的时钟被设置为将来的时间,那么运行中的Scheduler将再也意识不到那个结点已经宕掉了。另一方面,如果某个节点的时钟被设置为过去的时间,也许另一节点就会认定那个节点已宕掉并试图接过它的Job重运行。最简单的同步计算机时钟的方式是使用某一个Internet时间服务器(Internet Time Server ITS)。

  3. 节点争抢Job问题
    因为Quartz使用了一个随机的负载均衡算法,Job以随机的方式由不同的实例执行。Quartz官网上提到当前,还不存在一个方法来指派(钉住) 一个 Job 到集群中特定的节点。

  4. 从集群获取Job列表问题
    当前,如果不直接进到数据库查询的话,还没有一个简单的方式来得到集群中所有正在执行的Job列表。请求一个Scheduler实例,将只能得到在那个实例上正运行Job的列表。Quartz官网建议可以通过写一些访问数据库JDBC代码来从相应的表中获取全部的Job信息。

Quartz实战

实战目标

基于mysql数据库搭建Quartz集群,并整合springboot、mybatisplus实现一个轻量企业级定时任务框架。

  1. 要求项目启动,自动扫描加载定时job。
  2. 对job支持动态的增删改查功能。
  3. 对job的执行耗时和日志做记录。

具体实现代码

  1. 自定义JOB注解,并设置启动装载所有job
package com.zkc.quartzdemo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2021-04-15
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ScheduleAnn {

    /**
     * 定时任务的名称
     */
    String name() default "";

    /**
     * 定时任务的定时表达式
     */
    String cronExpression() default "";

    /**
     * 定时任务所属群组
     */
    String group() default "";

    /**
     * 当前定时任务的描述
     */
    String description() default "";

}
package com.zkc.quartzdemo.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2021-04-15
 */
@Configuration
public class QuartzConfig {

    @Bean(initMethod = "init")
    @ConditionalOnMissingBean
    public QuartzInit bootStarter() {
        return new QuartzInit();
    }

}

package com.zkc.quartzdemo.config;

import com.zkc.quartzdemo.annotation.ScheduleAnn;
import com.zkc.quartzdemo.dto.ScheduleJobVO;
import com.zkc.quartzdemo.service.QuartzService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.Job;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import java.util.List;
import java.util.regex.Pattern;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2021-04-15
 */
@Slf4j
public class QuartzInit {

    @Value("${spring.quartz.package-scan:com.zkc}")
    private String packageScan;
    private final static Pattern expressionPattern = Pattern.compile("\\$\\{(.*)}");
    @Autowired
    private QuartzService quartzService;

    public void init() {
        List<String> allJobClassNames = quartzService.getAllScheduleJobClassNames();
        new Reflections(packageScan).getTypesAnnotatedWith(ScheduleAnn.class).forEach(jobClass -> {
            String jobClassName = jobClass.getName();
            if (allJobClassNames.contains(jobClassName)) return;
            // 如果当前jobClass不是org.quartz.Job的子类则不做插入任务操作
            if (!Job.class.isAssignableFrom(jobClass)) {
                log.error("类[{}]未继承[org.quartz.Job]无法初始化为定时任务。", jobClassName);
                return;
            }
            ScheduleAnn annotate = jobClass.getAnnotation(ScheduleAnn.class);
            String name = annotate.name();
            String cronExpression = annotate.cronExpression();
            String group = annotate.group();
            String description = annotate.description();
            ScheduleJobVO scheduleJobVO = new ScheduleJobVO()
                    .setClassName(jobClassName)
                    .setName(name)
                    .setCron(cronExpression)
                    .setGroup(group)
                    .setDescription(description);
            // 只创建数据库中不存在的定时任务 存在的则不做创建更新
            boolean existsFlag = quartzService.scheduleExists(name, group);
            if (!existsFlag) {
                quartzService.addJob(scheduleJobVO);
                quartzService.addInitJob(jobClassName);
            }
        });
    }

}

  1. 定义调度servcie 与 controller
package com.zkc.quartzdemo.service;

import com.zkc.quartzdemo.dto.*;

import java.util.List;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2021-04-15
 */
public interface QuartzService {

    List<String> getAllScheduleJobClassNames();

    boolean scheduleExists(String name, String group);

    void addInitJob(String jobClassName);

    boolean addJob(ScheduleJobVO scheduleJobVO);

    boolean addDynamicScheduleJob(ScheduleAddCO scheduleAddCO);

    List<String> getAllJobGroups();

    List<QuartzJobDetail> getJobDetails();

    boolean updateScheduleJob(ScheduleUpdateCO scheduleUpdateCO);

    boolean deleteDynamicScheduleById(String id);

    List<ScheduleJobVO> getScheduleJobs(ScheduleQryCO scheduleQryCo);

    boolean changeScheduleJobStatus(ScheduleStatusCO statusCO);

    List<JobClass> getSystemAllJobs();

}

package com.zkc.quartzdemo.controller;

import com.zkc.quartzdemo.common.MultiResponse;
import com.zkc.quartzdemo.common.SingleResponse;
import com.zkc.quartzdemo.dto.*;
import com.zkc.quartzdemo.service.QuartzService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;

/**
 * @author kczhang@wisedu.com
 * @version 1.0.0
 * @since 2021-04-15
 */
@RestController
@RequiredArgsConstructor
@RequestMapping("/admin/api/schedule_job")
@Validated
public class ScheduleController {

    private final QuartzService quartzService;

    /**
     * 添加一个新的定时任务
     *
     * @param scheduleAddCO 任务详情
     * @return 任务添加成功与否
     */
    @PostMapping(value = "/create")
    public SingleResponse<Boolean> addNewSchedule(@Valid @RequestBody ScheduleAddCO scheduleAddCO) {
        return SingleResponse.of(quartzService.addDynamicScheduleJob(scheduleAddCO));
    }

    /**
     * 获取任务的组集合
     *
     * @return 组s
     */
    @GetMapping(value = "/groups")
    public MultiResponse<String> findAllScheduleJobGroups() {
        return MultiResponse.ofWithoutTotal(quartzService.getAllJobGroups());
    }

    /**
     * 更新定时任务
     *
     * @param scheduleUpdateC0 任务详情
     * @return 任务添加成功与否
     */
    @PostMapping(value = "/update")
    public SingleResponse<Boolean> updateScheduleJob(@Valid @RequestBody ScheduleUpdateCO scheduleUpdateC0) {
        return SingleResponse.of(quartzService.updateScheduleJob(scheduleUpdateC0));
    }

    /**
     * 删除定时任务
     *
     * @param id 任务的id
     */
    @GetMapping(value = "/remove")
    public SingleResponse<Boolean> deleteScheduleJob(@NotEmpty String id) {
        return SingleResponse.of(quartzService.deleteDynamicScheduleById(id));
    }

    /**
     * 获取定时任务列表
     *
     * @param scheduleQryCO 搜索条件
     * @return 定时任务列表
     */
    @GetMapping(value = "/list")
    public MultiResponse<ScheduleJobVO> getScheduleJobs(ScheduleQryCO scheduleQryCO) {
        return MultiResponse.ofWithoutTotal(quartzService.getScheduleJobs(scheduleQryCO));
    }

    /**
     * 更新定时任务状态
     *
     * @param statusCO 任务状态
     * @return 定时任务状态更新成功与否
     */
    @PostMapping(value = "/updateScheduleJobStatus")
    public SingleResponse<Boolean> changeScheduleJobStatus(@Valid @RequestBody ScheduleStatusCO statusCO) {
        return SingleResponse.of(quartzService.changeScheduleJobStatus(statusCO));
    }

    /**
     * 任务集合
     *
     * @return 继承Job的SpringBean集合信息
     */
    @GetMapping(value = "/jobs")
    public MultiResponse<JobClass> getSystemAllJobs() {
        return MultiResponse.ofWithoutTotal(quartzService.getSystemAllJobs());
    }

}

  1. 除了quartz官方持久化表,新增一个job初始化表
DROP TABLE IF EXISTS `qrtz_auto_initialized`;
CREATE TABLE `qrtz_auto_initialized` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `job_class_name` varchar(500) DEFAULT NULL,
  `init_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

完整代码已分享到码云

https://gitee.com/zhang_kaicheng/quartz-demo

参考文献

  1. https://www.cnblogs.com/Dorae/p/9357180.html
  2. https://blog.csdn.net/yangshangwei/article/details/78539433#withmisfirehandlinginstructiondonothing
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值