一、业务场景
首先我们会在哪些场景用到定时器呢?
比如预发布微信公众号中,健康管理的支付超时,订单取消(其中控制订单超时的就是定时器)
再比如
- 每天十二点同步昨日新增患者
- 定时清除当天号源
- 过期签约自动取消
- 定时下载电话录音
1、运营平台定时器管理
我们来看运营平台的系统管理下的计划任务
- 其中可以看到由名称、CRON表达式、执行状态和操作等信息
- 与代码块中的接口绑定,并通过cron设置没此执行的时间
- 可以控制代码中写好的定时器的运行状态,可以透露,这里的操作接口都是在scheduler模块的ScheduleService中
- 其中最终会调用queryByPages()方法
通过QuartzJobDao.queryQuartzJobs() 查询到符合条件的定时器。public QueryResult<QuartzJobVO> queryByPages(String methodName, int start, int limit) {
methodName = StringUtils.trim(methodName);
QueryResult<QuartzJob> result = DAOFactory.getDAO(QuartzJobDao.class).queryQuartzJobs(methodName,start, limit);
List<QuartzJob> jobList = result.getItems();
List<QuartzJobVO> jobVOList = Lists.newArrayList();
for (QuartzJob job : jobList) {
QuartzJobVO vo = generateQuartzJobVO(job);
jobVOList.add(vo);
}
return new QueryResult<QuartzJobVO>(
(long)result.getTotal(),
(int)result.getStart(),
(int)result.getLimit(),
jobVOList);
}
将结果封装到一个QueryResult中
2、CRON表达
上面提到了cron,那啥是cron表达式?
Cron表达式是一个字符串,字符串以5或6个空格隔开,分为6或7个域,每一个域代表一个含义,Cron有如下两种语法格式:
- Seconds Minutes Hours DayofMonth Month DayofWeek Year
- 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,表示某月的第二个星期三。
常用表达式:
- 0 0 2 1 ? 表示在每月的1日的凌晨2点调整任务
- 0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
- 0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
- 0 0 10,14,16 ? 每天上午10点,下午2点,4点
- 0 0/30 9-17 ? 朝九晚五工作时间内每半小时
- 0 0 12 ? * WED 表示每个星期三中午12点
- 0 0 12 ? 每天中午12点触发
- 0 15 10 ? 每天上午10:15触发
- 0 15 10 ? 2005 2005年的每天上午10:15触发
- 0 14 * ? 在每天下午2点到下午2:59期间的每1分钟触发
- 0 0/5 14 ? 在每天下午2点到下午2:55期间的每5分钟触发
- 0 0/5 14,18 ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
- 0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
- 0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
- 0 15 10 15 * ? 每月15日上午10:15触发
- 0 15 10 L * ? 每月最后一日的上午10:15触发
- 0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
- 0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
- 0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
当然也会有一些Cron表达式生成器 —>在线
二、结构梳理
1、基本流程
从 ehealth-scheduler模块来看,启动ScheduleService接口下有开始、查询、删除、修改等接口。
以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分钟执行一次同步更新
- 首先我们先写一个需要定时处理的接口:
在运营平台添加一个定时器信息,与接口要对应
点击启动即可!管理页面实时刷新,sql要重启服务
重启
三、数据结构
模块用到的数据库有eh_schedulr 中的quartz_job表
下面的信息与运营平台的数据基本对应,当我们插入一个计划任务时,会调用:
将信息存入quartz_job,就是我们在运营平台看到的那样!