分布式定时任务Quartz框架介绍

Quartz

之前在项目中用定时任务都是直接用的Spring的@Scheduled注解,因为遇到的业务都比较简单,比如定时更新榜单,支付功能中的定时查单,最近在写一个课堂助手的项目,要实现定时任务的业务对任务有增删改查的需求,@Scheduled已经无法满足了,so,我选择了Quartz

Quartz是OpenSymphony开源的一个项目,是一个由Java编写的开源作业调度框架。

  1. 支持分布式高可用
  2. 支持持久化,支持调度数据的多种存储方式
  3. 支持多任务调度和管理

存储方式

  • RAMJobStore
    • 不要外部数据库,配置容易,运行快
    • 调度程序信息存在内存中,当应用程序停止运行时,所有调度信息将丢失
    • 而且因为存在内存中,Job和Trigger的数量将会受到限制
  • JDBCJobStore
    • 支持集群
    • 所有任务信息都会保存在数据库中,不会因为程序停止运行丢失
    • 运行速度取决于数据库的速度

组件

Quartz的组成

JobDetail

Job相当于是线程池中的task,是定时任务真正业务逻辑的部分,Job需要封装成JobDetail,一个Job可以对应多JobDetail

通过JobBuilder创建

Trigger

触发器,定义触发时间,常用的有以下两种

  • SimleTrigger:用于实现简单的定时,比如定频率的执行某个任务
  • CronTrigger:配合Cron表达式使用,可以实现相对复杂的业务,比如到某个具体日期执行,每个月几号执行等

通过TriggerBuilder创建

Scheduler

调度器,帮我们把JobDetail和Trigger绑定在一起,按照Trigger中定义的触发时间去触发JobDetail中的Job

使用SchedulerFactory创建

  • DirectSchedulerFactory:需要在代码中定义一些属性,不常用
  • StdSchedulerFactory:从配置文件中获取配置,推荐使用
java 复制代码
//调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 这里简单获取了一个默认的,可以在配置文件中配置属性并获取
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();

任务每次执行Scheduler都会根据JobDetail创建一个新的Job实例,让任务并发执行,如果我们不想要这种特性,想要前一个任务执行完了才会执行下一个,可以使用@DisallowConcurrentExecution注解关闭

由于Scheduler每次执行都会根据JobDetail创建一个新的Job实例,jobDataMap属于JobDetail,那么每次也是一个新的,我们可以使用@PersistJobDataAfterExecution来让JobDataMap持久化

JobDataMap

我们可以看到JobDetail和Trigger中都有JobDataMap,jobDataMap可以用于在启动这个定时器时,往任务中传递一些参数

任务类获取参数的方式有两种

  • 通过获取jobDataMap再获取对应键值
  • 在任务类中定义相关字段,设置好set方法,框架会自动往里面设置值,如果JobDetail和Trigger里设置相同名称的话,JobDetail的会被覆盖掉
go 复制代码
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
        .withIdentity("job1","group1")
        .usingJobData("job","gwj's job") // 用来设置JobDetail中的JobDataMap中的值
        .usingJobData("name","jobdetail")
		.usingJobData("tong","jobde")
        .build();

Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1","triggerGroup1")
                .usingJobData("trigger","my trigger")  // 用来设置Trigger中的JobDataMap中的值
                .usingJobData("name","trigger")
				.usingJobData("tong","triggde")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1)
                        .repeatForever())
                .build();

任务类

java 复制代码
@Data
public class MyJob implements Job {
    private String name;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDataMap jobDetailMap = jobExecutionContext.getJobDetail().getJobDataMap(); // 获取JobDetail的JobDataMap
        JobDataMap triggerMap = jobExecutionContext.getTrigger().getJobDataMap(); // 获取Trigger的JobDataMap
        JobDataMap mergeMap = jobExecutionContext.getMergedJobDataMap();  // 获取混合JobDataMap
        System.out.println(mergeMap.get("job"));  // gwj's job
        System.out.println(mergeMap.get("trigger")); // my trigger
        System.out.println(mergeMap.get("tong"));  // triggde
        System.out.println(name); // trigger
        
        // 具体任务流程......
    }
}

整合SpringBoot

导入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

配置文件

yml 复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test_db
    username: root
    password: 111111
    type: com.alibaba.druid.pool.DruidDataSource
  servlet:
    multipart:
      max-file-size: 2048MB
      max-request-size: 2048MB

  # 定时任务配置
  quartz:
    # 数据库方式
    job-store-type: jdbc
    jdbc:
      initialize-schema: always # 数据库架构初始化模式
      # never:从不 always:每次都清空数据库初始化 embedded:只初始化内存数据库(默认)
    # quartz 相关属性配置
    properties:
      org:
        quartz:
          scheduler:
            instanceName: demoScheduler
            instanceId: AUTO
          jobStore:
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true
            clusterCheckinInterval: 10000
            useProperties: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true

创建表

Quartz支持对任务进行crud,可以使用mysql存储记录我们的任务

方式一

配置文件中配置initialize-schema: always,项目启动后我们就可以看到数据库中自动生成了11张表,后面再删掉或者改成never就行

方式二

可以在Quartz的jar包中找到对应数据库的SQL脚本,拷贝出来执行即可

表说明

表名 说明
qrtz_blob_triggers 以Blob 类型存储的触发器
qrtz calendars 存放日历信息,quartz可配置一个日历来指定一个时间范围
qrtz_cron triggers 存放cron类型的触发器
qrtz fired triggers 存放已触发的触发器
qrtz job _details 存放一个jobDetail信息
qrtz job listeners job监听器
qrtz_locks 存储程序的悲观锁的信息(假如使用了悲观锁)
qrtz_paused trigger_graps 存放暂停掉的触发器
qrtz scheduler state 调度器状态
qrtz simple triggers 简单触发器的信息
qrtz_trigger_listeners 触发器监听器

编写业务类

示例

java 复制代码
/**
 * 任务业务类,用于动态处理任务信息
 * @author gwj
 * @date 2024/11/29 下午2:17
 */
public interface JobService {


    /**
     * 任务数据
     */
    String TASK_DATA = "taskData";

    /**
     * 添加定时任务
     * @param jobClass
     * @param jobName
     * @param cron
     * @param data
     */
    void addCronJob(Class jobClass, String jobName, String cron, String data);

    /**
     * 添加立即执行的任务
     * @param jobClass
     * @param jobName
     * @param data
     */
    void addCronJob(Class jobClass, String jobName, String data);

    /**
     * 暂停任务
     * @param jobName
     * @param jobGroup
     */
    void pauseJob(String jobName, String jobGroup);

    /**
     * 恢复任务
     * @param triggerName
     * @param triggerGroup
     */
    void resumeJob(String triggerName, String triggerGroup);

    /**
     * 删除job
     * @param jobName
     * @param jobGroup
     */
    void deleteJob(String jobName, String jobGroup);
}
java 复制代码
/**
 * @author gwj
 */
@Slf4j
@Service
public class JobServiceImpl implements JobService {

    /**
     * Quartz定时任务核心的功能实现类
     */
    @Autowired
    private Scheduler scheduler;


    @Override
    public void addCronJob(Class jobClass, String jobName, String cron, String data) {


        String jobGroup = JobGroup.SYSTEM;

        // 自动命名
        if(StringUtils.isEmpty(jobName)){
            jobName = jobClass.getSimpleName().toUpperCase() + "_"+ IdWorker.getIdStr();
        }

        try {
            JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
            JobDetail jobDetail = scheduler.getJobDetail(jobKey);
            if (jobDetail != null) {
                log.info("++++++++++任务:{} 已存在", jobName);
                this.deleteJob(jobName, jobGroup);
            }

            log.info("++++++++++构建任务:{},{},{},{},{} ", jobClass.toString(), jobName, jobGroup, cron, data);

            //构建job信息
            jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).build();
            //用JopDataMap来传递数据
            jobDetail.getJobDataMap().put(TASK_DATA, data);

            //按新的cronExpression表达式构建一个新的trigger
            Trigger trigger = null;

            // 有表达式的按表达式
            if(!StringUtils.isEmpty(cron)){
                log.info("+++++表达式执行:"+ JSON.toJSONString(jobDetail));
                //表达式调度构建器
                CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
                trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup)
                        .withSchedule(scheduleBuilder).build();
            }else{
                // 无表达式则立即执行
                log.info("+++++立即执行:"+ JSON.toJSONString(jobDetail));
                trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).startNow().build();
            }

            scheduler.scheduleJob(jobDetail, trigger);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    @Override
    public void addCronJob(Class jobClass, String jobName, String data) {
        // 立即执行任务
        this.addCronJob(jobClass, jobName, null, data);
    }


    @Override
    public void pauseJob(String jobName, String jobGroup) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
            scheduler.pauseTrigger(triggerKey);
            log.info("++++++++++暂停任务:{}", jobName);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void resumeJob(String jobName, String jobGroup) {
        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
            scheduler.resumeTrigger(triggerKey);
            log.info("++++++++++重启任务:{}", jobName);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void deleteJob(String jobName, String jobGroup) {
        try {
            JobKey jobKey = JobKey.jobKey(jobName,jobGroup);
            scheduler.deleteJob(jobKey);
            log.info("++++++++++删除任务:{}", jobKey);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

测试

java 复制代码
/**
 * @author gwj
 * @date 2024/11/30 21:18
 */
@RestController
@RequestMapping("/test")
public class TestController {

    @Resource
    private JobService jobService;

    /**
     * 创建一个一分钟后执行的定时任务
     *
     * @param id
     * @return
     */
    @PostMapping
    public String addTask(Long id) {

        String jobName = JobPrefix.BREAK_EXAM + id;
        Date date = new Date();
        long l = date.getTime() + 1000 * 60;
        Date until = new Date(l);
        jobService.addCronJob(BreakExamJob.class,jobName, CronUtils.dateToCron(until),id+"");
        return "定时任务创建成功";
    }
}
相关推荐
极客先躯5 分钟前
高级java每日一道面试题-2024年12月03日-JVM篇-什么是Stop The World? 什么是OopMap? 什么是安全点?
java·jvm·安全·工作原理·stop the world·oopmap·safepoint
一只大侠30 分钟前
计算S=1!+2!+3!+…+N!的值:JAVA
java·开发语言
一只大侠33 分钟前
输入一串字符,以“?”结束。统计其中字母个数,数字个数,其它符号个数。:JAVA
java·开发语言·算法
以后不吃煲仔饭33 分钟前
面试小札:线程池
java·后端·面试
Oneforlove_twoforjob34 分钟前
【Java基础面试题011】什么是Java中的自动装箱和拆箱?
java·开发语言
优雅的落幕1 小时前
多线程---线程安全(synchronized)
java·开发语言·jvm
Charlie__ZS1 小时前
帝可得-设备管理
java·maven·intellij-idea·idea
爱上语文1 小时前
请求响应:常见参数接收及封装(数组集合参数及日期参数)
java·开发语言·spring boot·后端
孙同学_1 小时前
【Linux篇】权限管理 - 用户与组权限详解
java·linux·服务器
CQU_JIAKE1 小时前
926[study]Docker,DHCP
java·开发语言