Spring Boot实现定时任务
只需要在类上加上@EnableScheduling,在方法上加上@Scheduled两个注解即可启用定时任务
Spring Boot提供的定时任务存在的小坑
@EnalbleScheduling
@EnableScheduling可以放在启动类,方便整体把控。
@Scheduled几个属性
不同的属性有不同的功效
fixedDelay距离上一次任务的时间
效果:上一个任务结束后间隔多久执行
@Slf4j
@Component
@EnableScheduling
public class TaskOne {
@Scheduled(fixedDelay = 2 * 1000)
public void taskOne(){
log.info("taskOne Begin...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("task One End...");
}
}
下一个任务开始在上一个任务结束后 间隔 fixedDelay 时间执行
fixedRate : 距离上一次任务开始间隔的时间 执行下一次任务开始
@Scheduled(fixedRate = 5 * 1000)
不论上一个任务执行是否大于间隔时间,下一个任务都会在上一个任务开始后的 fixedRate 间隔后执行下一个任务
但spring boot的定时任务默认是单线程的,即使下一个任务从上一个任务开始后等待了超过了fixedRate时间,仍然要等待上一个任务执行完成。
- fixDelay 上一个任务结束与下一个任务开始的时间间隔
- fixRate 上一个任务开始与下一个任务开始的时间间隔
如何解决fixedRate,当下一个任务执行等待时间超过了fixedRate时间后会等待上一个任务执行完问题?
为定时任务指定线程池,每个任务都跑在独立的线程中,不存在等待上一个任务的情况 问题变成如何将SpringBoot的定时任务配置为多线程模式呢?
initialDelay:启动多少秒后开始首次任务
fixedDelay、fixedRate都是项目启动后就立即执行,随后按照指定间隔时间重复执行。 推迟首次任务执行时间,用initialDelay指定
@Scheduled(fixedRate = 5 * 1000,initialDelay = 2 * 1000)
cron:定时执行(最常用)
cron表达式,一般写6位即可。分别代表@Schedule(cron = "秒 分 时 日 月 星期[年]"),第7位年可以不写
cron表达式实用规则:
*/number 表示“每隔nubmer”
逗号表示“或”,如12,13,16表示12或13或16
在线cron生成器:www.pppet.net/
一个线上的坑
@Slf4j
@Component
@EnableScheduling
public class TaskOne {
@Scheduled(cron = "*/10 * * * * ?")
public void taskOne(){
log.info("taskOne Begin...");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("taskOne End...");
}
@Scheduled(cron = "*/10 * * * * ?")
public void taskTwo(){
log.info("taskTwo Begin...");
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("taskTwo End...");
}
}
设定多个定时任务都是同一时刻开启的,当一个定时任务执行时间很长或卡死,会导致另外的定时任务等待,为什么?
Spring Boot定时任务默认时单线程的。
SpringBoot定时任务配置多线程:
/**
* 线程池配置
*
*/
@EnableAsync // 来了,这里挖了一个坑
@Configuration
public class ThreadPoolTaskConfig implements WebMvcConfigurer {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(5);
executor.setKeepAliveSeconds(10);
executor.setThreadNamePrefix("async-task-");
// 线程池对拒绝任务的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}
然后在定时任务上加上@Async("taskExecutor"),
项目中可能会配置多个线程池,创建线程池时给定一个有意义的名字,是一个好的习惯,专池专用。 但是此时还是会有问题,但一个任务执行完成后,线程会随机去执行一个任务,当走到任务2时,线程就会卡死在任务2中。最终所有线程卡死在任务2
如何保证定时任务可用
当定时任务异常时,光配置定时任务池是不行的,最终线程池仍然会枯竭,导致所有线程阻塞,除非每个定时任务有专门的线程池。
所以发生定时任务耗时异常这种情况时,最重要的是及时发现并修复。
在配置线程池时,可以指定拒绝策略,线程池队列满了之后触发。
如何替换Spring定时任务中的线程池呢?
查看源码上发现SpringBoot对外暴露了三种自定义定时任务池的方法:
- 如果存在SchedulingConfigurer类型的Bean,用SchedulingConfigurer
- 如果存在TaskSheduler类型的Bean,用TaskScheduler
- 如果存在ScheduledExecutorService的Bean,用ScheduledExecutorService
- 以上都没有用默认的 上面的任意一种定时任务线程池都会调用ScheduledTaskRegistrar#setScheduler为taskScheduler赋值。
实际开发中建议使用自定义的拒绝策略,为什么?
实际开发中发现线程池不够用了,能直接跑主线程吗,那可能主线程也被阻塞,能直接丢弃吗? 不确定是否对业务没有影响,如果业务本身不在乎请求失败,那就没关系,否则不适合使用丢弃策略。
一般可以选择在自定义丢弃策略中使用消息队列(延后缓冲),或者发邮箱告警(及时发现),拒绝策略只要实现RejectedExecutionHandler接口即可:
@Configuration
@EnableAsync
@Slf4j
public class ThreadPoolTaskConfig {
@Bean("executor")
public Executor getExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(3);
executor.setQueueCapacity(5);
executor.setKeepAliveSeconds(10);
executor.setThreadNamePrefix("async-task-");
// 线程池对拒绝任务的处理策略
executor.setRejectedExecutionHandler((r, executor1) -> {
log.info("发送告警邮件======>:嘿沙雕,线上定时任务卡爆了, 当前线程名称为:{}, 当前线程池队列长度为:{}",
r.toString(),
executor1.getQueue().size());
});
// 初始化
executor.initialize();
return executor;
}
}
这样就具备了完善的告警机制了,可以及时发现线上的问题了。
当项目是多节点部署时,就需要考虑不同节点重复执行的问题。这时就需要考虑使用分布式锁了,但是使用分布式锁,有需要考虑是否会产生死锁。
Spring Boot定时任务的不足:
- 不支持集群时单节点启动(可能会产生不同节点重复执行同一定时任务)
- 不支持分片任务
- 不支持失败重试(一个任务失败了就失败了,不会重试)
- 动态调整比较繁琐(配置动态配置任务启动的时间点很复杂)
建议:在多节点部署,分布式项目中,使用Elastic-job或者XXL-Job
上面线程池严格意义上来说不是定时任务线程池,而是异步任务线程池,定时任务理论上只要加@Scheduled,不需要特意指定@Async,@Async是异步线程相关的。