分布式定时任务
1,什么是分布式定时任务;
2,为什么要采用分布式定时任务;
3,怎么样设计实现一个分布式定时任务;
4,当前比较流行的分布式定时任务框架;
1,什么是分布式定时任务:
首先,我们要了解计划任务这个概念,计划任务是指由计划的定时运行或者周期性运行的程序。我们最常见的就是Linux的‘crontab’和Windows的‘计划任务’。
那么什么是分布式定时任务,个人总结为:把分散的,可靠性差的计划任务纳入统一的平台,并实现集群管理调度和分布式部署的一种定时任务的管理方式。叫做分布式定时任务。
2,为什么要采用分布式定时任务:
单点定时任务的缺点:
分布式定时任务的优势:
3,怎么样设计和实现一个分布式定时任务:
3.1 分时方案
untitled.png
3.2 HA高可用方案:
untitled1.png
3.3 多路心跳方案:
untitled2.png
3.4 任务抢占方案:
untitled4.png
3.5 任务轮询或任务轮询+抢占排队方案
untitled5.png
通过以上这些方案,可以看出3.5的方案才是优先选择的,扩展性好,开发复杂度不是很高。那么这种方案需要的需要的技术原理是什么呢,那就是分布式互斥锁和队列。
3.6 原理:
untitled7.png
有两台服务器运行定时任务,其中serverA的T2做了加锁操作,其他程序必须等它释放锁了才能运行。 那么如果serverA在加锁的过程中,出现宕机怎么办,是否会一直处于别锁状态。那么我们可以在每个锁都设置一个超时阈值,一旦超时便自动解锁。这样就不会因为宕机导致锁一直不被释 放。另外我们还要考虑命名空间的问题,主要是防止出现同名锁,导致被覆盖。
untitled8.png
从上图中可以看出,TaskQueue中排队情况,运行是自上而下的,当然这个顺序可以自己设置规则,只需要先进先出的远程即可。另外,Task Queue我们需要做至少两个节点,他们遵循主 从结构的原则,主节点需要实时向从节点同步数据,保证在主节点不可用,从节点可以替代。当然,这里可以使用权重轮询的方式,加上数据异步同步,让所有节点都可以做主从的切换, 根据运行状况来分配,可能会更好,但是这样开发难度也有所提高,但是大大增加了高可用性。
3.7 总结:
最后,我们要根据我们实际的情况,需要提供数据库和缓存方面的一些配套服务,这里就不做详解;
这样我们整体的一个分布式定时任务平台就可以实现了,就可以保证计划任务的分布式运行。
4,当前比较流行的分布式定时任务框架:
4.1 Quartz:
4.2 Elastic-job:
Elastic-Job是ddframe中dd-job的作业模块中分离出来的分布式弹性作业框架。去掉了和dd-job中的监控和ddframe接入规范部分。该项目基于成熟的开源产品Quartz和 Zookeeper及其客户端Curator进行二次开发
项目开源地址:https://github.com/dangdangdotcom/elastic-job
特点:
支持多种作业执行模式:支持OneOff,Perpetual和SequenecePerpetual三种作业模式;
失效转移:运行中的作业服务器崩溃不会导致重新分片,只会在下次作业启动时分片。启用失效转移功能可以在本次作业执行过程中,监测其他作业服务器空闲,抓取未完成的孤儿分片项 执行;
运行时状态收集:监控作业运行时状态,统计最近一段时间处理的数据成功和失败数量,记录作业上次运行开始时间,结束时间和下次运行时间;
作业停止,恢复和禁用:用于操作作业启动和停止,并可以禁止某作业运行,一般在上线时常用;
被错过执行的作业重触发:自动记录错过执行的作业,并在上次作业完成后自动触发。
多线程快速处理数据:使用多线程处理抓取到的数据,提升吞吐量;
幂等性:重复作业任务项判定,不重复执行已运行的作业任务项;
容错处理:作业服务器和Zookeeper服务器通信失败后则立即停止作业运行,防止作业注册中心将失效的分片分项配给其他作业服务器,而当前作业服务器任在执行任务,导致重复执行。
Spring支持:支持Spring容器,自定义命名空间,支持占位符;
运维平台:提供了运维平台,可以管理作业和注册中心。
从以上可以看出Elastic-job是在Quartz的基础上又做了一次全面的升级,做了配套的周边基础服务工作,完全成为了一个成熟的分布式定时任务框架。后面会分别介绍Quartz和 Elastic-job的详细原理和具体的使用方法。
///////////////////////
Quartz应用和集群原理分析:
使用的环境版本:spring4.x+quartz2.2.x
****1.1 如何在spring中集成quartz集群****
1.1.1 基于maven项目,需要在pom.xml引入的j依赖为:
org.quartz-scheduler
quartz
1.1.2 Quartz集群的基本配置信息:命名为quartz.properties
#调度标识名 集群中每一个实例都必须使用相同的名称
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
#远程管理相关的配置,全部关闭
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
ThreadPool 实现的类名
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
#线程数量
org.quartz.threadPool.threadCount: 10
#线程优先级
org.quartz.threadPool.threadPriority: 5
#自创建父线程
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
#容许的最大作业延
org.quartz.jobStore.misfireThreshold: 60000
#ID设置为自动获取 每一个必须不同
org.quartz.scheduler.instanceId: AUTO
#数据保存方式为持久化
org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
#加入集群
org.quartz.jobStore.isClustered: true
#调度实例失效的检查时间间隔
org.quartz.jobStore.clusterCheckinInterval: 10000
1.1.3 在项目中加入Quartz的初始化信息: 命名spring-quartz.xml
0 30 00 * * ?
dispatcherServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:/spring/spring-quartz.xml
1
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* Created by lyndon on 16/9/13.
*/
@Component
public class jobFactory extends AdaptableJobFactory {
//这个对象Spring会帮我们自动注入进来,也属于Spring技术范畴.
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
//调用父类的方法
Object jobInstance = super.createJobInstance(bundle);
//进行注入,这属于Spring的技术,不清楚的可以查看Spring的API.
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
****1.2 quartz框架实现分布式定时任务的原理****;
Quartz集群中每个节点都是一个单独的Quartz应用,它又管理着其他的节点。这个集群需要每个节点单独的启动或停止;和我们的应用服务器集群不同,独立的Quratz节点之间是不需要 通信的。不同节点之间是通过数据库表来感知另一个应用。只有使用持久的JobStore才能完成Quartz集群。
untitled21.jpg
untitled22.png
1.2.2 Quartz线程模型:
Quartz中有两类线程:Scheduler调度线程和任务执行线程。
任务执行线程: Quartz不会在主线程(QuartzSchedulerThread)中处理用户job。Quratz是将线程管理的职责委托给ThreadPool,一般的设置使用SimpleThreadPool,SimpleThreadPool创建一定数量的工作线程(WorkerThread),当然这样就意味所有的线程都是异步操作的,所以我们在工作线程的job里面实现业务的时候是没必要重新去创建一个新的线程的,在Quartz创建工作线程的时候已经完成了异步任务的创建。
Scheduler调度线程:QuartzScheduler被创建的时候会创建一个QuratzSchedulerThread实例。
1.2.3 Quartz源码分析:
QuartzSchedulerThreand包含有决定何时下一个Job将被触发的处理循环,主要的逻辑在其的run()方法中,如下图:
untitled23.png
由此可知,QuartzSchedulerThread不断的在获取trigger,触发trigger,释放trigger。
那么具体又是如何获取trigger的呢,可以从上面的源码中可以发现:qsRsrcs.getJobStore()返回对象是JobStore ,具体的集群配置参考1.1.2. org.quartz.jobStore.class:org.quartz.impl.jdbcjobstore.JobStoreTX
JobStoreTx继承自JobStoreSupport,而JobStoreSupport的acquireNextTrigger,triggerFired,releaseAcquiredTrigger方法负责具体trigger相关操作,都必须获得TRIGGER-ACCESS锁。核心逻辑在executeInNonManagedTxLock方法中。
untitled24.png
由上代码可知Quartz集群基于数据库锁的同步操作流程如下图所示:
untitled25.png
/////////////////////////////////
Quartz分布式定时任务的暂停和恢复等:
前两篇我们了解了quartz分布式定时任务的基本原理和实现方式,知道所有的定时任务都会被持久化到数据库。那么我们肯定可以通过操作数据库来做定时任务的暂停,恢复,立即启动,添加等操作。
事实上,quartz已经给我们提供来一些列的api接口来操作对应的定时任务,我们只需要在这个基础之上做进一步的扩展和封装就可以实现我们自己业务,下面,将围绕定时任务的控制,提供一个简单的实现方式。
使用的环境版本:spring4.x+quartz2.2.x
1,首先,我们需要创建一个我们自己job的实体类ScheduleJob:
/**
* Created by lyndon on 16/9/13. * job的实体类
*/
public class ScheduleJob {
private String jobNo; //任务编号
private String jobName; //任务名称
private String jobGroup; //任务所属组
private String desc; //任务描述
private String jobStatus; //任务状态
private String cronExpression; //任务对应的时间表达式
private String triggerName; //触发器名称
//此处省略get和set方法
}
2, 创建我们自己的QuartzImplService服务层:
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.*;
/**
* Created by lyndon on 16/9/13.
* quartz_job的工具类
*/
@Service
public class QuartzUtils {
private final Logger logger = LoggerFactory.getLogger(QuartzUtils.class);
@Resource
private Scheduler scheduler;
/**
*
* 获取计划任务列表
* @return List
*/
public List getPlanJobList() throws SchedulerException{
List jobList = new ArrayList<>();
GroupMatcher matcher = GroupMatcher.anyJobGroup();
Set jobKeys = scheduler.getJobKeys(matcher);;
jobKeys = scheduler.getJobKeys(matcher);
for (JobKey jobKey : jobKeys) {
List extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
for (Trigger trigger : triggers) {
ScheduleJob job = new ScheduleJob();
job.setJobName(jobKey.getName());
job.setJobGroup(jobKey.getGroup());
// 此处是我自己业务需要,给每个定时任务配置类对应的编号和描述
String value = PropertiesUtils.getStringCN(jobKey.getName());
if(null != value && !"".equals(value)){
job.setJobNo(value.split("/")[0]);
job.setDesc(value.split("/")[1]);
}else{
job.setJobNo("0000");
job.setDesc("未监控任务");
}
job.setTriggerName("触发器:" + trigger.getKey());
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
job.setJobStatus(triggerState.name());
if (trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger) trigger;
String cronExpression = cronTrigger.getCronExpression();
job.setCronExpression(cronExpression);
}
jobList.add(job);
}
}
// 对返回的定时任务安装编号做排序
Collections.sort(jobList,new Comparator(){
public int compare(ScheduleJob arg0, ScheduleJob arg1) {
return arg0.getJobNo().compareTo(arg1.getJobNo());
}
});
return jobList;
}
/**
* 获取正在运行的任务列表
* @return List
*/
public List getCurrentJobList() throws SchedulerException{
List executingJobs = scheduler.getCurrentlyExecutingJobs();;
List jobList = new ArrayList(executingJobs.size());;
for (JobExecutionContext executingJob : executingJobs) {
ScheduleJob job = new ScheduleJob();
JobDetail jobDetail = executingJob.getJobDetail();
JobKey jobKey = jobDetail.getKey();
Trigger trigger = executingJob.getTrigger();
job.setJobName(jobKey.getName());
job.setJobGroup(jobKey.getGroup());
String value = PropertiesUtils.getStringCN(jobKey.getName());
if(null != value && !"".equals(value)){
job.setJobNo(value.split("/")[0]);
job.setDesc(value.split("/")[1]);
}else{
job.setJobNo("0000");
job.setDesc("未监控任务");
}
job.setTriggerName("触发器:" + trigger.getKey());
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
job.setJobStatus(triggerState.name());
if (trigger instanceof CronTrigger) {
CronTrigger cronTrigger = (CronTrigger) trigger;
String cronExpression = cronTrigger.getCronExpression();
job.setCronExpression(cronExpression);
}
jobList.add(job);
}
Collections.sort(jobList,new Comparator(){
public int compare(ScheduleJob arg0, ScheduleJob arg1) {
return arg0.getJobNo().compareTo(arg1.getJobNo());
}
});
return jobList;
}
/**
* 暂停当前任务
* @param scheduleJob
*/
public void pauseJob(ScheduleJob scheduleJob) throws SchedulerException{
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
if(scheduler.checkExists(jobKey)){
scheduler.pauseJob(jobKey);
}
}
/**
* 恢复当前任务
* @param scheduleJob
*/
public void resumeJob(ScheduleJob scheduleJob) throws SchedulerException{
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
if(scheduler.checkExists(jobKey)){
//并恢复
scheduler.resumeJob(jobKey);
//重置当前时间
this.rescheduleJob(scheduleJob);
}
}
/**
* 删除任务
* @param scheduleJob
* @return boolean
*/
public boolean deleteJob(ScheduleJob scheduleJob) throws SchedulerException{
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
if(scheduler.checkExists(jobKey)){
return scheduler.deleteJob(jobKey);
}
return false;
}
/**
* 立即触发当前任务
* @param scheduleJob
*/
public void triggerJob(ScheduleJob scheduleJob) throws SchedulerException{
JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
if(scheduler.checkExists(jobKey)){
scheduler.triggerJob(jobKey);
}
}
/**
* 更新任务的时间表达式
* @param scheduleJob
* @return Date
*/
public Date rescheduleJob(ScheduleJob scheduleJob) throws SchedulerException{
TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(),
scheduleJob.getJobGroup());
if(scheduler.checkExists(triggerKey)){
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob
.getCronExpression());
//按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey)
.withSchedule(scheduleBuilder).build();
//按新的trigger重新设置job执行
return scheduler.rescheduleJob(triggerKey, trigger);
}
return null;
}
/**
* 查询其中一个任务的状态
* @param scheduleJob
* @return
* @throws SchedulerException
*/
public String scheduleJob(ScheduleJob scheduleJob) throws SchedulerException {
String status = null;
TriggerKey triggerKey = TriggerKey.triggerKey(scheduleJob.getJobName(), scheduleJob.getJobGroup());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (null != trigger) {
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
status = triggerState.name();
}
return status;
}
/**
* 校验job是否已经加载
* @param scheduleJob JOB基本信息参数
* @return 是否已经加载
*/
public boolean checkJobExisted(ScheduleJob scheduleJob) throws SchedulerException {
return scheduler.checkExists(new JobKey(scheduleJob.getJobName(), scheduleJob.getJobGroup()));
}
private String getStatuDesc(String status){
if(status.equalsIgnoreCase("NORMAL")){
return "正常";
}else if(status.equalsIgnoreCase("PAUSED")){
return "暂停";
}else{
return "异常";
}
}
}
3,提供对应的Controller
import com.innmall.hotelmanager.common.Result;
import com.innmall.hotelmanager.service.quartz.QuartzUtils;
import com.innmall.hotelmanager.service.quartz.ScheduleJob;
import org.quartz.SchedulerException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* Created by lyndon on 16/9/13.
*/
@RestController
@RequestMapping(value = {"/v1/job"})
public class QuartzController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private QuartzUtils quartzUtils;
//获取定时任务的列表
@RequestMapping(value = {"/getJobList"})
public Result getPlanJobList(String openId){
//QuartzUtils quartzUtils = new QuartzUtils();
List list = null;
try {
list = quartzUtils.getPlanJobList();
} catch (SchedulerException e) {
e.printStackTrace();
}
return Result.success(list);
}
//暂停任务
@RequestMapping(value = {"/pauseJob"})
public Result pauseJob(String openId){
//QuartzUtils quartzUtils = new QuartzUtils();
ScheduleJob job = new ScheduleJob();
job.setJobGroup("innmall_job");
job.setJobName("refreshWxToKenJobDetail");
try {
quartzUtils.pauseJob(job);
} catch (SchedulerException e) {
e.printStackTrace();
}
return Result.success("暂停成功");
}
//恢复任务
@RequestMapping(value = {"/resumeJob"})
public Result resumeJob(String openId){
//QuartzUtils quartzUtils = new QuartzUtils();
ScheduleJob job = new ScheduleJob();
job.setJobGroup("innmall_job");
job.setJobName("refreshWxToKenJobDetail");
try {
quartzUtils.resumeJob(job);
} catch (SchedulerException e) {
e.printStackTrace();
}
return Result.success("恢复成功");
}
//立即触发任务
@RequestMapping(value = {"/triggerJob"})
public Result triggerJob(String openId){
//QuartzUtils quartzUtils = new QuartzUtils();
ScheduleJob job = new ScheduleJob();
job.setJobGroup("innmall_job");
job.setJobName("refreshWxToKenJobDetail");
try {
quartzUtils.triggerJob(job);
} catch (SchedulerException e) {
e.printStackTrace();
}
return Result.success("触发成功");
}
//删除任务
@RequestMapping(value = {"/deleteJob"})
public Result deleteJob(String openId){
//QuartzUtils quartzUtils = new QuartzUtils();
ScheduleJob job = new ScheduleJob();
job.setJobGroup("innmall_job");
job.setJobName("refreshWxToKenJobDetail");
try {
quartzUtils.deleteJob(job);
} catch (SchedulerException e) {
e.printStackTrace();
}
return Result.success("触发成功");
}
}
4,接下来,我们就可以进行单元测试了。
5,需要注意的地方:
5.1 service层:
@Resource
private Scheduler scheduler;
这里是因为我们在xml里面已经配置对应的工厂bean,所以可以在这里可以直接注入:
5.2 关于区分不同业务的触发器和任务,可以配置job和trigger的group属性,这样我们便以区分,如果不设置,quartz将使用default关键字:
5.3 关于定时任务恢复后,我们如果不需要让之前错过的定时任务再执行一次,可以设置misfireInstruction的属性,其实就是
CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING
进去可以看见对应的值为2.
并且需要在我们恢复任务的时候调用更新的方法,可以见上文的QuartzUtil中的方法。
//重置当前时间
this.rescheduleJob(scheduleJob);
5.4 如果需要定时任务恢复后,需要将之前错过的执行一次,那么只需要在xml里面去除misfireInstruction属性,其实就是使用默认配置,并且在恢复的时候不调用更新的方法。
关于quartz的使用方法,暂时就介绍到这里,如果有什么地方有问题,欢迎指正,后面将持续研究对应的异常处理机制,敬请关注~