年底出去面试的的时候,被问到如下问题: 定时任务是如何实现的?定时任务是如何监控的?因为我们项目中的定时任务就是使用Spring的@Scheduled(cron = "0 59 23 * * ?")来实现的,至于监控方面的,没有,就是通过在定时任务代码里面打一些日志,特别重要的定时任务,失败了额外发个邮件通知下,人工补偿。然后他又问了下现在需要重构定时任务,你有没有什么想法?然后我就简单的说了下,最好是提供个可视化界面,可以动态的操作定时任务,尤其是监控方面。当晚回来后找了些资料,无意间在"纯洁的微笑"博主分享的一篇博文中讲到了这个,他的博文原文地址找不到了,他上面是基于spring项目实现的,本人根据quartz的api在springboot中重新实现了个,项目地址:https://github.com/simonsfan/springboot-quartz-demo。该项目用springboot+mybatis+mysql+quartz实现的,提供定时任务可视化动态添加、修改、执行,并提供定时任务监控,界面大致如图:
关于这个项目, 还有些地方需要完善,比如定时任务的查找、分页展示、我这里均没有实现,如果你在实际项目中需要使用,请自行完善下,还有比如更新定时任务执行情况记录表quartz_task_records的时机,需要再斟酌斟酌。如有任何疑问,欢迎留言讨论。
关于quartz,可在官网http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/查看,这里着重要说的是其中几个比较重要的:
1、maven引quartz依赖
org.springframework.boot
spring-boot-starter-quartz
2、quartz核心组件包括
使用示例代码:
/**
* 实现Job接口,重写execute方法
*/
public class QuartzJobFactory implements Job {
protected Logger logger = Logger.getLogger(QuartzJobFactory.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
//TODO 这里是定时任务逻辑
logger.info("=================我是定时任务,每隔5秒执行一次==============");
}
}
/**
* @ClassName QuartzService
* @Description 系统初始化触发定时任务
* @Author simonsfan
* @Date 2019/1/1
* Version 1.0
*/
@Component
public class QuartzService {
private static final Logger logger = LoggerFactory.getLogger(QuartzService.class);
private final String CRON_TIME = "*/5 * * * * ?";
private final String TRIGGER_KEY_NAME = "00000000001";
@PostConstruct
public void taskInit() {
logger.info("=========系统初始化加载定时任务开始========");
try {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
TriggerKey triggerKey = TriggerKey.triggerKey(TRIGGER_KEY_NAME, Scheduler.DEFAULT_GROUP);
JobDetail jobDetail = JobBuilder.newJob(QuartzJobFactory.class).withDescription("quartz测试定制化定时任务").withIdentity(TRIGGER_KEY_NAME, Scheduler.DEFAULT_GROUP).build();
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(CRON_TIME);
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
scheduler.scheduleJob(jobDetail, cronTrigger);
scheduler.start();
logger.info("=========初始化定时任务加载完成=========");
} catch (Exception e) {
logger.info("==============初始化加载定时任务失败==============" + e);
e.printStackTrace();
}
}
}
1、系统初始化加载这里是使用@PostConstruct注解触发,也可以通过实现InitializingBean接口的方式;
2、调度器Scheduler实例是通过new构造的,也可以直接注入类SchedulerFactoryBean,如下
/**
* @ClassName QuartzService
* @Description 系统初始化触发定时任务
* @Author simonsfan
* @Date 2019/1/1
* Version 1.0
*/
@Component
public class QuartzService implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(QuartzService.class);
private final String CRON_TIME = "*/5 * * * * ?";
private final String TRIGGER_KEY_NAME = "00000000001";
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Override
public void afterPropertiesSet() throws Exception {
logger.info("=========系统初始化加载定时任务开始========");
try {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
TriggerKey triggerKey = TriggerKey.triggerKey(TRIGGER_KEY_NAME, Scheduler.DEFAULT_GROUP);
JobDetail jobDetail = JobBuilder.newJob(QuartzJobFactory.class).withDescription("quartz测试定制化定时任务").withIdentity(TRIGGER_KEY_NAME, Scheduler.DEFAULT_GROUP).build();
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(CRON_TIME);
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
scheduler.scheduleJob(jobDetail, cronTrigger);
logger.info("=========初始化定时任务加载完成=========");
} catch (Exception e) {
logger.info("==============初始化加载定时任务失败==============" + e);
e.printStackTrace();
}
}
}
如果是spring配置文件项目,则需要将SchedulerFactoryBean注入到spring中:
下面来详细讲解下这个springboot-quartz-demo里面是如何做的。还是围绕着功能点来看,如开头截图所示,主要有: 定时任务展示、新增、修改、暂停或启动、立刻运行、详情(监控),首先新建了三张表,如下:
分别用来记录-定时任务基础信息、执行情况、执行出错信息。然后是主要的执行定时任务逻辑编写,写在实现了Job接口的QuartzMainJobFactory类中:
这里设置了采用Http或Kafka的方式来执行定时任务里面的具体逻辑,方式可以自己扩展或替换,主要还是针对你的定时任务类型及现有的项目架构来具体选型。
1、展示和新增功能就不用说了,直接对quartz_task_informations表select和insert即可。
2、初始化加载,就是说,每次重启系统,要把之前建的定时任务加载进quartz的Scheduler调度器,他这里采用的就是@PostConstruct注解,在注入service前就先加载:
3、修改功能,这个要注意的是修改前一定要先暂停这个定时任务才能修改,然后就是注意下并发修改的情况,加入了乐观锁控制
4、实时暂停或启动定时任务,启动表示使定时任务立刻生效,就是把该定时任务加入任务调度系统中;而暂停本质就是从任务调度系统中删除此定时任务
5、立刻运行定时任务,这里使用到了AtomicInteger原子类来标识"立即运行定时任务"这个操作是否出现成功,关于AtomicInteger的一些知识,请移步至https://blog.csdn.net/fanrenxiang/article/details/80623884
6、监控详情,如上所说,quartz_task_records表和quartz_task_errors表分别用来记录定时任务每次执行情况和执行失败的定时任务信息,每当定时任务跑成功一次(不管成功与否)都持久化到quartz_task_records表,每失败一次(这里的标志就是AtomicInteger)持久化至quartz_task_errors表,这样每次执行的定时任务都能被比较好的"监控",这两个保存操作夹杂在"立即运行一次" 和 "QuartzMainJobFactory类" 代码中。
补充:
1、有小伙伴反馈说,当定时任务的执行时间超过了任务执行周期,这个正在执行的定时任务是什么状态?被抛弃杀死进程了?还是正常执行?关于这点,我在上面的项目测试了下,把定时任务的执行时间表达式改为"*/3 * * * * ?"每3秒执行一次定时任务,但是在任务执行逻辑中加入Thread.currentThread().sleep(5000);模拟定时任务执行时长要消耗5秒,测试结果是:超过3s的任务会继续执行,而下一个任务开始时间会变成这个任务执行完成后的5秒,依次类推,也就是说类似于时间表达式改为了"*/5 * * * * ?"的执行效果了。
2、我自己还想到的一个问题是: 当使用http方式调用定时任务逻辑时,如果接口逻辑过于复杂导致处理时间过长,或者可能是IO密集计算型任务,这个时候怎么办?还没验证,后期再补吧。
来波广告: 推荐个微信公众号:720电影社,里面可以免费获取电影、电视剧资源,欢迎关注。
quartz官网:http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/
springboot版本项目地址:https://github.com/simonsfan/springboot-quartz-demo
spring项目版本地址:https://github.com/justdojava/zx-quartz
推荐阅读:elastic search搜索引擎实战demo:https://github.com/simonsfan/springboot-quartz-demo,分支:feature_es
推荐阅读:分布式环境下保证定时任务单点运行:https://blog.csdn.net/fanrenxiang/article/details/79990386