Spring Task

Lu Lv3

Spring Task

简介

Spring Task 是 Spring 框架提供的任务调度工具,可以 按照约定的时间自动执行某个代码逻辑

  • 定位
    • 定时任务框架(单体架构下)
  • 应用场景
    • 信用卡每月还款提醒
    • 银行贷款每月还款提醒
    • 入职纪念日为用户发送通知

快速入门

  1. 导入 Maven 坐标 spring-context(已存在)
image-20241111170000555
  1. 在启动类上添加注解 @EnableScheduling 开启任务调度
1
2
3
4
5
6
7
@SpringBootApplication
@EnableScheduling
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApplication.class, args);
}
}
  1. 通过 @Scheduled 注解方式自定义定时任务
1
2
3
4
5
6
7
8
@Component
public class ScheduledBean {
@Scheduled(cron = "0/5 * * * * ?")
public void printLog(){
System.out.println(Thread.currentThread().getName()+":run...");
}

}

重启服务:

image-20241111170441188


相关配置

以上是基本使用,还可配置一些通用配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
task:
scheduling:
# 任务调度线程池的核心线程数大小,默认是 1
pool:
size: 1
# 任务调度线程池的线程名称前缀,默认是 scheduling-
thread-name-prefix: ssm_
shutdown:
# 线程池关闭时,是否等待所有任务完成
await-termination: true
# 调度线程关闭前最大等待时间,确保最后一定关闭 (如果等待,最多等几秒)
await-termination-period: 10s

@Scheduled 注解

@Scheduled 注解的常用属性:

  • cron:cron 表达式,具体语法:cron 表达式语法,cron 在线表达式生成器:https://cron.ciding.cc/

    • cron 表达式其实就是一个字符串,通过 cron 表达式可以 定义任务触发的时间。构成规则:分为 6 或 7 个域,由空格分隔开,每个域代表一个含义,分别为:秒、分钟、小时、日、月、周、年(可选) 【日和周一般不能同时设置】
  • fixedDelay:固定延迟。从上次任务执行结束的时间开始,到下一个任务开始的时间间隔。不关心任务逻辑、任务本身执行多长时间。下图为 fixedDelay 为 4s 的示意图:

image-20241111173558035
  • fixedRate:固定频率。在理想情况下,下一次开始和上一次开始之间的时间间隔是一定的,但当如果上一次任务因为其他原因超时好久,而 pool.size 的默认值为 1,即默认情况下 Spring Boot 定时任务是单线程执行的,那下一轮任务就会被阻塞。类比地铁每隔 10 分钟发一列,也就是说所有列车其实已经安排好了时刻表,理想情况下,每列车准点发就行了,互不影响,但是如果其中一列晚点,那么就会导致下一列晚点。
image-20241111173830752
  • initialDelay:初始化延迟时间,也就是第一次延迟执行的时间。这个参数对 cron 属性无效,只能配合 fixedDelay 或 fixedRate 使用。如 @Scheduled(initialDelay = 5000, fixedDelay = 1000) 表示第一次延迟 5000 毫秒执行,下一次任务在上一次任务结束后 1000 毫秒后执行。

fixedDelay 和 fixedRate,都是和两轮任务有关,但 前者关注的是第一轮的结束时间和第二轮的开始时间的这个间隔。而后者关注的都是两轮的开始时间中间的这个间隔。

Spring Task 单线程下的阻塞问题

demo 代码,演示两个任务在单线程下的阻塞:

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
@Component
public class ScheduleTask {

/**
* 上一次任务执行完后,歇一秒,再执行下一轮
* 执行一次任务耗时5秒
*/
@Scheduled(fixedDelay = 1000)
public void task1() throws InterruptedException {

System.out.println(Thread.currentThread().getName()
+ "<mark>> spring task 1 </mark>> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS"))
);

Thread.sleep(5000);
}

/**
* 下轮任务在上一轮任务开始后2秒执行.
* 执行一次任务耗时可忽略
*/
@Scheduled(fixedRate = 2000)
public void task2() {
System.out.println(Thread.currentThread().getName()
+ "<mark>> spring task2 </mark>> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss:SSS"))
);
}

}

执行效果:

image-20241111174456740

可以看到 task2 被连续执行三次,且不妙的是两次任务开始时间没有间隔 2s。这就是单线程下阻塞导致的问题,task1 执行的 5 秒内,task2 按预定的间隔触发的任务被阻塞,等 task1 一执行完,就会立刻执行这些阻塞的任务。这个延迟和堆积在生产中还是很严重的。

Spring Task 阻塞问题的处理思路

第一种是直接改配置文件:

既然问题在单线程,一个线程处理不过来而导致的问题,那让定时任务的执行改为多线程就行了:

1
2
3
4
5
6
7
spring:
task:
scheduling:
# 任务调度线程池的核心线程数大小,默认是 1
pool:
size: 1
# 像上面的demo,设size为2即可

第二种是定义配置类,实现 SchedulingConfigurer 接口,设置 taskScheduler:

1
2
3
4
5
6
7
8
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
//设定一个长度10的定时任务线程池,这个大小自己判断
taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
}
}

第三种是加@Async 注解开启异步任务:

启动类加 @EnableAsyn 开启注解支持 c,在定时任务方法上加入注解 @Async

1
2
3
4
5
6
7
8
9
10
11
12
@Async
@Schedule(...)
public void task1(){
//...
}


@Async
@Schedule(...)
public void task2(){
//...
}

如果有@Async 这个注解的额外配置需求,参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//非必须,看自己需求
@Bean
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor poolTaskExecutor = new ThreadPoolTaskExecutor();
poolTaskExecutor.setCorePoolSize(4);
poolTaskExecutor.setMaxPoolSize(6);
// 设置线程活跃时间(秒)
poolTaskExecutor.setKeepAliveSeconds(120);
// 设置队列容量
poolTaskExecutor.setQueueCapacity(40);
poolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
return poolTaskExecutor;
}

Spring Task 在分布式环境中

Spring Task 并不是为分布式环境设计的,在分布式环境下,这种定时任务是不支持集群配置的,如果部署到多个节点上,各个节点之间并没有任何协调通讯机制,集群的节点之间是不会共享任务信息的,每个节点上的任务都会按时执行,导致任务的重复执行。我们可以使用支持分布式的定时任务调度框架,比如 Quartz、XXL-Job、Elastic Job。也可以借助 zookeeper、redis等实现分布式锁来处理各个节点的协调问题。或者把所有的定时任务抽成单独的服务单独部署。

  • Title: Spring Task
  • Author: Lu
  • Created at : 2024-07-18 14:18:58
  • Updated at : 2024-07-18 16:38:20
  • Link: https://lusy.ink/2024/07/18/Spring Task/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments