Quartz学习

Quartz

1. 简介

1.1 quartz介绍

Quartz是一套轻量级的,基于java实现的任务调度框架任务调度框架,只需要定义了 Job(任务),Trigger(触发器)和 Scheduler(调度器),即可实现一个定时调度能力。支持基于数据库的集群模式,可做到任务幂等执行

1.2 使用场景

  • 电商平台达到促销时间时,修改商品价格
  • 信用卡每笔消费30天后的还款提醒
  • 每周五的周报提醒
  • 每月最后一天的月报提醒
  • … …


2. 入门

Quartz的模型:

  • 一个触发器只能调度一个任务,但是一个任务可以被多个触发器调度
  • 一个任务详情只能关联一个任务,但是一个任务可以被多个详情关联

在这里插入图片描述


2.1 入门案例

在这里插入图片描述

  • JobBuilder下newJob进行创建任务
    • HelloJob类需要实现Job接口
  • TriggerBuilder下newTrigger进行创建触发器
  • SimpleScheduleBuilder下simpleSchedule进行创建调度器

QuartzTest.class

package com.hyxs.share.quartz.first;

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

import java.util.concurrent.TimeUnit;


/**
 * @author 火云勰神
 * @date 2023-10-07 11:25
 * @description 第一个Quartz测试类
 */
public class QuartzTest {
    public static void main(String[] args) {
        try {
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            scheduler.start();

//            newJob由JobBuilder创建
            JobDetail job = JobBuilder.newJob(HelloJob.class)
                    .withIdentity("job1","group1")
                    .build();

//            newTrigger由TriggerBuilder创建
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1","group1")
                    .startNow()   //指定执行时间为当前
//                    simpleSchedule由SimpleScheduleBuilder创建
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
//                            创建一个简单的调度策略,每隔5秒执行一次
                                .withIntervalInSeconds(5)
                                .repeatForever())
                    .build();

            scheduler.scheduleJob(job,trigger);

            TimeUnit.SECONDS.sleep(20);

            scheduler.shutdown();
        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

HelloJob.class

  • 实现Job接口
    • 里面就execute一个方法
package com.hyxs.share.quartz.first;

import com.hyxs.share.quartz.utils.DateFormatUtil;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

import java.util.Date;

/**
 * @author 火云勰神
 * @date 2023-10-07 11:42
 * @description
 */
public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("HelloJob.execute " + " 时间: " + DateFormatUtil.getStrDate(new Date()) + " 当前线程:" +Thread.currentThread().getName());
    }
}

执行结果

在这里插入图片描述

  • 休眠20秒,每5秒执行一次,最终执行了五次是因为休眠开始前就已经执行了一次任务

2.2 触发器

在JobDetail和Trigger 的name和group中,group是用来标记的、方便管理的,name是用来标记的

  • group不做指定时,底层会默认给一个DEFAUT 的组名
  • name和group都不指定(如果就一个定时任务),同样能够运行,底层会默认生成一个MD5的name
2.2.1 触发器 任务之间的关系

一个触发器只能调度一个任务,但是一个任务可以被多个触发器调度

  • 触发器 ->任务 一对一
  • 任务 -> 触发器 一对多

在这里插入图片描述

QuartzTest2.java

public class QuartzTest2 {
    public static void main(String[] args) {
        try {
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            scheduler.start();

            JobDetail job = JobBuilder.newJob(HelloJob.class)
                    .withIdentity("job1","group1")
                    .build();

//            第一个触发器
            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity("trigger1","group1")
                    .startNow()
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                                .withIntervalInSeconds(1)
                                .repeatForever())
                    .build();

//            第二个触发器
            Trigger trigger2 = TriggerBuilder.newTrigger()
                    .withIdentity("trigger2","group1")
//                    如果是同样的调度方式,不能保证每次都能拿到job,因此使用forjob
                    .forJob("job1","group1")
                    .startNow()
                    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                                .withIntervalInSeconds(3)
                                .repeatForever())
                    .build();

            scheduler.scheduleJob(job,trigger);
            scheduler.scheduleJob(trigger2);

            TimeUnit.SECONDS.sleep(3);

            scheduler.shutdown();
        } catch (SchedulerException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

HelloJob.java

public class HelloJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        StringJoiner outStr = new StringJoiner(" ")
                .add("HelloJob.execute ")
                .add(" 时间: " + DateFormatUtil.getStrDate(new Date()))
                .add(" 当前线程:" +Thread.currentThread().getName())
                .add(" 触发器" + context.getTrigger().getKey().getName());
        System.out.println("outStr = " + outStr);
    }
}

执行结果:

在这里插入图片描述

2.2.2 CronExcepression

几类触发器:

  • CronTriggerImpl(重要!!)
  • SimpleTriggerImpl(上面用到的SimpleScheduleBuilder就是来自这个实现类)
  • CalendarIntervalTriggerImpl
  • DailyTimeIntervalTriggerImpl

cron规则:

在这里插入图片描述

  • ” * ” 字符用于指定所有值。例如,分钟宇段中表示”每分钟”。
  • “ ?” 字在 日和 星期字段允许使用。它用于指定"无特定值”。
    • 即不知道是什么,和"*"是不一样的
      • 例如,每个月15号 ,应该写成* * * 15 * ? *,理解为不知道每个月的15号是星期几,而不能写为"*"去让它任意匹配,到这一天星期几是固定的,只是不知道具体是星期几
  • “ - “ 字符用于指定范围。例如,小时字段中的”10-12”表示”小时10、11和12”。
  • “ ,” 字符用于指定其他值。例如,"星期几"字段中的“MON,WED,FRI"表示"星期一,星期三和星期五的日子”。
  • " / “ 字符用于指定增量。例如,秒字段中的"0/15"表示"秒0、15、30和45”。
    • 在”/"之前指定”*"等同于指定0为开头的值。本质上,对于表达式中的每个字段,都有一组可以打开或关闭的数字。
    • 对于秒和分钟,数字范围为0到59,对于小时0到23,对于每月的0到31,以及对于月0到11 JAN到DEC)。因此,"月"字段中的“7/6"仅打开“7"月,并不意味着每6个月一次,最大就到11,和只写一个7没有区别
  • “ L ” 字符可以在月”和“周”字段中使用。表示最后一天。
    • “月”字段中的值”L“表示”月的最后一天”,即非润年的1月31日,2月28日。
    • “星期”字段中,表示”7”或”SAT”(周六) ==> 最后一天是周六。
      • 如果 L与其他值配合使用,则表示"该月的最后一个xxx天"
        • 例如,"6L表示"该月的最后一个星期五”
    • 还可以指定与该月最后一天的偏移量
      • 例如“L-3”,这表示日历月的倒数第三天
    • 使用“L“时,不要指定列表或值的范围,会导致混淆
  • “ W ” 字符 在“日”字段中允许使用。此字符用于指定最接近给定日期的工作日(星期一至星期五)
    • 如指定”15W“,则含义是:“离月15日最近的工作日”。
      • 如果15号是星期六,那么触发器将在14号星期五触发,如果15日是星期日,则将在16日星期一触发
    • 如”1W“为指定月份的值,而这个月的第一天是星期六,则触发器将在第3天,星期一才触发,并且不会跳过月与月的边界,即不会再上个月的周五触发
  • 还可将“L“和”W“字符组合为一个月中的一天的表达式”LW”,这表示该月的最后一个工作日
  • ” # “ 字符在”星期”字段允许使用。此字符用于指定月份的"第n个”XXX天。
    • “星期"字段中的“6#3”的值表示该月的第三个星期五
    • 如果指定了不存在的星期数,该月将不会触发
      • 如某个月只有第五个星期三,那么 "4 # 5"就不会被触发
    • 如果使用“#”字符,则“星期几"字段中只能有一个表达式
      • (”3 #1,6#3")无效,因为有两个表达式
  • 法定字符以及月份和星期几的名称不区分大小写。
  • 年是可选的,即可写可不写

注:

  • 图例中月份的范围为0-11,实际使用范围1-12
    • 虽然在CronExpression底层的map集合定义月份时,是0-11(这与cron表达式的设计初衷有关系) 但是最后的使用是和当前的月份取值范围 1-12一致,因为后序的逻辑中有一个取月份值然后加一的动作
2.2.3 传入变量

JobDetail和Trigger传参可以使用和job相关的方法进行传参,如果job和trigger同时传入相同的key,将会以trigger为主

在这里插入图片描述

JobDetail job = JobBuilder.newJob(HelloJob.class)
                    .usingJobData("1","2")
                    .withIdentity("job1","group1")
                    .build();
  JobDataMap mergedJobDataMap = context.getMergedJobDataMap();
        String value = (String)mergedJobDataMap.get("1");
        System.out.println("value = " + value);
2.2.4 quartz配置文件

quartz的默认配置文件 quartz.properties,如果不指定配置文件,就会使用下面的默认配置文件

# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#

org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false

org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true

org.quartz.jobStore.misfireThreshold: 60000

org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore


具体可配置项官方文档已经给出:

文档地址


2.3 集成spring

方法一:

SpringJob

public class SpringJob extends QuartzJobBean {

    @Autowired
    private HelloSpringService helloSpringService;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        StringJoiner outStr = new StringJoiner(" ")
                .add("SpringJob.executeInternal ")
                .add(" 时间: " + DateFormatUtil.getStrDate(new Date()))
                .add(" 当前线程:" +Thread.currentThread().getName())
                .add(" helloService:" +helloSpringService.toString())
                .add(" 方法调用:" +helloSpringService.helloSpring());
        System.out.println("outStr = " + outStr);
    }
}

JobInit 初始化方法

@Component
public class JobInit {
    @Autowired
    public  Scheduler scheduler;

    /**
     * PostConstruct 注解的作用:
     * 在spring初始化前,完成某个对象的初始化
     * 如:通过Autowired注解相当于new 一个对象,但是当前想拿到一个有值的对象
     * 那么此时就可以通过PostConstruct提前初始化这个对象,其他地方进行调用时,就能拿到一个有值的对象
     */
    @PostConstruct
    public void initJob() throws SchedulerException {
//        任务详情
        JobDetail job = JobBuilder.newJob(SpringJob.class)
                .build();

//        触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .startNow()
                .build();

        scheduler.scheduleJob(job,trigger);
    }

}

service

@Service
public class HelloSpringService {

    public String helloSpring() {
        return "hello spring";
    }
}

除了上述的方式,spring还提供了另外一种

方法二:

  • JobDetail和Trigger的bean会自动与scheduler相关联
@Configuration
public class JobConfig {
    @Bean
    public JobDetail initJobDetail(){
        return JobBuilder.newJob(SpringJob.class)
                        .withIdentity("springJob")
//                这个方法保证当前的detail没被使用时也会存在,否则通过名称查找detail会报错
                        .storeDurably()
                        .build();
    }

    @Bean
    public Trigger initTrigger(){
        return TriggerBuilder.newTrigger()
                .forJob("springJob")
                .startNow()
                .build();
    }
}

2.4 持久化

​ quartz框架默认的是RAM存储,也就是基于内存的存储,它可以提供很快的访问速度,但是当服务停止,数据也就会丢失,因此需要去实现持久化

将任务信息存储在数据库中,好处:

  • 可以在数据库看到quartz当前有多少任务、trigger,并且方便的看出状态
  • 出现异常数据时,可以帮助定位问题

在这里插入图片描述

配置文件:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: 123456
  quartz:
    job-store-type: jdbc
    jdbc:
      initialize-schema: always

其中,jdbc:initialize-schema: always 的含义是,每次启动,都会执行脚本

可以在一下路径找到quartz的建表脚本

在这里插入图片描述

脚本的基本逻辑是 先删除表再建表,一共会创建11张表

表名称实际含义
QRTZ_CRON_TRIGGERS存储CRON类型的Trigger,包括名称、所属组、Cron表达式、时区信息
QRTZ_SIMPLE_TRIGGERS存储SIMPLE类型的Trigger,存储名称、分组、重复次数、执行时间间隔已经已经执行的次数。失火策略该表不存,存放于ORTZ_TRIGGER表中。
QRTZ_TRIGGERS存储所有的Trigger信息,包括名称、分组、类型(CRON或SIMPLE)、当前运行状态、上下次执行时间、MisFire策略
QRTZ_FIRED_TRIGGERS存储正在执行任务中的Trgigger。主要存储运行服务器节点ID、Trigger名称分组、触发器执行状态、触发与调度时间
QRTZ_PAUSED_TRIGGER_GRPS存储暂停的Trigger组
QRTZ_JOB_DETAILS存放JobDetail的具体信息。核心:JobName,JobGroup,描述,ClassName与是否独立存储等
QRTZ_LOCKS存储程序的悲观锁的信息
QRTZ_SCHEDULER_STATE存储调度器状态。上次检查时间与检测状态
QRTZ_CALENDARS存储Quartz的Calendar信息
QRTZ_BLOB_TRIGGERSTrigger做为Blob类型存储
QRTZ_SIMPROP_TRIGGERS触发器相关的表

多数据源的方式,如上图中第二个示例,spring可以使用如下的方式:

  @Bean
    @QuartzDataSource
    public DataSource quartzSource(){
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("");
        dataSource.setUsername("");
        dataSource.setUrl("");
        dataSource.setPassword("");
        return dataSource;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值