前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >​分布式定时任务框架Quartz

​分布式定时任务框架Quartz

作者头像
乱敲代码
发布2020-05-21 14:27:39
4.7K0
发布2020-05-21 14:27:39
举报
文章被收录于专栏:Java系列文章Java系列文章

前言

项目中总要写那么几个定时任务来处理一些事情。一些简单的定时任务使用Spring自带的定时任务就能完成。但是如果需要大量的定时任务的话要怎么才能统一管理呢?

本文介绍Quartz分布式调度框架。

介绍

Quartz介绍

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,是完全由java开发的一个开源的任务日程管理系统。目前是 Terracotta 旗下的一个项目。官网地址 http://www.quartz-scheduler.org/ 可以 下载 Quartz 的发布版本及其源代码。

特点

  • 集成方便(完全使用Java编写)
  • 无需依赖可集群部署也可单机运行
  • 可以通过JVM独立运行

Job

创建一个任务只需要实现Job接口即可

触发器

  • 可以通过 Calendar 执行(排除节假日)
  • 指定某个时间无线循环执行 比如每五分钟执行一次
  • 固定时间执行 例如每周周一上午十点执行

一般情况使用SimpleTrigger,和CronTrigger,这些触发器实现了Trigger接口。或者 ScheduleBuilder 子类 SimpleScheduleBuilder和CronScheduleBuilder。

对于简单的时间来说,比如每天执行几次,使用SimpleTrigger。对于复杂的时间表达式来说,比如每个月15日上午几点几分,使用CronTrigger以及CromExpression 类。

注意 :一个job可以被多个Trigger 绑定,但是一个Trigger只能绑定一个job

存储

有两种存储方式 RAMJobStore和 JDBCJobStore 。

RAMJobStore不需要外部数据库调度信息存储在JVM内存中 所以,当应用程序停止运行时,所有调度信息将被丢失存储多少个Job和Trigger也会受到限制。

JDBCJobStore 支持集群所有触发器和job都存储在数据库中无论服务器停止和重启都可以恢复任务同时支持事务处理。

实战

准备

上面简单的介绍了一下Quartz,然后现在开始实战,本文使用SpringBoot整合。

项目地址:https://gitee.com/lqlm/toolsList_lqcoder

首先创建数据库表因为太多了就不放在文章中了可以去官方网站下载,也可以用我的下载地址下载

地址:https://lqcoder.com/quartz.sql

创建完成之后:

Table Name

Description

QRTZ_CALENDARS

存储Quartz的Calendar信息

QRTZ_CRON_TRIGGERS

存储CronTrigger,包括Cron表达式和时区信息

QRTZ_FIRED_TRIGGERS

存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息

QRTZ_PAUSED_TRIGGER_GRPS

存储已暂停的Trigger组的信息

QRTZ_SCHEDULER_STATE

存储少量的有关Scheduler的状态信息,和别的Scheduler实例

QRTZ_LOCKS

存储程序的悲观锁的信息

QRTZ_JOB_DETAILS

存储每一个已配置的Job的详细信息

QRTZ_JOB_LISTENERS

存储有关已配置的JobListener的信息

QRTZ_SIMPLE_TRIGGERS

存储简单的Trigger,包括重复次数、间隔、以及已触的次数

QRTZ_BLOG_TRIGGERS

Trigger作为Blob类型存储

QRTZ_TRIGGER_LISTENERS

存储已配置的TriggerListener的信息

QRTZ_TRIGGERS

存储已配置的Trigger的信息

本文统一使用Cron方式来创建。

注意:cron方式需要用到的4张数据表:qrtz_triggers,qrtz_cron_triggers,qrtz_fired_triggers,qrtz_job_details

整合项目

创建一个SpringBoot项目然后加入quartz依赖,同时也要加入c3p0的依赖因为quartz使用的数据库是和项目分开的。

代码语言:javascript
复制
  <!--spring boot集成quartz-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.0.2</version>
        </dependency>

同时在resources下创建quartz.properties文件内容

代码语言:javascript
复制
org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 10
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = myDS
org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql:数据库地址
org.quartz.dataSource.myDS.user = 数据库账号
org.quartz.dataSource.myDS.password = 数据库密码
org.quartz.dataSource.myDS.maxConnections = 连接数

然后创建一个Job类

代码语言:javascript
复制
/**
 * @author snluomeng
 * @date 2019/12/19 16:27
 */
@Slf4j
public class MyJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("==================开始执行任务==================");
    }
}

创建一个工具类然后进行定时任务的增删改

首先创建一个调度工厂

代码语言:javascript
复制
private static SchedulerFactory schedulerFactory = new StdSchedulerFactory();

添加定时任务

代码语言:javascript
复制
 public static void addJob(String jobName, String jobGroupName,
                              String triggerName, String triggerGroupName, Class jobClass, String cron) {
        try {
            Scheduler sched = schedulerFactory.getScheduler();
            // 任务名,任务组,任务执行类
//            Trigger.TriggerState state = sched.getTriggerState();

            JobDetail jobDetail=  JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName).build();
            // 触发器
            TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
            // 触发器名,触发器组
            triggerBuilder.withIdentity(triggerName, triggerGroupName);
            triggerBuilder.startNow();
            // 触发器时间设定
            triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
            // 创建Trigger对象
            CronTrigger trigger = (CronTrigger) triggerBuilder.build();

            // 调度容器设置JobDetail和Trigger
            sched.scheduleJob(jobDetail, trigger);

            // 启动
            if (!sched.isShutdown()) {
                sched.start();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

创建流程

通过工厂获取 Scheduler对象

Scheduler sched = schedulerFactory.getScheduler();

设置Job的实现类和一些静态信息

代码语言:javascript
复制
  //jobClass 设置Job的实现类
  //jobName Job名称
  //jobGroupName Job组名称
JobDetail jobDetail=  JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroupName).build();

构建触发器

代码语言:javascript
复制
// 触发器
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
 // 触发器名,触发器组
triggerBuilder.withIdentity(triggerName, triggerGroupName);
triggerBuilder.startNow();
// 触发器时间设定
triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
// 创建Trigger对象
CronTrigger trigger = (CronTrigger) triggerBuilder.build();

然后把Job和触发器都设置到Scheduler对象中

代码语言:javascript
复制
// 调度容器设置JobDetail和Trigger
sched.scheduleJob(jobDetail, trigger);

启动

代码语言:javascript
复制
// 启动
sched.start();

运行

因为使用的是SpringBoot项目所以就直接在启动类加入添加定时任务

参数分别为:JobName JsobgropName 中间省略 实现类,任务执行时间

代码语言:javascript
复制
QuartUtil.addJob("测试定时任务","test","测试定时任务","testTrigger",MyJob.class,"0/5 * * * * ?");

然后查看输出日志:

可以看到已经在执行了,现在我们去看一下数据库中的数据要查看的表有qrtz_triggers,qrtz_cron_triggers,qrtz_fired_triggers,qrtz_job_details

qrtz_job_details:

已经存在

现在把项目停止然后在重新启动会发生什么?

发现抛出了异常,因为我们已经添加过这个定时任务了所以重复添加是行不通的。

这时候我们直接启动即可。

同样封装启动方法

代码语言:javascript
复制
   public static void startJobs() {
        try {
            Scheduler sched = schedulerFactory.getScheduler();
            sched.start();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

启动所有定时任务非常简单直接获取Scheduler对象然后start即可。

修改定时任务

修改定时任务同样需要获取Scheduler对象,和添加流程基本一致,只不过最后不是调用的scheduleJob()而是调用的rescheduleJob()方法.有两种方式都需要指定定时器名称

  • 第一种是调用rescheduleJob()直接修改
  • 第二种是先删除然后在新增
代码语言:javascript
复制
  /**
     * @Description: 修改一个任务的触发时间
     *
     * @param jobName
     * @param jobGroupName
     * @param triggerName 触发器名
     * @param triggerGroupName 触发器组名
     * @param cron   时间设置,参考quartz说明文档
     */
    public static void modifyJobTime(String jobName,
                                     String jobGroupName, String triggerName, String triggerGroupName, String cron) {
        try {
            Scheduler sched = schedulerFactory.getScheduler();
            TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
            CronTrigger trigger = (CronTrigger) sched.getTrigger(triggerKey);
            if (trigger == null) {
                return;
            }

            String oldTime = trigger.getCronExpression();
            if (!oldTime.equalsIgnoreCase(cron)) {
                /** 方式一 :调用 rescheduleJob 开始 */
                // 触发器
                TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
                // 触发器名,触发器组
                triggerBuilder.withIdentity(triggerName, triggerGroupName);
                triggerBuilder.startNow();
                // 触发器时间设定
                triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
                // 创建Trigger对象
                trigger = (CronTrigger) triggerBuilder.build();
                // 方式一 :修改一个任务的触发时间
                sched.rescheduleJob(triggerKey, trigger);
                /** 方式一 :调用 rescheduleJob 结束 */

                /** 方式二:先删除,然后在创建一个新的Job  */
                //JobDetail jobDetail = sched.getJobDetail(JobKey.jobKey(jobName, jobGroupName));
                //Class<? extends Job> jobClass = jobDetail.getJobClass();
                //removeJob(jobName, jobGroupName, triggerName, triggerGroupName);
                //addJob(jobName, jobGroupName, triggerName, triggerGroupName, jobClass, cron);
                /** 方式二 :先删除,然后在创建一个新的Job */
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

删除任务

删除定时任务在修改的时候已经有实例.注意都需要指定任务名称 任务分组和触发器名称触发器分组

代码语言:javascript
复制
    /**
     * @Description: 移除一个任务
     *
     * @param jobName
     * @param jobGroupName
     * @param triggerName
     * @param triggerGroupName
     */
    public static void removeJob(String jobName, String jobGroupName,
                                 String triggerName, String triggerGroupName) {
        try {
            Scheduler sched = schedulerFactory.getScheduler();
            TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroupName);
            sched.pauseTrigger(triggerKey);// 停止触发器
            sched.unscheduleJob(triggerKey);// 移除触发器
            sched.deleteJob(JobKey.jobKey(jobName, jobGroupName));// 删除任务
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

传递参数

现在还有一个问题就是我想把参数传递到Job实现类里面咋整?

在添加定时任务时,创建JobDetail的时候有一个setJobData()方法参数为JobDataMap,看下JobBuilder源码

可以看到JobBuilder提供了setJobData方法传递的参数为JobDataMap是Map类型.

在创建定时任务的时候可以:

代码语言:javascript
复制
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("testKey","测试传递参数");
JobDetail jobDetail=  JobBuilder.newJob(jobClass).setJobData(jobDataMap).withIdentity(jobName, jobGroupName).build();

然后在Job实现类方法中直接取

代码语言:javascript
复制
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("==================开始执行任务==================");
        log.info("执行任务线程ID{}",Thread.currentThread().getId());
        JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
        log.info("参数为{}",jobDataMap.get("testKey"));
    }

JobDataMap可以直接当作Map进行操作.

单个参数可以使用usingJobData()来添加,参数为K V 取值方法一致,同时参数也是持久化到数据库的

如果需要查询管理的话可以直接查询数据库

原理解析

上面简单的介绍了一下怎么使用,那么你一定对它是怎么运行的感兴趣.接下来就分析一下Quartz到底是怎么实现的

注意到上面增删改都要先通过schedulerFactory工厂(工厂模式)来先获取Scheduler实例,现在就从第一步开始分析

本文就简单分析一下Scheduler工厂和添加定时任务这两步骤.

Scheduler工厂

代码语言:javascript
复制
    public Scheduler getScheduler() throws SchedulerException {
        //读取quartz配置文件,未指定则顺序遍历各个path下的quartz.properties文件
        if (this.cfg == null) {
            //如果为空就初始化
            this.initialize();
        }
        // 获取调度器池,采用了单例模式
        // 为了避免并发getInstance是synchronized加锁的
        SchedulerRepository schedRep = SchedulerRepository.getInstance();
        // 从调度器池中取出当前配置所用的调度器
        Scheduler sched = schedRep.lookup(this.getSchedulerName());
        if (sched != null) {
            if (!sched.isShutdown()) {
                return sched;
            }

            schedRep.remove(this.getSchedulerName());
        }
        // 如果调度器池中没有当前配置的调度器,则实例化一个调度器,主要动作包括:
        // 1)初始化threadPool(线程池):开发者可以通过org.quartz.threadPool.class配置指定使用哪个线程池类,比如SimpleThreadPool。先class load线程池类,接着动态生成线程池实例bean,然后通过反射,使用setXXX()方法将以org.quartz.threadPool开头的配置内容赋值给bean成员变量;
        // 2)初始化jobStore(任务存储方式):开发者可以通过org.quartz.jobStore.class配置指定使用哪个任务存储类,比如RAMJobStore。先class load任务存储类,接着动态生成实例bean,然后通过反射,使用setXXX()方法将以org.quartz.jobStore开头的配置内容赋值给bean成员变量;
        // 3)初始化dataSource(数据源):开发者可以通过org.quartz.dataSource配置指定数据源详情,比如哪个数据库、账号、密码等。jobStore要指定为JDBCJobStore,dataSource才会有效;
        // 4)初始化其他配置:包括SchedulerPlugins、JobListeners、TriggerListeners等;
        // 5)初始化threadExecutor(线程执行器):默认为DefaultThreadExecutor;
        // 6)创建工作线程:根据配置创建N个工作thread,执行start()启动thread,并将N个thread顺序add进threadPool实例的空闲线程列表availWorkers中;
        // 7)创建调度器线程:创建QuartzSchedulerThread实例,并通过threadExecutor.execute(实例)启动调度器线程;
        // 8)创建调度器:创建StdScheduler实例,将上面所有配置和引用组合进实例中,并将实例存入调度器池中
        sched = instantiate();
        return sched;
    }

添加定时任务

代码语言:javascript
复制
public Date scheduleJob(JobDetail jobDetail,
                            Trigger trigger) throws SchedulerException {
        // 检查调度器是否开启
        validateState();
        //参数校验省略
        if (jobDetail == null) {
            throw new SchedulerException("JobDetail cannot be null");
        }.....
        OperableTrigger trig = (OperableTrigger)trigger;
        //校验触发器参数
        if (trigger.getJobKey() == null) {
            trig.setJobKey(jobDetail.getKey());
        } else if (!trigger.getJobKey().equals(jobDetail.getKey())) {
            throw new SchedulerException(
                    "Trigger does not reference given job!");
        }
        trig.validate();
        Calendar cal = null;
        if (trigger.getCalendarName() != null) {
            cal = resources.getJobStore().retrieveCalendar(trigger.getCalendarName());
        }
        //获取时间
        Date ft = trig.computeFirstFireTime(cal);
        // 把job和trigger注册进调度器的jobStore
        resources.getJobStore().storeJobAndTrigger(jobDetail, trig);
        // 通知job监听者
        notifySchedulerListenersJobAdded(jobDetail);
        // 通知调度器线程
        notifySchedulerThread(trigger.getNextFireTime().getTime());
        // 通知trigger监听者
        notifySchedulerListenersSchduled(trigger);
        return ft;
    }
    public void validateState() throws SchedulerException {
        //如果关闭则抛出异常
        if (isShutdown()) {
            throw new SchedulerException("The Scheduler has been shutdown.");
        }
        // other conditions to check (?)
    }
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 乱敲代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 介绍
    • Quartz介绍
      • 特点
        • Job
          • 触发器
            • 存储
            • 实战
              • 准备
                • 整合项目
                • 添加定时任务
                  • 创建流程
                    • 运行
                    • 修改定时任务
                    • 删除任务
                    • 传递参数
                    • 原理解析
                      • Scheduler工厂
                      • 添加定时任务
                      相关产品与服务
                      对象存储
                      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档