任务调度:

Spring Task:

Spring Task 是Spring 3.0提供的定时任务解决方案,不仅提供单线程和多线程两种执行任务的方式,而且还提供了线程池的方式合理利用资源。SpringTask提供注解和xml配置的方式进行和SpringBoot无缝连接。

​ Spring Boot 默认在无任何第三方依赖的情况下使用 spring-context 模块下提供的定时任务工具 Spring Task。我们只需要使用 @EnableScheduling 注解就可以开启相关的定时任务功能。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

// 开启SpringTask定时任务
@EnableScheduling
@SpringBootApplication
@MapperScan("com.yeb.mapper")
public class YebServerApplication {
public static void main(String[] args) {
SpringApplication.run(YebServerApplication.class, args);
}

}

只要在方法上加上@Scheduled 注解就可以定义任务;在类上加上@Component注解,Spring 容器就可以自动执行任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class TestTask {
/**
*每两秒执行一次任务
* @throws InterruptedException
*/
@Scheduled(cron = "*/2 * * * * ?")
public void task1() throws InterruptedException {
System.out.println("Task1开始,线程 id=>" + Thread.currentThread().getId());
Thread.sleep(10000);
System.out.println("Task任务结束,线程 id=》"+ Thread.currentThread().getId());
}

/**
* 每四秒,执行一次任务
* @throws InterruptedException
*/
@Scheduled(cron = "*/4 * * * * ?")
public void task2() throws InterruptedException {
System.out.println("Task2开始,线程 id=>" + Thread.currentThread().getId());
Thread.sleep(2000);
System.out.println("Task2结束,线程 id =>" + Thread.currentThread().getId());
}
}

运行结果:

image-20220316220345248

​ 通过结果可以看出,可以发现两个任务都是由同一个线程执行的。这就会引发一个问题,Task1需要10执行完,而Task2两秒就能够执行完。当执行Task1时,本应该执行5次Task2,但是却因为Task1任务的线程占用,少执行4次,因为Task1的10秒执行的时间超出了同期望执行的时间的2秒,所以已没有办法进行2秒执行一次。这就是单线程执行多任务时的弊端。

​ 将同步任务修改为异步任务,只需要在原代码的基础上加上两个注解。在启动类上加@EnableAsync注解

1
2
3
4
5
6
7
8
9
10
11
12
13
// 开启SpringTask定时任务
@EnableScheduling
@EnableAsync
@SpringBootApplication
@MapperScan("com.yeb.mapper")
public class YebServerApplication {

public static void main(String[] args) {
SpringApplication.run(YebServerApplication.class, args);
}

}

在任务类上加入@Async注解

1
2
3
4
5
6
7
8
9
10
11
12
/**
*每两秒执行一次任务
* @throws InterruptedException
*/
@Async
@Scheduled(cron = "*/2 * * * * ?")
public void task1() throws InterruptedException {
System.out.println("Task1开始,线程 id=>" + Thread.currentThread().getId());
Thread.sleep(10000);
System.out.println("Task任务结束,线程 id=》"+ Thread.currentThread().getId());
}

image-20220316230741362

观察 Task1和Task2任务,会发现每一个新的任务都会一个新的线程来进行执行

利用 线程池可以更好的利用线程资源。

在config类中创建 TaskConfig,并在其中配置具有5个线程的线程池来执行任务调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

@Configuration
public class TaskConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}

@Bean
public Executor taskExecutor() {
return Executors.newScheduledThreadPool(10);
}
}

image-20220316234304730

可以发现任务的调度不在随意创建新的线程,而是会利用线程池的线程资源,提高效率

SpringTask 简单易用,在Springboot中,不需要对application.xml做任何的配置,也不要导入额外的依赖。

Quartz:

Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,完全基于 Java 实现。无论是单任务调度还是成千上万的任务调度,甚至是分布式集群任务调度,都可以游刃有余。

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

特点:

  • 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
  • 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
  • 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。

图 1. Quartz 核心元素关系图

  • Scheduler:任务调度器,是实际执行任务调度的控制器
  • Trigger:触发器,用于定义任务调度的时间规则
  • Job:任务,即被调度的任务

Quartz调度的核心元素:

Job接口:只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中。实现Job接口的任务,默认是无状态的,若要将Job设置成有状态的,在quartz中是给实现的Job添加@DisallowConcurrentExecution注解(以前是实现StatefulJob接口,现在已被Deprecated),在与spring结合中可以在spring配置文件的job detail中配置concurrent参数。

JobDetail: 用来描述Job实现类以及其他的相关静态操作,如Job名字、关联监听器等信息。在spring中有JobDetailFactoryBean和 MethodInvokingJobDetailFactoryBean两种实现,如果任务调度只需要执行某个类的某个方法,就可以通过MethodInvokingJobDetailFactoryBean来调用。一个JobDetail有多个Trigger,但是一个Trigger 只能对应一个JobDetail.

Trigger:触发器,用于定义任务调度的时间规则,有SimpleTrigger,CronTrigger,DateIntervalTrigger和NthIncludedDayTrigger,当仅当需要触发一次时,或者以固定的时间间隔周期执行时,SimpleTriggers是最合适的选择,其中CronTrigger用的比较多,CronTrigger在spring中封装在CronTriggerFactoryBean中。

Scheduler:任务调度器:是实际执行任务调度的控制器。在spring中通过SchedulerFactoryBean封装起来。Trigger和JobDetail可以被注册到Scheduler中,Scheduler可以将Trigger绑定到某一个JobDetail中,Scheduler可以将Trigger绑定到一个JobDetail中,这样当Trigger被触发时,对应的Job就会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class TestJob1 extends QuartzJobBean {


@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("Job1开始,线程 id=>" + Thread.currentThread().getId());
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Job1结束,线程 id=>" + Thread.currentThread().getId());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public class TestJob2 extends QuartzJobBean {

@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
System.out.println("Job2开始,线程 id=>" + Thread.currentThread().getId());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Job2结束,线程 id=>" + Thread.currentThread().getId());
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.yeb.config;


import com.yeb.utils.TestJob1;
import com.yeb.utils.TestJob2;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 用来设置Trigger以调度任务 两个任务调度中使用了SimpleTrigger
*/
@Configuration
public class QuartzConfig {
//对任务1进行配置
public class Job1 {
@Bean
public JobDetail createJobDetail1() {
return JobBuilder.newJob(TestJob1.class).withIdentity("TestJob1").storeDurably().build();
}

@Bean
public Trigger createTrigger1() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(2) //设置时间周期单位秒
.repeatForever();//执行次数:无限
SimpleTrigger trigger = TriggerBuilder.newTrigger().forJob(createJobDetail1())
.withIdentity("TestJob1Trigger")
.withSchedule(scheduleBuilder)
.build();
return trigger;
}


//对任务2进行配置
public class Job2 {
@Bean
public JobDetail testJob2() {
return JobBuilder.newJob(TestJob2.class).withIdentity("TestJob2").storeDurably().build();
}

@Bean
public Trigger testTrigger2() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(4) //设置时间周期单位秒
.withRepeatCount(3);//执行次数:3次
SimpleTrigger trigger= TriggerBuilder.newTrigger().forJob(testJob2())
.withIdentity("TestJob2Trigger")
.withSchedule(scheduleBuilder)
.build();
return trigger;
// // 基于 Quartz Cron 表达式的调度计划的构造器
// CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("*/4 * * * * ?");
// CronTrigger trigger = TriggerBuilder.newTrigger()
// .forJob(testJob2())
// .withIdentity("quartzJob02Trigger")
// .withSchedule(scheduleBuilder)
// .build();
// return trigger;
}
}
}
}

两次任务调度中使用了SimpleTrigger ,其中将Job1设置成了2秒执行一次,执行无限次;将Job2设置为每2秒执行一次,执行无限次数;将Job设置为每4秒执行一次,共执行3次。

Cron介绍

在线Cron表达式生成器 (qqe2.com)

cron是linux系统中用来定期执行命令或指定程序任务的一种服务或软件。可以满足周期性执行任务的需求

corn表达式格式为七个域,从左到右进行记数,使用空格间隔

1
corn表达式格式为七个域,如: 秒 分 时 日 月 周 年

img

“/”:表示从几开始,每隔固定时间执行一次。比如说:

  • 在分钟字段下的值是 0/25,指从第 0 分钟开始,每 25 分钟执行一次
  • 在分钟字段下的值是 3/4,指从第 3 分钟开始,每 4 分钟执行一次

这里有一个点要注意:cron 表达式不会记录上次执行的时间,因此上面的 0/25 的执行时间为 0:0, 0:25, 0:50, 1:0, 1:25, 1:50,而不是从当前时间向后推 25 分钟执行。这可能和你印象中的执行时间不大一样。

“?”:表示每月的某一天,或第几周的某一天。表示不确定的值

“L”:用于每月,或每周,表示为每月的最后一天,或每个月的最后星期几如“6L”表示“每月的最后一个星期五”

“W”:表示为最近工作日(周一到周五,国外没有调休),如“15W”放在每月(day-of-month)字段上表示为“到本月15日最近的工作日”

“#”:是用来指定该月第几个周 X。比如说:

  • 在每周字段下的值是 6#3, 则表示“每月第三个星期五”

“,”字符:指定数个值,每个值之间用逗号分隔。

“-”字符:指定一个值的范围,比如说:

  • 分钟下 0-5,指从第 0 分钟到第 5 分钟。

执行时间为:0, 1, 2, 3, 4, 5

“L”字符:用在日表示一个月中的最后一天,用在周表示该月最后一个星期 X

img