1. 程式人生 > >補習系列(9)-springboot 定時器,你用對了嗎

補習系列(9)-springboot 定時器,你用對了嗎

empty cront apps 任務並發 轉發 gis execute 大小 定義

目錄

  • 簡介
  • 一、應用啟動任務
  • 二、JDK 自帶調度線程池
  • 三、@Scheduled
    • 定制 @Scheduled 線程池
  • 四、@Async
    • 定制 @Async 線程池
  • 小結

簡介

大多數的應用程序都離不開定時器,通常在程序啟動時、運行期間會需要執行一些特殊的處理任務。
比如資源初始化、數據統計等等,SpringBoot 作為一個靈活的框架,有許多方式可以實現定時器或異步任務。
我總結了下,大致有以下幾種:

    1. 使用 JDK 的 TimerTask
    1. 使用 JDK 自帶調度線程池
    1. 使用 Quartz 調度框架
    1. 使用 @Scheduled 、@Async 註解

其中第一種使用 TimerTask 的方法已經不建議使用,原因是在系統時間跳變時TimerTask存在掛死的風險


第三種使用 Quartz 調度框架可以實現非常強大的定時器功能,包括分布式調度定時器等等。
考慮作為大多數場景使用的方式,下面的篇幅將主要介紹 第二、第四種。

一、應用啟動任務

在 SpringBoot 應用程序啟動時,可以通過以下兩個接口實現初始化任務:

  1. CommandLineRunner
  2. ApplicationRunner

兩者的區別不大,唯一的不同在於:
CommandLineRunner 接收一組字符串形式的進程命令啟動參數;
ApplicationRunner 接收一個經過解析封裝的參數體對象。

詳細的對比看下代碼:

public class CommandLines {

    private static final Logger logger = LoggerFactory.getLogger(CommandLines.class);

    @Component
    @Order(1)
    public static class CommandLineAppStartupRunner implements CommandLineRunner {

        @Override
        public void run(String... args) throws Exception {
            logger.info(
                    "[CommandLineRunner]Application started with command-line arguments: {} .To kill this application, press Ctrl + C.",
                    Arrays.toString(args));
        }
    }
    
    @Component
    @Order(2)
    public static class AppStartupRunner implements ApplicationRunner {
        
        @Override
        public void run(ApplicationArguments args) throws Exception {
            logger.info("[ApplicationRunner]Your application started with option names : {}", args.getOptionNames());
        }
    }
}

二、JDK 自帶調度線程池

為了實現定時調度,需要用到 ScheduledThreadpoolExecutor
初始化一個線程池的代碼如下:

    /**
     * 構造調度線程池
     * 
     * @param corePoolSize
     * @param poolName
     * @return
     */
    public static ScheduledThreadPoolExecutor newSchedulingPool(int corePoolSize, String poolName) {

        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize);

        // 設置變量
        if (!StringUtils.isEmpty(poolName)) {
            threadPoolExecutor.setThreadFactory(new ThreadFactory() {
                
                @Override
                public Thread newThread(Runnable r) {
                    Thread tr = new Thread(r, poolName + r.hashCode());
                    return tr;
                }
            });
        }
        return threadPoolExecutor;
    }

可以將 corePoolSize 指定為大於1,以實現定時任務的並發執行。

為了在 SpringBoot 項目中使用,我們利用一個CommandLineRunner來實現:

@Component
@Order(1)
public class ExecutorTimer implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(ExecutorTimer.class);

    private ScheduledExecutorService schedulePool;

    @Override
    public void run(String... args) throws Exception {
        logger.info("start executor tasks");

        schedulePool = ThreadPools.newSchedulingPool(2);

        schedulePool.scheduleWithFixedDelay(new Runnable() {

            @Override
            public void run() {
                logger.info("run on every minute");

            }
        }, 5, 60, TimeUnit.SECONDS);
    }
}

schedulePool.scheduleWithFixedDelay 指定了調度任務以固定的頻率執行。

三、@Scheduled

@Scheduled 是 Spring3.0 提供的一種基於註解實現調度任務的方式。
在使用之前,需要通過 @EnableScheduling 註解啟用該功能。

代碼如下:

/**
 * 利用@Scheduled註解實現定時器
 * 
 * @author atp
 *
 */
@Component
public class ScheduleTimer {

    private static final Logger logger = LoggerFactory.getLogger(ScheduleTimer.class);

    /**
     * 每10s
     */
    @Scheduled(initialDelay = 5000, fixedDelay = 10000)
    public void onFixDelay() {
        logger.info("schedule job on every 10 seconds");
    }

    /**
     * 每分鐘的0秒執行
     */
    @Scheduled(cron = "0 * * * * *")
    public void onCron() {
        logger.info("schedule job on every minute(0 second)");
    }

    /**
     * 啟用定時器配置
     * 
     * @author atp
     *
     */
    @Configuration
    @EnableScheduling
    public static class ScheduleConfig {
    }
}

說明
上述代碼中展示了兩種定時器的使用方式:

第一種方式
指定初始延遲(initialDelay)、固定延遲(fixedDelay);

第二種方式
通過 cron 表達式定義
這與 unix/linux 系統 crontab 的定義類似,可以實現非常靈活的定制。

一些 cron 表達式的樣例:

表達式 說明
0 0 * * * * 每天的第一個小時
/10 * * * * 每10秒鐘
0 0 8-10 * * * 每天的8,9,10點鐘整點
0 * 6,19 * * * 每天的6點和19點每分鐘
0 0/30 8-10 * * * 每天8:00, 8:30, 9:00, 9:30 10:00
0 0 9-17 * * MON-FRI 工作日的9點到17點
0 0 0 25 12 ? 每年的聖誕夜午夜

定制 @Scheduled 線程池

默認情況下,@Scheduled 註解的任務是由一個單線程的線程池進行調度的。
這樣會導致應用內的定時任務只能串行執行。

為了實現定時任務並發,或是更細致的定制,
可以使用 SchedulingConfigurer 接口。

代碼如下:

    @Configuration
    @EnableScheduling
    public class ScheduleConfig implements SchedulingConfigurer {
        
        @Override
        public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
            taskRegistrar.setScheduler(taskExecutor());
        }
     
        @Bean(destroyMethod="shutdown")
        public Executor taskExecutor() {
            //線程池大小
            return Executors.newScheduledThreadPool(50);
        }
    }

四、@Async

@Async 註解的意義在於將 Bean方法的執行方式改為異步方式。
比如 在前端請求處理時,能通過異步執行提前返回結果。

類似的,該註解需要配合 @EnableAsync 註解使用。

代碼如下:

    @Configuration
    @EnableAsync
    public static class ScheduleConfig {

    }

使用 @Async 實現模擬任務

@Component
public class AsyncTimer implements CommandLineRunner {

    private static final Logger logger = LoggerFactory.getLogger(AsyncTimer.class);

    @Autowired
    private AsyncTask task;

    @Override
    public void run(String... args) throws Exception {
        long t1 = System.currentTimeMillis();
        task.doAsyncWork();

        long t2 = System.currentTimeMillis();
        logger.info("async timer execute in {} ms", t2 - t1);
    }


    @Component
    public static class AsyncTask {

        private static final Logger logger = LoggerFactory.getLogger(AsyncTask.class);

        @Async
        public void doAsyncWork() {
            long t1 = System.currentTimeMillis();

            try {
                Thread.sleep((long) (Math.random() * 5000));
            } catch (InterruptedException e) {
            }

            long t2 = System.currentTimeMillis();
            logger.info("async task execute in {} ms", t2 - t1);
        }
    }

示例代碼中,AsyncTask 等待一段隨機時間後結束。
而 AsyncTimer 執行了 task.doAsyncWork,將提前返回。

執行結果如下:

- async timer execute in 2 ms
- async task execute in 3154 ms

這裏需要註意一點,異步的實現,其實是通過 Spring 的 AOP 能力實現的。
對於 AsyncTask 內部方法間的調用卻無法達到效果。

定制 @Async 線程池

對於 @Async 線程池的定制需使用 AsyncConfigurer接口。

代碼如下:

    @Configuration
    @EnableAsync
    public static class ScheduleConfig implements AsyncConfigurer {

        @Bean
        public ThreadPoolTaskScheduler taskScheduler() {
            ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
            //線程池大小
            scheduler.setPoolSize(60);
            scheduler.setThreadNamePrefix("AsyncTask-");
            scheduler.setAwaitTerminationSeconds(60);
            scheduler.setWaitForTasksToCompleteOnShutdown(true);
            return scheduler;
        }

        @Override
        public Executor getAsyncExecutor() {
            return taskScheduler();
        }

        @Override
        public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
            return null;
        }

    }

小結

定時異步任務是應用程序通用的訴求,本文收集了幾種常見的實現方法。
作為 SpringBoot 應用來說,使用註解是最為便捷的。
在這裏我們對 @Scheduled、@Async 幾個常用的註解進行了說明,
並提供定制其線程池的方法,希望對讀者能有一定幫助。

歡迎繼續關註"美碼師的補習系列-springboot篇" ,如果覺得老司機的文章還不賴,請多多分享轉發^-^

補習系列(9)-springboot 定時器,你用對了嗎