Spring Boot 2.4 集成定时任务 Quartz 单机模式以及集群模式


1 摘要

Quartz 作为经典的定时任务框架,有这广泛的应用,支持集群模式。本文将介绍基于 Spring Boot 2.4 集成 Quart 单机模式和集群模式。

Quart 官方文档: http://www.quartz-scheduler.org/documentation

Quartz 数据库表脚本文件: 下载官网压缩包,解压后在 ./quartz-2.4.0-SNAPSHOT/src/org/quartz/impl/jdbcjobstore 目录下找到对应的数据库脚本

2 单机模式

2.1 核心 Maven 依赖

./demo-schedule-quartz/pom.xml
        <!-- Quartz 定时任务 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
            <version>${springboot.version}</version>
        </dependency>

其中 ${springboot.version} 的版本为 2.4.0

2.2 核心代码

2.2.1 定时执行的业务代码
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/service/UserService.java
package com.ljq.demo.springboot.quartz.service;

/**
 * @Description: 用户业务层接口
 * @Author: junqiang.lu
 * @Date: 2020/11/14
 */
public interface UserService {

    /**
     * 查询所有用户数量
     *
     * @return
     */
    int countAll();
}
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/service/impl/UserServiceImpl.java
package com.ljq.demo.springboot.quartz.service.impl;

import com.ljq.demo.springboot.quartz.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Random;

/**
 * @Description: 用户业务层实现类
 * @Author: junqiang.lu
 * @Date: 2020/11/14
 */
@Slf4j
@Service("userService")
@Transactional(rollbackFor = {Exception.class})
public class UserServiceImpl implements UserService {

    /**
     * 查询所有用户数量
     *
     * @return
     */
    @Override
    public int countAll() {
        int count = Math.abs(new Random().nextInt());
        log.debug("用户总数为: {}", count);
        return count;
    }
}

2.2.2 定时任务负载类
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/job/UserJob.java
package com.ljq.demo.springboot.quartz.job;

import com.ljq.demo.springboot.quartz.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Description: 用户模块工作负载
 * @Author: junqiang.lu
 * @Date: 2020/11/14
 */
@Slf4j
public class UserJob extends QuartzJobBean {

    @Autowired
    private UserService userService;

    private final AtomicInteger counts = new AtomicInteger();

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.debug("【定时任务】第【{}】次执行,用户总数:{}", counts.incrementAndGet(), userService.countAll());
    }
}
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/job/UserJob2.java
package com.ljq.demo.springboot.quartz.job;

import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
 * @Description: 用户工作负载2
 * @Author: junqiang.lu
 * @Date: 2020/11/14
 */
@Slf4j
public class UserJob2 extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.debug("------定时任务开始执行-------");
    }
}

2.2.3 定时任务配置类
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/common/config/QuartzScheduleConfig.java
package com.ljq.demo.springboot.quartz.common.config;

import com.ljq.demo.springboot.quartz.job.UserJob;
import com.ljq.demo.springboot.quartz.job.UserJob2;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description: Quartz 定时任务配置信息
 * @Author: junqiang.lu
 * @Date: 2020/11/14
 */
@Configuration
public class QuartzScheduleConfig {

    public static class UserJobConfig {

        /**
         * 工作负载名称
         */
        private static final String JOB_NAME = "userJob";
        /**
         * 触发器名称
         */
        private static final String TRIGGER_NAME = "userJobTrigger";

        @Bean
        public JobDetail userJob() {
            return JobBuilder.newJob(UserJob.class)
                    .withIdentity(JOB_NAME)
                    .storeDurably()
                    .build();
        }

        @Bean
        public Trigger userJobTrigger(){
            SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(5)
                    .repeatForever();
            return TriggerBuilder.newTrigger()
                    .forJob(JOB_NAME)
                    .withIdentity(TRIGGER_NAME)
                    .withSchedule(scheduleBuilder)
                    .build();
        }

    }

    public static class UserJob2Config {

        /**
         * 工作负载名称
         */
        private static final String JOB_NAME = "userJob2";
        /**
         * cron 表达式
         */
        private static final String CRON_EXP = "0/10 * * * * ? *";
        /**
         * 触发器名称
         */
        private static final String TRIGGER_NAME = "userJob2Trigger";

        @Bean
        public JobDetail userJob2() {
            return JobBuilder.newJob(UserJob2.class)
                    .withIdentity(JOB_NAME)
                    .storeDurably()
                    .build();
        }

        @Bean
        public Trigger userJob2Trigger() {
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(CRON_EXP);
            return TriggerBuilder.newTrigger()
                    .forJob(JOB_NAME)
                    .withIdentity(TRIGGER_NAME)
                    .withSchedule(scheduleBuilder)
                    .build();
        }

    }
}

2.2.4 SpringBoot 启动类
./demo-schedule-quartz/src/main/java/com/ljq/demo/springboot/quartz/DemoScheduleQuartzApplication.java
package com.ljq.demo.springboot.quartz;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author junqiang.lu
 */
@SpringBootApplication
public class DemoScheduleQuartzApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoScheduleQuartzApplication.class, args);
    }

}

2.3 application.yml 配置文件

./demo-schedule-quartz/src/main/resources/application.yml
## spring config
spring:
  quartz:
    scheduler-name: userSchedule
    job-store-type: memory
    auto-startup: true
    startup-delay: 1s
    wait-for-jobs-to-complete-on-shutdown: true
    overwrite-existing-jobs: false
    properties:
      org:
        quartz:
          threadPool:
            threadCount: 25
            threadPriority: 5
            class: org.quartz.simpl.SimpleThreadPool

配置信息参考官方文档: http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html

2.4 运行日志

2020-11-25 19:48:15 | DEBUG | userSchedule_Worker-1 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob
2020-11-25 19:48:15 | DEBUG | userSchedule_Worker-1 | c.l.d.s.quartz.service.impl.UserServiceImpl 28| 用户总数为: 2034175457
2020-11-25 19:48:15 | DEBUG | userSchedule_Worker-1 | com.ljq.demo.springboot.quartz.job.UserJob 27| 【定时任务】第【1】次执行,用户总数:2034175457
2020-11-25 19:48:18 | DEBUG | userSchedule_Worker-2 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob
2020-11-25 19:48:18 | DEBUG | userSchedule_QuartzSchedulerThread | org.quartz.core.QuartzSchedulerThread 291| batch acquisition of 1 triggers
2020-11-25 19:48:18 | DEBUG | userSchedule_Worker-2 | c.l.d.s.quartz.service.impl.UserServiceImpl 28| 用户总数为: 1515972786
2020-11-25 19:48:18 | DEBUG | userSchedule_Worker-2 | com.ljq.demo.springboot.quartz.job.UserJob 27| 【定时任务】第【1】次执行,用户总数:1515972786
2020-11-25 19:48:20 | DEBUG | userSchedule_QuartzSchedulerThread | org.quartz.core.QuartzSchedulerThread 291| batch acquisition of 1 triggers
2020-11-25 19:48:20 | DEBUG | userSchedule_Worker-3 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob2
2020-11-25 19:48:20 | DEBUG | userSchedule_Worker-3 | com.ljq.demo.springboot.quartz.job.UserJob2 17| ------定时任务开始执行-------
2020-11-25 19:48:23 | DEBUG | userSchedule_QuartzSchedulerThread | org.quartz.core.QuartzSchedulerThread 291| batch acquisition of 1 triggers
2020-11-25 19:48:23 | DEBUG | userSchedule_Worker-4 | org.quartz.core.JobRunShell 201| Calling execute on job DEFAULT.userJob
2020-11-25 19:48:23 | DEBUG | userSchedule_Worker-4 | c.l.d.s.quartz.service.impl.UserServiceImpl 28| 用户总数为: 807472199
2020-11-25 19:48:23 | DEBUG | userSchedule_Worker-4 | com.ljq.demo.springboot.quartz.job.UserJob 27| 【定时任务】第【1】次执行,用户总数:807472199

3 集群模式

3.1 核心 Maven 依赖

./demo-schedule-quartz-group/pom.xml
        <!-- Quartz 定时任务 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
            <version>${springboot.version}</version>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <version>${springboot.version}</version>
        </dependency>

其中 ${springboot.version} 的版本为 2.4.0

3.2 核心代码

3.2.1 定时任务业务代码

与单机模式一致

./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/service/UserService.java
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/service/impl/UserServiceImpl.java

3.2.2 定时任务负载类

基本与单机模式一致,但是在类上添加了 @DisallowConcurrentExecution 注解,意为禁止并发运行,从而保证了在集群环境中,定时任务一次只有一台服务器在运行

./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/job/UserJob.java
package com.ljq.demo.springboot.quartz.group.job;

import com.ljq.demo.springboot.quartz.group.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Description: 用户模块工作负载
 * @Author: junqiang.lu
 * @Date: 2020/11/14
 */
@Slf4j
@DisallowConcurrentExecution
public class UserJob extends QuartzJobBean {

    @Autowired
    private UserService userService;

    private final AtomicInteger counts = new AtomicInteger();

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.debug("【定时任务】第【{}】次执行,用户总数:{}", counts.incrementAndGet(), userService.countAll());
    }
}
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/job/UserJob2.java
package com.ljq.demo.springboot.quartz.group.job;

import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

/**
 * @Description: 用户工作负载2
 * @Author: junqiang.lu
 * @Date: 2020/11/14
 */
@Slf4j
@DisallowConcurrentExecution
public class UserJob2 extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.debug("------定时任务开始执行-------");
    }
}

3.2.3 定时任务配置类

与单机模式一致

./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/common/config/QuartzScheduleConfig.java

3.2.4 application.yml 配置文件

这里配置两个数据源,是为了将 Quartz 数据源和业务的数据源区分开, Quartz 使用独立的数据源效率更高

./demo-schedule-quartz-group/src/main/resources/application.yml
## config

## server
server:
  port: 8551

## spring config
spring:
  datasource:
    user:
      url: "jdbc:mysql://172.16.140.10:3306/demo?useUnicode=true&characterEncoding=utf8\
            &useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8\
            &useSSL=true&allowMultiQueries=true&autoReconnect=true&nullCatalogMeansCurrent=true\
            &nullCatalogMeansCurrent=true"
      username: root
      password: "Qwert12345!"
      driver-class-name: com.mysql.cj.jdbc.Driver

    quartz:
      url: "jdbc:mysql://172.16.140.10:3306/schedule_quartz?useUnicode=true&characterEncoding=utf8\
            &useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2b8\
            &useSSL=true&allowMultiQueries=true&autoReconnect=true&nullCatalogMeansCurrent=true\
            &nullCatalogMeansCurrent=true"
      username: root
      password: "Qwert12345!"
      driver-class-name: com.mysql.cj.jdbc.Driver
  quartz:
    scheduler-name: userSchedule
    job-store-type: jdbc
    auto-startup: true
    startup-delay: 1s
    wait-for-jobs-to-complete-on-shutdown: true
    overwrite-existing-jobs: false
    jdbc:
      initialize-schema: never
    properties:
      org:
        quartz:
          jobStore:
            dataSource: quartzDataSource
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true
            clusterCheckinInterval: 1000
            useProperties: false
          threadPool:
            threadCount: 25
            threadPriority: 5
            class: org.quartz.simpl.SimpleThreadPool

3.2.5 数据源配置类
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/common/config/DataSourceConfig.java
package com.ljq.demo.springboot.quartz.group.common.config;

import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.quartz.QuartzDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;

/**
 * @Description: 数据源配置
 * @Author: junqiang.lu
 * @Date: 2020/11/17
 */
@Configuration
public class DataSourceConfig {

    /**
     * 用户数据源配置(主数据源)
     *
     * @return
     */
    @Primary
    @Bean("userDatasourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.user")
    public DataSourceProperties userDataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * 用户数据源(主数据源)
     *
     * @return
     */
    @Primary
    @Bean("userDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.user.hikari")
    public DataSource userDataSource() {
        DataSourceProperties properties = this.userDataSourceProperties();
        return createHikariDataSource(properties);
    }

    /**
     * Quartz 数据源配置
     *
     * @return
     */
    @Bean("quartzDataSourceProperties")
    @ConfigurationProperties(prefix = "spring.datasource.quartz")
    public DataSourceProperties quartzDataSourceProperties() {
        return new DataSourceProperties();
    }

    /**
     * Quartz 数据源
     *
     * @return
     */
    @Bean("quartzDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.quartz.hikari")
    @QuartzDataSource
    public DataSource quartzDataSource() {
        DataSourceProperties properties = this.quartzDataSourceProperties();
        return createHikariDataSource(properties);
    }

    /**
     * 创建 Hikari 数据库连接池
     *
     * @param properties
     * @return
     */
    private HikariDataSource createHikariDataSource(DataSourceProperties properties) {
        HikariDataSource dataSource = properties.initializeDataSourceBuilder()
                .type(HikariDataSource.class)
                .build();
        if (StringUtils.hasText(properties.getName())) {
            dataSource.setPoolName(properties.getName());
        }
        return dataSource;
    }

}

@Primary 表名数据源为主数据源

@QuartzDataSource 指明该数据源为 Quartz 框架的数据源

3.2.6 SpringBoot 启动类

这里为了模拟集群工作环境,写两个 SpringBoot 启动类,端口号区分开

./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/DemoScheduleQuartzGroupApplication.java
package com.ljq.demo.springboot.quartz.group;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author apple
 */
@SpringBootApplication
public class DemoScheduleQuartzGroupApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoScheduleQuartzGroupApplication.class, args);
    }

}
./demo-schedule-quartz-group/src/main/java/com/ljq/demo/springboot/quartz/group/DemoScheduleQuartzGroupApplication2.java
package com.ljq.demo.springboot.quartz.group;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @Description: Quartz 定时任务集群模式应用启动类2
 * @Author: junqiang.lu
 * @Date: 2020/11/18
 */
@SpringBootApplication
public class DemoScheduleQuartzGroupApplication2 {

    public static void main(String[] args) {
        System.setProperty("server.port", "8552");
        SpringApplication.run(DemoScheduleQuartzGroupApplication2.class);
    }
}

依次将启动这两个程序,即可实现集群工作的效果,观察两个控制台的输出日志,会发现两边都会有日志输出,但是在同一时间只有一个定时任务在运行

3.2.7 手动创建定时任务

直接启动 SpringBoot 项目,程序会自动创建定时任务,但定时任务也可以通过手动的方式创建,可以选择是否覆盖已有任务

测试类

./demo-schedule-quartz-group/src/test/java/com/ljq/demo/springboot/quartz/group/common/config/QuartzScheduleConfigTest.java
package com.ljq.demo.springboot.quartz.group.common.config;

import com.ljq.demo.springboot.quartz.group.DemoScheduleQuartzGroupApplication;
import com.ljq.demo.springboot.quartz.group.job.UserJob;
import com.ljq.demo.springboot.quartz.group.job.UserJob2;
import org.junit.jupiter.api.Test;
import org.mockito.internal.util.collections.Sets;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * 定时任务配置测试类
 */
@SpringBootTest(classes = DemoScheduleQuartzGroupApplication.class)
class QuartzScheduleConfigTest {


    @Autowired
    private Scheduler scheduler;

    private static final String USER_JOB_DETAIL_NAME = "userJob001";
    private static final String USER_JOB_TRIGGER_NAME = "userJobTrigger001";

    private static final String USER_JOB_2_DETAIL_NAME = "userJob002";
    private static final String USER_JOB_2_TRIGGER_NAME = "userJob2Trigger002";

    private static final String USER_JOB_2_CRON = "0/10 * * * * ? *";

    /**
     * 手动添加用户定时任务配置
     *
     * @throws SchedulerException
     */
    @Test
    public void addUserJobConfig() throws SchedulerException {
        // 创建 JobDetail
        JobDetail jobDetail = JobBuilder.newJob(UserJob.class)
                .withIdentity(USER_JOB_DETAIL_NAME)
                .storeDurably()
                .build();
        // 创建 Trigger
        SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(5)
                .repeatForever();
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(USER_JOB_TRIGGER_NAME)
                .withSchedule(scheduleBuilder)
                .build();
        // 添加调度任务
        // 不覆盖已有任务
//        scheduler.scheduleJob(jobDetail, trigger);

        // 覆盖已有任务
        scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);
    }


    /**
     * 手动创建用户2定时任务
     *
     * @throws SchedulerException
     */
    @Test
    public void addUserJob2Config() throws SchedulerException {
        // 创建 JobDetail
        JobDetail jobDetail = JobBuilder.newJob(UserJob2.class)
                .withIdentity(USER_JOB_2_DETAIL_NAME)
                .storeDurably()
                .build();
        // 创建 Trigger
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(USER_JOB_2_CRON);
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withIdentity(USER_JOB_2_TRIGGER_NAME)
                .withSchedule(scheduleBuilder)
                .build();
        // 添加调度任务
        // 不覆盖已有任务
//        scheduler.scheduleJob(jobDetail, trigger);
        // 覆盖已有任务
        scheduler.scheduleJob(jobDetail, Sets.newSet(trigger), true);

    }

}

无论是手动创建还是自动创建,效果是一样的

4 推荐参考资料

Quartz 官方: http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/index.html

Spring Job?Quartz?XXL-Job

5 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/springBootDemo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值