一、业务场景

首先我们会在哪些场景用到定时器呢?
比如预发布微信公众号中,健康管理的支付超时,订单取消(其中控制订单超时的就是定时器)
image.png
再比如

  • 每天十二点同步昨日新增患者
  • 定时清除当天号源
  • 过期签约自动取消
  • 定时下载电话录音

等等,这么多定时器我们能在哪统一管理呢?


1、运营平台定时器管理

我们来看运营平台的系统管理下的计划任务

  • 其中可以看到由名称、CRON表达式、执行状态和操作等信息

image.png

  • 与代码块中的接口绑定,并通过cron设置没此执行的时间

image.png

  • 可以控制代码中写好的定时器的运行状态,可以透露,这里的操作接口都是在scheduler模块的ScheduleService中

image.png

  • 其中最终会调用queryByPages()方法
    1. public QueryResult<QuartzJobVO> queryByPages(String methodName, int start, int limit) {
    2. methodName = StringUtils.trim(methodName);
    3. QueryResult<QuartzJob> result = DAOFactory.getDAO(QuartzJobDao.class).queryQuartzJobs(methodName,start, limit);
    4. List<QuartzJob> jobList = result.getItems();
    5. List<QuartzJobVO> jobVOList = Lists.newArrayList();
    6. for (QuartzJob job : jobList) {
    7. QuartzJobVO vo = generateQuartzJobVO(job);
    8. jobVOList.add(vo);
    9. }
    10. return new QueryResult<QuartzJobVO>(
    11. (long)result.getTotal(),
    12. (int)result.getStart(),
    13. (int)result.getLimit(),
    14. jobVOList);
    15. }
    通过QuartzJobDao.queryQuartzJobs() 查询到符合条件的定时器。
    将结果封装到一个QueryResult

2、CRON表达

上面提到了cron,那啥是cron表达式?
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:

  1. Seconds Minutes Hours DayofMonth Month DayofWeek Year
  2. Seconds Minutes Hours DayofMonth Month DayofWeek

结构:corn从左到右(用空格隔开):秒 分 小时 月份中的日期 月份 星期中的日期 年份

字段 允许值 允许的特殊字符
秒(Seconds) 0~59的整数 , - * / 四个字符
分(Minutes 0~59的整数 , - * / 四个字符
小时(Hours 0~23的整数 , - * / 四个字符
日期(DayofMonth 1~31的整数(但是你需要考虑你月的天数) ,- * ? / L W C 八个字符
月份(Month 1~12的整数或者 JAN-DEC , - * / 四个字符
星期(DayofWeek 1~7的整数或者 SUN-SAT (1=SUN) , - * ? / L C # 八个字符
年(可选,留空)(Year 1970~2099 , - * / 四个字符


注意事项:
每一个域都使用数字,但还可以出现如下特殊字符,它们的含义是:

  • *:表示匹配该域的任意值。假如在Minutes域使用*, 即表示每分钟都会触发事件。
  • ?:只能用在DayofMonth和DayofWeek两个域。它也匹配域的任意值,但实际不会。因为DayofMonth和DayofWeek会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 ?, 其中最后一位只能用?,而不能使用,如果使用*表示不管星期几都会触发,实际上并不是这样。
  • -:表示范围。例如在Minutes域使用5-20,表示从5分到20分钟每分钟触发一次
  • /:表示起始时间开始触发,然后每隔固定时间触发一次。例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次.
  • ,:表示列出枚举值。例如:在Minutes域使用5,20,则意味着在5和20分每分钟触发一次。
  • L:表示最后,只能出现在DayofWeek和DayofMonth域。如果在DayofWeek域使用5L,意味着在最后的一个星期四触发。
  • W: 表示有效工作日(周一到周五),只能出现在DayofMonth域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日(周一)触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份 。
  • LW: 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。
  • #: 用于确定每个月第几个星期几,只能出现在DayofMonth域。例如在4#2,表示某月的第二个星期三。

常用表达式:

  1. 0 0 2 1 ? 表示在每月的1日的凌晨2点调整任务
  2. 0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
  3. 0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
  4. 0 0 10,14,16 ? 每天上午10点,下午2点,4点
  5. 0 0/30 9-17 ? 朝九晚五工作时间内每半小时
  6. 0 0 12 ? * WED 表示每个星期三中午12点
  7. 0 0 12 ? 每天中午12点触发
  8. 0 15 10 ? 每天上午10:15触发
  9. 0 15 10 ? 2005 2005年的每天上午10:15触发
  10. 0 14 * ? 在每天下午2点到下午2:59期间的每1分钟触发
  11. 0 0/5 14 ? 在每天下午2点到下午2:55期间的每5分钟触发
  12. 0 0/5 14,18 ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
  13. 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
  14. 0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
  15. 0 15 10 15 * ? 每月15日上午10:15触发
  16. 0 15 10 L * ? 每月最后一日的上午10:15触发
  17. 0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
  18. 0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
  19. 0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发

当然也会有一些Cron表达式生成器 —>在线
image.png

二、结构梳理

老规矩先看流程!

1、基本流程

ehealth-scheduler模块来看,启动ScheduleService接口下有开始、查询、删除、修改等接口。
image.png
以start()方法举例
调用QuartzJobService接口的start()方法(完成了工作进程条件的封装)

@RpcService
    public QuartzJobVO start(Integer idx) {
            QuartzJob job = quartzJobService.start(idx);
            QuartzJobVO quartzJobVO = new QuartzJobVO();
            BeanUtils.copy(job,quartzJobVO);
            quartzJobVO.setIdx(idx);
            quartzJobVO.setStateDesc(JobState.getDescribe(job.getState()));
            return quartzJobVO;
    }

启动schedule()方法在DefaultSchedulerManager中实现

public boolean schedule(String name, String group, CronScheduleBuilder cronScheduleBuilder, JobDetail jobDetail) {
        try {
            String key = getTriggerKey(name, group);
            Scheduler sched = SCHEDULER_MAP.get(key);
            if (sched == null) {
                SchedulerFactory sf = new StdSchedulerFactory();
                sched = sf.getScheduler();
                CronTrigger cronTrigger = TriggerBuilder
                        .newTrigger()
                        .withIdentity(name,group)
                        .withSchedule(cronScheduleBuilder)
                        .build();
                sched.scheduleJob(jobDetail, cronTrigger);
                sched.getListenerManager().addJobListener(jobListener);
                sched.start();
                Scheduler ifAbsentSched = SCHEDULER_MAP.putIfAbsent(key, sched);
                if (ifAbsentSched!=null) {
                    sched = ifAbsentSched;
                }
            }else{
                CronTrigger newTrigger = TriggerBuilder
                        .newTrigger()
                        .withIdentity(name,group)
                        .withSchedule(cronScheduleBuilder)
                        .build();
                CronTriggerImpl oldTrigger = (CronTriggerImpl)sched.getTrigger(TriggerKey.triggerKey(name, group));
                String cron = "";
                if (oldTrigger!=null) {
                    cron = oldTrigger.getCronExpression();
                }
                if (newTrigger.getCronExpression().equals(cron)) {
                    sched.resumeJob(JobKey.jobKey(name ,group));
                }else{
                    //防止暂停之后又修改cron表达式,再次重启时cron不是最新的
                    removeJob(name,group);
                    SchedulerFactory sf = new StdSchedulerFactory();
                    sched = sf.getScheduler();
                    sched.scheduleJob(jobDetail, newTrigger);
                    sched.getListenerManager().addJobListener(jobListener);
                    sched.start();
                    SCHEDULER_MAP.putIfAbsent(key, sched);
                }
            }
        } catch (SchedulerException e) {
            e.printStackTrace();
            LOGGER.error(e.getMessage(),e);
            throw new RuntimeException(e.getMessage(), e);
        }
        return true;
    }

判断Scheduler对象是否存在

 if (sched == null)
  • 如果不存在就创建Scheduler定时器对象,包括启动细节、定时触发、监听等属性封装,然后启动Scheduler.start() ,并把信息存入SCHEDULER_MAP属性中

    SchedulerFactory sf = new StdSchedulerFactory();
                  sched = sf.getScheduler();
                  CronTrigger cronTrigger = TriggerBuilder
                          .newTrigger()
                          .withIdentity(name,group)
                          .withSchedule(cronScheduleBuilder)
                          .build();
                  sched.scheduleJob(jobDetail, cronTrigger);
                  sched.getListenerManager().addJobListener(jobListener);
                  sched.start();
    
  • 如果存在就去对比、更新,如果条件cron语句都不变Scheduler.resumeJob()重启定时器,否则就要删除之前的执行器重新加载启动

    if (newTrigger.getCronExpression().equals(cron))
    

    当然还有其他删除、查找接口,流程差不多就不多演示了

2、快速开始

接下来我们快速开始一个定时器逻辑!

举例:比如我们每天中午十二点开始每30分钟执行一次同步更新

  • 首先我们先写一个需要定时处理的接口:

image.png
在运营平台添加一个定时器信息,与接口要对应
image.png
点击启动即可!管理页面实时刷新,sql要重启服务
重启

三、数据结构

模块用到的数据库有eh_schedulr 中的quartz_job
image.png
下面的信息与运营平台的数据基本对应,当我们插入一个计划任务时,会调用:
image.png
将信息存入quartz_job,就是我们在运营平台看到的那样!