Quartz引入

介绍

Quartz 是 Java 领域最著名的开源任务调度工具。

在上篇文章中,我们详细的介绍了 Quartz 的单体应用实践,如果只在单体环境中应用,Quartz 未必是最好的选择,例如Spring Scheduled一样也可以实现任务调度,并且与SpringBoot无缝集成,支持注解配置,非常简单,但是它有个缺点就是在集群环境下,会导致任务被重复调度!

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

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

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

数据表初始化

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

因为使用的是2.2.5.RELEASE版本的spring-boot,因此对应的quartz版本为2.3.2,通过quartz下载地址http://www.quartz-scheduler.org/downloads/

因为没有2.3.2版本的文件,因此我下载了quartz-2.3.0-distribution.tar.gz

下载完成之后将其解压,在文件中搜索sql,在里面选择适合当前环境的数据库脚本文件,然后将其初始化到数据库中即可!

因为使用是Mariadb,因此选用的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(190) NOT NULL,
JOB_GROUP VARCHAR(190) 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(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
JOB_NAME VARCHAR(190) NOT NULL,
JOB_GROUP VARCHAR(190) 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(190) 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(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) 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(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) 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(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) 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(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) 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(190) 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(190) 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(190) NOT NULL,
TRIGGER_GROUP VARCHAR(190) NOT NULL,
INSTANCE_NAME VARCHAR(190) 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(190) NULL,
JOB_GROUP VARCHAR(190) 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(190) 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);

commit;

具体表描述如下:

表名描述
QRTZ_BLOB_TRIGGERSTrigger作为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的信息

这里创建了一个名为quartz-config的数据库作为Quartz数据,将以上sql在该数据库运行。

Quartz 集群实践

引入和配置

1、引入

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.6</version>
</dependency>

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

2、在对应的application.yml中增加Quartz配置

spring:
  quartz:
    properties:
      org:
        quartz:
          dataSource:
            qzDS:
              #数据库连接
              URL: jdbc:mysql://127.0.0.1:3306/quartz-config?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
              #数据库连接池,将其设置为druid
              connectionProvider:
                class: com.starnet.aec.model.quartz.config.DruidConnectionProvider
              #数据库引擎
              driver: com.mysql.cj.jdbc.Driver
              #允许最大连接
              maxConnection: 10
              #数据库密码
              password: zhkjmysql@002396
              #数据库用户
              user: root
              #验证查询sql,可以不设置
              validationQuery: select 0 from dual
          # 10秒后延迟启动 这个很重要,必须要有足够长的时间让你的应用先启动完成后再让 Scheduler启动,
          startupDelay: 10s
          #持久化方式配置
          jobStore:
            #数据保存方式为数据库持久化
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            #调度实例失效的检查时间间隔,单位毫秒
            clusterCheckinInterval: 2000
            #数据库别名 随便取
            dataSource: qzDS
            #数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            #是否以集群方式运行
            isClustered: true
            #最大能忍受的触发超时时间
            misfireThreshold: 60000
            #数据表的前缀,默认QRTZ_
            tablePrefix: QRTZ_
            #JobDataMaps是否都为String类型
            useProperties: true
          #调度配置
          scheduler:
            #调度器实例编号自动生成
            instanceId: AUTO
            #调度器实例名称,不同微服务用不同的名字,已达到微服务隔离
            instanceName: ${spring.application.name}
            #是否在Quartz执行一个job前使用UserTransaction
            wrapJobExecutionInUserTransaction: false
          #线程池配置
          threadPool:
            #线程池的实现类
            class: org.quartz.simpl.SimpleThreadPool
            #线程池中的线程数量
            threadCount: 10
            #线程优先级
            threadPriority: 5
            #配置是否启动自动加载数据库内的定时任务,默认true
            threadsInheritContextClassLoaderOfInitializingThread: true

3、重新设置 Quartz 数据连接池

默认 Quartz 的数据连接池是 c3p0,由于性能不太稳定,不推荐使用,因此我们将其改成driud数据连接池,配置如下(这个类全类名要与配置中的connectionProvider一致):

public class DruidConnectionProvider implements ConnectionProvider {

    /**
     * 常量配置,与quartz.properties文件的key保持一致(去掉前缀),同时提供set方法,Quartz框架自动注入值。
     * @return
     * @throws SQLException
     */

    //JDBC驱动
    public String driver;
    //JDBC连接串
    public String URL;
    //数据库用户名
    public String user;
    //数据库用户密码
    public String password;
    //数据库最大连接数
    public int maxConnection;
    //数据库SQL查询每次连接返回执行到连接池,以确保它仍然是有效的。
    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;

    //Druid连接池
    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("DBPool could not be created: DB URL cannot be null");
        }

        if (this.driver == null) {
            throw new SQLException("DBPool driver could not be created: DB driver class name cannot be null!");
        }

        if (this.maxConnection < 0) {
            throw new SQLException("DBPool maxConnectins could not be created: Max connections must be greater than zero!");
        }

        datasource = new DruidDataSource();
        try{
            datasource.setDriverClassName(this.driver);
        } catch (Exception e) {
            try {
                throw new SchedulerException("Problem setting driver class name on datasource: " + e.getMessage(), e);
            } catch (SchedulerException e1) {
            }
        }

        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);
        }
    }

    // 省略了get set
}

基础服务编写

1、编写基础QuartzTaskJob

以后要开发Quartz的定时任务继承这个类就好了

public abstract class QuartzTaskJob implements Job {

    // 每interval秒执行一次
    private Integer interval;
    // 定时任务执行的cron表达式
    private String cronExp;
    // 额外参数,这个可以在定时任务中读取到的
    private Map<String, Object> param;

    private Logger log = LoggerFactory.getLogger(this.getClass());

    public final static String CRON_PER_SECONDS_EXEC = "0/%s * * * * ?";

    // 默认的间隔时间为10s
    private final static int DEFAULT_INTERVAL = 10;
    private final static String CRON_DEFAULT_INTERVAL = "0/10 * * * * ?";

    public QuartzTaskJob() {

    }

    public abstract void taskHandle(JobExecutionContext context);

    public String getQuartzCron() {

        // interval有值,就用interval的值
        if (interval != null) {
            return String.format(CRON_PER_SECONDS_EXEC, interval);
        }

        // interval没值,cronExp有值,就用cronExp
        if (cronExp != null) {
            return cronExp;
        }

        // 都没有值,用默认的每10s执行一次
        return CRON_DEFAULT_INTERVAL;
    }

    @Override
    public void execute(JobExecutionContext context) {

        log.debug("start task 【{}】", this.getClass().toString());

        try {
            taskHandle(context);
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            log.debug("end task 【{}】", this.getClass().toString());
        }

    }

    // 省略的get、set
}

2、编写和实现 Quartz 服务层接口

接口

public interface QuartzJobService {
    /**
     * 添加任务可以传参数
     */
   void addJob(QuartzTaskJob quartzTaskJob);

    /**
     * 暂停任务
     */
    void pauseJob(QuartzTaskJob quartzTaskJob);

    /**
     * 恢复任务
     */
    void resumeJob(QuartzTaskJob quartzTaskJob);

    /**
     * 立即运行一次定时任务
     */
    void runOnce(QuartzTaskJob quartzTaskJob);

    /**
     * 更新任务
     */
    void updateJob(QuartzTaskJob quartzTaskJob);

    /**
     * 删除任务
     */
    void deleteJob(QuartzTaskJob quartzTaskJob);
    void deleteJob(String jobName, String groupName);

    /**
     * 启动所有任务
     */
    void startAllJobs();

    /**
     * 暂停所有任务
     */
    void pauseAllJobs();

    /**
     * 恢复所有任务
     */
    void resumeAllJobs();

    /**
     * 关闭所有任务
     */
    void shutdownAllJobs();

}

实现

@Service
public class QuartzJobServiceImpl implements QuartzJobService {

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

    @Autowired
    Scheduler scheduler;

    @Value("${spring.application.name}")
    String applicationName;

    @Override
    public void addJob(QuartzTaskJob quartzTaskJob) {

        addJob(quartzTaskJob.getClass().getName(),
                quartzTaskJob.getClass().getName(),
                applicationName, quartzTaskJob.getQuartzCron(),
                quartzTaskJob.getParam());
    }

    private void addJob(String clazzName, String jobName, String groupName, String cronExp, Map<String, Object> param) {

        try {

            // 生成job key
            JobKey jobKey = JobKey.jobKey(jobName, groupName);
            if (scheduler.checkExists(jobKey)) {
                log.info("job {}, group {}, className {} has exist", jobName, groupName, clazzName);
                return;
            }

            // 启动调度器,默认初始化的时候已经启动
//            scheduler.start();
            //构建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(QuartzTaskJob quartzTaskJob) {

        pauseJob(quartzTaskJob.getClass().getName(), applicationName);
    }

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

    @Override
    public void resumeJob(QuartzTaskJob quartzTaskJob) {

        resumeJob(quartzTaskJob.getClass().getName(), applicationName);
    }

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

    @Override
    public void runOnce(QuartzTaskJob quartzTaskJob) {

        runOnce(quartzTaskJob.getClass().getName(), applicationName);
    }

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


    @Override
    public void updateJob(QuartzTaskJob quartzTaskJob) {

        updateJob(quartzTaskJob.getClass().getName(), applicationName, quartzTaskJob.getQuartzCron(), quartzTaskJob.getParam());
    }

    private  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(triggerKey).withSchedule(scheduleBuilder).build();
            }
            //修改map
            if (param != null) {
                trigger.getJobDataMap().putAll(param);
            }
            // 按新的trigger重新设置job执行
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (Exception e) {
            log.error("更新任务失败", e);
        }
    }

    @Override
    public void deleteJob(QuartzTaskJob quartzTaskJob) {

        deleteJob(quartzTaskJob.getClass().getName(), applicationName);
    }


    @Override
    public void deleteJob(String jobName, String groupName) {
        try {
            // 生成job key
            JobKey jobKey = JobKey.jobKey(jobName, groupName);
            if (!scheduler.checkExists(jobKey)) {
                log.info("job {}, group {} not exist, so exit delete job handle", jobName, groupName);
                return;
            }

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

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

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

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

    @Override
    public void shutdownAllJobs() {
        try {

            if (!scheduler.isShutdown()) {
                // 需谨慎操作关闭scheduler容器
                // scheduler生命周期结束,无法再 start() 启动scheduler
                scheduler.shutdown(true);
            }
        } catch (Exception e) {
            log.error("关闭所有的任务失败", e);
        }
    }
}

基础测试

1、编写测试Quartz定时任务

// 需要实现抽象类QuartzTaskJob,在抽象方法taskHandle中编写定时任务需要执行的代码
// 因为使用的是Spring.Boot2.x因此,可以直接使用@Autowired进行Bean的注入和使用
public class TestQuartJobTask extends QuartzTaskJob {

    @Override
    public void taskHandle(JobExecutionContext context) {

        log.info("test quartz job task, exec time {}", new Date());
    }
}

2、将新增定时任务到Quartz中

@Autowired
QuartzJobService quartzJobService;

@PostConstruct
public void init() {

    TestQuartJobTask quartzJobTask = new TestQuartJobTask();

    // 设置每多少秒执行一次
    // quartzJobTask.setInterval(10);
    // 设置定时执行的cron表达式
    // quartzJobTask.setCronExp("0/10 * * * * ?");
    // 如果都没有设置默认是每10秒执行一次!!!
    // 如果都设置了,以Interval为准!!!!!

    quartzJobService.addJob(quartzJobTask);
}

3、启动两个启用了Quartz定时任务的同名微服务

两个微服务(A1和A2)都会注册到Quartz调度中心中,先注册的微服务A1会进行定时任务的执行;

若此时将微服务A1关掉,那么微服务A2会接替A1进行定时任务的执行;

将A1重新开启,微服务A2还是继续执行定时任务;

将微服务A2关掉之后,微服务A1会接替进行定时任务的执行。

4、再启动一个启用了Quartz定时任务的不同名的微服务

微服务B与微服务A1和A2是不同名的微服务,微服务B启动之后会执行自己的Quartz定时任务,而微服务A1和A2也会根据3中规则执行自己的定时任务;

达到分布式微服务同一任务、同一时间只会有一个微服务执行,且不同微服务互相隔离的效果。

附录

Cron表达式

Cron表达式 是用来配置 CronTrigger 实例,它是一个由 7 个子表达式组成的字符串,每个字符都表示不同的日程细节:

  1. Seconds:秒
  2. Minutes:分钟
  3. Hours:小时
  4. Day-of-Month:月中的哪几天
  5. Month:月
  6. Day-of-Week:周中的哪几天
  7. Year:年
字段是否必填允许值可用特殊字符
0-59, - * /
0-59, - * /
小时0-23, - * /
月中的哪几天1-31, - * / ? L W C
1-12 或 JAN-DEC, - * /
周中的哪几天1-7 或 SUN-SAT, - * / ? L C #
不填写 或 1970-2099, - * /
  • 特殊符号
特殊符号含义
*可用在所有字段中,表示对应时间域的每一个时刻,例如,***** 在分钟字段时,表示“每分钟”
?该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符
-表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12
,表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五
/x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y
L该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值 X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五
W该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围
#该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发

Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐