问题背景:神秘的执行延迟
在近期维护的项目中,发现两个设置在同一时间(每天00:10)执行的定时任务出现了意外行为
// 任务A:数据归档任务
@Scheduled(cron = "0 10 0 * * ?")
public void saticschedule() {
// 数据归档操作
}
// 任务B:日志分区维护任务
@Scheduled(cron = "0 10 0 * * ?")
public void cycleTask() {
// 日志分区维护
}
通过日志排查发现,虽然两个任务配置了相同的触发时间,但任务B总是比任务A晚执行,且延迟时间恰好接近任务A的执行时长。这种看似"排队"的现象引发了我们的深入排查。
问题根源:Spring的定时任务单线程模型
通过源码分析和日志跟踪,我们定位到根本原因在于Spring默认的单线程任务执行机制:
1.单线程任务调度器
Spring的@Scheduled注解底层使用SingleThreadScheduledExecutor,这意味着:
- 所有定时任务共享同一线程
- 任务按注册顺序排队执行
- 前序任务会阻塞后续任务
2.任务注册顺序决定执行顺序
在我们的项目中:
java
@Configuration
public class SaticScheduleTask { /* 先注册 */ }
@Configuration
public class ScheduledTask { /* 后注册 */ }
由于SaticScheduleTask类定义在前,它的任务会先进入执行队列
3.耗时操作加剧阻塞
任务A中的数据库归档操作需要较长时间执行,导致任务B即使到达触发时间也必须等待
解决方案:多线程并发执行
方案一:自定义线程池
通过实现SchedulingConfigurer接口配置线程池:
@Configuration
@EnableScheduling
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
ThreadPoolTaskScheduler threadPool = new ThreadPoolTaskScheduler();
threadPool.setPoolSize(5); // 根据任务数量调整
threadPool.setThreadNamePrefix("sched-");
threadPool.setAwaitTerminationSeconds(60);
threadPool.setWaitForTasksToCompleteOnShutdown(true);
threadPool.initialize();
registrar.setTaskScheduler(threadPool);
}
}
方案二:异步执行
结合@Async实现异步任务:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncTask-");
executor.initialize();
return executor;
}
}
// 在定时任务上添加注解
@Async
@Scheduled(cron = "0 10 0 * * ?")
public void cycleTask() { ... }