任务调度框架 Quartz

发布时间 2024-01-04 21:43:28作者: yanggdgg

一、定时任务概述

在 Java 中开发定时任务主要有三种解决方案:一是使用JDK 自带的 Timer,二是使用 Spring Task,三是使用第三方组件 ,如Quartz

Timer 是 JDK 自带的定时任务工具,其简单易用,但是对于复杂的定时规则无法满足,在实际项目开发中也很少使用到。而 Spring Task使用起来很简单,除 Spring 相关的包外不需要额外的包,而且支持注解和配置文件两种形式。 Quartz 功能强大,但是使用起来相对笨重。

建议:

  • 单体项目架构使用Spring Task

  • 分布式项目架构使用Quartz

1. Timer实现任务调度

/**
 * 基于jdk的任务调度
 */
public class JdkTaskDemo {
    public static void main(String[] args) {
        //创建定时类
        Timer timer = new Timer();
        //创建任务类
        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                System.out.println("定时任务执行了......"+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            }
        };
        //执行定时任务
        timer.schedule(task,new Date(),2000);
    }
}

2. Spring-task 实现任务调度

搭建SpringBoot工程,导入spring-boot-starter-web即可,不需导入任何其他依赖

在启动类上使用@EnableScheduling开启任务调度

@SpringBootApplication
@EnableScheduling
public class TaskStudyApplication {

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

编写任务类测试

@Component
public class SpringTask {
    @Scheduled(cron = "*/1 * * * * *")
    public void task1() throws InterruptedException {
        System.out.println(Thread.currentThread().getName()+":task1--->"+ LocalDateTime.now());
    }
}

注意:

  • Spring-task 执行任务按照单线程执行并合理执行,不会因为第一个执行任务时间过长而执行第二个

  • Spring-task是单线程的处理任务能力有限,不建议处理分布式架构的任务调度。

2.1 Cron表达式

关于 cronExpression 表达式有至少 6 个(也可能是 7 个)由空格分隔的时间元素。从左至右,这些元素的定义如下:

1.秒(0–59)

2.分钟(0–59)

3.小时(0–23)

4.月份中的日期(1–31)

5.月份(1–12 或 JAN–DEC)

6.星期中的日期(1–7 或 SUN–SAT)

7.年份(1970–2099)

0 0 10,14,16 * * ? 
    
每天上午 10 点,下午 2 点和下午 4 点 
0 0,15,30,45 * 1-10 * ? 
    
每月前 10 天每隔 15 分钟 
30 0 0 1 1 ? 2012 
    
在 2012 年 1 月 1 日午夜过 30 秒时 

在线生成cron表达式:https://cron.qqe2.com/

二、Quartz 

1. Quartz 介绍

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务,类似于java.util.Timer。但是相较于Timer, Quartz增加了很多功能:

  • 持久性作业 - 就是保持调度定时的状态;

  • 作业管理 - 对调度作业进行有效的管理;

官方文档:

Quartz的一些主要特性和概念:

  1. 作业(Job)和触发器(Trigger):

    • Quartz中的基本概念是作业和触发器。作业表示要执行的任务,触发器定义了作业执行的时间规则。

    • 触发器可以基于特定的时间规则(例如,每天凌晨执行一次)或特定的条件来触发作业。

  2. 调度器(Scheduler):

    • Quartz的核心组件是调度器,负责管理所有作业的调度和执行。

    • 调度器使用作业和触发器来配置和执行任务。

  3. 持久性和集群支持:

    • Quartz支持持久性存储,可以将作业和触发器的配置信息存储在数据库中,确保在应用程序重启后作业调度信息不丢失。

    • 还支持集群模式,在集群中多个调度器实例可以协同工作,提高可用性和扩展性。

  4. 监听器(Listener):

    • Quartz提供了监听器机制,允许用户在作业调度的不同阶段附加监听器来执行特定的逻辑,如在作业执行前后执行特定操作。

  5. 灵活的调度配置:

    • Quartz提供了丰富的配置选项,允许对作业和触发器进行灵活的配置,包括执行时间、执行频率、执行策略等。

  6. 异常处理和错过的任务处理:

    • Quartz提供了对任务执行期间的异常处理机制,还可以配置错过触发器的处理方式。

  7. 基于日历的调度:

    • 可以基于特定的日历规则来触发作业,比如排除节假日等。

2. JobDetail

JobDetail 的作用是绑定 Job,是一个任务实例,它为 Job 添加了许多扩展参数,用于定义和描述被调度的作业(Job)。

主要字段含义
name 任务名称
group 任务分组,默认分组DEFAULT
jobClass 要执行的Job实现类
jobDataMap 任务参数信息,JobDetail、Trigger都可以使用JobDataMap来设置一些参数或者信息

每次Scheduler调度执行一个Job的时候,首先会拿到对应的Job,然后创建该Job实例,再去执行Job中的execute()的内容,任务执行结束后,关联的Job对象实例会被释放,且会被JVM GC清除。

为什么设计成JobDetail + Job,不直接使用Job?

JobDetail 定义的是任务数据,而真正的执行逻辑是在Job中。

这是因为任务是有可能并发执行,如果Scheduler直接使用Job,就会存在对同一个Job实例并发访问的问题。

JobDetail & Job 方式,Sheduler每次执行,都会根据JobDetail创建一个新的Job实例,这样就可以 规避并发访问 的问题。

2.1 JobDataMap

JobDataMap是Quartz框架中的一个类,用于在JobDetailTrigger中传递参数和数据。它允许你将任意的数据关联到作业实例和触发器实例中,从而在作业执行时提供所需的上下文信息。

关于JobDataMapJobDetail的关系,下面是一些重要的概念:

  1. JobDetail中的JobDataMap:

    • 每个JobDetail对象都可以关联一个JobDataMap。这个JobDataMap用于存储与特定的作业实例相关联的数据。这些数据可以是任意的键值对,用于传递给作业实例的执行上下文信息。

  2. 传递参数给Job:

    • 当你调度一个JobDetail时,JobDataMap中的数据会被传递给作业实例。作业实例可以通过这个JobDataMap获取在调度时设置的参数。这为作业提供了一种方式来接收外部的配置和数据。

  3. Trigger中的JobDataMap:

    • 除了JobDetail中的JobDataMap,Quartz还允许你在Trigger中使用另一个JobDataMap。这个TriggerJobDataMap中的数据将与JobDetailJobDataMap合并,作为作业实例的完整上下文。

  4. 动态修改JobDataMap:

    • 你可以在运行时动态修改JobDataMap中的数据,以更新作业实例的上下文。这使得你可以在不修改JobDetailTrigger定义的情况下,灵活地调整作业的执行参数。

3. Trigger

3.1 SimpleTrigger

这是比较简单的一类触发器,用它能实现很多基础的应用。使用它的主要场景包括:

  • 在指定时间段内,执行一次任务

最基础的 Trigger 不设置循环,设置开始时间。

  • 在指定时间段内,循环执行任务

Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger2","group1")
                .startNow()
                .withSchedule(
                        //使用简单触发器
                        SimpleScheduleBuilder.simpleSchedule().
                        //3s间隔执行
                        withIntervalInSeconds(3).
                        //始终执行
                        repeatForever())
                        //执行6次 count+1 启动时也会执行一次,所以是6次
                        //.withRepeatCount(5))
                .build();

3.2 CronTrigger

CronTrigger 是基于日历的任务调度器,在实际应用中更加常用。虽然很常用,但是知识点都一样,只是可以通过表达式来设置时间而已。使用方式就是绑定调度器时换一下:

 

Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger2","group1")
                .startNow()
                .withSchedule(
                        //使用日历触发器
                        CronScheduleBuilder.cronSchedule("0/1 * * * * ? "))
                .build();

三、SpringBoot整合Quartz

引入依赖

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.60</version>
        </dependency>
    </dependencies>
pom.xml

配置application.yml

server:
  port: 80
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
  # 定时配置
  quartz:
    # 相关属性配置
    properties:
      org:
        quartz:
          # 数据源
          dataSource:
            globalJobDataSource:
              # URL必须大写
              URL: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
              driver: com.mysql.cj.jdbc.Driver
              maxConnections: 5
              username: root
              password: 123456
              # 必须指定数据源类型
              provider: hikaricp
          scheduler:
            instanceName: globalScheduler
            # 实例id
            instanceId: AUTO
            type: com.alibaba.druid.pool.DruidDataSource
          jobStore:
            # 数据源
            dataSource: globalJobDataSource
            # JobStoreTX将用于独立环境,提交和回滚都将由这个类处理
            class: org.quartz.impl.jdbcjobstore.JobStoreTX
            # 驱动配置
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            # 表前缀
            tablePrefix: QRTZ_
            # 失效阈值(只有配置了这个时间,超时策略根据这个时间才有效)
            misfireThreshold: 100
            # 集群配置
            isClustered: true
          # 线程池配置
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            # 线程数
            threadCount: 10
            # 优先级
            threadPriority: 5
View Code

这里面有quartz的数据源,线程池,集群和misfire相关配置,简单配置,更多的配置可以到官网查看。

http://www.quartz-scheduler.org/documentation/quartz-2.3.0/configuration/

配置application.properties 自动生成表

注意:第一次启动要配置,自动生成表,第二次启动不要配置,否则会覆盖先前生成的表

spring.quartz.jdbc.initialize-schema: always
spring.quartz.job-store-type: jdbc

实体类

@Data
public class JobInfo {
    /**
     * 任务名称
     */
    private String jobName;
    /**
     * 任务组
     */
    private String jobGroup;
    /**
     * 触发器名称
     */
    private String triggerName;
    /**
     * 触发器组
     */
    private String triggerGroup;
    /**
     * cron表达式
     */
    private String cron;
    /**
     * 类名
     */
    private String className;
    /**
     * 状态
     */
    private String status;
    /**
     * 下一次执行时间
     */
    private String nextTime;
    /**
     * 上一次执行时间
     */
    private String prevTime;
    /**
     * 配置信息(data)
     */
    private String config;
}
JobInfo.java

任务类

@DisallowConcurrentExecution
@PersistJobDataAfterExecution
@Slf4j
@Component
public class MyTask extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) {
        System.out.println("TimeEventJob正在执行..." + LocalDateTime.now());
        // 执行9秒
        try {
            Thread.sleep(9000);
            System.out.println("TimeEventJob执行完毕..." + LocalDateTime.now());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
View Code

说明:

Job 接口:

  • Job 是 Quartz 框架中定义的接口,用于表示要执行的任务。
  • 它有一个方法 execute(JobExecutionContext context),该方法定义了任务执行的逻辑。
  • 在实现 Job 接口的类中,你需要实现 execute 方法,并在其中编写你的任务逻辑。

QuartzJobBean 类:

  • QuartzJobBean 是 Spring 对 Quartz 提供的 Job 接口的一个实现,它充当了一个适配器的角色。
  • 它扩展了 Quartz 的 Job 类,并提供了对 Spring 管理 bean 的支持。
  • QuartzJobBean 提供了一个回调方法 executeInternal(JobExecutionContext context),它与 Job 接口中的 execute 方法类似,是实际任务逻辑的执行点。

@DisallowConcurrentExecution

  • 该注解用于防止同一个 JobDetail(任务细节)实例同时运行多个任务实例。

  • 如果一个任务的执行时间超过了其触发器的间隔时间,而另一个触发器试图启动同一个任务,@DisallowConcurrentExecution 会阻止并发执行,确保前一个任务完成后才能启动下一个。

@PersistJobDataAfterExecution

  • 该注解用于在每次任务执行后持久化 JobDataMap 中的数据。

  • JobDataMap 是 Quartz 中存储任务执行时所需数据的地方。通过使用 @PersistJobDataAfterExecution,你可以确保任务执行后,JobDataMap 中的数据保持不变,而不是在每次执行后都被清除。

JobHandler(任务的开关停删操作)

@Configuration
public class JobHandler {

    @Resource
    private Scheduler scheduler;

    /**
     * 添加任务
     */
    @SuppressWarnings("unchecked")
    public void addJob(JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
        Objects.requireNonNull(jobInfo, "任务信息不能为空");

        // 生成job key
        JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup());
        // 当前任务不存在才进行添加
        if (!scheduler.checkExists(jobKey)) {
            Class<Job> jobClass = (Class<Job>)Class.forName(jobInfo.getClassName());
            // 任务明细
            JobDetail jobDetail = JobBuilder
                    .newJob(jobClass)
                    .withIdentity(jobKey)
                    .withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup())
                    .withDescription(jobInfo.getJobName())
                    .build();
            // 配置信息
            jobDetail.getJobDataMap().put("config", jobInfo.getConfig());
            // 定义触发器
            TriggerKey triggerKey = TriggerKey.triggerKey(jobInfo.getTriggerName(), jobInfo.getTriggerGroup());
            // 设置任务的错过机制
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(triggerKey)
                    .withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getCron()).withMisfireHandlingInstructionDoNothing())
                    .build();
            scheduler.scheduleJob(jobDetail, trigger);
        } else {
            throw new SchedulerException(jobInfo.getJobName() + "任务已存在,无需重复添加");
        }
    }

    /**
     * 任务暂停
     */
    public void pauseJob(String jobGroup, String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        if (scheduler.checkExists(jobKey)) {
            scheduler.pauseJob(jobKey);
        }
    }

    /**
     * 继续任务
     */
    public void continueJob(String jobGroup, String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        if (scheduler.checkExists(jobKey)) {
            scheduler.resumeJob(jobKey);
        }
    }

    /**
     * 删除任务
     */
    public boolean deleteJob(String jobGroup, String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        if (scheduler.checkExists(jobKey)) {
            // 这里还需要先删除trigger相关
            //TriggerKey triggerKey = TriggerKey.triggerKey(jobInfo.getTriggerName(), jobInfo.getTriggerGroup());
            //scheduler.getTrigger()
            //scheduler.rescheduleJob()
            return scheduler.deleteJob(jobKey);
        }
        return false;
    }

    /**
     * 获取任务信息
     */
    public JobInfo getJobInfo(String jobGroup, String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
        if (!scheduler.checkExists(jobKey)) {
            return null;
        }
        List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);
        if (Objects.isNull(triggers)) {
            throw new SchedulerException("未获取到触发器信息");
        }
        TriggerKey triggerKey = triggers.get(0).getKey();
        Trigger.TriggerState triggerState = scheduler.getTriggerState(triggerKey);
        JobDetail jobDetail = scheduler.getJobDetail(jobKey);

        JobInfo jobInfo = new JobInfo();
        jobInfo.setJobName(jobGroup);
        jobInfo.setJobGroup(jobName);
        jobInfo.setTriggerName(triggerKey.getName());
        jobInfo.setTriggerGroup(triggerKey.getGroup());
        jobInfo.setClassName(jobDetail.getJobClass().getName());
        jobInfo.setStatus(triggerState.toString());

        if (Objects.nonNull(jobDetail.getJobDataMap())) {
            jobInfo.setConfig(JSONObject.toJSONString(jobDetail.getJobDataMap()));
        }

        CronTrigger theTrigger = (CronTrigger) triggers.get(0);
        jobInfo.setCron(theTrigger.getCronExpression());
        return jobInfo;
    }
}
JobHandler.java

Controller

@RestController
@RequestMapping("/job")
public class QuartzController {

    @Resource
    private JobHandler jobHandler;
    @Resource
    private Scheduler scheduler;

    /**
     * 查询所有的任务
     */
    @RequestMapping("/all")
    public List<JobInfo> list() throws SchedulerException {
        List<JobInfo> jobInfos = new ArrayList<>();
        List<String> triggerGroupNames = scheduler.getTriggerGroupNames();
        for (String triggerGroupName : triggerGroupNames) {
            Set<TriggerKey> triggerKeySet = scheduler
                    .getTriggerKeys(GroupMatcher.triggerGroupEquals(triggerGroupName));
            for (TriggerKey triggerKey : triggerKeySet) {
                Trigger trigger = scheduler.getTrigger(triggerKey);
                JobKey jobKey = trigger.getJobKey();
                JobInfo jobInfo = jobHandler.getJobInfo(jobKey.getGroup(), jobKey.getName());
                jobInfos.add(jobInfo);
            }
        }
        return jobInfos;
    }

    /**
     * 添加任务
     */
    @PostMapping("/add")
    public JobInfo addJob(@RequestBody JobInfo jobInfo) throws SchedulerException, ClassNotFoundException {
        jobHandler.addJob(jobInfo);
        return jobInfo;
    }

    /**
     * 暂停任务
     */
    @RequestMapping("/pause")
    public void pauseJob(@RequestParam("jobGroup") String jobGroup, @RequestParam("jobName") String jobName)
            throws SchedulerException {
        jobHandler.pauseJob(jobGroup, jobName);
    }

    /**
     * 继续任务
     */
    @RequestMapping("/continue")
    public void continueJob(@RequestParam("jobGroup") String jobGroup, @RequestParam("jobName") String jobName)
            throws SchedulerException {
        jobHandler.continueJob(jobGroup, jobName);
    }

    /**
     * 删除任务
     */
    @RequestMapping("/delete")
    public boolean deleteJob(@RequestParam("jobGroup") String jobGroup, @RequestParam("jobName") String jobName)
            throws SchedulerException {
        return jobHandler.deleteJob(jobGroup, jobName);
    }
}
QuartzController.java

四、开启服务自动执行任务

在JobHandler.java中 增加init方法,并为方法添加@PostConstruct注解,然后在init方法中调用添加任务的方法。

 

@PostConstruct 是 Java EE(现在更名为 Jakarta EE)规范中定义的一个注解,用于指定在依赖注入完成之后需要执行的方法。

具体来说,@PostConstruct 注解标注的方法将会在对象创建后,但在依赖注入完成之后被调用。这使得开发者可以执行一些在对象初始化之后需要进行的操作。

主要作用包括:

  初始化操作: 通过 @PostConstruct 注解,你可以在对象创建后执行一些初始化逻辑。这对于那些需要在对象被完全构建之后执行的操作很有用。

  依赖注入完成后的处理: 通常,@PostConstruct 方法用于确保依赖注入已经完成,可以在这个方法中执行那些需要依赖注入值的操作。

五、相关问题

1. 单线程与多线程任务调度的区别

单线程运行任务不同任务之间串行,任务A运行时间会响应任务B运行间隔,这是我们不想看到的。

多线程任务调度直接不互相影响,因为使用不同的线程执行任务。

2. 任务调度持久化的好处

如果任务调度没有持久化,而任务又是基于动态设置,不是开机自启的,会有一个问题,服务重启之后设置的任务都会失效了。如果任务整合持久化之后,设置的动态任务信息就会保存到数据库,开机自启就会加载这些数据库信息,就会按照原来的设置运行任务。

注意第二次启动要把自动生成表的配置关掉。

3. Quartz 集群执行与单机执行区别

Quartz是一个开源的作业调度框架,用于在Java应用程序中调度任务。Quartz集群和非集群的区别主要体现在以下几个方面:

  1. 高可用性:Quartz集群可以提供高可用性,即使其中一个节点出现故障,其他节点仍然可以继续工作。而非集群模式下,如果应用程序所在的服务器出现故障,任务调度将会停止。

  2. 负载均衡:Quartz集群可以通过将任务分配给不同的节点来实现负载均衡。这意味着任务将在集群的各个节点上分布,从而提高系统整体的性能和吞吐量。非集群模式下,所有的任务将在单个节点上运行,可能会导致性能瓶颈。

  3. 数据共享:Quartz集群可以共享任务调度的数据,包括作业和触发器等。这意味着当一个节点添加或删除任务时,其他节点也能够感知到。非集群模式下,每个节点都有自己独立的任务调度数据,可能导致数据不一致。

需要注意的是,Quartz集群需要配置和管理多个节点,可能需要更多的系统资源和维护工作。非集群模式则相对简单,适用于小规模的应用程序。选择使用哪种模式应根据具体的需求和系统要求来决定。