Quartz实现分布式可动态配置的定时任务

关键词:

1. 定时任务

2. 分布式

3. 可动态配置触发时间

一般通过Quartz实现定时任务很简单。如果实现分布式定时任务需要结合分布式框架时间也可以实现。但我们有个实际需求是,页面可动态配置定时任务周期触发(比如,假如下班前如果把先决条件完成了,正常可以18:00触发完成批量任务,假如完不成,需要讲任务设置到很晚,等到先决条件完成再触发)。这个时候需要满足1,2,3都要满足。这样实现起来就有一定难度了。

下面来看看我是怎么样实现的,如有更好的实现方案,欢迎在评论区提出,谢谢!

1. Quartz实现定时任务

我通过一个工具类来实现,如下: 


import java.text.SimpleDateFormat
import java.util.Date

import org.quartz.{CronScheduleBuilder, CronTrigger, JobBuilder, JobKey, TriggerBuilder, TriggerKey, _}
import org.quartz.impl.StdSchedulerFactory

import scala.collection.JavaConverters._

/**
* 定时任务管理类
*
* @author BarryWang create at 2018/5/11 14:22
* @version 0.0.1
*/
object QuartzManager {
private val stdSchedulerFactory = new StdSchedulerFactory
private val JOB_GROUP_NAME = "JOB_GROUP_NAME"
private val TRIGGER_GROUP_NAME = "TRIGGER_NAME"

/**
* 根据指定格式(yyyy-MM-dd HH:mm:ss)时间字符串添加定时任务,使用默认的任务组名,触发器名,触发器组名
* @param jobName 任务名
* @param time 时间设置,参考quartz说明文档
* @param jobClass 任务类名
*/
def addJobByTime(jobName: String, time: String, jobClass: Class[_ <: Job]) : Unit = {
QuartzManager.addJobByTime(jobName, time, jobClass, Map("1"->"otherData"))
}

/**
* 根据指定时间(java.util.Date)添加定时任务,使用默认的任务组名,触发器名,触发器组名
*
* @param jobName 任务名
* @param date 日期
* @param jobClass 任务类名
*/
def addJobByDate(jobName: String, date: Date, jobClass: Class[_ <: Job]): Unit = {
QuartzManager.addJobByDate(jobName, date, jobClass, Map("1"->"otherData"))
}

/**
* 根据指定cron表达式添加定时任务,使用默认的任务组名,触发器名,触发器组名
*
* @param jobName 任务名
* @param jobClass 任务类名
* @param cron cron表达式
*/
def addJobByCron(jobName: String, cron : String, jobClass: Class[_ <: Job]): Unit = {
QuartzManager.addJobByCron(jobName, cron, jobClass, Map("1"->"otherData"))
}

/**
* 函数描述: 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名
* @param jobName 任务名
* @param time 时间字符串, 格式为(yyyy-MM-dd HH:mm:ss)
* @param jobClass 任务类名
* @param paramsMap 定时器需要额外数据
*/
def addJobByTime(jobName: String, time: String, jobClass: Class[_ <: Job], paramsMap: Map[_ <: String, _ <: AnyRef]): Unit = {
addJobByTime(jobName, time, "yyyy-MM-dd HH:mm:ss", jobClass, paramsMap)
}

/**
* 函数描述: 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名
* @param jobName 任务名
* @param time 时间设置,参考quartz说明文档
* @param jobClass 任务类名
* @param paramsMap 定时器需要额外数据
*/
def addJobByTime(jobName: String, time: String, timePattern: String, jobClass: Class[_ <: Job], paramsMap: Map[_ <: String, _ <: AnyRef]): Unit = {
val df = new SimpleDateFormat(timePattern)
val cron = getCron(df.parse(time))
addJobByCron(jobName, cron, jobClass, paramsMap)
}

/**
* Description: 添加一个定时任务,使用默认的任务组名,触发器名,触发器组名
*
* @param jobName 任务名
* @param date 日期
* @param cls 任务
* @param paramsMap 定时器需要额外数据
*/
def addJobByDate(jobName: String, date: Date, cls: Class[_ <: Job], paramsMap: Map[_ <: String, _ <: AnyRef]): Unit = {
val cron = getCron(date)
addJobByCron(jobName, cron, cls, paramsMap)
}

/**
* 函数描述: 根据cron表达式添加定时任务(默认触发器组名及任务组名)
* @param jobId 任务ID
* @param cron 时间设置 表达式,参考quartz说明文档
* @param jobClass 任务的类
* @param paramsMap 可变参数需要进行传参的值
*/
def addJobByCron(jobId: String, cron: String, jobClass: Class[_ <: Job], paramsMap: Map[_ <: String, _ <: AnyRef]): Unit = {
addJob(jobId, cron, jobClass, paramsMap, JOB_GROUP_NAME, TRIGGER_GROUP_NAME)
}

/**
* 函数描述: 根据cron表达式添加定时任务
* @param jobId 任务ID
* @param cron 时间设置 表达式,参考quartz说明文档
* @param jobClass 任务的类类型 eg:TimedMassJob.class
* @param paramsMap 可变参数需要进行传参的值
* @param jobGroupName 任务组名
* @param triggerGroupName 触发器组名
*/
def addJob(jobId: String, cron: String, jobClass: Class[_ <: Job], paramsMap: Map[_ <: String, _ <: AnyRef],
jobGroupName: String, triggerGroupName: String): Unit = {
val scheduler = stdSchedulerFactory.getScheduler
// 任务名,任务组,任务执行类
val jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobId, jobGroupName).build
//设置参数
jobDetail.getJobDataMap.putAll(paramsMap.asJava)

val triggerBuilder = TriggerBuilder.newTrigger
// 触发器名,触发器组
//默认设置触发器名与任务ID相同
val triggerName = jobId
triggerBuilder.withIdentity(triggerName, triggerGroupName)
triggerBuilder.startNow
// 触发器时间设定
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron))
// 创建Trigger对象
val trigger = triggerBuilder.build.asInstanceOf[CronTrigger]
// 调度容器设置JobDetail和Trigger
scheduler.scheduleJob(jobDetail, trigger)
// 启动
if (!scheduler.isShutdown) scheduler.start()
}

/**
* 函数描述: 修改一个任务的触发时间(使用默认的任务组名,触发器名,触发器组名)
* @param cron 时间字符串
*/
def modifyJobTime(jobId: String, cron: String, jobClass: Class[_ <: Job]): Unit = {
modifyJobTime(jobId, cron, jobClass, Map("1"->"otherData"), JOB_GROUP_NAME, TRIGGER_GROUP_NAME)
}

/**
* 函数描述: 修改一个任务的触发时间(使用默认的任务组名,触发器名,触发器组名)
* @param cron 时间字符串
*/
def modifyJobTime(jobId: String, cron: String, jobClass: Class[_ <: Job], paramsMap: Map[_ <: String, _ <: AnyRef]): Unit = {
modifyJobTime(jobId, cron, jobClass, paramsMap, JOB_GROUP_NAME, TRIGGER_GROUP_NAME)
}

/**
* 函数描述: 修改一个任务的触发时间
* @param jobId 任务ID
* @param cron cron表达式
* @param jobClass 任务类名
* @param paramsMap 其他参数
* @param jobGroupName 任务组名
* @param triggerGroupName 触发器组
*/
def modifyJobTime(jobId: String, cron: String, jobClass: Class[_ <: Job], paramsMap: Map[_ <: String, _ <: AnyRef],
jobGroupName: String, triggerGroupName: String): Unit = {
val scheduler = stdSchedulerFactory.getScheduler()
//默认设置触发器名与任务ID相同
val triggerName = jobId
val triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName)
var trigger = scheduler.getTrigger(triggerKey).asInstanceOf[CronTrigger]
if (trigger != null) {
removeJob(jobId)
}
addJob(jobId, cron, jobClass, paramsMap, jobGroupName, triggerGroupName)
}

/**
* 函数描述: 移除一个任务(使用默认的任务组名,触发器名,触发器组名)
* @param jobId 任务名称
*/
def removeJob(jobId: String): Unit = {
val scheduler = stdSchedulerFactory.getScheduler
//默认设置触发器名与任务ID相同
val triggerName = jobId
val triggerKey = TriggerKey.triggerKey(triggerName, TRIGGER_GROUP_NAME)
// 停止触发器
scheduler.pauseTrigger(triggerKey)
// 移除触发器
scheduler.unscheduleJob(triggerKey)
// 删除任务
scheduler.deleteJob(JobKey.jobKey(jobId , JOB_GROUP_NAME))
}

/**
* 函数描述: 移除一个任务
* @param jobId 任务ID
* @param jobGroupName 任务组
* @param triggerName 触发器名称
* @param triggerGroupName 触发器组名
*/
def removeJob(jobId: String, jobGroupName: String, triggerName: String, triggerGroupName: String): Unit = {
val scheduler = stdSchedulerFactory.getScheduler
val triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName)
// 停止触发器
scheduler.pauseTrigger(triggerKey)
// 移除触发器
scheduler.unscheduleJob(triggerKey)
// 删除任务
scheduler.deleteJob(JobKey.jobKey(jobId , jobGroupName))
}

/**
* 函数描述:启动所有定时任务
*/
def startJobs(): Unit = {
stdSchedulerFactory.getScheduler.start()
}

/**
* 函数描述:关闭所有定时任务
*
*/
def shutdownJobs(): Unit = {
val sched = stdSchedulerFactory.getScheduler
if (!sched.isShutdown) sched.shutdown()
}

/**
* 根据时间获取Cron表达式
* @param date 日期
* @return
*/
def getCron(date: Date): String = {
val dateFormat = "ss mm HH dd MM ? yyyy"
formatDateByPattern(date, dateFormat)
}

/**
* 日期格式转换
* @param date 日期
* @param dateFormat 格式
* @return
*/
def formatDateByPattern(date : Date, dateFormat : String): String = {
val sdf = new SimpleDateFormat(dateFormat)
sdf.format(date)
}
}

2.分布式定时任务实现
分布式定时任务:即在分布式服务的环境下,启动定时任务。分布式定时任务需要解决的问题是:同一定时任务,同一时间点,只能在一台服务器上启动。
为了解决分布式定时任务的问题,我们需要框架层面解决定时任务触发时,选举一台服务器作为master节点。
实现思路如下:
1. 服务启动注册服务时,为服务编号;
2.从注册服务中随机选中一台服务器作为master节点;
3. 服务挂掉或添加时重新选举。

代码如下: 

import java.util.HashMap;
import java.util.Map;

/**
* Created by tangliu on 2016/7/13.
*/
public class MasterHelper {

public static Map<String, Boolean> isMaster = new HashMap<>();

/**
* 根据serviceName, versionName,判断当前服务是否集群中的master
* todo 服务版本号是否作为master判断的依据??
* @param servieName
* @param versionName
* @return
*/
public static boolean isMaster(String servieName, String versionName) {

String key = generateKey(servieName, versionName);

if (!isMaster.containsKey(key))
return false;
else
return isMaster.get(key);

}

public static String generateKey(String serviceName, String versionName) {
return serviceName + ":" + versionName;
}
}

竞选master:
/**
* 监听服务节点下面的子节点(临时节点,实例信息)变化
*/
public void watchInstanceChange(RegisterContext context) {
String watchPath = context.getServicePath();
try {
List<String> children = zk.getChildren(watchPath, event -> {
LOGGER.warn("ServerZk::watchInstanceChange zkEvent:" + event);
//Children发生变化,则重新获取最新的services列表
if (event.getType() == Watcher.Event.EventType.NodeChildrenChanged) {
LOGGER.info("容器状态:{}, {}子节点发生变化,重新获取子节点...", ContainerFactory.getContainer().status(), event.getPath());
if (ContainerFactory.getContainer().status() == Container.STATUS_SHUTTING
|| ContainerFactory.getContainer().status() == Container.STATUS_DOWN) {
LOGGER.warn("Container is shutting down");
return;
}
watchInstanceChange(context);
}
});
boolean _isMaster = false;
if (children.size() > 0) {
_isMaster = checkIsMaster(children, MasterHelper.generateKey(context.getService(), context.getVersion()), context.getInstanceInfo());
}
//masterChange响应
LifecycleProcessorFactory.getLifecycleProcessor().onLifecycleEvent(
new LifeCycleEvent(LifeCycleEvent.LifeCycleEventEnum.MASTER_CHANGE,
context.getService(), _isMaster));
} catch (KeeperException | InterruptedException e) {
LOGGER.error(e.getMessage(), e);
create(context.getServicePath() + "/" + context.getInstanceInfo(), context, true);
}
}

//-----竞选master---
private static Map<String, Boolean> isMaster = MasterHelper.isMaster;

/**
* @param children 当前方法下的实例列表, eg 127.0.0.1:9081:1.0.0,192.168.1.12:9081:1.0.0
* @param serviceKey 当前服务信息 eg com.github.user.UserService:1.0.0
* @param instanceInfo 当前服务节点实例信息 eg 192.168.10.17:9081:1.0.0
*/
public boolean checkIsMaster(List<String> children, String serviceKey, String instanceInfo) {
if (children.size() <= 0) {
return false;
}

boolean _isMaster = false;

/**
* 排序规则
* a: 192.168.100.1:9081:1.0.0:0000000022
* b: 192.168.100.1:9081:1.0.0:0000000014
* 根据 lastIndexOf : 之后的数字进行排序,由小到大,每次取zk临时有序节点中的序列最小的节点作为master
*/
try {
Collections.sort(children, (o1, o2) -> {
Integer int1 = Integer.valueOf(o1.substring(o1.lastIndexOf(":") + 1));
Integer int2 = Integer.valueOf(o2.substring(o2.lastIndexOf(":") + 1));
return int1 - int2;
});

String firstNode = children.get(0);
LOGGER.info("serviceInfo firstNode {}", firstNode);

String firstInfo = firstNode.replace(firstNode.substring(firstNode.lastIndexOf(":")), "");

if (firstInfo.equals(instanceInfo)) {
isMaster.put(serviceKey, true);
_isMaster = true;
LOGGER.info("({})竞选master成功, master({})", serviceKey, CURRENT_CONTAINER_ADDR);
} else {
isMaster.put(serviceKey, false);
_isMaster = false;
LOGGER.info("({})竞选master失败,当前节点为({})", serviceKey);
}
} catch (NumberFormatException e) {
LOGGER.error("临时节点格式不正确,请使用新版,正确格式为 etc. 192.168.100.1:9081:1.0.0:0000000022");
}

return _isMaster;
}

分布式环境判断是否是master:
if (!MasterHelper.isMaster("com.today.api.financetask.service.FinanceScheduledService", "1.0.0")) {
  //excute the task
}

3. 动态配置定时任务触发时间
实现定时任务可动态配置,需要通过数据库表保存最新一次修改的cron表达式来实现:
建表如下:
CREATE TABLE t_scheduled_task  (
job_name varchar(50) NOT NULL COMMENT 'job名称'
, job_id varchar(40) NOT NULL COMMENT 'job ID'
, job_cron varchar(50) NOT NULL COMMENT 'Job cron表达式'
, job_type int COMMENT 'Job类型'
, is_start tinyint(2) DEFAULT 1 NOT NULL COMMENT '是否已启动,0:否(no);1:是(yes)'
, remark varchar(256) COMMENT '备注'
, updated_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '更新时间'
, created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '创建时间'
, has_deleted tinyint(2) DEFAULT 0 NOT NULL COMMENT '是否已删除,0:否(no);1:是(yes)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='定时任务配置表';
CREATE UNIQUE INDEX uk_t_scheduled_task ON t_scheduled_task ( job_id);

建表后需要页面调用接口实现先停掉上次的定时任务,再根据最新修改的触发时间新建一个新的定时任务:
如下:
//页面设置每天触发的时间,格式:HH:mm
val cron = convertHourMinuteToCron(processTime)
//修改定时任务时间, 保存入库
ScheduledTaskQuerySql.isExists(jobId) match {
case true => ScheduledTaskActionSql.updateTaskCron(jobId, cron)
case false => ScheduledTaskActionSql.insertTScheduledTask(
TScheduledTask( report.name,
jobId,
cron,
None,
TScheduledTaskIsStartEnum.YES.id,
None,
null,
null,
TScheduledTaskHasDeletedEnum.NO.id))
}
//关掉老的定时任务,添加新的定时任务
QuartzManager.modifyJobTime(jobId, cron, classOf[DailyGenIncomeDetailJob])

/**
* 每天定时触发-转换时分格式(hh:mm)为cron表达式
* @param hourMibuteStr
* @return
*/
def convertHourMinuteToCron(hourMibuteStr : String) : String = {
val splitHm = hourMibuteStr.split(":")
s"0 ${splitHm(1).trim.toInt} ${splitHm(0).trim.toInt} * * ?"
}

/**
* 每天定时触发-转换时分格式(hh:hh:mm)为cron表达式
* @param dayStr
* @return
*/
def convertDayToCron(dayStr : String) : String = {
val splitHm = dayStr.split(":")
s"0 ${splitHm(1).trim.toInt} ${splitHm(0).trim.toInt} * * ?"
}

4. 重启服务器,启动所有定时任务
重启定时任务,需要重启所有定时任务
这个过程需要将所有定时任务及触发周期保存到数据库,重启后,读取数据库启动恢复所有定时任务
代码如下(Spring框架下启动服务自动会启动ApplicationListener.onApplicationEvent(event: ContextRefreshedEvent)):

import com.today.api.financetask.scala.enums.{TReportTypeEnum, TScheduledTaskIsStartEnum}
import com.today.api.financetask.scala.request.QueryAutoConfigRequest
import com.today.service.financetask.job._
import com.today.service.financetask.job.define.JobEnum
import com.today.service.financetask.query.sql.{AutoConfigQuerySql, ScheduledTaskQuerySql}
import com.today.service.financetask.util.QuartzManager
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationListener
import org.springframework.context.event.ContextRefreshedEvent
import org.springframework.stereotype.Service

/**
* 类功能描述: 定时器监听器, 服务启动时启动定时器
*
* @author BarryWang create at 2018/5/11 12:04
* @version 0.0.1
*/
@Service
class ScheduleStartListener extends ApplicationListener[ContextRefreshedEvent] {
/** 日志 */
val logger = LoggerFactory.getLogger(getClass)
/**
* 启动加载执行定时任务
*/
override def onApplicationEvent(event: ContextRefreshedEvent): Unit = {
logger.info("=======服务器重启定时任务启动start=======")
//1. 恢复日次处理定时任务
recoveryDayTimeProcessJob()
//2. 恢复每天营收明细报表生成定时任务
recoveryImcomeDetailGenJob()
logger.info("=======服务器重启定时任务启动end=======")
}

/**
* 恢复日次处理定时任务
*/
private def recoveryDayTimeProcessJob(): Unit ={
try {
ScheduledTaskQuerySql.queryByJobId(JobEnum.DAY_TIME_PROCESS.jobId) match {
case Some(x) =>
if(x.isStart == TScheduledTaskIsStartEnum.YES.id)
QuartzManager.addJobByCron(JobEnum.DAY_TIME_PROCESS.jobId, x.jobCron, classOf[DayTimeProcessJob])
else
logger.info("定时任务:" + JobEnum.DAY_TIME_PROCESS.jobName + "is_start标志为0,不启动")
case None =>
QuartzManager.addJobByCron(JobEnum.DAY_TIME_PROCESS.jobId, "0 30 2 * * ?", classOf[DayTimeProcessJob])
}
} catch {
case e : Exception => logger.error(JobEnum.DAY_TIME_PROCESS.jobName + "启动失败, 失败原因:", e)
}

}

/**
* 恢复营收明细报表生成定时任务
*/
private def recoveryImcomeDetailGenJob(): Unit = {
val jobName = TReportTypeEnum.INCOMEDETAIL_REPORT.name
try {
val jobId = TReportTypeEnum.INCOMEDETAIL_REPORT.id.toString
ScheduledTaskQuerySql.queryByJobId(jobId) match {
case Some(x) =>
if (x.isStart == TScheduledTaskIsStartEnum.YES.id)
QuartzManager.addJobByCron(jobId, x.jobCron, classOf[DailyGenIncomeDetailJob])
else
logger.info("定时任务:" + jobName + "is_start标志为0,不启动")
case None =>
QuartzManager.addJobByCron(jobId, "0 10 0 * * ?", classOf[DailyGenIncomeDetailJob])
}
}catch {
case e : Exception => logger.error(jobName + "启动失败, 失败原因:", e)
}
}
}

至此整个实现分布式环境下实现动态可配置的定时任务过程已完成
 
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值