分布式定时任务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 "定时任务创建成功";
    }
}
相关推荐
程序猿chen5 分钟前
JVM考古现场(二十五):逆熵者·时间晶体的永恒之战(进阶篇)
java·jvm·git·后端·程序人生·java-ee·改行学it
CopyLower13 分钟前
Spring Boot的优点:赋能现代Java开发的利器
java·linux·spring boot
细心的莽夫15 分钟前
Elasticsearch复习笔记
java·大数据·spring boot·笔记·后端·elasticsearch·docker
程序员阿鹏25 分钟前
实现SpringBoot底层机制【Tomcat启动分析+Spring容器初始化+Tomcat 如何关联 Spring容器】
java·spring boot·后端·spring·docker·tomcat·intellij-idea
大叔比较胖1 小时前
VSCode 用于JAVA开发的环境配置,JDK为1.8版本时的配置
java·ide·vscode·jdk·1.8
OpenTiny社区1 小时前
TinyVue v3.22.0 正式发布:深色模式上线!集成 UnoCSS 图标库!TypeScript 类型支持全面升级!
前端·vue.js·开源
种时光的人1 小时前
多线程出bug不知道如何调试?java线程几种常见状态
java·python·bug
小尹哥-程序员1 小时前
springboot2.X创建maven多模块工程
java·maven
web安全工具库1 小时前
Python内存管理之隔代回收机制详解
java·jvm·算法
大学生亨亨2 小时前
蓝桥杯之递归
java·笔记·算法·蓝桥杯