现象
在项目里定义了2个任务,一个是每天晚上23点定时执行,一个是每隔10分钟执行一次用于数据同步。除了@Scheduled外,无其他额外配置项。
某天,运维突然找到我们说业务方的数据昨天就创建了,今天还没同步过来,帮忙排查一下原因。于是立马排查日志,发现同步任务在昨天8点执行了一次后都没有执行,而且线上3台容器用于同步,同时没有执行,着实让人疑惑。
又dump了线上的线程,发现也没有异常。突然小G说:会不会是定时任务线程池有问题。于是在日志中查找线程名称,果然有一个线程一直在运行,这下找到真凶了。
原因分析
原来用Springboot开启定时任务,如果有多个任务,那么多个任务会共享同一个线程池,
@SpringBootApplication
@EnableScheduling
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
如下图,即使定义了多个定时任务,同时只会有一个任务执行
可以看到线程的名字为
scheduleing-1,表示都是用的同一个线程,默认@Schedule是开启一个线程。
具体到上面的事故,其实是因为23点执行的任务一直都没有结束,导致没有线程去执行每隔10分钟的定时任务导致的。
解决方案
第一种:异步执行
给耗时长的方法加上@Async进行异步处理,比如如下2个方法:
@Async
@Scheduled(fixedRate = 100) // 执行时间长的方法
public void print() throws InterruptedException {
log.info("begin to print method");
for (int i = 0 ;i < 10000; i++) {
Thread.sleep(10000);
}
log.info("end to print method");
}
@Scheduled(fixedRate = 100, initialDelay = 200) // 执行时间短的方法
public void eat() throws InterruptedException {
log.info("begin to eat method");
for (int i = 0 ;i < 100; i++) {
Thread.sleep(10);
}
log.info("end to eat method");
}
在执行时间长的print()方法上加上@Async注解,执行如下图所示:
执行时间短的eat()方法执行不受影响。但
print()方法在同时启动默认的8个线程后也会阻塞,如下图。
反之如果把@Async注解放在执行时间短的方法
eat()上。则在执行时间长的方法print()被调度后,执行时间短的方法eat()也会阻塞,如下图
具体原因留作待办
第二种:调整定时任务的线程池数量
Spring Boot 版本2.1.0之后,可以直接在配置文件中设置 spring.task.scheduling.pool.size的值来配置计划任务线程池的容量,而不需要额外再写一个类。
spring.task.scheduling.pool.size=5
第三种:自定义定时任务线程池
//启用自动化配置
@Configuration
//启用定时任务
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
scheduledTaskRegistrar.setScheduler(getExecutor());
}
/**
* 获取线程池
*
* @return 返回队列
*/
protected ScheduledExecutorService getExecutor() {
//线程名称
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("schedule-pool-%d").build();
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2, namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
//最小线程数
executor.setCorePoolSize(2);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//最大线程数
executor.setMaximumPoolSize(5);
//允许空闲时间(秒)
executor.setKeepAliveTime(60, TimeUnit.SECONDS);
return executor;
}
}
参考文章
@Schedule
https://www.cnblogs.com/mmzs/p/16057742.html
https://blog.csdn.net/qq_33505611/article/details/112547734
https://www.jianshu.com/p/adbbb7e2b6b6
@Async
https://developer.aliyun.com/article/768513
https://cloud.tencent.com/developer/article/1426027
https://juejin.cn/post/6858854987280809997