一、背景
在洞窝智能营销平台中存在很多定时调度场景,我们选择了Quartz调度框架。
我们最初选择Quartz有以下原因:
1、Quartz是一套轻量级的任务调度框架,可以方便的集成到我们的应用中,不用额外部署服务;
2、Quartz基于数据库实现了分布式调度能力;
3、Quartz提供了丰富的调度管理API,可以方便的动态添加、修改、删除、暂停、重启调度任务;
4、SpringBoot提供了spring-boot-starter-quartz,可以快速与SpringBoot项目整合;
我们不仅把Quartz作为调度引擎,并且利用Quartz的内置线程池作为任务的执行引擎。所有任务都通过Quartz的线程池去执行,这样我们统一了任务执行的入口,实现对任务的统一管理。
二、简介
1、基本介绍
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合,也可以单独使用。
Quartz是开源且具有丰富特性的“任务调度库”,能够集成于任何的Java应用,小到独立的应用,大至电子商业系统。Quartz能够创建亦简单亦复杂的调度,以执行上十、上百,甚至上万的任务。任务job被定义为标准的Java组件,能够执行任何你想要实现的功能。Quartz调度框架包含许多企业级的特性,如JTA事务、集群的支持。
简而言之,Quartz就是基于Java实现的任务调度框架,用于执行你想要执行的任何任务。
官方网址:www.quartz-scheduler.org/
官方文档:www.quartz-scheduler.org/documentati…
原码地址:github.com/quartz-sche…
2、Quartz运行环境
Quartz可以运行嵌入在另一个独立式应用程序。
Quartz可以在应用程序服务器(或Servlet容器)内被实例化,并且参与事务。
Quartz可以作为一个独立的程序运行(其自己的Java虚拟机内),可以通过RMI使用。
Quartz可以被实例化,作为独立的项目集群(负载平衡和故障转移功能),用于作业的执行。
3、Quartz核心概念
任务 Job
Job 就是你想要实现的任务类,每一个 Job 必须实现 org.quartz.job 接口,且只需实现接口定义的 execute() 方法。
触发器 Trigger
Trigger 为你执行任务的触发器,比如你想每天定时3点发送一份统计邮件,Trigger 将会设置3点执行该任务。
Trigger 主要包含两种 SimplerTrigger 和 CronTrigger 两种。
调度器 Scheduler
Scheduler 为任务的调度器,它会将任务 Job 及触发器 Trigger 整合起来,负责基于 Trigger 设定的时间来执行 Job。
三、整合步骤
1、引入maven依赖
org.springframework.boot
spring-boot-starter-quartz
2、创建Quartz依赖的数据表
在Quartz的jar包里有各种数据库的sql脚本,脚本路径:org/quartz/impl/jdbcjobstore,因为使用的mysql数据库,所以选择tables_mysql_innodb.sql。
3、在yaml配置文件中加入Quartz配置
spring:
quartz:
auto-startup: true
job-store-type: jdbc
startup-delay: 10000
jdbc:
initialize-schema: never
properties:
org:
quartz:
scheduler:
instanceName: dmaQuartzScheduler
instanceId: AUTO
jobStore:
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 100
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
4、实现任务接口
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.scheduling.quartz.QuartzJobBean;
import javax.annotation.Resource;
@Slf4j
@DisallowConcurrentExecution
public class QuartzDemoJob extends QuartzJobBean {
@Resource
private ExecuteManager executeManager;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
JobDetail jobDetail = context.getJobDetail();
JobKey key = jobDetail.getKey();
String name = key.getName();
log.info("job start, name: {}", name);
// 下面是具体的任务逻辑,省略
}
}
通过以上四步就完成了Quartz的整合,可以在业务代码里使用Quartz来创建定时任务了。
import com.easyhome.dma.data.enums.JobStatus;
import com.easyhome.dma.data.job.QuartzDemoJob;
import com.easyhome.dma.data.mapper.label.BaseLabelMapper;
import org.quartz.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class QuartzDemoManager {
@Resource
private BaseLabelMapper baseLabelMapper;
@Resource
private Scheduler scheduler;
@Transactional(rollbackFor = Exception.class)
public void createLabel(Label label) throws SchedulerException {
//添加调度
JobDetail jobDetail = JobBuilder.newJob(QuartzDemoJob.class)
.withIdentity(String.valueOf(label.getId()))
.storeDurably()
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(String.valueOf(label.getId()))
.withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(30).withMisfireHandlingInstructionIgnoreMisfires())
.startNow()
.build();
scheduler.scheduleJob(jobDetail, trigger);
//保存标签
baseLabelMapper.insertSelective(label);
}
}
四、应用
1、异步执行
我们的系统还支持了任务的“手动计算”功能,如果等待任务执行完再返回给前端,用户需要等待很长时间,体验不太好。因此我们在service层只更新了任务的执行状态,就交给Quartz框架异步执行,返回前端。
public void compute(Long labelId) throws SchedulerException {
Label label = new Label();
label.setId(labelId);
label.setState(JobStatus.EXECUTING.name());
//更新状态
baseLabelMapper.updateSelective(label);
//异步执行
scheduler.triggerJob(new JobKey(String.valueOf(labelId)));
}
2、并发控制
有两种情况会导致同一任务的不同实例同时执行:
1、用户重复对同一任务执行手动计算。
2、有的任务执行时间比较长,上一次还没有执行完,下一次调度已经开始。
如果同一个任务的不同实例同时调度会造成数据混乱,为了避免这种情况,我们在任务接口上增加@DisallowConcurrentExecution
注解,让同一个任务的不同实例串行执行。
3、资源控制
由于计算任务都要占用一定的服务器资源,当大量任务同时执行时会造成服务器负载过高,因此我们通过调整Quartz的threadPool.threadCount参数来限制任务的最大并发数。
4、依赖管理
对于有依赖关系的调度任务,假如底层数据没有更新,上层任务每次计算得到的结果都是一样的,这种情况会造成服务器资源的浪费。我们不希望这种情况发生,因此在任务执行前加了前置逻辑来判断任务是否真的需要执行。
1)如果用户手动更新,可以执行;
2)如果当前任务没有依赖,可以执行;
3)如果当前任务没有执行过,可以执行;
4)如果有依赖任务更新过(依赖任务的最后执行时间晚于当前任务的最后执行时间),可以执行;
通过上述逻辑,简单实现了依赖任务的调度编排。
import com.easyhome.dma.data.model.ScheduleTask;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
public abstract class DependExecutor extends IExecutor {
@Override
public boolean needExecute(ScheduleTask task, ScheduleContext sc, LocalDateTime startTime) {
//手动调度直接执行
if (sc.getTriggerType() == 3) {
return true;
}
List dependTasks = scheduleTaskManager.getDependTasks(task.getId());
//没有依赖直接执行
if (dependTasks.size() == 0) {
return true;
}
//没有执行过直接执行
if (task.getLastExecuteTime() == null) {
return true;
}
//依赖任务有更新
List updatedTasks = dependTasks.stream().filter(item -> item.getLastExecuteTime() != null && item.getLastExecuteTime().isAfter(task.getLastExecuteTime())).collect(Collectors.toList());
if (updatedTasks.size() != 0) {
return true;
} else {
return false;
}
}
}
5、事务控制
在洞窝智能营销平台中,新建一个标签要同步创建一个调度,删除标签也要同步删除调度。既然Quartz和业务用了同一个数据库,那么这两个操作是否在同一个事务中呢?我们用代码试验了一下。
import com.easyhome.dma.data.enums.JobStatus;
import com.easyhome.dma.data.job.QuartzDemoJob;
import com.easyhome.dma.data.mapper.label.BaseLabelMapper;
import org.quartz.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
@Service
public class QuartzDemoManager {
@Resource
private BaseLabelMapper baseLabelMapper;
@Resource
private Scheduler scheduler;
@Transactional(rollbackFor = Exception.class)
public void createLabel(Label label) throws SchedulerException {
//添加调度
JobDetail jobDetail = JobBuilder.newJob(QuartzDemoJob.class)
.withIdentity(String.valueOf(label.getId()))
.storeDurably()
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(String.valueOf(label.getId()))
.withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(30).withMisfireHandlingInstructionIgnoreMisfires())
.startNow()
.build();
scheduler.scheduleJob(jobDetail, trigger);
// 抛异常
int i = 1 / 0;
//保存标签
baseLabelMapper.insertSelective(label);
}
}
试验结果说明确实在一个事务里,可以保证两个操作的原子性。那么为什么呢?带着这个疑问我们阅读了下Quartz的源码。
当datasource不为null时,设置JobStore为LocalDataSourceJobStore。
LocalDatasourceJobStore提供了一个匿名的ConnectionProvider。
当有事务控制时,从当前线程上下文获取connection,而这个connection和spring事务使用的connection是同一个,因此Quartz和业务操作是在同一个事务里。
五、遇到的问题
当我们使用cron表达式设置调度时,会出现还没到定时时间就会执行一次的问题,后来发现是因为默认的misfire策略的问题。因此我又研究了一下Quartz的misfire机制。
如果持久性触发器由于调度程序被关闭或因为 Quartz 的线程池中没有可用于执行作业的线程而“错过”其触发时间,则会发生失火。不同的触发器类型有不同的失火指令可供使用。默认情况下,它们使用“智能策略”指令 - 该指令具有基于触发器类型和配置的动态行为。当计划程序启动时,它会搜索任何已触发错误的持久性触发器,然后根据其单独配置的错误触发指令更新每个触发器。
有以下几种misfire策略:
实际使用时根据实际需求设置相应的策略即可。
六、现状
目前,洞窝智能营销平台有100多个定时任务,每天调度几千次。Quartz作为平台的调度引擎,保证我们的任务准时无误的运行。同时作为执行引擎保证我们的集群负载维持在一个稳定的水平。同时支持业务的事务,提高了平台的可靠性。