分布式定时任务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 "定时任务创建成功";
    }
}
相关推荐
今天背单词了吗98015 分钟前
算法学习笔记:8.Bellman-Ford 算法——从原理到实战,涵盖 LeetCode 与考研 408 例题
java·开发语言·后端·算法·最短路径问题
天天摸鱼的java工程师18 分钟前
使用 Spring Boot 整合高德地图实现路线规划功能
java·后端
东阳马生架构33 分钟前
订单初版—2.生单链路中的技术问题说明文档
java
咖啡啡不加糖1 小时前
暴力破解漏洞与命令执行漏洞
java·后端·web安全
风象南1 小时前
SpringBoot敏感配置项加密与解密实战
java·spring boot·后端
DKPT1 小时前
Java享元模式实现方式与应用场景分析
java·笔记·学习·设计模式·享元模式
ajassi20001 小时前
开源 C# .net mvc 开发(八)IIS Express轻量化Web服务器的配置和使用
linux·开源·c#·mvc·.net
Percep_gan1 小时前
idea的使用小技巧,个人向
java·ide·intellij-idea
缘来是庄1 小时前
设计模式之迭代器模式
java·设计模式·迭代器模式
HelloGitHub1 小时前
从被喷“假开源”到登顶 GitHub 热榜,这个开源项目上演王者归来!
开源·github