https://cloud.tencent.com/developer/article/1582434
在日常项目开发中我们经常要使用定时任务。比如在凌晨进行统计结算,开启策划活动,自动将超过24小时的未付款的单改为取消状态,自动将超过14天客户未签收的订单改为已签收状态等等。今天我们就来看看如何在 Spring Boot 中使用 Spring 内置的定时任务。
Spring Task:Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。
Spring Boot 默认在无任何第三方依赖的情况下使用 spring-context 模块下提供的定时任务工具 Spring Task。我们只需要使用 @EnableScheduling 注解就可以开启相关的定时任务功能。如:
@SpringBootApplication
@EnableScheduling
public class SpringbootScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootScheduleApplication.class, args);
}
}
然后我们就可以通过注解的方式实现自定义定时任务。
@Component
public class Task {
@Scheduled(fixedDelay = 1000)
public void task() {
System.out.println("Thread Name : "
+ Thread.currentThread().getName() + " i am a task : date -> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
结果:
注意:@Scheduled
注解中一定要声明定时任务的执行策略 cron
、fixedDelay
、fixedRate
三选一。
fixedDelay: 它的间隔时间是根据上次的任务结束的时候开始计时的,只要盯紧上一次执行结束的时间即可,跟任务逻辑的执行时间无关,两个轮次的间隔距离是固定的。
fixedRate: 这个相对难以理解一些。在理想情况下,下一次开始和上一次开始之间的时间间隔是一定的。但是默认情况下 Spring Boot 定时任务是单线程执行的。当下一轮的任务满足时间策略后任务就会加入队列,也就是说当本次任务开始执行时下一次任务的时间就已经确定了,由于本次任务的“超时”执行,下一次任务的等待时间就会被压缩甚至阻塞,算了画张图就明白了。
initialDelay: 初始化延迟时间,也就是第一次延迟执行的时间。这个参数对 cron
属性无效,只能配合 fixedDelay
或 fixedRate
使用。如 @Scheduled(initialDelay=5000,fixedDelay = 1000)
表示第一次延迟 5000 毫秒执行,下一次任务在上一次任务结束后 1000 毫秒后执行。
原文链接:https://cloud.tencent.com/developer/article/1674682
Spring 的定时任务默认是单线程执行,多任务情况下,如果使用多线程会影响定时策略
@Component
public class TaskService {
/**
* 上一次任务结束后 1 秒,执行下一次任务,任务消耗 5秒
*
* @throws InterruptedException the interrupted exception
*/
@Scheduled(fixedDelay = 1000)
public void task() throws InterruptedException {
System.out.println("Thread Name : "
+ Thread.currentThread().getName()
+ " i am a task : date -> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
Thread.sleep(5000);
}
/**
* 下轮任务在本轮任务开始2秒后执行. 执行时间可忽略不计
*/
@Scheduled(fixedRate = 2000)
public void task2() {
System.out.println("Thread Name : "
+ Thread.currentThread().getName()
+ " i am a task2 : date -> "
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
}
}
结果:
Thread Name : scheduling-1 i am a task : date -> 2022-03-07 15:30:58
Thread Name : scheduling-1 i am a task2 : date -> 2022-03-07 15:30:58
Thread Name : scheduling-1 i am a task2 : date -> 2022-03-07 15:30:58
Thread Name : scheduling-1 i am a task2 : date -> 2022-03-07 15:30:59
Thread Name : scheduling-1 i am a task : date -> 2022-03-07 15:30:59
Thread Name : scheduling-1 i am a task : date -> 2022-03-07 15:31:04
结果分析:
也就是说因为单线程阻塞发生了“连锁反应”,导致了任务执行的错乱。@EnableScheduling
注解引入了ScheduledAnnotationBeanPostProcessor
其 setScheduler(Object scheduler)
有以下的注释:
如果
TaskScheduler
或者ScheduledExecutorService
没有定义为该方法的参数,该方法将在 Spring IoC 中寻找唯一的TaskScheduler
或者 名称为taskScheduler
的 Bean 作为参数,当然你按照查找 TaskScheduler 的方法找一个ScheduledExecutorService 也可以。要是都找不到那么只能使用本地单线程调度器了。
Spring Task 的调用顺序关系为:任务调度线程 调度 任务执行线程 执行 定时任务 所以我们按照上面定义一个 TaskScheduler
在 Spring Boot 自动配置中提供了 TaskScheduler
的自动配置:
@ConditionalOnClass({ThreadPoolTaskScheduler.class})
@Configuration(
proxyBeanMethods = false
)
@EnableConfigurationProperties({TaskSchedulingProperties.class})
@AutoConfigureAfter({TaskExecutionAutoConfiguration.class})
public class TaskSchedulingAutoConfiguration {
public TaskSchedulingAutoConfiguration() {
}
@Bean
@ConditionalOnBean(
name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
)
@ConditionalOnMissingBean({SchedulingConfigurer.class, TaskScheduler.class, ScheduledExecutorService.class})
public ThreadPoolTaskScheduler taskScheduler(TaskSchedulerBuilder builder) {
return builder.build();
}
@Bean
public static LazyInitializationExcludeFilter scheduledBeanLazyInitializationExcludeFilter() {
return new ScheduledBeanLazyInitializationExcludeFilter();
}
@Bean
@ConditionalOnMissingBean
public TaskSchedulerBuilder taskSchedulerBuilder(TaskSchedulingProperties properties, ObjectProvider<TaskSchedulerCustomizer> taskSchedulerCustomizers) {
TaskSchedulerBuilder builder = new TaskSchedulerBuilder();
builder = builder.poolSize(properties.getPool().getSize());
Shutdown shutdown = properties.getShutdown();
builder = builder.awaitTermination(shutdown.isAwaitTermination());
builder = builder.awaitTerminationPeriod(shutdown.getAwaitTerminationPeriod());
builder = builder.threadNamePrefix(properties.getThreadNamePrefix());
builder = builder.customizers(taskSchedulerCustomizers);
return builder;
}
}
# 任务调度线程池
# 任务调度线程池大小 默认 1 建议根据任务加大
spring.task.scheduling.pool.size=1
# 调度线程名称前缀 默认 scheduling-
spring.task.scheduling.thread-name-prefix=scheduling-
# 线程池关闭时等待所有任务完成
spring.task.scheduling.shutdown.await-termination=
# 调度线程关闭前最大等待时间,确保最后一定关闭
spring.task.scheduling.shutdown.await-termination-period=
# 任务执行线程池配置
# 是否允许核心线程超时。这样可以动态增加和缩小线程池
spring.task.execution.pool.allow-core-thread-timeout=true
# 核心线程池大小 默认 8
spring.task.execution.pool.core-size=8
# 线程空闲等待时间 默认 60s
spring.task.execution.pool.keep-alive=60s
# 线程池最大数 根据任务定制
spring.task.execution.pool.max-size=
# 线程池 队列容量大小
spring.task.execution.pool.queue-capacity=
# 线程池关闭时等待所有任务完成
spring.task.execution.shutdown.await-termination=true
# 执行线程关闭前最大等待时间,确保最后一定关闭
spring.task.execution.shutdown.await-termination-period=
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-
配置完后你就会发现定时任务可以并行异步执行了。
Timer:这是java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。
public class TestTimer {
public static void main(String[] args) {
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
System.out.println("task run:"+ new Date());
}
};
Timer timer = new Timer();
//安排指定的任务在指定的时间开始进行重复的固定延迟执行。这里是每3秒执行一次
timer.schedule(timerTask,10,3000);
}
}
ScheduledExecutorService:也jdk自带的一个类;是基于线程池设计的定时任务类,每个调度任务都会分配到线程池中的一个线程去执行,也就是说,任务是并发执行,互不影响。
public class TestScheduledExecutorService {
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
// 参数:1、任务体 2、首次执行的延时时间
// 3、任务执行间隔 4、间隔时间单位
service.scheduleAtFixedRate(()->System.out.println("task ScheduledExecutorService "+new Date()), 0, 3, TimeUnit.SECONDS);
}
}