3分鐘帶你掌握Spring Boot中的定時排程服務

潘志的研发笔记發表於2024-07-19

一、背景介紹

在實際的業務開發過程中,我們經常會需要定時任務來幫助我們完成一些工作,例如每天早上 6 點生成銷售報表、每晚 23 點清理髒資料等等。

如果你當前使用的是 SpringBoot 來開發專案,那麼完成這些任務會非常容易!

SpringBoot 預設已經幫我們完成了相關定時任務元件的配置,我們只需要新增相應的註解@Scheduled就可以實現任務排程!

二、方案實踐

2.1、pom 包配置

pom包裡面只需要引入Spring Boot Starter包即可!

<dependencies>
    <!--spring boot核心-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--spring boot 測試-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2.2、啟動類啟用定時排程

在啟動類上面加上@EnableScheduling即可開啟定時

@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {

    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class, args);
    }
}

2.3、建立定時任務

Spring Scheduler支援四種形式的任務排程!

  • fixedRate:固定速率執行,例如每5秒執行一次
  • fixedDelay:固定延遲執行,例如距離上一次呼叫成功後2秒執行
  • initialDelay:初始延遲任務,例如任務開啟過5秒後再執行,之後以固定頻率或者間隔執行
  • cron:使用 Cron 表示式執行定時任務
2.3.1、固定速率執行

你可以透過使用fixedRate引數以固定時間間隔來執行任務,示例如下:

@Component
public class SchedulerTask {

    private static final Logger log = LoggerFactory.getLogger(SchedulerTask.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * fixedRate:固定速率執行。每5秒執行一次。
     */
    @Scheduled(fixedRate = 5000)
    public void runWithFixedRate() {
        log.info("Fixed Rate Task,Current Thread : {},The time is now : {}", Thread.currentThread().getName(), dateFormat.format(new Date()));
    }
}

執行ScheduleApplication主程式,即可看到控制檯輸出效果:

Fixed Rate Task,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:00
Fixed Rate Task,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:10
...
2.3.2、固定延遲執行

你可以透過使用fixedDelay引數來設定上一次任務呼叫完成與下一次任務呼叫開始之間的延遲時間,示例如下:

@Component
public class SchedulerTask {

    private static final Logger log = LoggerFactory.getLogger(SchedulerTask.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * fixedDelay:固定延遲執行。距離上一次呼叫成功後2秒後再執行。
     */
    @Scheduled(fixedDelay = 2000)
    public void runWithFixedDelay() {
        log.info("Fixed Delay Task,Current Thread : {},The time is now : {}", Thread.currentThread().getName(), dateFormat.format(new Date()));
    }
}

控制檯輸出效果:

Fixed Delay Task,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:00
Fixed Delay Task,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:02
...
2.3.3、初始延遲任務

你可以透過使用initialDelay引數與fixedRate或者fixedDelay搭配使用來實現初始延遲任務排程。

@Component
public class SchedulerTask {

    private static final Logger log = LoggerFactory.getLogger(SchedulerTask.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * initialDelay:初始延遲。任務的第一次執行將延遲5秒,然後將以5秒的固定間隔執行。
     */
    @Scheduled(initialDelay = 5000, fixedRate = 5000)
    public void reportCurrentTimeWithInitialDelay() {
        log.info("Fixed Rate Task with Initial Delay,Current Thread : {},The time is now : {}", Thread.currentThread().getName(), dateFormat.format(new Date()));
    }
}

控制檯輸出效果:

Fixed Rate Task with Initial Delay,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:05
Fixed Rate Task with Initial Delay,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:10
...
2.3.4、使用 Cron 表示式

Spring Scheduler同樣支援Cron表示式,如果以上簡單引數都不能滿足現有的需求,可以使用 cron 表示式來定時執行任務。

關於cron表示式的具體用法,可以點選參考這裡: https://cron.qqe2.com/

@Component
public class SchedulerTask {

    private static final Logger log = LoggerFactory.getLogger(SchedulerTask.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * cron:使用Cron表示式。每6秒中執行一次
     */
    @Scheduled(cron = "*/6 * * * * ?")
    public void reportCurrentTimeWithCronExpression() {
        log.info("Cron Expression,Current Thread : {},The time is now : {}", Thread.currentThread().getName(), dateFormat.format(new Date()));
    }
}

控制檯輸出效果:

Cron Expression,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:06
Cron Expression,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 11:46:12
...

2.4、非同步執行定時任務

在介紹非同步執行定時任務之前,我們先看一個例子!

在下面的示例中,我們建立了一個每隔2秒執行一次的定時任務,在任務裡面大概需要花費 3 秒鐘,猜猜執行結果如何?

@Component
public class AsyncScheduledTask {

    private static final Logger log = LoggerFactory.getLogger(AsyncScheduledTask.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Scheduled(fixedRate = 2000)
    public void runWithFixedDelay() {
        try {
            TimeUnit.SECONDS.sleep(3);
            log.info("Fixed Delay Task, Current Thread : {} : The time is now {}", Thread.currentThread().getName(), dateFormat.format(new Date()));
        } catch (InterruptedException e) {
            log.error("錯誤資訊",e);
        }
    }
}

控制檯輸入結果:

Fixed Delay Task, Current Thread : scheduling-1 : The time is now 2020-12-15 17:55:26
Fixed Delay Task, Current Thread : scheduling-1 : The time is now 2020-12-15 17:55:31
Fixed Delay Task, Current Thread : scheduling-1 : The time is now 2020-12-15 17:55:36
Fixed Delay Task, Current Thread : scheduling-1 : The time is now 2020-12-15 17:55:41
...

很清晰的看到,任務排程頻率變成了每隔5秒排程一次!

這是為啥呢?

Current Thread : scheduling-1輸出結果可以很看到,任務執行都是同一個執行緒!預設的情況下,@Scheduled任務都在 Spring 建立的大小為 1 的預設執行緒池中執行!

更直觀的結果是,任務都是序列執行!

下面,我們將其改成非同步執行緒來執行,看看效果如何?

@Component
@EnableAsync
public class AsyncScheduledTask {

    private static final Logger log = LoggerFactory.getLogger(AsyncScheduledTask.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    @Async
    @Scheduled(fixedDelay = 2000)
    public void runWithFixedDelay() {
        try {
            TimeUnit.SECONDS.sleep(3);
            log.info("Fixed Delay Task, Current Thread : {} : The time is now {}", Thread.currentThread().getName(), dateFormat.format(new Date()));
        } catch (InterruptedException e) {
            log.error("錯誤資訊",e);
        }
    }
}

控制檯輸出結果:

Fixed Delay Task, Current Thread : SimpleAsyncTaskExecutor-1 : The time is now 2020-12-15 18:55:26
Fixed Delay Task, Current Thread : SimpleAsyncTaskExecutor-2 : The time is now 2020-12-15 18:55:28
Fixed Delay Task, Current Thread : SimpleAsyncTaskExecutor-3 : The time is now 2020-12-15 18:55:30
...

任務的執行頻率不受方法內的時間影響,以並行方式執行!

2.5、自定義任務執行緒池

雖然預設的情況下,@Scheduled任務都在 Spring 建立的大小為 1 的預設執行緒池中執行,但是我們也可以自定義執行緒池,只需要實現SchedulingConfigurer類即可!

自定義執行緒池示例如下:

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        //執行緒池大小為10
        threadPoolTaskScheduler.setPoolSize(10);
        //設定執行緒名稱字首
        threadPoolTaskScheduler.setThreadNamePrefix("scheduled-thread-");
        //設定執行緒池關閉的時候等待所有任務都完成再繼續銷燬其他的Bean
        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        //設定執行緒池中任務的等待時間,如果超過這個時候還沒有銷燬就強制銷燬,以確保應用最後能夠被關閉,而不是阻塞住
        threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
        //這裡採用了CallerRunsPolicy策略,當執行緒池沒有處理能力的時候,該策略會直接在 execute 方法的呼叫執行緒中執行被拒絕的任務;如果執行程式已關閉,則會丟棄該任務
        threadPoolTaskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskScheduler.initialize();

        scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }
}

我們啟動服務,看看cron任務示例排程效果:

Cron Expression,Current Thread : scheduled-thread-1,The time is now : 2020-12-15 20:46:00
Cron Expression,Current Thread : scheduled-thread-2,The time is now : 2020-12-15 20:46:06
Cron Expression,Current Thread : scheduled-thread-3,The time is now : 2020-12-15 20:46:12
Cron Expression,Current Thread : scheduled-thread-4,The time is now : 2020-12-15 20:46:18
....

當前執行緒名稱已經被改成自定義scheduled-thread的字首!

三、小結

本文主要圍繞Spring scheduled應用實踐進行分享,如果是單體應用,使用SpringBoot內建的@scheduled註解可以解決大部分業務需求,上手非常容易!

專案原始碼地址:spring-boot-example-scheduled

四、參考

1、https://springboot.io/t/topic/2758

相關文章