SpringBoot @Schedule使用注意與原理

簡介

以前使用@Schedule一直沒有遇到什麼問題,那種拿來就用的感受還挺好,最近使用@Schedule遇到一點問題,才仔細的研究了一下@Schedule的一些細節和原理問題。java

這篇文章就將分享一下,使用@Schedule一些可能被忽略的問題。spring

注意事項

@Schedule默認線程池大小

我相信@Schedule默認線程池大小的問題確定是被不少拿來就用的朋友忽略的問題,默認狀況下@Schedule使用線程池的大小爲1。app

通常狀況下沒有什麼問題,可是若是有多個定時任務,每一個定時任務執行時間可能不短的狀況下,那麼有的定時任務可能一直沒有機會執行。ide

有興趣的朋友,能夠試一下:post

@Component
public class BrigeTask {

    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Scheduled(cron = "*/5 * * * * ?")
    private void cron() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-cron:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }

    @Scheduled(fixedDelay = 5000)
    private void fixedDelay() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-fixedDelay:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }

    @Scheduled(fixedRate = 5000)
    private void fixedRate() throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + "-fixedRate:" + LocalDateTime.now().format(FORMATTER));
        TimeUnit.SECONDS.sleep(6);
    }
}

上面的任務中,fixedDelay與cron,可能好久都不會被執行。測試

任務執行機會少

要解決上面的問題,能夠把執行任務的線程池設置大一點,怎樣設置經過實現SchedulingConfigurer接口,在configureTasks方法中配置,這種方式參見後面的代碼,這裏能夠直接注入一個TaskScheduler來解決問題。this

@Bean
public TaskScheduler taskScheduler() {
    ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
    taskScheduler.setPoolSize(5);
    return taskScheduler;
}

固然也可使用ScheduledExecutorService:線程

@Bean
public ScheduledExecutorService scheduledExecutorService() {
    return Executors.newScheduledThreadPool(10);
}

爲啥這樣有效,請參考後面@Schedule原理。debug

固定延遲與固定速率

@Schedule的三種方式cron、fixedDelay、fixedRate無論線程夠不夠都會阻塞到上一次執行完成,纔會執行下一次。code

若是任務方法執行時間很是短,上面三種方式其實基本沒有太多的區別。

若是,任務方法執行時間比較長,大於了設置的執行週期,那麼就有很大的區別。例如,假設執行任務的線程足夠,執行週期是5s,任務方法會執行6s。

cron的執行方式是,任務方法執行完,遇到下一次匹配的時間再次執行,基本就會10s執行一次,由於執行任務方法的時間區間會錯過一次匹配。

fixedDelay的執行方式是,方法執行了6s,而後會再等5s再執行下一次,在上面的條件下,基本就是每11s執行一次。

fixedRate的執行方式就變成了每隔6s執行一次,由於按固定區間執行它沒5s就應該執行一次,可是任務方法執行了6s,沒辦法,只好6s執行一次。

上面的結論均可以經過,最上面的示例驗證,有興趣的朋友能夠調整一下休眠時間測試一下。

SpringBoot @Schedule原理

在SpringBoot中,咱們使用@EnableScheduling來啓用@Schedule。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

EnableScheduling註解沒什麼特殊,須要注意import了SchedulingConfiguration。

SchedulingConfiguration一看名字就知道是一個配置類,確定是爲了添加相應的依賴類。

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

	@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
	public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
		return new ScheduledAnnotationBeanPostProcessor();
	}
}

咱們能夠看到在SchedulingConfiguration建立了一個ScheduledAnnotationBeanPostProcessor。

看樣子SpringBoot定時任務的核心就是ScheduledAnnotationBeanPostProcessor類了,下面咱們來看一看ScheduledAnnotationBeanPostProcessor類。

ScheduledAnnotationBeanPostProcessor

public class ScheduledAnnotationBeanPostProcessor
		implements ScheduledTaskHolder, MergedBeanDefinitionPostProcessor, DestructionAwareBeanPostProcessor,
		Ordered, EmbeddedValueResolverAware, BeanNameAware, BeanFactoryAware, ApplicationContextAware,
		SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
}

ScheduledAnnotationBeanPostProcessor實現了不少接口,這裏重點關注2個,ApplicationListener和DestructionAwareBeanPostProcessor。

DestructionAwareBeanPostProcessor封裝任務

DestructionAwareBeanPostProcessor繼承了BeanPostProcessor。

BeanPostProcessor相信你們已經很是熟悉了,就是在Bean建立執行setter以後,在自定義的afterPropertiesSet和init-method先後提供攔截點,大體執行的前後順序是:

Bean實例化 -> setter -> BeanPostProcessor#postProcessBeforeInitialization ->
-> InitializingBean#afterPropertiesSet -> init-method -> BeanPostProcessor#postProcessAfterInitialization

咱們看一下ScheduledAnnotationBeanPostProcessor的postProcessAfterInitialization方法:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler ||
            bean instanceof ScheduledExecutorService) {
        // Ignore AOP infrastructure such as scoped proxies.
        return bean;
    }

    Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean);
    if (!this.nonAnnotatedClasses.contains(targetClass) &&
            AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled.class, Schedules.class))) {
        Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> {
                    Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                            method, Scheduled.class, Schedules.class);
                    return (!scheduledMethods.isEmpty() ? scheduledMethods : null);
                });
        if (annotatedMethods.isEmpty()) {
            this.nonAnnotatedClasses.add(targetClass);
            if (logger.isTraceEnabled()) {
                logger.trace("No @Scheduled annotations found on bean class: " + targetClass);
            }
        }
        else {
            // Non-empty set of methods
            annotatedMethods.forEach((method, scheduledMethods) ->
                    scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean)));
            if (logger.isTraceEnabled()) {
                logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
                        "': " + annotatedMethods);
            }
        }
    }
    return bean;
}

簡單說一下流程:

找到全部的Schedule方法,把它封裝爲ScheduledMethodRunnable類(ScheduledMethodRunnable類實現了Runnable接口),並把其作爲一個任務註冊到ScheduledTaskRegistrar中。

若是對具體的邏輯感興趣,能夠從postProcessAfterInitialization方法順着processScheduled方法一次debug。

ApplicationListener執行任務

前面咱們介紹經過BeanPostProcessor解析出了全部的任務,接下來要作的事情就是提交任務了。

@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
    if (event.getApplicationContext() == this.applicationContext) {
        // Running in an ApplicationContext -> register tasks this late...
        // giving other ContextRefreshedEvent listeners a chance to perform
        // their work at the same time (e.g. Spring Batch's job registration).
        finishRegistration();
    }
}

ScheduledAnnotationBeanPostProcessor監聽的事件是ContextRefreshedEvent,就是在容器初始化,或者刷新的時候被調用。

監聽到ContextRefreshedEvent事件以後,值調用了finishRegistration方法,這個方法的基本流程以下:

  1. 找到容器中的SchedulingConfigurer,並調用它的configureTasks,SchedulingConfigurer的做用主要就是配置ScheduledTaskRegistrar類,例如線程池等參數,例如:
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executors;

@Configuration
public class MyScheduleConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
    }
}
  1. 調用ScheduledTaskRegistrar的afterPropertiesSet方法執行任務,若是對具體的邏輯感興趣,能夠閱讀ScheduledTaskRegistrar的scheduleTasks方法。

關於爲啥直接在容器中注入一個TaskScheduler、ScheduledExecutorService也能夠有效,也能夠在finishRegistration方法中找到答案。

相關文章
相關標籤/搜索