Spring Boot

Spring Boot Scheduling Tasks 定時任務排程器及Cron表示式

61 / 100

Scheduling Tasks 定時任務排程器概述

專案開發中經常會用到定時任務,在實現定時任務都方式也是多種多樣。下面列舉幾種常見的定時任務實現方式:

  1. Quartz:Quartz的使用相當廣泛,它是一個功能強大的排程器,當然使用起來也相對麻煩。參考: [Java] quartz 工作定時排程器 – 我的工作日記
  2. java.util包裡的Timer,它也可以實現定時任務但是功能過於單一所有使用很少。
  3. Spring Boot 自帶的定時任務Schedule,其實可以把它看作是一個簡化版的,輕量級的Quartz,使用起來也相對方便很多。參考: Spring Boot Scheduling Tasks Guide

實作 Scheduling Tasks 定時任務排程器

  • 建立 Scheduling Tasks 定時任務排程器

建立一個 Class: ScheduleTasks , 並用 @Component 注冊

@Component
public class ScheduledTasks {

 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

 @Scheduled(fixedDelay = 60000) // fixedDelay = 60000 表示當前方法執行完畢 60000ms(1分鐘) 後,Spring scheduling會再次呼叫該方法
 public void testFixDelay() {
  logger.info("===fixedDelay: 時間:{}", dateFormat.format(new Date()));
 }

 @Scheduled(fixedRate = 60000) // fixedRate = 60000 表示當前方法開始執行 60000ms(1分鐘) 後,Spring scheduling會再次呼叫該方法
 public void testFixedRate() {
  logger.info("===fixedRate: 時間:{}", dateFormat.format(new Date()));
 }

 @Scheduled(initialDelay = 180000, fixedRate = 5000) // initialDelay = 180000 表示延遲 180000 (3秒) 執行第一次任務, 然後每 5000ms(5 秒) 再次呼叫該方法
 public void testInitialDelay() {
  logger.info("===initialDelay: 時間:{}", dateFormat.format(new Date()));
 }

 @Scheduled(cron = "0 0/1 * * * ?") // cron接受cron表示式,根據cron表示式確定定時規則
 public void testCron() {
  logger.info("===cron: 時間:{}", dateFormat.format(new Date()));
 }

}

使用@Scheduled annotations, 來建立定時任務 這個註解用來標註一個定時任務方法。通過看@Scheduled annotations,原始碼可以看出它支援多種引數:

  1. cron: cron表示式,指定任務在特定時間執行;
  2. fixedDelay: 表示上一次任務執行完成後多久再次執行,引數型別為long,單位ms;
  3. fixedDelayString: 與fixedDelay含義一樣,只是引數型別變為String;
  4. fixedRate: 表示按一定的頻率執行任務,引數型別為long,單位ms;
  5. fixedRateString: 與fixedRate的含義一樣,只是將引數型別變為String;
  6. initialDelay: 表示延遲多久再第一次執行任務,引數型別為long,單位ms;
  7. initialDelayString: 與initialDelay的含義一樣,只是將引數型別變為String;
  8. zone: 時區,預設為當前時區,一般沒有用到
  • 啟用 Scheduling Tasks 定時任務排程器

要加入 @EnableScheduling 才會有作用

@SpringBootApplication
@EnableScheduling
public class AppsApplication {

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

}
  • 執行結果 (單執行緒)

由執行結果可以發現, fixedRate 速度比 fixedDelay 快執行

#執行結果
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:09:45:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:09:45:25
[           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 443 (https) 80 (http) with context path ''
[           main] com.apps.AppsApplication                 : Started AppsApplication in 9.446 seconds (JVM running for 11.074)
[   scheduling-1] com.apps.component.ScheduledTasks        : ===cron: 時間:09:46:00
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:09:46:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:09:46:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===cron: 時間:09:47:00
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:09:47:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:09:47:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===cron: 時間:09:48:00
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:09:48:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:09:48:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:09:48:25
[   scheduling-1] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:09:48:30
[   scheduling-1] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:09:48:35
[   scheduling-1] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:09:48:40
[   scheduling-1] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:09:48:45
[   scheduling-1] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:09:48:50
  • 多執行緒處理定時任務(multiple thread scheduler)

控制台(Console)輸出的結果,所有的定時任務都是通過一個執行緒來處理的,從 Class: ScheduledAnnotationBeanPostProcessor 類開始一路找下去。果然,在ScheduledTaskRegistrar(定時任務註冊類)中的ScheduleTasks中又這樣一段判斷:

/**
 * Schedule all registered tasks against the underlying
 * {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}.
 */
@SuppressWarnings("deprecation")
protected void scheduleTasks() {
 if (this.taskScheduler == null) {
  this.localExecutor = Executors.newSingleThreadScheduledExecutor();
  this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor);
 }
 if (this.triggerTasks != null) {
  for (TriggerTask task : this.triggerTasks) {
   addScheduledTask(scheduleTriggerTask(task));
  }
 }
 if (this.cronTasks != null) {
  for (CronTask task : this.cronTasks) {
   addScheduledTask(scheduleCronTask(task));
  }
 }
 if (this.fixedRateTasks != null) {
  for (IntervalTask task : this.fixedRateTasks) {
   addScheduledTask(scheduleFixedRateTask(task));
  }
 }
 if (this.fixedDelayTasks != null) {
  for (IntervalTask task : this.fixedDelayTasks) {
   addScheduledTask(scheduleFixedDelayTask(task));
  }
 }
}

這就說明如果 taskScheduler 為空,那麼就給定時任務做了一個單執行緒的執行緒池,正好在這個類  ScheduledTaskRegistrar 中還有一個設定taskScheduler的方法:

/**
 * Set the {@link TaskScheduler} to register scheduled tasks with, or a
 * {@link java.util.concurrent.ScheduledExecutorService} to be wrapped as a
 * {@code TaskScheduler}.
 */
public void setScheduler(@Nullable Object scheduler) {
 if (scheduler == null) {
  this.taskScheduler = null;
 }
 else if (scheduler instanceof TaskScheduler) {
  this.taskScheduler = (TaskScheduler) scheduler;
 }
 else if (scheduler instanceof ScheduledExecutorService) {
  this.taskScheduler = new ConcurrentTaskScheduler(((ScheduledExecutorService) scheduler));
 }
 else {
  throw new IllegalArgumentException("Unsupported scheduler type: " + scheduler.getClass());
 }
}

這樣問題就很簡單了,我們只需用呼叫這個方法顯式的設定一個ScheduledExecutorService就可以達到同時執行的多執行緒效果了。我們要做的僅僅是實現SchedulingConfigurer介面,重寫configureTasks方法就OK了

/**
 * Description: 多執行緒執行定時任務
 */
@Configuration
//所有的定時任務都放在一個執行緒池中,定時任務啟動時使用不同都執行緒。
public class ScheduleConfig implements SchedulingConfigurer {
 @Override
 public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
  // 設定一個長度10的定時任務執行緒池
  taskRegistrar.setScheduler(Executors.newScheduledThreadPool(10));
 }
}
  • 執行結果 (多執行緒)

[pool-1-thread-2] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:15:41:37
[pool-1-thread-1] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:15:41:37
[           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 443 (https) 80 (http) with context path ''
[           main] com.apps.AppsApplication                 : Started AppsApplication in 11.321 seconds (JVM running for 12.98)
[pool-1-thread-2] com.apps.component.ScheduledTasks        : ===cron: 時間:15:42:00
[pool-1-thread-1] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:15:42:37
[pool-1-thread-3] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:15:42:37
[pool-1-thread-4] com.apps.component.ScheduledTasks        : ===cron: 時間:15:43:00
[pool-1-thread-6] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:15:43:37
[pool-1-thread-5] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:15:43:37
[pool-1-thread-2] com.apps.component.ScheduledTasks        : ===cron: 時間:15:44:00
[pool-1-thread-1] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:15:44:37
[pool-1-thread-7] com.apps.component.ScheduledTasks        : ===fixedRate: 時間:15:44:37
[pool-1-thread-8] com.apps.component.ScheduledTasks        : ===fixedDelay: 時間:15:44:37
[pool-1-thread-3] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:15:44:42
[pool-1-thread-4] com.apps.component.ScheduledTasks        : ===initialDelay: 時間:15:44:47

 

Cron表示式

  • cron表示式定義

Cron表示式是一個字串,是由空格隔開的6或7個域組成,每一個域對應一個含義(秒 分 時 每月第幾天 月 星期 年)其中年是可選欄位。

 ┌───────────── second (0-59)
 │ ┌───────────── minute (0 - 59)
 │ │ ┌───────────── hour (0 - 23)
 │ │ │ ┌───────────── day of the month (1 - 31)
 │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
 │ │ │ │ │ ┌───────────── day of the week (0 - 7)
 │ │ │ │ │ │          (0 or 7 is Sunday, or MON-SUN)
 │ │ │ │ │ │
 * * * * * *
  • 每個域可出現的字元型別和各字元的含義

秒:可出現: ”, – * /” 左列的四個字元,有效範圍為0-59的整數

分:可出現: ”, – * /” 左列的四個字元,有效範圍為0-59的整數

時:可出現: ”, – * /” 左列的四個字元,有效範圍為0-23的整數

每月第幾天:可出現: ”, – * / ? L W C” 左列的八個字元,有效範圍為0-31的整數

月:可出現: ”, – * /” 左列的四個字元,有效範圍為1-12的整數或JAN-DEc

星期:可出現: ”, – * / ? L C #” 左列的八個字元,有效範圍為1-7的整數或SUN-SAT兩個範圍。1表示星期天,2表示星期一, 依次類推
  • 特殊字元含義

* : 表示匹配該域的任意值,比如在秒*, 就表示每秒都會觸發事件。;

? : 只能用在每月第幾天和星期兩個域。表示不指定值,當2個子表示式其中之一被指定了值以後,為了避免衝突,需要將另一個子表示式的值設為“?”;

– : 表示範圍,例如在分域使用5-20,表示從5分到20分鐘每分鐘觸發一次  

/ : 表示起始時間開始觸發,然後每隔固定時間觸發一次,例如在分域使用5/20,則意味著5分,25分,45分,分別觸發一次.  

, : 表示列出列舉值。例如:在分域使用5,20,則意味著在5和20分時觸發一次。  

L : 表示最後,只能出現在星期和每月第幾天域,如果在星期域使用1L,意味著在最後的一個星期日觸發。  

W : 表示有效工作日(週一到週五),只能出現在每月第幾日域,系統將在離指定日期的最近的有效工作日觸發事件。注意一點,W的最近尋找不會跨過月份  

LW : 這兩個字元可以連用,表示在某個月最後一個工作日,即最後一個星期五。  

# : 用於確定每個月第幾個星期幾,只能出現在每月第幾天域。例如在1#3,表示某月的第三個星期日。
  • Macros

You can use these macros instead of the six-digit value, thus: @Scheduled(cron = "@hourly").

MacroMeaning

@yearly (or @annually)

once a year (0 0 0 1 1 *)

@monthly

once a month (0 0 0 1 * *)

@weekly

once a week (0 0 0 * * 0)

@daily (or @midnight)

once a day (0 0 0 * * *), or

@hourly

once an hour, (0 0 * * * *)

參考

  1. SpringBoot定時任務及Cron表示式詳解
  2. Task Execution and Scheduling