阅读完此文你会对Springboot的schedule程序有更加深入的了解和认识。
案例:
有两个调度器,一个调度器每3分钟运行一次从数据库中获取数据,另一个调度器每分钟运行一次调用REST API。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
public class YourSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(YourSpringBootApplication.class, args);
}
}
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class FirstScheduler {
@Scheduled(fixedRate = 3 * 60 * 1000) // 每3分钟运行一次
public void getDataFromDB() {
// 从数据库获取数据的代码
}
}
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class SecondScheduler {
@Scheduled(fixedRate = 1 * 60 * 1000) // 每1分钟运行一次
public void consumeRestApi() {
// 调用REST API的代码
}
}
问题:
当第一个调度器运行时间超过预期时间,例如执行了10分钟,那么第二个调度器则不会运行,一直到第一个调度器运行结束后。
上述代码存在的问题,当一个调度器运行时,第二个调度器会被阻塞。
解决方案:
为了避免第二个调度器因第一个调度器而被阻塞,可以使用Spring的异步执行。这样做每个调度器都将在自己单独线程中运行从而实现独立工作。
实现代码:
在使用Spring的异步执行支持时,最佳做法是配置一个自定义线程池来控制异步任务执行所用的线程数量。默认情况下Spring使用SimpleAsyncTaskExecutor(这在生产环境中可能不太合适,因为它不提供更多对线程池的控制方法。)
为了解决这个问题建议在应用程序中创建一个自定义线程池bean。以下是实现代码:
步骤1:定义一个配置类创建自定义线程池bean。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class AsyncConfiguration {
@Bean(name = "asyncTaskExecutor")
public ThreadPoolTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 设置线程池中的初始线程数
executor.setMaxPoolSize(10); // 设置线程池中的最大线程数
executor.setQueueCapacity(25); // 设置用于保存挂起任务的队列容量
executor.setThreadNamePrefix("AsyncTask-"); // 设置线程名前缀
executor.initialize();
return executor;
}
}
步骤2:修改第一个调度器,使其使用此自定义线程池。
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class FirstScheduler {
@Async("asyncTaskExecutor") // 指定自定义线程池bean名称
@Scheduled(fixedRate = 3 * 60 * 1000) // 每3分钟运行一次
public void getDataFromDB() {
// 从数据库获取数据的代码
// 此方法将在自定义线程池中异步运行
}
}
通过这种配置,第一个调度器方法(getDataFromDB)将在自定义线程池中异步运行,而第二个调度器方法(consumeRestApi)则在默认调度器的线程中运行。
根据应用程序需求和可用系统资源调整corePoolSize、maxPoolSize和queueCapacity值。线程池配置对应用程序的性能产生比较重要的影响,因此需要适当调整这些值。
要使第二个调度器也使用自定义线程池进行异步执行,需要在consumeRestApi方法的@Async注解中添加taskExecutor属性。这样确保两个调度器都在相同的自定义线程池中异步运行。以下是更新后的代码:
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class SecondScheduler {
@Async("asyncTaskExecutor") // 指定自定义线程池bean名称
@Scheduled(fixedRate = 1 * 60 * 1000) // 每1分钟运行一次
public void consumeRestApi() {
// 调用REST API的代码
// 此方法将在自定义线程池中异步运行
}
}
通过这种设置,第一个调度器(getDataFromDB)和第二个调度器(consumeRestApi)都将在相同的自定义线程池中异步运行。这将允许它们独立地工作,即使其中一个任务需要更长时间来完成。
使用自定义线程池
记录错误消息,当所需线程池大小 > 配置的线程池大小时:
要在所需线程池大小超过配置的线程池大小时记录错误消息,可以利用Spring的ThreadPoolTaskExecutor的RejectedExecutionHandler。当线程池的任务队列已满且线程池无法接受更多任务时,将调用此处理程序。可以使用此回调来记录错误消息。
以下是更新的配置类,其中包含RejectedExecutionHandler:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
@Sl4J
public class AsyncConfiguration {
@Bean(name = "asyncTaskExecutor")
public ThreadPoolTaskExecutor asyncTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 设置线程池中的初始线程数
executor.setMaxPoolSize(10); // 设置线程池中的最大线程数
executor.setQueueCapacity(25); // 设置用于保存挂起任务的队列容量
executor.setThreadNamePrefix("AsyncTask-"); // 设置线程名前缀
// 设置RejectedExecutionHandler以记录错误消息
executor.setRejectedExecutionHandler((Runnable r, ThreadPoolExecutor e) -> {
// 记录错误消息,队列已满,任务被拒绝
// 根据需要自定义此消息
log.error("任务被拒绝:线程池已满。请增加线程池大小。");
});
executor.initialize();
return executor;
}
}
通过这种配置,当线程池的队列已满且尝试提交额外任务时,RejectedExecutionHandler会被触发。
你可以根据应用程序的需求自定义错误消息或采取其他操作。
请注意,设置正确的线程池大小和队列容量对于应用程序的性能和资源利用率至关重要。
如果因为队列容量不足而任务持续被拒绝则需要增加线程池大小或调整队列容量。
总结:
通过配置共享的自定义线程池、利用@Async注解以及整合RejectedExecutionHandler,可以使应用程序有效地管理和执行多个调度器并发从而确保调度器独立运行,并且系统能够优雅地响应线程池限制。