實戰Spring Boot 2.0系列(六) - 單機定時任務的幾種實現

前言

定時任務 通常會存在 中大型企業級 項目中,爲了減小 服務器數據庫 的壓力,每每會以 定時任務 的方式去完成某些業務邏輯。java

本系列文章

  1. 實戰Spring Boot 2.0系列(一) - 使用Gradle構建Docker鏡像
  2. 實戰Spring Boot 2.0系列(二) - 全局異常處理和測試
  3. 實戰Spring Boot 2.0系列(三) - 使用@Async進行異步調用詳解
  4. 實戰Spring Boot 2.0系列(四) - 使用WebAsyncTask處理異步任務
  5. 實戰Spring Boot 2.0系列(五) - Listener, Servlet, Filter和Interceptor
  6. 實戰Spring Boot 2.0系列(六) - 單機定時任務的幾種實現

常見的就是 金融服務系統 推送回調,通常支付系統訂單在沒有收到成功的回調返回內容時會 持續性的回調,這種回調通常都是 定時任務 來完成。web

還有就是 報表的生成,咱們通常會在客戶 訪問量小 時完成這個操做,也能夠採用 定時任務 來完成。spring

正文

定時任務的幾種方式

Timer

這是 Java 自帶的 java.util.Timer 類,這個類容許調度一個名爲 java.util.TimerTask 任務。使用這種方式可讓你的程序按照某一個 頻度 執行,但不能在 指定時間 運行。如今通常用的較少。數據庫

ScheduledExecutorService

JDK 自帶的一個類,是基於 線程池 設計的定時任務類,每一個 調度任務 都會分配到 線程池 中的一個 線程 去執行。也就是說,任務是 併發執行,互不影響的。編程

Spring Task

Spring 3.0 之後自帶的 Task,支持 多線程 調度,能夠將它當作一個 輕量級Quartz,並且使用起來比 Quartz 簡單許多,可是適用於 單節點定時任務調度後端

Quartz

這是一個 功能比較強大 的的調度器,可讓你的程序在指定時間執行,也能夠按照某一個頻度執行,配置起來 稍顯複雜Quartz 功能強大,能夠結合 數據庫持久化,進行 分佈式任務延時調度緩存

Cron表達式簡介

Cron 表達式是一個字符串,字符串以 56空格 隔開,分爲 67,每個域表明一個含義,Cron 有以下兩種語法格式:springboot

  1. Seconds Minutes Hours DayofMonth Month DayofWeek Year
  2. Seconds Minutes Hours DayofMonth Month DayofWeek

每一個域對應的含義、域值範圍和特殊表示符,從左到右依次以下:bash

字段 容許值 容許的特殊字符
0-59 , - * /
0-59 , - * /
小時 0-23 , - * /
日期 1-31 , - * / L W C
月份 1-12 或者 JAN-DEC , - * /
星期 1-7 或者 SUN-SAT , - * / L C #
年(可選) 留空, 1970-2099 , - * /

如上面的表達式所示:服務器

  • ""字符: 被用來指定全部的值。如:在分鐘的字段域裏表示"每分鐘"。

  • "-"字符: 被用來指定一個範圍。如:"10-12" 在小時域意味着 "10點、11點、12點"。

  • ","字符: 被用來指定另外的值。如:"MON,WED,FRI" 在星期域裏表示 "星期1、星期3、星期五"。

  • "?"字符: 只在日期域和星期域中使用。它被用來指定"非明確的值"。當你須要經過在這兩個域中的一個來指定一些東西的時候,它是有用的。看下面的例子你就會明白。

  • "L"字符: 指定在月或者星期中的某天(最後一天)。即 "Last" 的縮寫。可是在星期和月中 "L" 表示不一樣的意思,如:在月子段中 "L" 指月份的最後一天 - 1月31日,2月28日。

    • 若是在星期字段中則簡單的表示爲 "7" 或者 "SAT" 字符。
    • 若是在星期字段中在某個 value 值得後面,則表示 "某月的最後一個星期value",如 "6L" 表示某月的最後一個星期五。
  • "W"字符: 只能用在月份字段中,該字段指定了離指定日期最近的那個星期日。

  • "#"字符: 只能用在星期字段,該字段指定了第幾個星期 value 在某月中

每個元素均可以顯式地規定一個值(如 6),一個區間(如 9-12),一個列表(如 9,11,13)或一個通配符(如 *)。"月份中的日期""星期中的日期" 這兩個元素是 互斥的,所以應該經過設置一個 問號?)來代表你不想設置的那個字段。下表顯示了一些 cron 表達式的 例子 和它們的意義:

表達式 意義
"0 0 12 * * ?" 天天中午12點觸發
"0 15 10 ? * *" 天天上午10:15觸發
"0 15 10 * * ?" 天天上午10:15觸發
"0 15 10 * * ? *" 天天上午10:15觸發
"0 15 10 * * ? 2005" 2005年的天天上午10:15觸發
"0 * 14 * * ?" 在天天下午2點到下午2:59期間的每1分鐘觸發
"0 0/5 14 * * ?" 在天天下午2點到下午2:55期間的每5分鐘觸發
"0 0/5 14,18 * * ?" 在天天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
"0 0-5 14 * * ?" 在天天下午2點到下午2:05期間的每1分鐘觸發
"0 10,44 14 ? 3 WED" 每一年三月的星期三的下午2:10和2:44觸發
"0 15 10 ? * MON-FRI" 週一至週五的上午10:15觸發
"0 15 10 15 * ?" 每個月15日上午10:15觸發
"0 15 10 L * ?" 每個月最後一日的上午10:15觸發
"0 15 10 ? * 6L" 每個月的最後一個星期五上午10:15觸發
"0 15 10 ? * 6L 2002-2005" 2002年至2005年的每個月的最後一個星期五上午10:15觸發
"0 15 10 ? * 6#3" 每個月的第三個星期五上午10:15觸發
0 6 * * * 天天早上6點
0 /2 * * 每兩個小時
0 23-7/2,8 * * * 晚上11點到早上8點之間每兩個小時,早上八點
0 11 4 * 1-3 每月的4號和每一個禮拜的禮拜一到禮拜三的早上11點
0 4 1 1 * 1月1日早上4點

環境準備

配置gradle依賴

利用 Spring Initializer 建立一個 gradle 項目 spring-boot-scheduler-task-management,建立時添加相關依賴。獲得的初始 build.gradle 以下:

buildscript {
    ext {
        springBootVersion = '2.0.3.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'io.ostenant.springboot.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter')
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}
複製代碼

Spring Boot 入口類上配置 @EnableScheduling 註解開啓 Spring 自帶的定時處理功能。

@SpringBootApplication
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
複製代碼

配置Timer任務

這個 API 目前在項目中不多用,直接給出示例代碼。具體的介紹能夠查看 APITimer 的內部只有 一個線程,若是有 多個任務 的話就會 順序執行,這樣任務的 延遲時間循環時間 就會出現問題。

TimerService.java

public class TimerService {
    private static final Logger LOGGER = LoggerFactory.getLogger(TimerService.class);
    private AtomicLong counter = new AtomicLong();

    public void schedule() {
        TimerTask timerTask = new TimerTask() {
            @Override
            public void run() {
                long count = counter.incrementAndGet();
                LOGGER.info("Schedule timerTask {} times", count);
            }
        };
        Timer timer = new Timer();
        timer.schedule(timerTask, 1000L, 10 * 1000L;
    }
}
複製代碼

上面的代碼定義了一個 TimerTask,在 TimerTask 中累加 執行次數,並經過 slf4j 進行打印 (自帶執行時間)。而後經過 Timer 調度工具類調度 TimerTask 任務,設置 初始化延遲時間1s定時執行間隔10s,測試代碼以下:

public static void main(String[] args) {
    TimerService timerService = new TimerService();
    timerService.schedule();
}
複製代碼

觀察測試結果,可以發現 TimerTask 配置的任務每隔 10s 被執行了一次,執行線程默認都是 Timer-0 這個線程。

17:48:18.731 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 1 times
17:48:28.730 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 2 times
17:48:38.736 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 3 times
17:48:48.738 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 4 times
17:48:58.743 [Timer-0] INFO io.ostenant.springboot.sample.timer.TimerService - Schedule timerTask 5 times
複製代碼

配置ScheduledExecutorService任務

ScheduledExecutorService延時執行 的線程池,對於 多線程 環境下的 定時任務,推薦用 ScheduledExecutorService 代替 Timer 定時器。

建立一個線程數量爲 4任務線程池,同一時刻並向它提交 4 個定時任務,用於測試延時任務的 併發處理。執行 ScheduledExecutorServicescheduleWithFixedDelay() 方法,設置任務線程池的 初始任務延遲時間2 秒,並在上一次 執行完畢時間點 以後 10 秒再執行下一次任務。

public void scheduleWithFixedDelay() {
    ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
    for (int i = 0; i < 4; i++) {
        scheduledExecutor.scheduleWithFixedDelay(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(10 * 1000L);
            } catch (InterruptedException e) {
                LOGGER.error("Interrupted exception", e);
            }
            long count = counter.incrementAndGet();
            LOGGER.info("Schedule executor {} times with fixed delay", count);
        }, 2000L, 10 * 1000L, TimeUnit.MILLISECONDS);
    }
    LOGGER.info("Start to schedule");
}
複製代碼

測試結果以下,咱們能夠發現每隔 20 秒的時間間隔,就會有 4 個定時任務同時執行。由於在任務線程池初始化時,咱們同時向線程池提交了 4 個任務,這 四個任務 會徹底利用線程池中的 4 個線程進行任務執行。

20 秒是怎麼來的?首先每一個任務的 時間間隔 設置爲 10 秒。其次由於採用的是 withFixedDelay 策略,即當前任務執行的 結束時間,做爲下次延時任務的 開始計時節點,而且每一個任務在執行過程當中睡眠了 10 秒的時間,累計起來就是 20 秒的時間。

19:42:02.444 [main] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Start to schedule
19:42:14.449 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 3 times with fixed delay
19:42:14.449 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 1 times with fixed delay
19:42:14.449 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 2 times with fixed delay
19:42:14.449 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 4 times with fixed delay
19:42:34.458 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 7 times with fixed delay
19:42:34.458 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 5 times with fixed delay
19:42:34.458 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 8 times with fixed delay
19:42:34.458 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 6 times with fixed delay
複製代碼

建立一個線程數量爲 4任務線程池,同一時刻並向它提交 4 個定時任務,用於測試延時任務的 併發處理。每一個任務分別執行 ScheduledExecutorServicescheduleAtFixedRate() 方法,設置任務線程池的 初始任務延遲時間2 秒,並在上一次 開始執行時間點 以後 10 秒再執行下一次任務。

public void scheduleAtFixedRate() {
    ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);
    for (int i = 0; i < 4; i++) {
        scheduledExecutor.scheduleAtFixedRate(() -> {
            long count = counter.incrementAndGet();
            LOGGER.info("Schedule executor {} times at fixed rate", count);
        }, 2000L, 10 * 1000L, TimeUnit.MILLISECONDS);
    }
    LOGGER.info("Start to schedule");
}
複製代碼

測試結果以下,咱們能夠發現每隔 10 秒的時間間隔,就會有 4 個定時任務同時執行,由於在任務線程池初始化時,咱們同時向線程池提交了 4 個任務,這 四個任務 會徹底利用線程池中的 4 個線程進行任務執行。

19:31:46.837 [main] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Start to schedule
19:31:48.840 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 1 times at fixed rate
19:31:48.840 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 3 times at fixed rate
19:31:48.840 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 2 times at fixed rate
19:31:48.840 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 4 times at fixed rate
19:31:58.839 [pool-1-thread-2] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 6 times at fixed rate
19:31:58.840 [pool-1-thread-4] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 8 times at fixed rate
19:31:58.839 [pool-1-thread-3] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 7 times at fixed rate
19:31:58.839 [pool-1-thread-1] INFO io.ostenant.springboot.sample.executor.ScheduledExecutorsService - Schedule executor 5 times at fixed rate
複製代碼

配置Spring Task任務

Spring 提供了 @Scheduled 註解來實現 定時任務@Scheduled 參數能夠接受 兩種 定時的設置,一種是咱們經常使用的 格林時間表達式 cron = "*/10 * * * * *",另外一種是 fixedRate = 10 * 1000L,兩種都表示每隔 10 秒執行一次目標任務。

參數說明:

  • @Scheduled(fixedRate = 10 * 1000L):上一次 開始執行時間點 以後 10 秒再執行。
@Scheduled(fixedRate = 10 * 1000L)
public void scheduleAtFixedRate() throws Exception {
    long count = counter.incrementAndGet();
    LOGGER.info("Schedule executor {} times at fixed rate", count);
}
複製代碼
  • @Scheduled(fixedDelay = 10 * 1000L):上一次 執行完畢時間點 以後 10 秒再執行。
@Scheduled(fixedDelay = 10 * 1000L)
public void scheduleWithFixedDelay() throws Exception {
    try {
        TimeUnit.MILLISECONDS.sleep(10 * 1000L);
    } catch (InterruptedException e) {
        LOGGER.error("Interrupted exception", e);
    }
    long count = counter.incrementAndGet();
    LOGGER.info("Schedule executor {} times with fixed delay", count);
}
複製代碼
  • @Scheduled(initialDelay = 2000L, fixedRate = 10 * 1000L):第一次延遲 2 秒後執行,以後按 fixedRate 的規則每 10 秒執行一次。
@Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)
public void scheduleWithinitialDelayAndFixedDelay() throws Exception {
    try {
        TimeUnit.MILLISECONDS.sleep(10 * 1000L);
    } catch (InterruptedException e) {
        LOGGER.error("Interrupted exception", e);
    }
    long count = counter.incrementAndGet();
    LOGGER.info("Schedule executor {} times with fixed delay", count);
}
複製代碼
  • @Scheduled(cron = "0/10 * * * * *"):根據 cron 表達式定義,每隔 10 秒執行一次。
@Scheduled(cron = "0/10 * * * * *")
public void scheduleWithCronExpression() throws Exception {
    long count = counter.incrementAndGet();
    LOGGER.info("Schedule executor {} times with ", count);
}
複製代碼

完整的代碼以下:

SpringTaskService.java

@Component
public class SpringTaskService {
    private static final Logger LOGGER = LoggerFactory.getLogger(SpringTaskService.class);
    private AtomicLong counter = new AtomicLong();

    @Scheduled(fixedDelay = 10 * 1000L)
    public void scheduleWithFixedDelay() throws Exception {
        try {
            TimeUnit.MILLISECONDS.sleep(10 * 1000L);
        } catch (InterruptedException e) {
            LOGGER.error("Interrupted exception", e);
        }
        long count = counter.incrementAndGet();
        LOGGER.info("Schedule executor {} times with fixed delay", count);
    }

    @Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)
    public void scheduleWithinitialDelayAndFixedDelay() throws Exception {
        try {
            TimeUnit.MILLISECONDS.sleep(10 * 1000L);
        } catch (InterruptedException e) {
            LOGGER.error("Interrupted exception", e);
        }
        long count = counter.incrementAndGet();
        LOGGER.info("Schedule executor {} times with fixed delay", count);
    }

    @Scheduled(fixedRate = 10 * 1000L)
    public void scheduleAtFixedRate() throws Exception {
        long count = counter.incrementAndGet();
        LOGGER.info("Schedule executor {} times at fixed rate", count);
    }

    @Scheduled(cron = "0/10 * * * * *")
    public void scheduleWithCronExpression() throws Exception {
        long count = counter.incrementAndGet();
        LOGGER.info("Schedule executor {} times with ", count);
    }
}
複製代碼

查看日誌,任務每 20 秒的時間間隔執行一次。每次定時任務在上次 執行完畢時間點 以後 10 秒再執行,在任務中設置 睡眠時間10 秒。這裏只驗證了 @Scheduled(initialDelay = 2000L, fixedDelay = 10 * 1000L)。

2018-06-25 18:00:53.051  INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService    : Schedule executor 1 times with fixed delay
2018-06-25 18:01:13.056  INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService    : Schedule executor 2 times with fixed delay
2018-06-25 18:01:33.061  INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService    : Schedule executor 3 times with fixed delay
2018-06-25 18:01:53.071  INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService    : Schedule executor 4 times with fixed delay
2018-06-25 18:02:13.079  INFO 5190 --- [pool-1-thread-1] i.o.s.sample.spring.SpringTaskService    : Schedule executor 5 times with fixed delay
複製代碼

配置任務線程池

上述配置都是基於 單線程 的任務調度,如何引入 多線程 提升 延時任務併發處理 能力?

Spring Boot 提供了一個 SchedulingConfigurer 配置接口。咱們經過 ScheduleConfig 配置文件實現 ScheduleConfiguration 接口,並重寫 configureTasks() 方法,向 ScheduledTaskRegistrar 註冊一個 ThreadPoolTaskScheduler 任務線程對象便可。

@Configuration
public class ScheduleConfiguration implements SchedulingConfigurer {
    private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleConfiguration.class);

    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
        taskRegistrar.setTaskScheduler(taskScheduler());
    }

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(4);
        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        taskScheduler.setThreadNamePrefix("schedule");
        taskScheduler.setRemoveOnCancelPolicy(true);
        taskScheduler.setErrorHandler(t -> LOGGER.error("Error occurs", t));
        return taskScheduler;
    }
}
複製代碼

啓動 Spring Boot 引用,上面 SpringTaskService 配置的 4 個定時任務會同時生效。

2018-06-20 20:37:50.746  INFO 8142 --- [      schedule1] i.o.s.sample.spring.SpringTaskService    : Schedule executor 1 times at fixed rate
2018-06-20 20:38:00.001  INFO 8142 --- [      schedule3] i.o.s.sample.spring.SpringTaskService    : Schedule executor 2 times with 
2018-06-20 20:38:00.751  INFO 8142 --- [      schedule1] i.o.s.sample.spring.SpringTaskService    : Schedule executor 3 times at fixed rate
2018-06-20 20:38:02.748  INFO 8142 --- [      schedule2] i.o.s.sample.spring.SpringTaskService    : Schedule executor 4 times with fixed delay
2018-06-20 20:38:10.005  INFO 8142 --- [      schedule4] i.o.s.sample.spring.SpringTaskService    : Schedule executor 5 times with 
2018-06-20 20:38:10.747  INFO 8142 --- [      schedule3] i.o.s.sample.spring.SpringTaskService    : Schedule executor 6 times at fixed rate
2018-06-20 20:38:20.002  INFO 8142 --- [      schedule2] i.o.s.sample.spring.SpringTaskService    : Schedule executor 7 times with 
2018-06-20 20:38:20.747  INFO 8142 --- [      schedule4] i.o.s.sample.spring.SpringTaskService    : Schedule executor 8 times at fixed rate
複製代碼

觀察日誌,線程名前綴schedule,能夠發現 Spring Task@Scheduled 註解配置的 4 個任務,分發給咱們配置的 ThreadPoolTaskScheduler 中的 4 個線程併發執行。

小結

本文介紹了基於單節點的定時任務調度及實現,包括 JDK 原生的 TimerScheduledExecutorService,以及 Spring 3.0 之後自帶的基於註解的 Spring Task 任務調度方式。除此以外,重點闡述了基於 固定延時固定頻率cron 表達式 的不一樣之處,並對 ScheduledExecutorServiceSpring Scheduler線程池併發處理 進行了測試。


歡迎關注技術公衆號: 零壹技術棧

零壹技術棧

本賬號將持續分享後端技術乾貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分佈式和微服務,架構學習和進階等學習資料和文章。

相關文章
相關標籤/搜索