使用Quartz实现分布式定时任务(包含管理界面)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/fanrenxiang/article/details/85539918

引言

年底出去面试的的时候,被问到如下问题: 定时任务是如何实现的?定时任务是如何监控的?因为我们项目中的定时任务就是使用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

关于quartz,可在官网http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/查看,这里着重要说的是其中几个比较重要的:

1、maven引quartz依赖

<!--quartz依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2、quartz核心组件包括

  • Scheduler:与quartz schedule交互的主要api
  • Job:Scheduler执行组件需要实现的接口
  • JobDetail:用于定义实现了Job接口的实例
  • Trigger:用于执行给定作业的计划的组件
  • JobBuilder:用于构建JobDetail实例,或者说定义Job的实例
  • TriggerBuilder:用于构建触发器实例

使用示例代码:

/**
 * 实现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中:

 <bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean" />

核心功能

下面来详细讲解下这个springboot-quartz-demo里面是如何做的。还是围绕着功能点来看,如开头截图所示,主要有: 定时任务展示、新增、修改、暂停或启动、立刻运行、详情(监控),首先新建了三张表,如下:

分别用来记录-定时任务基础信息、执行情况、执行出错信息。然后是主要的执行定时任务逻辑编写,写在实现了Job接口的QuartzMainJobFactory类中:

定时任务主要逻辑类

这里设置了采用Http或Kafka的方式来执行定时任务里面的具体逻辑,方式可以自己扩展或替换,主要还是针对你的定时任务类型及现有的项目架构来具体选型。

books 1、展示和新增功能就不用说了,直接对quartz_task_informations表select和insert即可。

books 2、初始化加载,就是说,每次重启系统,要把之前建的定时任务加载进quartz的Scheduler调度器,他这里采用的就是@PostConstruct注解,在注入service前就先加载:

初始化加载定时任务代码简图

books 3、修改功能,这个要注意的是修改前一定要先暂停这个定时任务才能修改,然后就是注意下并发修改的情况,加入了乐观锁控制

修改定时任务代码简图

books 4、实时暂停或启动定时任务,启动表示使定时任务立刻生效,就是把该定时任务加入任务调度系统中;而暂停本质就是从任务调度系统中删除此定时任务

暂停或启动定时任务代码简图

books 5、立刻运行定时任务,这里使用到了AtomicInteger原子类来标识"立即运行定时任务"这个操作是否出现成功,关于AtomicInteger的一些知识,请移步至https://blog.csdn.net/fanrenxiang/article/details/80623884

立即运行一次定时任务代码简图

books 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电影社,里面可以免费获取电影、电视剧资源,欢迎关注。


books quartz官网:http://www.quartz-scheduler.org/documentation/quartz-2.2.x/tutorials/

books springboot版本项目地址:https://github.com/simonsfan/springboot-quartz-demo

books spring项目版本地址:https://github.com/justdojava/zx-quartz

books 推荐阅读:elastic search搜索引擎实战demo:https://github.com/simonsfan/springboot-quartz-demo,分支:feature_es

books 推荐阅读:分布式环境下保证定时任务单点运行:https://blog.csdn.net/fanrenxiang/article/details/79990386

猜你喜欢

转载自blog.csdn.net/fanrenxiang/article/details/85539918