简介
背景
在项目开发过程中,我们经常需要执行具有周期性的任务。通过定时任务可以很好的帮助我们实现。
我们拿常用的几种定时任务框架做一个比较:
| 定时任务框架 |
Cron表达式 |
固定间隔执行 |
固定频率执行 |
任务持久化 |
开发难易度 |
| JDK TimerTask |
不支持 |
支持 |
支持 |
不支持 |
一般 |
| Spring Schedule |
支持 |
支持 |
支持 |
不支持 |
简单 |
| Quartz |
支持 |
支持 |
支持 |
支持 |
困难 |
优点
基于注解来设置调度器。
非常方便实现简单的调度。
对代码不具有入侵性,非常轻量级。
所以我们会发现,spring schedule 用起来很简单,非常轻量级, 对代码无侵入性, 我们只需要注重业务的编写, 不需要关心如果构造Scheduler。
缺点
一旦调度任务被创建出来, 不能动态更改任务执行周期, 对于复杂的任务调度有一定的局限性。
使用说明
@Component
public class Demo{
@Scheduled(fixedRate = 1000)
public void do(){
doSomething();
}
}
以上是1秒执行一次。
注解详解
spring schedule的核心就是Scheduled注解的使用
public @interface Scheduled {
String cron() default "";
String zone() default "";
long fixedDelay() default -1L;
String fixedDelayString() default "";
long fixedRate() default -1L;
String fixedRateString() default "";
long initialDelay() default -1L;
String initialDelayString() default "";
}
SpringBoot集成schedule
1、添加maven依赖包
由于Spring Schedule包含在spring-boot-starter基础模块中,所有不需要增加额外的依赖
<dependencies>
<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>
</dependencies>
2、启动类,添加启动注解
在springboot入口或者配置类中增加@EnableScheduling注解即可启用定时任务。
@EnableScheduling
@SpringBootApplication
public class ScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class, args);
}
}
添加定时任务
我们将对Spring Schedule三种任务调度器分别举例说明。
1.3.1Cron表达式
类似于Linux下的Cron表达式时间定义规则。Cron表达式由6或7个空格分隔的时间字段组成,如下图:
| 位置 |
时间域名 |
允许值 |
允许的特殊字符 |
| 1 |
秒 |
0-59 |
,-*/ |
| 2 |
分钟 |
0-59 |
,-*/ |
| 3 |
小时 |
0-23 |
,-*/ |
| 4 |
日期 |
1-31 |
,-*/L W C |
| 5 |
月份 |
1-12 |
,-*/ |
| 6 |
星期 |
1-7 |
,-*/L C # |
| 7 |
年(可选) |
空值 1970-2099 |
,-*/ |
常用表达式:
| 表达式 |
描述 |
| 0/30 * * * * * |
每30秒执行一次 |
| 0 0/5 * * * * |
每5分钟执行一次 |
| 0 0 0 * * * |
每天凌晨执行 |
| 0 0 8, 12, 17 * * * |
每天8点、12点、17点整执行 |
| 0 30 3-5 * * * |
每天3点~5点 30分时执行 |
举个栗子:
添加一个work()方法,每10秒执行一次。
注意:当方法的执行时间超过任务调度频率时,调度器会在下个周期执行。
如:假设work()方法在第0秒开始执行,方法执行了12秒,那么下一次执行work()方法的时间是第20秒。
@Component
public class MyTask {
@Scheduled(cron = "0/10 * * * * *")
public void work() {
}
}
1.3.2 固定间隔任务
下一次的任务执行时间,是从方法最后一次任务执行结束时间开始计算。并以此规则开始周期性的执行任务。
举个栗子:
添加一个work()方法,每隔10秒执行一次。
例如:假设work()方法在第0秒开始执行,方法执行了12秒,那么下一次执行work()方法的时间是第22秒。
@Scheduled(fixedDelay = 1000*10)
public void work() {
}
1.3.3 固定频率任务
按照指定频率执行任务,并以此规则开始周期性的执行调度。
举个栗子:
添加一个work()方法,每10秒执行一次。
注意:当方法的执行时间超过任务调度频率时,调度器会在当前方法执行完成后立即执行下次任务。
例如:假设work()方法在第0秒开始执行,方法执行了12秒,那么下一次执行work()方法的时间是第12秒。
@Scheduled(fixedRate = 1000*10)
public void work() {
}
配置TaskScheduler线程池
在实际项目中,我们一个系统可能会定义多个定时任务。那么多个定时任务之间是可以相互独立且可以并行执行的。
通过查看org.springframework.scheduling.config.ScheduledTaskRegistrar源代码,发现spring默认会创建一个单线程池。这样对于我们的多任务调度可能会是致命的,当多个任务并发(或需要在同一时间)执行时,任务调度器就会出现时间漂移,任务执行时间将不确定。
protected void scheduleTasks() {
if (this.taskScheduler == null) {
this.localExecutor = Executors.newSingleThreadScheduledExecutor();
this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
}
}
自定义线程池
新增一个配置类,实现SchedulingConfigurer接口。重写configureTasks方法,通过taskRegistrar设置自定义线程池。
@Configuration
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setTaskScheduler(taskScheduler());
}
@Bean()
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(20);
taskScheduler.setThreadNamePrefix("xx-task-scheduler-thread-");
taskScheduler.setAwaitTerminationSeconds(30);
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
return taskScheduler;
}
}
使用SpringBoot的@Async来执行多线程并行处理
@Async配置
@Configuration
public class ThreadConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(40);
executor.setQueueCapacity(100);
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
定时任务使用
@Component
@Slf4j
@Async
public class ScheduleTask {
@Autowired
private XXXTask xxxTask;
@Scheduled(cron = "0 */2 * * * ?")
public void xxxTask(){
log.info("定时任务xxxTask开始时间:"+ DateUtils.currentTime());
try{
xxxTask.xxx();
}catch (WalletException e){
log.error("--------------------------------发生时间{},异常是{}",
DateUtils.currentTime(),e.getResultVoError().getMsg());
}
log.info("定时任务xxxTask结束时间:"+ DateUtils.currentTime());
}
}
参考网站
www.cnblogs.com/skychenjiaj…
www.jianshu.com/p/587901245…
www.cnblogs.com/dolphin0520…