前言

上一篇文章中我们介绍了Quartz的架构及单体应用的实现,有需要的朋友可以去看一下SpringBoot Quartz架构及单体应用介绍,有的同学可能说了,Quartz官网提供的是分布式的数据表结构,而且目前比较流行的也是分布式开发,一个程序可能通过Nginx负载均衡部署在不同的机器上,这时候就会出现一个任务在不同的机器上都会执行。这是不对的,今天我们就来解决这个问题,依据官网的介绍,我们来实现一下Quartz集群部署及分布式任务调度系统的实现。

Quartz集群架构

Quartz 是 Java 领域最著名的开源任务调度工具,在单体环境中应用,Quartz 未必是最好的选择,例如Spring Scheduled一样也可以实现任务调度,并且与SpringBoot无缝集成,支持注解配置,非常简单,但是它有个缺点就是在集群环境下,会导致任务被重复调度。

与之对应的 Quartz 提供了极为广用的特性,如任务持久化、集群部署和分布式调度任务等等,正因如此,基于 Quartz 任务调度功能在系统开发中应用极为广泛。

在集群环境下,Quartz 集群中的每个节点是一个独立的 Quartz 应用,没有负责集中管理的节点,而是通过数据库表来感知另一个应用,利用数据库锁的方式来实现集群环境下进行并发控制,每个任务当前运行的有效节点有且只有一个。

java 集群任务调度 java quartz 集群_java 集群任务调度


特别需要注意的是:分布式部署时需要保证各个节点的系统时间一致

数据表初始化

数据库表结构官网已经提供,我们可以直接访问Quartz对应的官方网站,找到对应的版本,然后将其下载

Quartz下载连接

java 集群任务调度 java quartz 集群_分布式_02


本次我们使用的是quartz-2.3.0-distribution.tar.gz,下载完成之后解压,在文件中搜索sql,我们使用的是MySQL数据库,所以选择文件tables_mysql_innodb.sql,目前官网下载已经不包含此文件了,大家可以去之前的版本查找,或者在整合的过程中程序自己创建。

文件内容如下:

DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;

CREATE TABLE QRTZ_JOB_DETAILS(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_CRON_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SIMPROP_TRIGGERS(          
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13,4) NULL,
DEC_PROP_2 NUMERIC(13,4) NULL,
BOOL_PROP_1 VARCHAR(1) NULL,
BOOL_PROP_2 VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_BLOB_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_CALENDARS (
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (SCHED_NAME,CALENDAR_NAME))
ENGINE=InnoDB;

CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;

CREATE TABLE QRTZ_FIRED_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID))
ENGINE=InnoDB;

CREATE TABLE QRTZ_SCHEDULER_STATE (
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME))
ENGINE=InnoDB;

CREATE TABLE QRTZ_LOCKS (
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME))
ENGINE=InnoDB;

CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);

具体表描述如下:

表名

描述

QRTZ_BLOG_TRIGGERS

Trigger作为Blob类型存储

QRTZ_CALENDARS

存储Quartz的Calendar信息

QRTZ_CRON_TRIGGERS

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

QRTZ_FIRED_TRIGGERS

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

QRTZ_JOB_DETAILS

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

QRTZ_LOCKS

存储程序的悲观锁的信息

QRTZ_PAUSED_TRIGGER_GRPS

存储已暂停的Trigger组的信息

QRTZ_SCHEDULER_STATE

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

QRTZ_SIMPLE_TRIGGERS

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

QRTZ_SIMPROP_TRIGGERS

存储CalendarIntervalTrigger和DailyTimeIntervalTrigger两种类型的触发器

QRTZ_TRIGGERS

存储已配置的Trigger的信息

QRTZ_LOCKS 就是 Quartz 集群实现同步机制的行锁表

Quartz集群实践

创建项目,导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.17</version>
</dependency>

添加配置

server:
  port: 8088

spring:
  application:
    name: Quartz_001
  datasource:
  	driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/quartz?useSSL=false&serverTimezone=UTC&useUnicode=true&characterEncoding=utf8
    username: root
    password: 123456

创建quartz.yml配置文件

org:
  quartz:
    scheduler: #调度配置
      instanceName: SsmScheduler #调度器实例名称
      instanceId: AUTO  #调度器实例编号自动生成
      wrapJobExecutionInUserTransaction: false #是否在Quartz执行一个job前使用UserTransaction
    threadPool: #线程池配置
      class: org.quartz.simpl.SimpleThreadPool #线程池的实现类
      threadCount: 10 #线程池中的线程数量
      threadPriority: 5 #线程优先级
      threadsInheritContextClassLoaderOfInitializingThread: true #配置是否启动自动加载数据库内的定时任务,默认true
#      makeThreadsDaemons: true #是否设置为守护线程,设置后任务将不会执行
    jobStore: #持久化方式配置
      useProperties: true #JobDataMaps是否都为String类型
      tablePrefix: QRTZ_ #数据表的前缀,默认QRTZ_
      misfireThreshold: 60000 #最大能忍受的触发超时时间
      isClustered: true #是否以集群方式运行
      clusterCheckinInterval: 2000 #调度实例失效的检查时间间隔,单位毫秒
      class: org.quartz.impl.jdbcjobstore.JobStoreTX #数据保存方式为数据库持久化
      driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate #数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
      dataSource: qzDS #数据库别名 随便取
    dataSource: #数据源配置
      qzDS:
        driver: com.mysql.cj.jdbc.Driver #数据库引擎
        URL: jdbc:mysql://localhost:3306/quartz?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true #数据库连接
        user: root #数据库用户
        password: 123456 #数据库密码
        maxConnection: 5 #允许最大连接
        validationQuery: select 0 from dual #验证查询sql,可以不设置
        connectionProvider: #数据库连接池,将其设置为druid
          class: com.example.cluster.quartz.config.DruidConnectionProvider

注册Quartz任务工厂

@Component
public class QuartzJobFactory extends AdaptableJobFactory {

    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        //调用父类的方法
        Object jobInstance = super.createJobInstance(bundle);
        //进行注入
        capableBeanFactory.autowireBean(jobInstance);
        return capableBeanFactory;
    }
}

注册调度工厂

@Configuration
public class QuartzConfig {

    @Autowired
    private QuartzJobFactory jobFactory;

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        //获取配置属性
        YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
        yamlPropertiesFactoryBean.setResources(new ClassPathResource("quartz.yml"));
        //在quartz.yml中的属性被读取并注入后再初始化对象
        yamlPropertiesFactoryBean.afterPropertiesSet();
        //创建SchedulerFactoryBean
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setQuartzProperties(yamlPropertiesFactoryBean.getObject());
        factory.setJobFactory(jobFactory); //支持在JOB实例中注入其他的业务对象
        factory.setApplicationContextSchedulerContextKey("applicationContextKey");
        //为true时,会等待所有已经启动的quartz job结束后spring才能完全shutdown。
        factory.setWaitForJobsToCompleteOnShutdown(true);
        factory.setOverwriteExistingJobs(false); //是否覆盖己存在的Job
        factory.setStartupDelay(10); //QuartzScheduler延时启动,应用启动完后QuartzScheduler再启动
        return factory;
    }

    /**
     * @Author fyy
     * @Description 通过SchedulerFactoryBean获取Scheduler的实例
     */
    @Bean(name = "scheduler")
    public Scheduler scheduler() throws Exception{
        Scheduler scheduler = schedulerFactoryBean().getScheduler();
        return scheduler;
    }

}

重新设置Quartz数据连接池

默认 Quartz 的数据连接池是 c3p0,由于性能不太稳定,不推荐使用,因此我们将其改成driud数据连接池,配置如下

public class DruidConnectionProvider implements ConnectionProvider {

    public String driver;
    public String URL;
    public String user;
    public String password;
    public int maxConnection;
    public String validationQuery;
    private boolean validateOnCheckout;
    private int idleConnectionValidationSeconds;
    public String maxCachedStatementsPerConnection;
    private String discardIdleConnectionsSeconds;
    public static final int DEFAULT_DB_MAX_CONNECTIONS = 10;
    public static final int DEFAULT_DB_MAX_CACHED_STATEMENTS_PER_CONNECTION = 120;

    private DruidDataSource dataSource;


    @Override
    public Connection getConnection() throws SQLException {
        return dataSource.getConnection();
    }

    @Override
    public void shutdown() throws SQLException {
        dataSource.close();
    }

    @Override
    public void initialize() throws SQLException {
        if(this.URL == null){
            throw new SQLException("数据库连接URL不能为空");
        }
        if(this.driver == null){
            throw new SQLException("数据库连接driver不能为空");
        }
        if(this.maxConnection < 0){
            throw new SQLException("数据库连接maxConnection必须大于0");
        }
        dataSource = new DruidDataSource();
        try{
            dataSource.setDriverClassName(this.driver);
        }catch (Exception e){
            try {
                throw new SchedulerException("设置数据库driver异常:" + e.getMessage(),e);
            } catch (SchedulerException ex) {
                ex.printStackTrace();
            }
        }
        dataSource.setUrl(this.URL);
        dataSource.setUsername(this.user);
        dataSource.setPassword(this.password);
        dataSource.setMaxActive(this.maxConnection);
        dataSource.setMinIdle(1);
        dataSource.setMaxWait(0);
        dataSource.setMaxPoolPreparedStatementPerConnectionSize(DEFAULT_DB_MAX_CONNECTIONS);

        if(this.validationQuery != null){
            dataSource.setValidationQuery(this.validationQuery);
            if(!this.validateOnCheckout){
                dataSource.setTestOnReturn(true);
            }else{
                dataSource.setTestOnBorrow(true);
            }
            dataSource.setValidationQueryTimeout(this.idleConnectionValidationSeconds);
        }

    }

    public String getDriver() {
        return driver;
    }

    public void setDriver(String driver) {
        this.driver = driver;
    }

    public String getURL() {
        return URL;
    }

    public void setURL(String URL) {
        this.URL = URL;
    }

    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getMaxConnection() {
        return maxConnection;
    }

    public void setMaxConnection(int maxConnection) {
        this.maxConnection = maxConnection;
    }

    public String getValidationQuery() {
        return validationQuery;
    }

    public void setValidationQuery(String validationQuery) {
        this.validationQuery = validationQuery;
    }

    public boolean isValidateOnCheckout() {
        return validateOnCheckout;
    }

    public void setValidateOnCheckout(boolean validateOnCheckout) {
        this.validateOnCheckout = validateOnCheckout;
    }

    public int getIdleConnectionValidationSeconds() {
        return idleConnectionValidationSeconds;
    }

    public void setIdleConnectionValidationSeconds(int idleConnectionValidationSeconds) {
        this.idleConnectionValidationSeconds = idleConnectionValidationSeconds;
    }

    public String getDiscardIdleConnectionsSeconds() {
        return discardIdleConnectionsSeconds;
    }

    public void setDiscardIdleConnectionsSeconds(String discardIdleConnectionsSeconds) {
        this.discardIdleConnectionsSeconds = discardIdleConnectionsSeconds;
    }

    public DruidDataSource getDataSource() {
        return dataSource;
    }

    public void setDataSource(DruidDataSource dataSource) {
        this.dataSource = dataSource;
    }
}

创建完成之后,还需要在quartz.yml配置文件中设置一下即可

...
connectionProvider: #数据库连接池,将其设置为druid
  class: com.fyy.quartz.config.DruidConnectionProvider

编写Job具体任务类

public class TfCommandJob implements Job {

    private static final Logger log = LoggerFactory.getLogger(TfCommandJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            System.out.println(context.getScheduler().getSchedulerInstanceId() + "--" + new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date()));
        } catch (SchedulerException e) {
            log.error("任务执行失败",e);
        }
    }
}

编写Quartz服务层接口

public interface QuartzJobService {

    /**
     * @Author fyy
     * @Description 添加任务可以传参数
     * @Date 2020/12/23 10:53
     */
    void addJob(String clazzName, String jobName, String groupName, String cronExp, Map<String, Object> param);

    /**
     * @Author fyy
     * @Description 暂停任务
     * @Date 2020/12/23 10:54
     */
    void pauseJob(String jobName, String groupName);

    /**
     * @Author fyy
     * @Description 恢复任务
     * @Date 2020/12/23 10:54
     */
    void resumeJob(String jobName, String groupName);

    /**
     * @Author fyy
     * @Description 立即运行一次定时任务
     * @Date 2020/12/23 10:55
     */
    void runOnce(String jobName, String groupName);

    /**
     * @Author fyy
     * @Description 更新任务
     * @Date 2020/12/23 10:55
     */
    void updateJob(String jobName, String groupName, String cronExp, Map<String, Object> param);

    /**
     * @Author fyy
     * @Description 删除任务
     * @Date 2020/12/23 10:55
     */
    void deleteJob(String jobName, String groupName);

    /**
     * @Author fyy
     * @Description 启动所有任务
     * @Date 2020/12/23 10:56
     */
    void startAllJobs();

    /**
     * @Author fyy
     * @Description 暂停所有任务
     * @Date 2020/12/23 10:56
     */
    void pauseAllJobs();

    /**
     * @Author fyy
     * @Description 恢复所有任务
     * @Date 2020/12/23 10:56
     */
    void resumeAllJobs();

    /**
     * @Author fyy
     * @Description 关闭所有任务
     * @Date 2020/12/23 10:57
     */
    void shutdownAllJobs();

}

接口实现类

@Service
public class QuartzJobServiceImpl implements QuartzJobService {

    private static final Logger log = LoggerFactory.getLogger(QuartzJobServiceImpl.class);
    @Autowired
    private Scheduler scheduler;

    @Override
    public void addJob(String clazzName, String jobName, String groupName, String cronExp, Map<String, Object> param) {
        try {
            //构建job信息
            Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(clazzName);
            JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName,groupName).build();
            //表达式调度构建器(即任务执行的时间)
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExp);
            //按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(jobName,groupName)
                    .withSchedule(scheduleBuilder)
                    .build();
            //获得JobDataMap,写入数据
            if(param != null){
                trigger.getJobDataMap().putAll(param);
            }
            scheduler.scheduleJob(jobDetail,trigger);
        } catch (Exception e) {
            log.error("创建任务失败",e);
        }
    }

    @Override
    public void pauseJob(String jobName, String groupName) {
        try {
            scheduler.pauseJob(JobKey.jobKey(jobName,groupName));
        } catch (SchedulerException e) {
            log.error("暂停任务失败",e);
        }
    }

    @Override
    public void resumeJob(String jobName, String groupName) {
        try {
            scheduler.resumeJob(JobKey.jobKey(jobName,groupName));
        } catch (SchedulerException e) {
            log.error("恢复任务失败",e);
        }
    }

    @Override
    public void runOnce(String jobName, String groupName) {
        try {
            scheduler.triggerJob(JobKey.jobKey(jobName,groupName));
        } catch (SchedulerException e) {
            log.error("立即运行一次定时任务失败",e);
        }
    }

    @Override
    public void updateJob(String jobName, String groupName, String cronExp, Map<String, Object> param) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName,groupName);
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            if(cronExp != null){
                // 表达式调度构建器
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExp);
                // 按新的cronExpression表达式重新构建trigger
                trigger = trigger.getTriggerBuilder()
                        .withIdentity(jobName,groupName)
                        .withSchedule(scheduleBuilder)
                        .build();
            }
            if(param != null){  //修改map
                trigger.getJobDataMap().putAll(param);
            }
            // 按新的trigger重新设置job执行
            scheduler.rescheduleJob(triggerKey,trigger);
        } catch (SchedulerException e) {
            log.error("更新任务失败",e);
        }
    }

    @Override
    public void deleteJob(String jobName, String groupName) {
        try {
            //暂停、移除、删除
            scheduler.pauseTrigger(TriggerKey.triggerKey(jobName,groupName));
            scheduler.unscheduleJob(TriggerKey.triggerKey(jobName,groupName));
            scheduler.deleteJob(JobKey.jobKey(jobName,groupName));
        } catch (SchedulerException e) {
            log.error("删除任务失败",e);
        }
    }

    @Override
    public void startAllJobs() {
        try {
            scheduler.start();
        } catch (SchedulerException e) {
            log.error("开启所有的任务失败",e);
        }
    }

    @Override
    public void pauseAllJobs() {
        try {
            scheduler.pauseAll();
        } catch (SchedulerException e) {
            log.error("暂停所有任务失败",e);
        }
    }

    @Override
    public void resumeAllJobs() {
        try {
            scheduler.resumeAll();
        } catch (SchedulerException e) {
            log.error("恢复所有任务失败",e);
        }
    }

    @Override
    public void shutdownAllJobs() {
        try {
            if(scheduler.isShutdown()){
                //scheduler生命周期结束,无法再 start() 启动scheduler
                scheduler.shutdown(true);
            }
        } catch (SchedulerException e) {
            log.error("关闭所有任务失败",e);
        }
    }
}

编写controller服务

  • 创建请求参数实体类
public class QuartzDto implements Serializable {

    private static final long serialVersionUID = 1L;
    private String jobName; //任务名称
    private String groupName; //任务所属组
    private String jobClass; //任务执行类
    private String cronExpression; //任务调度时间表达式
    private Map<String, Object> param; //附加参数

    public String getJobName() {
        return jobName;
    }

    public void setJobName(String jobName) {
        this.jobName = jobName;
    }

    public String getGroupName() {
        return groupName;
    }

    public void setGroupName(String groupName) {
        this.groupName = groupName;
    }

    public String getJobClass() {
        return jobClass;
    }

    public void setJobClass(String jobClass) {
        this.jobClass = jobClass;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public void setCronExpression(String cronExpression) {
        this.cronExpression = cronExpression;
    }

    public Map<String, Object> getParam() {
        return param;
    }

    public void setParam(Map<String, Object> param) {
        this.param = param;
    }
}
  • web服务接口
@RestController
@RequestMapping("/quartz")
public class QuartzController {

    private static Logger logger = LoggerFactory.getLogger(QuartzController.class);
    @Autowired
    private QuartzJobService quartzService;

    //添加新任务
    @RequestMapping("/addJob")
    public Object addJob(@RequestBody QuartzDto quartzDto){
        quartzService.addJob(quartzDto.getJobClass(),quartzDto.getJobName()
                ,quartzDto.getGroupName(),quartzDto.getCronExpression(),quartzDto.getParam());
        return HttpStatus.OK;
    }

    //暂停任务
    @RequestMapping("/pauseJob")
    public Object pauseJob(@RequestBody QuartzDto quartzDto){
        quartzService.pauseJob(quartzDto.getJobName(),quartzDto.getGroupName());
        return HttpStatus.OK;
    }

    //恢复任务
    @RequestMapping("/resumeJob")
    public Object resumeJob(@RequestBody QuartzDto quartzDto){
        quartzService.resumeJob(quartzDto.getJobName(),quartzDto.getGroupName());
        return HttpStatus.OK;
    }

    //立即运行一次定时任务
    @RequestMapping("/runOnceJob")
    public Object runOnceJob(@RequestBody QuartzDto quartzDto){
        quartzService.runOnce(quartzDto.getJobName(),quartzDto.getGroupName());
        return HttpStatus.OK;
    }

    //更新任务
    @RequestMapping("/updateJob")
    public Object updateJob(@RequestBody QuartzDto quartzDto){
        quartzService.updateJob(quartzDto.getJobName(),quartzDto.getGroupName()
                ,quartzDto.getCronExpression(),quartzDto.getParam());
        return HttpStatus.OK;
    }

    //删除任务
    @RequestMapping("/delJob")
    public Object delJob(@RequestBody QuartzDto quartzDto){
        quartzService.deleteJob(quartzDto.getJobName(),quartzDto.getGroupName());
        return HttpStatus.OK;
    }

    //启动所有任务
    @RequestMapping("/startAllJob")
    public Object startAllJob(){
        quartzService.startAllJobs();
        return HttpStatus.OK;
    }

    //暂停所有任务
    @RequestMapping("/pauseAllJob")
    public Object pauseAllJob(){
        quartzService.pauseAllJobs();
        return HttpStatus.OK;
    }

    //恢复所有任务
    @RequestMapping("/resumeAllJob")
    public Object resumeAllJob(){
        quartzService.resumeAllJobs();
        return HttpStatus.OK;
    }

    //关闭所有任务
    @RequestMapping("/shutdownAllJob")
    public Object shutdownAllJob(){
        quartzService.shutdownAllJobs();
        return HttpStatus.OK;
    }
    
}

注册监听器(选用)

  • 创建任务调度监听器
@Component
public class SimpleSchedulerListener extends SchedulerListenerSupport {
    @Override
    public void jobScheduled(Trigger trigger) {
        System.out.println("任务被部署时被执行");
    }

    @Override
    public void jobUnscheduled(TriggerKey triggerKey) {
        System.out.println("任务被卸载时被执行");
    }

    @Override
    public void triggerFinalized(Trigger trigger) {
        System.out.println("任务完成了它的使命,光荣退休时被执行");
    }

    @Override
    public void triggerPaused(TriggerKey triggerKey) {
        System.out.println(triggerKey + "(一个触发器)被暂停时被执行");
    }

    @Override
    public void triggersPaused(String triggerGroup) {
        System.out.println(triggerGroup + "所在组的全部触发器被停止时被执行");
    }

    @Override
    public void triggerResumed(TriggerKey triggerKey) {
        System.out.println(triggerKey + "(一个触发器)被恢复时被执行");
    }

    @Override
    public void triggersResumed(String triggerGroup) {
        System.out.println(triggerGroup + "所在组的全部触发器被回复时被执行");
    }

    @Override
    public void jobAdded(JobDetail jobDetail) {
        System.out.println("一个JobDetail被动态添加进来");
    }

    @Override
    public void jobDeleted(JobKey jobKey) {
        System.out.println(jobKey + "被删除时被执行");
    }

    @Override
    public void jobPaused(JobKey jobKey) {
        System.out.println(jobKey + "被暂停时被执行");
    }

    @Override
    public void jobsPaused(String jobGroup) {
        System.out.println(jobGroup + "(一组任务)被暂停时被执行");
    }

    @Override
    public void jobResumed(JobKey jobKey) {
        System.out.println(jobKey + "被恢复时被执行");
    }

    @Override
    public void jobsResumed(String jobGroup) {
        System.out.println(jobGroup + "(一组任务)被恢复时被执行");
    }

    @Override
    public void schedulerError(String msg, SchedulerException cause) {
        System.out.println("出现异常" + msg + "时被执行");
        cause.printStackTrace();
    }

    @Override
    public void schedulerInStandbyMode() {
        System.out.println("scheduler被设为standBy等候模式时被执行");
    }

    @Override
    public void schedulerStarted() {
        System.out.println("scheduler启动时被执行");
    }

    @Override
    public void schedulerStarting() {
        System.out.println("scheduler正在启动时被执行");
    }

    @Override
    public void schedulerShutdown() {
        System.out.println("scheduler关闭时被执行");
    }

    @Override
    public void schedulerShuttingdown() {
        System.out.println("scheduler正在关闭时被执行");
    }

    @Override
    public void schedulingDataCleared() {
        System.out.println("scheduler中所有数据包括jobs, triggers和calendars都被清空时被执行");
    }
}
  • 创建任务触发监听器
@Component
public class SimpleTriggerListener extends TriggerListenerSupport {

    @Override
    public String getName() {
        return "mySimpleTriggerListener";
    }

    //Trigger被激发 它关联的job即将被运行
    @Override
    public void triggerFired(Trigger trigger, JobExecutionContext context) {
        System.out.println("myTriggerListener.triggerFired()");
    }

    //Trigger被激发 它关联的job即将被运行,
    // TriggerListener 给了一个选择去否决 Job 的执行,
    // 如果返回TRUE 那么任务job会被终止
    @Override
    public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
        System.out.println("myTriggerListener.vetoJobExecution()");
        return false;
    }

    //当Trigger错过被激发时执行,比如当前时间有很多触发器都需要执行,
    // 但是线程池中的有效线程都在工作,
    //那么有的触发器就有可能超时,错过这一轮的触发。
    @Override
    public void triggerMisfired(Trigger trigger) {
        System.out.println("myTriggerListener.triggerMisfired()");
    }

    //任务完成时触发
    @Override
    public void triggerComplete(Trigger trigger, JobExecutionContext context, Trigger.CompletedExecutionInstruction triggerInstructionCode) {
        System.out.println("myTriggerListener.triggerComplete()");
    }
}
  • 创建任务执行监听器
@Component
public class SimpleJobListener extends JobListenerSupport {

    @Override
    public String getName() {
        return "mySimpleJobListener";
    }

    //任务被调度前
    @Override
    public void jobToBeExecuted(JobExecutionContext context) {
        System.out.println("simpleJobListener监听器,准备执行:"+context.getJobDetail().getKey());
    }

    //任务调度被拒了
    @Override
    public void jobExecutionVetoed(JobExecutionContext context) {
        System.out.println("simpleJobListener监听器,取消执行:"+context.getJobDetail().getKey());
    }

    //任务被调度后
    @Override
    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
        System.out.println("simpleJobListener监听器,执行结束:"+context.getJobDetail().getKey());
    }
}
  • 最后注册到scheduler 修改QuartzConfig.java
@Configuration
public class QuartzConfig {

    @Autowired
    private QuartzJobFactory jobFactory;
    @Autowired
    private SimpleSchedulerListener simpleSchedulerListener;
    @Autowired
    private SimpleTriggerListener simpleTriggerListener;
    @Autowired
    private SimpleJobListener simpleJobListener;

    ...
    
    /**
     * @Author fyy
     * @Description 通过SchedulerFactoryBean获取Scheduler的实例
     */
    @Bean(name = "scheduler")
    public Scheduler scheduler() throws Exception{
        Scheduler scheduler = schedulerFactoryBean().getScheduler();
        //全局添加监听器
        //添加SchedulerListener监听器
        scheduler.getListenerManager().addSchedulerListener(simpleSchedulerListener);
        // 添加triggerListener,设置全局监听
        scheduler.getListenerManager().addTriggerListener(simpleTriggerListener, EverythingMatcher.allTriggers());
        // 添加JobListener, 支持带条件匹配监听器
        scheduler.getListenerManager().addJobListener(simpleJobListener, KeyMatcher.keyEquals(JobKey.jobKey("myJob", "myGroup")));
        return scheduler;
    }

}

采集项目数据源(选用)

在上面的 Quartz 数据源配置中,我们使用了自定义的数据源,目的是和项目中的数据源实现解耦,当然有的同学不想单独建库,想和项目中数据源保持一致,配置也很简单

  • 在quartz.yml配置文件中,去掉org.quartz.jobStore.dataSource配置
#注释掉quartz的数据源配置
#org.quartz.jobStore.dataSource=qzDS
  • 在QuartzConfig配置类中加入dataSource数据源,并将其注入到quartz中
@Autowired
private DataSource dataSource;


@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
    //...

    SchedulerFactoryBean factory = new SchedulerFactoryBean();
    factory.setQuartzProperties(propertiesFactoryBean.getObject());
    //使用数据源,自定义数据源
    factory.setDataSource(dataSource);
    
    //...
    return factory;
}

任务调度测试

在实际开发部署中,项目都是集群部署的,所以我们可以把刚才的程序复制几份,修改一下端口分别启动,就可以模拟集群环境了。