Spring Boot 定时任务全解

在项目开发过程中,经常需要定时任务来帮助我们来做一些内容,springboot 默认已经帮我们实行了,开发者只需要添加相应的注解就可以实现。

一、Spring Task 定时任务

1.1 静态定时任务(基于 @Scheduled 注解)
1.1.1 pom 配置

pom 包里面只需要引入springboot starter包即可:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
1.1.2 启动类启用定时

在启动类上面加上@EnableScheduling即可开启定时:

1
2
3
4
5
6
7
@EnableScheduling
@SpringBootApplication
public class KingApplication {
public static void main(String[] args) {
SpringApplication.run(KingApplication.class, args);
}
}

注意:这里的@EnableScheduling注解,它的作用是发现注解@Scheduled的任务并由后台执行。没有它的话将无法执行定时任务。

引用官方文档原文:
@EnableScheduling ensures that a background task executor is created. Without it, nothing gets scheduled.

官方文档地址:http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#scheduling-enable-annotation-support

1.1.3 创建定时任务实现类

定时任务1:每过 6 秒执行

1
2
3
4
5
6
7
8
9
10
@Component
public class Scheduler1Task {
private int count=0;

@Scheduled(cron="*/6 * * * * ?")
private void process(){
System.out.println("this is scheduler task runing "+(count++));
}

}

定时任务2:每过 6 秒执行

注意:上一次执行完毕时间点之后 6 秒再执行,不会等待上一个定时任务执行完毕再启动下一个定时任务,不论上次定时任务执行时间是多少。

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Scheduler2Task {

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

@Scheduled(fixedRate = 6000)
public void reportCurrentTime() {
System.out.println("现在时间:" + dateFormat.format(new Date()));
}

}

定时任务3:每过上一个定时任务执行完毕之后的 6 秒执行

注意:上一次执行完毕时间点之后 6 秒再执行,等待上一次定时任务执行完毕之后再间隔 6 秒执行下一个定时任务。

1
2
3
4
5
6
7
8
9
10
11
@Component
public class Scheduler3Task {

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Scheduled(fixedDelay = 6000)
public void reportCurrentTime() {
System.out.println("现在时间:" + dateFormat.format(new Date()));
}

}
1.1.4 参数说明

@Scheduled 参数可以接受两种定时的设置每隔 6 秒执行定时任务:

一种是我们常用的cron="*/6 * * * * ?",另一种是 fixedRate = 6000

fixedRate 说明

  • fixedRate :上一次开始执行时间点之后再执行,参数类型为 long,单位 ms;
  • fixedRateString:与fixedRate的含义一样,只是将参数类型变为 String;
  • fixedDelay :上一次执行完毕时间点之后再执行,参数类型为 long,单位 ms;
  • fixedDelayString:与fixedDelay含义一样,只是参数类型变为 String;
  • initialDelay :表示延迟多久再第一次执行任务,参数类型为 long,单位 ms;
  • initialDelayString:与initialDelay的含义一样,只是将参数类型变为 String;
  • zone:时区,默认为当前时区,一般没有用到。
1.1.5 Cron 表达式

Cron 表达式有专门的语法,而且感觉有点绕人,不过简单来说,大家记住一些常用的用法即可,特殊的语法可以单独去查。

Cron 表达式是由一串字符串表示,使用数字+空格+特殊字符的形式组合成完整表达式,Cron 表达式由空格将其划分为 6 或 7 个域,每一个域代表一个含义解释:

1.1.5.1 域解释

1
2
3
4
5
6
7
* 第一位,表示秒,取值:0-59
* 第二位,表示分,取值:0-59
* 第三位,表示小时,取值:0-23
* 第四位,日期天/日,取值:1-31
* 第五位,日期月份,取值:1-12
* 第六位,星期,取值:1-7
* 第七位,年份,可以留空,取值:1970-2099

注意:第六位的取值:1-7 表示的是星期一至星期日

1.1.5.2 特殊符号解释

(*)星号

可以理解为每的意思,每秒,每分,每天,每月,每年;

(?)问号

只能用在每月第几天和星期两个域。表示不指定值,当 2 个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为?

(-)减号

表达一个范围,如在小时字段中使用 “10-12”,则表示从10到12点,即 10, 11, 12;

(,)逗号

表达一个列表值,如在星期字段中使用 “1, 2, 4”,则表示星期一,星期二,星期四,也可以使用单词缩写来指定,例如:”MON,WED,FRI” 在星期域里表示 “星期一、星期三、星期五;

(/)斜杠

表示起始时间开始触发,然后每隔固定时间触发一次,例如在分域使用 5/20 ,则意味着5分,25分,45分,分别触发一次,另外:*/y,等同于0/y

(L)字符

表示最后,只能出现在星期和每月第几天域,如果在星期域使用 1L,意味着在最后的一个星期日触发;

(W)字符

表示有效工作日(周一到周五),只能出现在每月第几日域,系统将在离指定日期的最近的有效工作日触发事件。注意一点,W 的最近寻找不会跨过月份; LW : 这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五。

(#)字符

用于确定每个月第几个星期几,只能出现在每月第几天域,例如在1#3,表示某月的第三个星期日。

官方例子:

1
2
3
4
5
6
7
8
9
10
11
"0 0 * * * *"              表示每小时0分0秒执行一次

" */10 * * * * *" 表示每10秒执行一次

"0 0 8-10 * * *" 表示每天8,9,10点执行

"0 0/30 8-10 * * *" 表示每天8点到10点,每半小时执行

"0 0 9-17 * * MON-FRI" 表示每周一至周五,9点到17点的0分0秒执行

"0 0 0 25 12 ?" 表示每年圣诞节(12月25日)0时0分0秒执行
1.2 动态定时任务(基于 SchedulingConfigurer 接口)

为了演示效果,这里选用 Mysql 数据库保存 Cron 表达式,使用 Mybatis 框架来查询和调整定时任务的执行周期,然后观察定时任务的执行情况。当然,可以使用静态配置文件的形式配置。

1.2.1 pom 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--添加Mybatis依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!--添加MySql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
1.2.2 添加数据库记录

在 mysql 数据库中创建socks数据库,并创建cron数据库表:

1
2
3
4
5
6
7
8
9
DROP DATABASE IF EXISTS `socks`;
CREATE DATABASE `socks`;
USE `SOCKS`;
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron` (
`cron_id` varchar(30),
`cron` varchar(30)
);
INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');

在项目中的application.yml添加数据源:

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://localhost:3306/socks?useSSL=false
username: root
password: root
1.2.3 创建定时器

数据库准备好数据之后,编写要定时执行的任务类,实现SchedulingConfigurer接口,重写configureTasks方法:

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
import java.text.SimpleDateFormat;
import java.util.Date;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.util.StringUtils;

@Configuration
@EnableScheduling
public class CompleteScheduleConfig implements SchedulingConfigurer{
protected final Logger logger = LoggerFactory.getLogger(this.getClass());

private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Mapper
public interface CronMapper {
@Select("select cron from cron limit 1")
String getCron();
}

@Autowired
CronMapper cronMapper;

/**
* 执行定时任务
*/
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(
//1.添加任务内容(Runnable)
() -> {
System.out.println("执行定时任务2: " + getNow());
logger.debug("执行定时任务2: " + getNow());
},
//2.设置执行周期(Trigger)
triggerContext -> {
//2.1 从数据库获取执行周期
String cron = cronMapper.getCron();
//2.2 合法性校验.
if (StringUtils.isEmpty(cron)) {
// Omitted Code ..
}
//2.3 返回执行周期(Date)
return new CronTrigger(cron).nextExecutionTime(triggerContext);
}
);
}

private String getNow() {
return dateFormat.format(new Date());
}

}

三、Timer 定时任务

这个API目前在项目中很少用,直接给出示例代码。具体的介绍可以查看APITimer的内部只有一个线程,如果多个任务的话就会顺序执行,这样任务的延迟时间循环时间就会出现问题。

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
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicLong;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TimerService {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());

private AtomicLong counter = new AtomicLong();

public void schedule() {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
long count = counter.incrementAndGet();
logger.info("Schedule timerTask {} times", count);
}
};
Timer timer = new Timer();
// 设置 初始化延迟时间 为 1s,定时执行间隔 为 2s
timer.schedule(timerTask, 1000L, 2 * 1000L);
}

public static void main(String[] args) {
TimerService timerService = new TimerService();
timerService.schedule();
}
}

观察测试结果,能够发现TimerTask配置的任务,每隔10s被执行了一次,执行线程默认都是Timer-0这个线程,并且启动的时机是 new 出 Timer 并执行schedule()方法的时候。

1
2
3
17:42:48.576 [Timer-0] INFO org.woodwhale.king.service.TimerService - Schedule timerTask 1 times
17:42:50.569 [Timer-0] INFO org.woodwhale.king.service.TimerService - Schedule timerTask 2 times
17:42:52.569 [Timer-0] INFO org.woodwhale.king.service.TimerService - Schedule timerTask 3 times

四、ScheduledExecutorService 定时任务

ScheduledExecutorService延时执行 的线程池,对于 多线程 环境下的 定时任务,推荐用 ScheduledExecutorService 代替 Timer 定时器。

4.1 等待定时任务执行完毕再进行下一次定时任务

创建一个线程数量为4任务线程池,同一时刻并向它提交4个定时任务,用于测试延时任务的并发处理。执行ScheduledExecutorServicescheduleWithFixedDelay()方法,设置任务线程池的初始任务延迟时间2秒,并在上一次执行完毕时间点之后2秒再执行下一次任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void scheduleWithFixedDelay() {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
for (int i = 0; i < 4; i++) {
scheduledExecutor.scheduleWithFixedDelay(() -> {
// 定时任务延迟完成 2 秒
try {
TimeUnit.MILLISECONDS.sleep(2 * 1000L);
} catch (InterruptedException e) {
LOGGER.error("Interrupted exception", e);
}
long count = counter.incrementAndGet();
logger.info("Schedule executor {} times with fixed delay", count);
}, 2000L, 2 * 1000L, TimeUnit.MILLISECONDS);
// 初始任务延迟时间 为 2 秒,并在上一次 执行完毕时间点 之后 2 秒再执行下一次任务
}
logger.info("Start to schedule");
}

测试结果如下,我们可以发现每隔2秒的时间间隔,就会有4个定时任务同时执行。因为在任务线程池初始化时,我们同时向线程池提交了4个任务,这四个任务会完全利用线程池中的4个线程进行任务执行。

4.2 固定时间进行定时任务

创建一个线程数量为4任务线程池,同一时刻并向它提交4个定时任务,用于测试延时任务的并发处理。每个任务分别执行ScheduledExecutorServicescheduleAtFixedRate()方法,设置任务线程池的初始任务延迟时间2秒,并在上一次开始执行时间点之后2秒再执行下一次任务。

1
2
3
4
5
6
7
8
9
10
11
public void scheduleAtFixedRate() {
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
for (int i = 0; i < 4; i++) {
scheduledExecutor.scheduleAtFixedRate(() -> {
long count = counter.incrementAndGet();
logger.info("Schedule executor {} times at fixed rate", count);
}, 2000L, 2 * 1000L, TimeUnit.MILLISECONDS);
// 初始任务延迟时间 为 2 秒,并在上一次 执行时间开始 之后 2 秒再执行下一次任务
}
logger.info("Start to schedule");
}

测试结果如下,我们可以发现每隔 2 秒的时间间隔,就会有 4 个定时任务同时执行,因为在任务线程池初始化时,我们同时向线程池提交了 4 个任务,这 四个任务会完全利用线程池中的 4个线程进行任务执行。

五、配置任务线程池(实现多线程并发处理)

上述配置都是基于单线程的任务调度,如何引入多线程提高延时任务并发处理能力?

Spring Boot提供了一个SchedulingConfigurer配置接口。我们通过 ScheduleConfig配置文件实现ScheduleConfiguration接口,并重写 configureTasks()方法,向ScheduledTaskRegistrar注册一个ThreadPoolTaskScheduler任务线程对象即可。

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
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

/**
* 多线程执行定时任务
* 所有的定时任务都放在一个线程池中,定时任务启动时使用不同都线程。
*/
@Configuration
public class ScheduleConfiguration implements SchedulingConfigurer {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
// 采用jdk 自带的执行器线程池 java.util.concurrent.Executors
// taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}

@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(4); //设定一个长度4的定时任务线程池
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.setThreadNamePrefix("schedule");
taskScheduler.setRemoveOnCancelPolicy(true);
taskScheduler.setErrorHandler(t -> logger.error("Error occurs", t));
return taskScheduler;
}
}

参考博文:

SpringBoot 创建定时任务(配合数据库动态执行)
https://www.jianshu.com/p/d160f2536de7

springboot(九):定时任务
https://www.cnblogs.com/ityouknow/p/6132645.html

spring boot项目中处理Schedule定时任务
https://www.rjkf.cn/springboot-schedule-cron/

SpringBoot定时任务及Cron表达式详解
https://my.oschina.net/jack90john/blog/1506474

实战Spring Boot 2.0系列(六) - 单机定时任务的几种实现
https://juejin.im/post/5b31b9eff265da598826c200

updated updated 2020-03-27 2020-03-27
本文结束感谢阅读

本文标题:Spring Boot 定时任务全解

本文作者:木鲸鱼

微信公号:木鲸鱼 | woodwhales

原始链接:https://woodwhales.cn/2018/12/17/005/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

0%