Quartz任务调度框架初探

什么是Quartz?

Quartz 是一个完全由 Java 编写的开源作业调度框架,为在 Java 应用程序中进行作业调度提供了简单却强大的机制。

Quartz 可以与 J2EE 与 J2SE 应用程序相结合也可以单独使用。

Quartz 允许程序开发人员根据时间的间隔来调度作业。

Quartz 实现了作业和触发器的多对多的关系,还能把多个作业与不同的触发器关联。

Quartz特点
  1. 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  2. 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  3. 分布式和集群能力,提供任务持久化以及分布部署任务调度;
定时任务调度框架对比
定时任务框架Cron表达式固定间隔执行固定频率执行任务持久化
JDK TimerTask不支持支持支持不支持
Spring Schedule支持支持支持不支持
Quartz支持支持支持支持

关于任务持久化,当应用程序停止运行时,所有调度信息不被丢失,当你重新启动时,调度信息还存在,这就是持久化任务。个人理解是对任务执行日志等相关信息进行持久化存储。任务持久化不一定需要通过框架进行操作,只是Quartz集成度较高。

Quartz分布式集群配置(了解)
Quartz使用持久的JobStore才能完成Quartz集群配置,关于JobStore是基于JDBC的,需要对任务调度Scheduler信息进行持久化。
由此Quartz自带的11张表就是做此事的。

关于这11张表:实例化采用数据库存储,基于数据库引擎及 High-Available 的策略自动协调每个节点。

表下载地址:http://www.quartz-scheduler.org/downloads/
解压缩之后:quartz-2.2.3\docs\dbTables\ 根据数据库类型选择SQL脚本文件即可

需求简述
  1. 管理数据表中的定时任务记录(CURD)
  2. 执行、停止定时任务

在这里我们首先考虑的是需求2:定时任务的执行和停止

依赖导入

数据库驱动:业务数据库为SqlServer

<dependency>
        <groupId>com.microsoft.sqlserver</groupId>
        <artifactId>sqljdbc4</artifactId>
        <version>4.0</version>
        <!--<scope>test</scope>-->
    </dependency>

Quartz:定时任务框架依赖

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.3.0</version>
</dependency>

context-support:注意,该依赖必须导入,Spring提供对schedule的支持

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>
开始前需要知道的几点(以下内容来源网络)
  1. Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

  2. JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

  3. Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;

  4. Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

核心成员组织图:
在这里插入图片描述
JobDetail是任务的定义,而Job是任务的执行逻辑。在JobDetail里会引用一个Job Class定义。

关于SpringBoot与Quartz整合

目前项目中采用的是SpringBoot 1.0 版本,并未集成Quartz。在SpringBoot 2.0版本中,Quartz已经被集成进来了。

由于底层框架采用SpringBoot,因此XML的配置方式被舍弃,可以通过配置类的方式进行配置。

QuartzConfigration.java:通过SchedulerFactoryBean,生成Schedule。

@Configuration  
public class QuartzConfigration {  

    @Autowired  
    private JobFactory jobFactory; 

    @Bean  
    public SchedulerFactoryBean schedulerFactoryBean() {  
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();  
        schedulerFactoryBean.setJobFactory(jobFactory);  
        // 用于quartz集群,QuartzScheduler 启动时更新己存在的Job
        schedulerFactoryBean.setOverwriteExistingJobs(true); 
        schedulerFactoryBean.setStartupDelay(1);  
        return schedulerFactoryBean;  
    }  

    @Bean  
    public Scheduler scheduler() {  
        return schedulerFactoryBean().getScheduler();  
    } 
}

AutowireCapableBeanFactory,通过看到的博客得知这个类会限制继承Job类的任务类中进行依赖注入。

这个并没有实测,文章 https://blog.csdn.net/qq_28483283/article/details/80623417 的作者构造了如下类进行该问题的解决:

@Component  
public class JobFactory extends AdaptableJobFactory {       
    @Autowired    
    private AutowireCapableBeanFactory capableBeanFactory;    

    @Override    
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {    
        // 调用父类的方法    
        Object jobInstance = super.createJobInstance(bundle);    
        // 进行注入    
        capableBeanFactory.autowireBean(jobInstance);    
        return jobInstance;    
    }    
} 
具体实施:DynamicScheduledTask + TaskManager

DynamicScheduledTask.java:动态定时任务

/**
 * description: 动态定时任务
 * author:jiangyanfei
 * date:2018/11/6
 * time:17:34
 */
public class DynamicScheduledTask implements Job {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicScheduledTask.class);

	// 上述继承Job类的任务类依赖注入问题指的就是这里,配置上述类后,可以使用@Autowired注解进行依赖注入

    @Override
    @DataSource(name = DSEnum.DATA_SOURCE_OPS)
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
		// 通过Job上下文对象进行JobDetail、Trigger获取相关信息,下面是获取了Trigger中的参数URL
        String url = (String) jobExecutionContext.getTrigger().getJobDataMap().get("url");
        // 定时任务请求业务逻辑
        LOGGER.info("当前时间:" + dateFormat.format(new Date()) + " 任务信息:" +
		jobExecutionContext.getJobDetail().getKey().getName() + " URL:" + url);
    }
}

TaskManager:任务管理器,负责任务的执行、停止逻辑,以及任务调度器Schedule的组装。

/**
 * description: 任务管理器
 * author:jiangyanfei
 * date:2018/11/7
 * time:15:26
 */
@Component
public class TaskManager {
	
	private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

	private static final Logger LOGGER = LoggerFactory.getLogger(TaskManager.class);

	// 这里需要说明,采用标准的定时器工厂类进行schedule的生成,作为全局对象等待被调用
	SchedulerFactory schedulerFactory = new StdSchedulerFactory();		

	// 1. taskConfig方法:组装Schedule
	// 2. 任务执行
	// 3. 任务停止
}
  1. taskConfig方法:Schedule的组装,这个方法是改造过的,传入了相关的业务参数OpsScriptInfo。

     private void taskConfig(Scheduler scheduler, OpsScriptInfo opsScriptInfo) throws SchedulerException {
     	// 后续业务请求的URL测试拼装,结合传递过来的OpsScriptInfo对象
         String url = "/" + opsScriptInfo.getId() + opsScriptInfo.getPath() + "/" + opsScriptInfo.getFileName();
         // Scheduler组件1: JobDetail 
     	// 构建新任务,指定动态定时任务类:DynamicScheduledTask
     	JobDetail jobDetail = JobBuilder.newJob(DynamicScheduledTask.class)
                 .withIdentity("Timing-task" + opsScriptInfo.getId(), "TimingTask")
                 .build();
     	// Scheduler组件2: Trigger(SimpleTrigger、CronTrigger)
         CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("Trigger-task-trigger" +
     			opsScriptInfo.getId(), "TimingTaskTrigger")
     			 // 传入OpsScriptInfo对象携带的Cron表达式
                 .withSchedule(CronScheduleBuilder.cronSchedule(opsScriptInfo.getJobCron()))
     			.usingJobData("url", url) // 在触发器中携带参数传递
                 .build();
     	// 将JobDetail、CronTrigger进行Scheduler进行组装
         scheduler.scheduleJob(jobDetail, cronTrigger);
     }
    
  2. 任务执行:

     public void startTask(OpsScriptInfo opsScriptInfo) throws SchedulerException {
     	// 全局schedule工厂获取scheduler对象,start()方法启动,并进行任务配置taskConfig
         Scheduler scheduler = schedulerFactory.getScheduler();
         scheduler.start();
         taskConfig(scheduler, opsScriptInfo);
     }
    
  3. 任务停止:

     public void stopTask(Long scriptId) throws SchedulerException {
         Scheduler scheduler = schedulerFactory.getScheduler();
     	// 通过传递参数任务ID,结合JobKey拼装Key,从JobKey中获取对应Id的Key
         JobKey key = JobKey.jobKey("Timing-task" + scriptId, "TimingTask");
         // 判断该Key下,任务调度器中任务是否存在,若存在,将该Key对应任务从调度器中移除
     	if (scheduler.checkExists(key)) {
             scheduler.deleteJob(key);
             LOGGER.info("当前时间:" + dateFormat.format(new Date()) + " 定时任务:Timing-task" + scriptId + " 已停止");
         }
     }
    
控制层中注入任务管理器,在对应的请求处理方法中进行调用即可
@Autowired
private TaskManager taskManager;
quartz.properties
#===============================================================     
#Configure Main Scheduler Properties 调度器属性    
#===============================================================       
org.quartz.scheduler.instanceName = DefaultQuartzScheduler       
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false     
 
#===============================================================     
#Configure ThreadPool 线程池属性    
#===============================================================       
org.quartz.threadPool.threadCount =  10       
org.quartz.threadPool.threadPriority = 5       
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool     
 
#===============================================================     
#Configure JobStore 作业存储设置    
#===============================================================       
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore     
 
#===============================================================     
#Configure Plugins 插件配置    
#===============================================================       
org.quartz.plugin.jobInitializer.class = org.quartz.plugins.xml.JobInitializationPlugin       
org.quartz.plugin.jobInitializer.overWriteExistingJobs = true      
org.quartz.plugin.jobInitializer.failOnFileNotFound = true      
org.quartz.plugin.jobInitializer.validating=false

主要内容为上述,quartz的默认配置文件中主要指定了线程池属性,默认的起了一个拥有10个线程的线程池。

在这里插入图片描述

org.quartz.threadPool.class 是要使用的ThreadPool实现的名称。Quartz附带的线程池是"org.quartz.simpl.SimpleThreadPool",并且应该能够满足几乎每个用户的需求。它有非常简单的行为,并经过很好的测试。它提供了一个固定大小的线程池,可以计划程序的生命周期。

org.quartz.threadPool.threadCount 可以是任何正整数,这是可用于并发执行作业的线程数。如果你只有几个工作每天执行几次,那么1个线程已经足够。如果你有成千上万的工作,每分钟都有很多工作,那么你可能希望一个线程数可能更多的是50或100(这很重要,取决于你的工作所执行的工作的性质,以及你的系统资源!)。

org.quartz.threadPool.threadPriority 可以是Thread.MIN_PRIORITY(即1)和Thread.MAX_PRIORITY(即10)之间的任何int。默认值为Thread.NORM_PRIORITY(5)。

请求测试:12个定时任务,Cron表达式均为:0/10 * * * * ? 在这里插入图片描述

可以看出,并发的定时任务中在执行时间是还是存在差异的,毫秒级的执行时间间隔,可以忽略。
线程池中的工作线程数默认为10,且线程池不关闭,并发任务循环使用线程池中的工作线程资源。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐