三种任务调度技术:Timer、Spring Task、Quartz

Java开发定时任务有三种方案:

1.JDK的Timer,简单易用,无法满足复杂的定时规则,很少用。

2.Spring Task,易用,支持注解和配置文件两种形式,适用于单体项目架构

3.第三方组件Quartz,功能强大,使用笨重,适用于分布式项目架构

JDK实现任务调度

public class JdkTaskTest {
    //使用main测试,test方法测试有个bug,方法运行直接结束,看不到定时任务的效果。
    public static void main(String[] args) {
        //创建Timer对象
        Timer timer = new Timer();
        //创建任务对象
        TimerTask timerTask = new TimerTask(){
            @Override
            public void run() {
                System.out.println("定时任务......");
            }
        };
        //执行定时任务timerTask,立刻执行,每两秒执行一次
        timer.schedule(timerTask,new Date(),2000);
    }
}

Spring-task实现任务调度

1.在IDEA中创建项目时,选择spring项目,并导入spring web依赖,再创建。

2.编写启动类,在启动类上添加 @EnableScheduling 注解,开启任务调度。

3.在main中创建任务类测试

@Component
public class MyTask {
    @Scheduled(cron = "*/1 * * * * *")  //每秒执行1次
    public void task1(){
        System.out.println(Thread.currentThread().getName()+":task1--->"+ LocalDateTime.now());
    }
}

可以看到运行后每秒执行一次

4.在task1中添加sleep,使其每次执行后睡5秒,会发现两次执行任务相差6秒。执行任务1秒,睡5秒。

5.添加task2,同样设置每秒执行一次,不设置sleep。然而运行的时候发现task2两次执行也差6秒。因为task2受到了task1的影响,Spring-task执行任务是单线程,处理任务能力有限,不适合分布式架构的任务调度。

6.Cron表达式

表达式中有6或7个时间元素,空格分隔,从左到右依次是:秒、分钟、小时、日、月、星期几、年。

*:表示该元素无限制,比如分,表示每分钟。

?:用在日和星期几,因为这两个元素可能会产生冲突,所以可以定义其中一个,另一个用?

-:表示区间,两个数字的连接符。

,:同元素指定多个值,用逗号分隔

/:表示增加幅度。分种0/15表示0,15,30,45分。*/15表示从0开始。

可在网站中自动生成:在线Cron表达式生成器

Quartz 基本应用

官方文档:Quartz Enterprise Job Scheduler 2.3.0-SNAPSHOT API

1.Quartz可以用来执行定时任务,比Timer增加了很多功能,比如持久性作业,保持调度定时的状态;作业管理,对调度作业进行有效管理。

2.Quartz 的核心类有三部分:任务 Job (实现execute()),触发器 Trigger(SimpleTrigger、CronTrigger),调度器 Scheduler(负责基于Trigger触发器执行任务)。

3.案例

(1)导入第三方组件Quartz的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

(2)在main中新建任务类

public class MyJob implements Job {
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("任务被执行了");
    }
}

(3)在test中新建测试类,创建任务调度器、Job任务类、触发器,确定定时任务的执行时机,最后执行任务。

public class QuartzTest {
    public static void main(String[] args) throws SchedulerException {
        //1.创建任务调度器
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();

        //2.创建Job 任务类
        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
                .withIdentity("job1","group1")
                .build();

        //3.创建触发器,定时任务的执行时机
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1","group1")
                //使用简单触发器,每3s执行一次
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever())
                //从现在开始执行
                .startNow()
                .build();

        //4.执行任务
        scheduler.scheduleJob(jobDetail,trigger);
        scheduler.start();
    }
}

(4)运行后可以看到每3秒执行一次

QuartzAPI

JobDetail

作用:绑定 Job,是一个任务实例,有许多扩展参数。

主要字段

含义

name

任务名称

group

任务分组,默认分组DEFAULT

jobClass

要执行的Job实现类

jobDataMap

任务参数信息,JobDetail、Trigger可以用它设置参数信息

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

没有JobDetail的话会出现并发访问的情况,数据不安全。

每次执行根据JobDetail创建新的Job示例,避免并发访问。

public class MyJob2 implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        //获得job参数
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        //获得trigger参数
        JobDataMap triggerDataMap = context.getTrigger().getJobDataMap();
        System.out.println("执行任务,job参数:"+jobDataMap.get("jk1")+","+jobDataMap.get("jk2")+",trigger参数:"+triggerDataMap.get("tk1")+","+triggerDataMap.get("tk2"));

    }
}
public class QuartzTest2 {
    public static void main(String[] args) throws SchedulerException {
        //1.创建任务调度器
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();

        //2.创建JobDetail对象
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put("jk1","jobvalue1");
        jobDataMap.put("jk2","jobvalue2");
        JobDetail jobDetail = JobBuilder.newJob(MyJob2.class)
                .withIdentity("job2","group1")
                //存放参数
                .setJobData(jobDataMap)
                .build();

        //3.创建触发器
        JobDataMap jobDataMap2 = new JobDataMap();
        jobDataMap2.put("tk1","tvalue1");
        jobDataMap2.put("tk2","tvalue2");
        Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1","group1")
                //每3s执行一次
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).repeatForever())
                .usingJobData(jobDataMap2)
                .build();
        //4.执行触发器
        scheduler.scheduleJob(jobDetail,trigger);
        scheduler.start();
    }
}

3秒执行一次,两个任务都执行。

SimpleTrigger

是简单触发器,可以实现指定时间段内执行一次任务或循环执行任务,还能设置运行次数。

执行指定次数的任务如下,每3秒执行一次,一共执行3次。次数限制写n,实际运行次数是n+1

Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("trigger1","group1")
                //每3s执行一次
                .startNow()
                //执行次数是n+1
                .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(3).withRepeatCount(2))
                .usingJobData(jobDataMap2)
                .build();

CronTrigger

基于日历的任务调度器,实际应用中较常用。

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

SpringBoot整合Quartz

实现动态任务调度,作业管理。

案例实现

1.编写pom.xml,添加需要的依赖,有web、quartz、jdbc、mysql、lombok、json处理。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>top.psjj</groupId>
    <artifactId>quartz-study2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>quartz-study2</name>
    <description>quartz-study2</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2.编写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: Bwu_2021320068
              # 必须指定数据源类型
              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

这些配置基本够用,如果要配置更多,查看官网

Configuration Reference

3.配置application.properties 自动生成表

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

4.实体类

@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;
}

5.任务类MyTask

@DisallowConcurrentExecution:同一个任务必须在上一次执行完毕之后,再按照corn时间执行,不会并行执行。

@PersistJobDataAfterExecution:下一个任务用到上一个任务的修改数据(定时任务里面的jobData数据流转)。

@DisallowConcurrentExecution
@PersistJobDataAfterExecution
@Slf4j
@Component
public class MyTask extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {

        log.info("TimeEventJob正在执行..." + LocalDateTime.now());
        // 执行9秒
        try {
            Thread.sleep(9000);
            log.info("TimeEventJob执行完毕..." + LocalDateTime.now());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

6.JobHandle(任务的开关停删操作)

@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;
    }
}

7.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);
    }
}

8.测试

(1)选择post方式,http://127.0.0.1:80/job/add,新增任务,任务开始执行。

(2)选择get方式,http://127.0.0.1:80/job/pause?jobGroup=group1&jobName=job1,任务暂停执行。

(3)选择get方式,http://127.0.0.1:80/job/all,查询所有任务,只有上面新增的一个任务。

9.开启服务自动执行任务

在JobHandler中添加init方法和addMyTask方法

@PostConstruct
public void init(){
    addMyTask();
}

public void addMyTask() {
    try {
        JobInfo jobInfo = new JobInfo();
        jobInfo.setJobName("job1");
        jobInfo.setJobGroup("group1");
        jobInfo.setTriggerName("trigger1");
        jobInfo.setTriggerGroup("triggerGroup1");
        jobInfo.setClassName("top.psjj.task.MyTask");
        jobInfo.setCron("0/1 * * * * ? *");
        this.addJob(jobInfo);
    } catch (SchedulerException e) {
        throw new RuntimeException(e);
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

开启后自动执行任务

单线程与多线程执行任务调度的区别

1.单线程运行任务不同任务之间串行,任务A运行时间会影响任务B运行间隔。

2.多线程中,不同任务各自在不同的线程上,不会互相干扰。

任务1相隔时间长,没影响到任务2。

任务调度持久化的好处

任务基于动态设置,任务调度没有持久化,服务重启后设置的任务会失效。

任务整合持久化,动态任务信息会保存到数据库,开机自启会加载数据库信息,按照原来的设置运行任务。

大概流程是,第一次运行application时添加任务,任务运行。停止application,任务信息自动存入数据库。再次重启application,发现任务仍然在执行。

需要注意的是,在重启application前,需要将application.properties里的内容注释掉,否则每次启动application时都会新建表,将存好数据的表替换成空表,无法实现任务调度持久化。

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

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

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

3. 数据共享:Quartz集群可以共享任务调度的数据,包括作业和触发器等。非集群模式下,每个节点都有自己独立的任务调度数据,可能导致数据不一致。

Quartz集群需要配置和管理多个节点,需要更多的系统资源和维护工作。

非集群模式相对简单,适用于小规模的应用程序。

总结

简述一下什么是任务调度?

答:任务调度就是按照特定时间规则执行系统某个固定的业务逻辑。任务调度底层是使用jdk的Timer实现的。单体项目建议使用Spring-task任务调度技术,分布式架构建议使用quartz任务调度框架。Spring-task是单线程运行旳,Quartz是多线程运行的,且功能更为丰富,支持作业管理。

说一下你都用过什么任务调度技术,他们的区别是什么?

答:Spring-task是单线程,且功能简单。执行任务只需开启开关@EnableScheduling,在要执行的任务方法上加

@Scheduled(cron = "*/1 * * * * *")注解。它的使用弊端:

1. 任务A的执行时间会影响任务B的执行间隔,但是任务A和任务B是两个任务,不应该相互影响。

2. 没有固定组件,持久化等功能,也就没法形成作业系统

Quartz是多线程的高可用的任务调度框架,支持持久化,多线程,集群模式,且有固定组件结构Job、Trigger、scheduler。他的优点一一说明

1. 有固定组件,有持久化功能,这样就能基于Quartz开发一个任务调度系统,通过UI界面去管理任务调度。

2. 任务进行持久化之后,重启服务器会加载持久化的任务继续执行。

3. 任务支持集群模式,如果任务调度模块是一个集群n个节点,那么任务调度不会因为一个节点挂掉而挂掉,且任务在集群之间形成负载均衡。

  • 23
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值