Spring Boot 集成 Quartz 二

293 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

前言

上一章讲解了Spring Boot集成quratz的基本实现,如果有不熟悉的朋友可以查看juejin.cn/post/708388…

遗留问题

  • 如何知道job是否执行成功还是失败呢?
  • job执行如果出错,是否能发送邮件、短信通知呢?
  • 能否通过图形界面操作重新调度job、停止job等操作呢?
  • Quartz默认是并行执行,是否支持串行执行呢?
  • 每个job的jobDetail的变量信息是否可以共享呢?

带着这些问题,本来将详细下讲解job其他特性和功能。

详细设计

job在实际的应用中,除了job自身保留的11张表之外,我们还需要额外的添加3张表,来满足项目的特性需求

图片.png

  • 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分布式集群的核心原理,后续的章节会重点进行讲解。感谢大家支持和点赞,你们的支持就是我最大的动力。