分布式调度任务功能加强
分布式调度任务,在前一篇文章中有提到,它在分布式系统中有很大的优势,但也存在问题,需要在实际开发中增强功能,管理相关任务的细节。
分布式调度任务优点:
集群分布式并发环境中使用QUARTZ定时任务调度,会在各个节点会上报任务,存到数据库中,执行时会从数据库中取出触发器来执行,如果触发器的名称和执行时间相同,则只有一个节点去执行此任务。如果此节点执行失败,则此任务则会被分派到另一节点执行,中途也会自动检查失效的定时调度,发现不成功的,其他节点立马接过来继续完成定时任务。
在实际开发中存在的问题
在前一篇文章中介绍了spring中配置分布式的调度任务,但是在实际开发中,有这样几类问题:
- 在开发过程中,调度任务需要调试或单次执行测试。
- 定义调度任务,但不一定要执行。
- 需要将正在运行的调度任务停止。
- 需要将停止的调度任务重新启动。
- 需要删除无用的调度任务。
- 需要时手动执行,即有时间规律,但需要时要手动执行一次,平时按规律执行。
以上需求可以通过重启或修改配置文件实现,但如果是这种需求是较为频繁的,或是调度任务的量比较大,那么之前的方式就比较难以维护,可扩展性也不好。于是就需要对调度任务进行管理,提供相关调度任务的管理界面,增加相关的操作,例如:新增,修改,删除,暂停,运行一次等,使调度任务达到完全可控。
实现调度任务管理
要实现调度任务的管理,spring的quartz任务的管理方式就不适用了,因为spring内部没有提供相关的任务的管理,只存在定义后执行,没有动态添加或删除,更不存在动态删除等相关操作。
增强功能实现
创建管理表,并实现工具类,调用Quartz的API,实现对quartz任务的管理。在管理表中定义任务,并通过Quartz的API同步。对于增,删,改等等相关操作,都有相关的Quartz的API进行同步调用,流程比较简单如下图(只列举新增并同步的步骤)。
添加调度任务管理表
因为有相关管理页面,所以要有表的数据支撑,我们没有必要去对quartz原有的11张表进行操作,因为这样会干扰quartz本身的调度逻辑,所以暂设计调试任务管理表如下:
字段名 | 描述 |
---|---|
job_id | 任务主键 |
job_type | 任务类型 |
job_name | 任务名称 |
job_expr | 调度公式 |
runtime_last | 最后一次执行时间 |
runtime_next | 下次执行时间 |
job_status | 任务状态 |
run_times | 运行次数 |
run_duration | 任务延时 |
job_memo | 任务描述 |
job_class | 执行任务的类名(带包名) |
job_method | 执行任务的方法名 |
job_object | 执行任务类在Springcontext中bean的名称 |
create_time | 创建时间 |
creater | 创建人 |
quartz操作API
@Component
public class SchedulerHelper
{
private static final Logger LOGGE = LoggerFactory.getLogger(SchedulerHelper.class);
private static final String CONFIG_FILE="quartz.properties";
private static final String IDENTITY_JOB_PREFIX="job_";
private static final String IDENTITY_TRIGGER_PREFIX="trigger_";
@Autowired
private IJobService jobService;//jobService 这个服务是实现管理任务的页面的服务实现
private Scheduler scheduler;
@Autowired
private StartJobSchedulerListener startJobSchedulerListener;//实现自己的Scheduler监听器,程序启动时,任务没创建时就创建
/**
* 容器一启动时,类实例化时就执行
*/
@PostConstruct
public void init()
{
try{
// 创建一个定时器工厂
StdSchedulerFactory sf = new StdSchedulerFactory();
//初始化quartz-job.properties配置文件
sf.initialize(Thread.currentThread().getContextClassLoader().getResource(CONFIG_FILE).getFile());
scheduler = sf.getScheduler();
//把jobService放到scheduler上下文,job执行是可以获取并访问。
scheduler.getContext().put(WebConst.SCHEDULER_KEY_JOBSERVICE,jobService);
startJobSchedulerListener.setSchedulerHelper(this);
//设置自己的监听器
scheduler.getListenerManager().addSchedulerListener(startJobSchedulerListener);
//启动定时器
scheduler.start();
LOGGE.info("====================job scheduler start");
}catch(SchedulerException e){
LOGGE.error("error",e);
}
}
/**
* 根据jobentity创建并开始任务
*/
public boolean createAndStartJob(JobEntity job)
{
JobDetail jobDetail=generateJobDetail(job);
Trigger trigger=generateTriggerBuilder(job).build();
try {
scheduler.scheduleJob(jobDetail, trigger);
return true;
} catch (SchedulerException e) {
LOGGE.error("scheduler.scheduleJob",e);
return false;
}
}
/**
* 清除
*/
public void clearAllScheduler()
{
try {
scheduler.clear();
} catch (SchedulerException e) {
LOGGE.error("clearAllScheduler",e);
}
}
/**
* 根据jobId和类型删除
*/
public boolean removeJob(String jobId,String jobType)
{
try {
scheduler.deleteJob(getJobKey(jobId,jobType));
return true;
} catch (SchedulerException e) {
LOGGE.error("removeJob",e);
return false;
}
}
/**
* 暂停任务,调度任务转为PAUSE
*/
public boolean pauseJob(String jobId,String jobType)
{
try {
scheduler.pauseJob(getJobKey(jobId,jobType));
return true;
} catch (SchedulerException e) {
LOGGE.error("resumeJob",e);
return false;
}
}
/**
* 继续执行,调试任务转为ACTIVE
* @param jobId
* @param jobType
* @return
*/
public boolean resumeJob(String jobId,String jobType)
{
try {
scheduler.resumeJob(getJobKey(jobId,jobType));
return true;
} catch (SchedulerException e) {
LOGGE.error("executeOneceJob",e);
return false;
}
}
/**
* 马上只执行一次任务
*/
public boolean executeOneceJob(String jobId,String jobType)
{
try {
Calendar end=Calendar.getInstance();
TriggerBuilder<SimpleTrigger> simpleTriggerBuilder=TriggerBuilder.newTrigger()
.withIdentity(getTriggerKey(jobId,jobType))
.forJob(getJobKey(jobId,jobType))
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2));
end.add(Calendar.SECOND, 2);
simpleTriggerBuilder.startAt(end.getTime());
end.add(Calendar.SECOND, 5);
simpleTriggerBuilder.endAt(end.getTime());
JobEntity job=jobService.getJobById(jobId);
JobDataMap jobDataMap=new JobDataMap();
jobDataMap.put("jobEntity", job);
simpleTriggerBuilder.usingJobData(jobDataMap);
Trigger trigger=simpleTriggerBuilder.build();
scheduler.scheduleJob(trigger);
return true;
} catch (SchedulerException e) {
LOGGE.error("executeOneceJob",e);
return false;
}
}
/**
* 启动一些scheduler里没有的active的jobDetail
*/
public void createActiveJobFromDB() throws SchedulerException
{
List<JobEntity> jobs=jobService.getActiveJob();
for(JobEntity job:jobs)
{
if(scheduler.getJobDetail(getJobKey(job))==null)
createAndStartJob(job);
}
}
/**
* 获得任务的jobKey
*/
public static JobKey getJobKey(String jobId,String jobType)
{
return new JobKey(IDENTITY_JOB_PREFIX+jobId,IDENTITY_JOB_PREFIX+jobType);
}
/**
* 获得任务的jobKey
*/
public static JobKey getJobKey(JobEntity job)
{
return new JobKey(IDENTITY_JOB_PREFIX+job.getJobId(),IDENTITY_JOB_PREFIX+job.getJobType());
}
/**
* 获得trigger的triggerkey
*/
public static TriggerKey getTriggerKey(JobEntity job)
{
return new TriggerKey(IDENTITY_TRIGGER_PREFIX+job.getJobId()+"_"+System.currentTimeMillis(), IDENTITY_TRIGGER_PREFIX+job.getJobType());
}
/**
* 获得trigger的triggerkey
*/
public static TriggerKey getTriggerKey(String jobId,String jobType)
{
return new TriggerKey(IDENTITY_TRIGGER_PREFIX+jobId+"_"+System.currentTimeMillis(), IDENTITY_TRIGGER_PREFIX+jobType);
}
public static JobDetail generateJobDetail(JobEntity job)
{
JobDataMap jobDataMap=new JobDataMap();
jobDataMap.put("jobEntity", job);
Class<? extends Job> clazz=null;
clazz=BeanJob.class;
return JobBuilder.newJob(clazz)
.withIdentity(getJobKey(job))
.usingJobData(jobDataMap)
.requestRecovery(true).storeDurably(true)
.build();
}
/**
* 根据jobEntity获得trigger
*/
public static TriggerBuilder<CronTrigger> generateTriggerBuilder(JobEntity job)
{
TriggerBuilder<CronTrigger> triggerBuilder =null;
try {
triggerBuilder = TriggerBuilder.newTrigger()
.withIdentity(getTriggerKey(job))
.withSchedule(CronScheduleBuilder.cronSchedule(job.getJobExpr())
.withMisfireHandlingInstructionDoNothing());
if(job.getSyncBeginTime()!=null)
triggerBuilder.startAt(job.getSyncBeginTime());
else
triggerBuilder.startNow();
if(job.getSyncEndTime()!=null)
triggerBuilder.endAt(job.getSyncEndTime());
} catch (ParseException e) {
e.printStackTrace();
}
return triggerBuilder;
}
public static IJobService getJobService(JobExecutionContext context)
{
try {
return (IJobService) context.getScheduler().getContext().get(WebConst.SCHEDULER_KEY_JOBSERVICE);
} catch (SchedulerException e) {
LOGGE.error("SchedulerHelper.getJobService",e);
return null;
}
}
}
自定义quartz监听
@Component(value="startJobSchedulerListener")
public class StartJobSchedulerListener extends SchedulerListenerSupport
{
private static final Logger LOGGE = LoggerFactory.getLogger(StartJobSchedulerListener.class);
@Autowired
private SchedulerHelper schedulerHelper;
@Override
public void schedulerStarted()
{
try {
schedulerHelper.createActiveJobFromDB();
} catch (SchedulerException e) {
LOGGE.error("createActiveJobFromDB",e);
}
}
public SchedulerHelper getSchedulerHelper() {
return schedulerHelper;
}
public void setSchedulerHelper(SchedulerHelper schedulerHelper) {
this.schedulerHelper = schedulerHelper;
}
}
改装JOb类,使满足从数据库中读取的类名与方法中加载定时任务。
public abstract class AbstractEdiJob implements Job
{
protected JobEntity jobEntity;
private static final Logger logger = LoggerFactory.getLogger(AbstractEdiJob.class);
private Long beginTime;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException
{
IJobService jobService=SchedulerHelper.getJobService(context);
preExcute(jobService,context);
exeucuteInternal(context);
postExcute(jobService,context);
}
abstract public void exeucuteInternal(JobExecutionContext context);
public void preExcute(IJobService jobService,JobExecutionContext context)
{
beginTime=System.currentTimeMillis();
//获取上下文中的jobentity
jobEntity =(JobEntity)context.getJobDetail().getJobDataMap().get("jobEntity");
}
public void postExcute(IJobService jobService,JobExecutionContext context)
{
//获得最新的jobEntiry
jobEntity=jobService.getJobById(jobEntity.getJobId());
if(jobEntity==null)
{
logger.warn(jobEntity.getJobId()+"job不能存在");
return;
}
if(context.getFireTime()!=null)
jobEntity.setRuntimeLast(context.getFireTime());
if(context.getNextFireTime()!=null)
jobEntity.setRuntimeNext(context.getNextFireTime());
Long times=jobEntity.getRunTimes();
jobEntity.setRunTimes((times==null?0l:times)+1);
Long duration=jobEntity.getRunDuration();
jobEntity.setRunDuration((duration==null?0l:duration)+(System.currentTimeMillis()-beginTime));
jobService.updateJob(jobEntity);
//jobEntity这里的改变不能改变JobDetail里的JobEntity,因为生产的job是JobDetail的JobEntity的复制
}
public void setJobEntity(JobEntity jobEntity) {
this.jobEntity = jobEntity;
}
}
实现 AbstractEdiJob的子类,扩展用。
/**
*执行具体类中的方法
**/
public class BeanJob extends AbstractEdiJob
{
private static final Logger logger = LoggerFactory.getLogger(BeanJob.class);
@Override
public void exeucuteInternal(JobExecutionContext context)
{
Object obj=SpringUtil.getApplicationContext().getBean(jobEntity.getJobObject());
System.out.println("task bean initiated!");
try {
Method method=obj.getClass().getMethod(jobEntity.getJobMethod());
method.invoke(obj);
} catch (SecurityException e) {
logger.error("error",e);
} catch (NoSuchMethodException e) {
logger.error("error",e);
} catch (IllegalArgumentException e) {
logger.error("error",e);
} catch (IllegalAccessException e) {
logger.error("error",e);
} catch (InvocationTargetException e) {
logger.error("error",e);
}
}
}
相关执行自建的调度任务管理表,是一般的数据库操作,这里给出接口
/**
* quartz定时任务管理
*/
public interface IJobService {
/**
* 通过编号查询任务
* @param id
* @return
*/
public JobEntity getJobById(String id);
/**
* 更新任务
* @param jobEntity
* @return
*/
public boolean updateJob(JobEntity jobEntity);
/**
* 获取激活任务
* @return
*/
public List<JobEntity> getActiveJob();
/**
* 任务列表
* @param example
* @param page
* @param limit
* @return
*/
public PageInfo<JobEntity> getJobWithPage(JobEntityExample example,int page,int limit);
/**
* 暂停任务
* @param id
* @return
*/
public boolean pauseJob(String id,String jobType);
/**
* 恢复执行任务
* @param id
* @param jobType
* @return
*/
public boolean resumeJob(String id,String jobType);
/**
* 删除任务
* @param id
* @return
*/
public boolean removeJob(String id,String jobType);
/**
* 执行一次
* @param id
* @param jobType
* @return
*/
public boolean executeOnce(String id,String jobType);
/**
* 保存任务
* @param jobEntity
* @return
*/
public boolean saveJob(JobEntity jobEntity);
/**
* 保存并执行
* @param jobEntity
* @return
*/
public boolean saveAndExecute(JobEntity jobEntity);
/**
* 同步并执行
* @param jobEntity
* @return
*/
public boolean syncAndExecute(String id,String jobType);
}
还有就是quartz的配置文件,与原来的有些不同
#==============================================================
#Configure Main Scheduler Properties
#==============================================================
#配置集群时,quartz调度器的id,由于配置集群时,只有一个调度器,必须保证每个服务器该值都相同,可以不用修改,只要每个ams都一样就行
org.quartz.scheduler.instanceName =quartzScheduler
#集群中每台服务器自己的id,AUTO表示自动生成,无需修改
org.quartz.scheduler.instanceId = AUTO
#==============================================================
#Configure ThreadPool
#==============================================================
#quartz线程池的实现类,无需修改
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#quartz线程池中线程数,可根据任务数量和负责度来调整
org.quartz.threadPool.threadCount = 5
#quartz线程优先级
org.quartz.threadPool.threadPriority = 5
#==============================================================
#Configure JobStore
#==============================================================
#表示如果某个任务到达执行时间,而此时线程池中没有可用线程时,任务等待的最大时间,如果等待时间超过下面配置的值(毫秒),本次就不在执行,而等待下一次执行时间的到来,可根据任务量和负责程度来调整
org.quartz.jobStore.misfireThreshold = 60000
#实现集群时,任务的存储实现方式,org.quartz.impl.jdbcjobstore.JobStoreTX表示数据库存储,无需修改
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
#quartz存储任务相关数据的表的前缀,无需修改
org.quartz.jobStore.tablePrefix = QRTZ_
#连接数据库数据源名称,与下面配置中org.quartz.dataSource.myDS的myDS一致即可,可以无需修改
org.quartz.jobStore.dataSource = myDS
#是否启用集群,启用,改为true,注意:启用集群后,必须配置下面的数据源,否则quartz调度器会初始化失败
org.quartz.jobStore.isClustered = true
#集群中服务器相互检测间隔,每台服务器都会按照下面配置的时间间隔往服务器中更新自己的状态,如果某台服务器超过以下时间没有checkin,调度器就会认为该台服务器已经down掉,不会再分配任务给该台服务器
org.quartz.jobStore.clusterCheckinInterval = 20000
#==============================================================
#Non-Managed Configure Datasource
#==============================================================
#配置连接数据库的实现类,可以参照IAM数据库配置文件中的配置
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
#配置连接数据库连接,可以参照IAM数据库配置文件中的配置
org.quartz.dataSource.myDS.URL = jdbc:mysql://127.0.0.1:3306/zlog?useUnicode=true&characterEncoding=utf-8
#配置连接数据库用户名
org.quartz.dataSource.myDS.user = root
#配置连接数据库密码
org.quartz.dataSource.myDS.password = root
#配置连接数据库连接池大小,一般为上面配置的线程池的2倍
org.quartz.dataSource.myDS.maxConnections = 10
界面相关
这里给一个比较龊的界面,大体如此。
此处只能对停止的任务或没有同步的任务进行删除。
创建任务时,可以创建后同步到quartz中去,也可以只是定义,测试完成后再同步。