一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
前言
上一章讲解了Spring Boot集成quratz的基本实现,如果有不熟悉的朋友可以查看juejin.cn/post/708388…
遗留问题
- 如何知道job是否执行成功还是失败呢?
- job执行如果出错,是否能发送邮件、短信通知呢?
- 能否通过图形界面操作重新调度job、停止job等操作呢?
- Quartz默认是并行执行,是否支持串行执行呢?
- 每个job的jobDetail的变量信息是否可以共享呢?
带着这些问题,本来将详细下讲解job其他特性和功能。
详细设计
job在实际的应用中,除了job自身保留的11张表之外,我们还需要额外的添加3张表,来满足项目的特性需求
- t_qrtz_job:这张表为job的定义表
- t_qrtz_sch_log:这张表为job的调度日志表,所有的调度日志都会记录在这张表中。
- t_qrtz_fire_log:这张表为job的执行日志表,job执行相关的日志信息都记录在这张表中。
如何知道job是否执行成功还是失败,如果失败了如何发送邮件呢?
我们需要通过添加job的监听器来监听job是否执行成功或者失败,需要自定义监听器实现JobListener即可,具体实现如下:
@Component
public class JobFireListener implements JobListener
{
private Logger logger = LoggerFactory.getLogger(JobFireListener.class);
@Autowired
private JobLogService jobLogService;
private String name = "simpleJobListener";
//job名称
@Override
public String getName()
{
return name;
}
//job执行
@Override
public void jobWasExecuted(JobExecutionContext context,
JobExecutionException e)
{
JobDataMap data = context.getMergedJobDataMap();
String fireUserId = data.getString(JobConstant.JOB_USER_SYS);
String jobOid = context.getJobDetail().getKey().getName();
logger.info("job :{} start....",jobOid);
Date date =new Date();
JobFireLog log = new JobFireLog();
log.setJobOid(Long.valueOf(jobOid));
log.setProcTime(context.getJobRunTime());
log.setCreateDate(date);
log.setCreateUser(fireUserId);
log.setFireTime(context.getFireTime());
log.setNextFireTime(context.getNextFireTime());
//job执行成功
if (e == null)
{
log.setResultCode("0");
}
//job执行失败,记录错误日志,且发送邮件
else
{
String msg = e.getMessage();
log.setResultCode("1");
log.setResultDesc(e.getMessage());
logger.error("job fire error",e.getMessage());
// send email
JobEntity jobEntity = (JobEntity) data.get(JobConstant.JOB_DATA_ENTRY);
//发送邮件
sendEmail(jobEntity, msg);
}
jobLogService.saveJobFireLog(log);
}
private void sendEmail(JobEntity jobEntity, String message)
{
//调用发送邮件方法
logger.info("sendEmail start..");
}
}
启动job时需要添加Job监听器
try
{
ListenerManager mgr = scheduler.getListenerManager();
//匹配所有job
mgr.addJobListener(jobFireListener,EverythingMatcher.allJobs());
mgr.addSchedulerListener(jobSchedulerListener);
scheduler.start();
}
catch (Exception e)
{
logger.error("init job error",e);
}
quartz2.0之后提供了很多匹配规则如:KeyMatcher、GroupMatcher、AndMatcher、OrMatcher、EverythingMatcher等。
KeyMatcher
根据JobKey进行匹配,每个JobDetail都有一个对应的JobKey,里面存储了JobName和JobGroup来定位唯一的JobDetail。它的常用方法有:
/************构造Matcher方法************/
KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(pickNewsJob.getKey());//构造匹配pickNewsJob中的JobKey的keyMatcher。
/*********使用方法************/
scheduler.getListenerManager().addJobListener(myJobListener, keyMatcher);//通过这句完成我们监听器对pickNewsJob的唯一监听
GroupMatcher
根据组名信息匹配,它的常用方法有:
GroupMatcher<JobKey> groupMatcher = GroupMatcher.jobGroupContains("group1");//包含特定字符串
GroupMatcher.groupEndsWith("test");//以特定字符串结尾
GroupMatcher.groupEquals("group");//以特定字符串完全匹配
GroupMatcher.groupStartsWith("gou");//以特定字符串开头
AndMatcher
对两个匹配器取交集,实例如下:
KeyMatcher<JobKey> keyMatcher = KeyMatcher.keyEquals(pickNewsJob.getKey());
GroupMatcher<JobKey> groupMatcher = GroupMatcher.jobGroupContains("group");
AndMatcher<JobKey> andMatcher = AndMatcher.and(keyMatcher,groupMatcher);//同时满足两个入参匹配
OrMatcher
对两个匹配器取并集,实例如下:
OrMatcher<JobKey> orMatcher = OrMatcher.or(keyMatcher, groupMatcher);//满足任意一个即可
EverythingMatcher
EverythingMatcher.allJobs();//对全部JobListener匹配
EverythingMatcher.allTriggers();//对全部TriggerListener匹配
能否通过图形界面操作重新调度job、停止job等操作呢?
Quartz 提供了很多对于job操作的API接口,我们只需要直接封装APi,提供接口即可。
@Autowired
private Scheduler scheduler;
@Autowired
private JobFireListener jobFireListener;
@Autowired
private JobSchedulerListener jobSchedulerListener;
@PostConstruct
public void init()
{
List<JobEntity> jobEntityList=getjobList();
if(jobEntityList==null || jobEntityList.isEmpty())
{
logger.info("no job definition found, init job terminated");
return;
}
try
{
ListenerManager mgr = scheduler.getListenerManager();
//匹配所有job
mgr.addJobListener(jobFireListener,EverythingMatcher.allJobs());
mgr.addSchedulerListener(jobSchedulerListener);
scheduler.start();
}
catch (Exception e)
{
logger.error("init job error",e);
}
for(JobEntity job:jobEntityList)
{
schedule(job, JobConstant.JOB_USER_SYS);
}
}
@PreDestroy
public void destroy()
{
try
{
scheduler.shutdown(true);
}
catch (SchedulerException e)
{
logger.error("shutdown schedule job failed", e);
}
}
@Override
public void schedule(Long jobOid, String userId)
{
JobEntity job = this.getJobByOid(jobOid);
schedule(job, userId);
}
@Override
public void schedule(JobEntity job, String userId)
{
Assert.notNull(job,"job不能为空");
JobDataMap data = new JobDataMap();
data.put(JobConstant.JOB_DATA_ENTRY, job);
data.put(JobConstant.JOB_DATA_USER_ID, userId);
JobDetail jobDetail = JobBuilder.newJob(BaseJob.class)
.withIdentity(job.getJobOid().toString()).usingJobData(data).build();
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder
.cronSchedule(job.getCronExp())
.withMisfireHandlingInstructionDoNothing();
TriggerKey triggerKey =getTriggerKey(job.getJobOid());
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey).startNow()
.withSchedule(scheduleBuilder).build();
try
{
JobKey jobKey = getJobKey(job.getJobOid());
if (!scheduler.isShutdown() && scheduler.getJobDetail(jobKey) != null)
{
logger.error("jobKey is already exists." + jobKey.toString());
return;
}
scheduler.deleteJob(jobKey);
scheduler.scheduleJob(jobDetail, trigger);
}
catch (SchedulerException e)
{
logger.error("init job error", e);
}
}
@Override
public void unschedule(Long jobOid, String userId)
{
try
{
scheduler.unscheduleJob(new TriggerKey(jobOid.toString()));
}
catch (SchedulerException e)
{
logger.error("unschedule error",e);
}
}
@Override
public void pause(Long jobOid, String userId)
{
try
{
scheduler.pauseJob(getJobKey(jobOid));
}
catch (SchedulerException e)
{
logger.error("pause job error",e);
}
}
@Override
public void delete(Long jobOid, String userId)
{
try
{
scheduler.unscheduleJob(new TriggerKey(jobOid.toString()));
}
catch (SchedulerException e)
{
logger.error("delete job error",e);
}
}
@Override
public void reschedule(Long jobOid, String userId)
{
unschedule(jobOid, userId);
schedule(jobOid, userId);
}
@Override
public void resume(Long jobOid, String userId)
{
try
{
scheduler.resumeJob(getJobKey(jobOid));
}
catch (SchedulerException e)
{
logger.error("resume job error",e);
}
}
public JobKey getJobKey(Long jobId)
{
return JobKey.jobKey(JobConstant.JOB_NAME + jobId);
}
public TriggerKey getTriggerKey(Long jobId)
{
return TriggerKey.triggerKey(JobConstant.JOB_NAME + jobId);
}
@Override
public JobEntity getJobByOid(Long jobOid)
{
return null;
}
@Override
public List<JobEntity> getjobList()
{
List<JobEntity> list =new ArrayList<>();
JobEntity jobEntity =new JobEntity();
jobEntity.setJobOid(1l);
jobEntity.setJobClass("testTask");
jobEntity.setCronExp("0/5 * * * * ?");
list.add(jobEntity);
return list;
}
@Override
public List<JobKey> getRunningJobList()
{
List<JobKey> jobList = new ArrayList<JobKey>();
try
{
List<JobExecutionContext> jobContextList = scheduler
.getCurrentlyExecutingJobs();
if (jobContextList!=null && !jobContextList.isEmpty())
{
for (JobExecutionContext jobContext : jobContextList)
{
jobList.add(jobContext.getJobDetail().getKey());
}
}
return jobList;
}
catch (SchedulerException se)
{
logger.error("getRunningJobList error", se);
}
return jobList;
}
job默认是并行执行,是否支持串行执行呢?
Quartz定时任务默认都是并发执行的,不会等待上一次任务执行完毕,只要间隔时间到就会执行, 如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。
@DisallowConcurrentExecution:告诉Quartz不要并发地执行同一个JobDetail实例。此注解作用在JObDetail上
每个job的jobDetail的变量信息是否可以共享呢?
@PersistJobDataAfterExecution :告诉Quartz在成功执行了Job实现类的execute方法后(没有发生任何异常),更新JobDetail中JobDataMap的数据,使得该JobDetail实例在下一次执行的时候,JobDataMap中是更新后的数据,而不是更新前的旧数据,如果要使jobDetail的数据不发生变化,需要联合@DisallowConcurrentExecution一起使用,才能保证JobDataMap中的jobDetail数据不改变。
Quartz分布式任务缺点:
-
需要把任务信息持久化到业务数据表,和业务有耦合。
-
调度逻辑和执行逻辑并存于同一个项目中,在机器性能固定的情况下,业务和调度之间不可避免地会相互影响,影响性能。
-
quartz集群模式下,是通过数据库独占锁来唯一获取任务,任务执行并没有实现完善的负载均衡机制,同时会增加数据库的压力。
-
quartz 实现了去中心化处理,但不支持数据库分片。
结语
对于Quartz分布式集群的核心原理,后续的章节会重点进行讲解。感谢大家支持和点赞,你们的支持就是我最大的动力。