1.概述
1.1 什麼是任務排程
我們可以考慮一下業務場景的解決方案:
- 某電商系統需要在每天的上午10點,下午3點,晚上8點發放一批優惠券。
- 某銀行系統需要在信用卡到期日的三天進行短信提醒,每天早上8點觸發定時任務 0 0 8 * * ?判斷哪些使用者信用卡三天之後到期,發送短信
- 某财務系統需要在每天淩晨0:10結算前一天的财務資料,統計彙總。
- 12306會根據車次的不同,設定某幾個時間點進行分批放票。
以上業務場景的解決方案就是任務排程。
任務排程是指系統為了自動完成特定任務,在約定的特定時刻去執行任務的過程。有了任務排程即可解放更多的人力,而是由系統自動去執行任務。
如何實作任務排程?
- 多線程方式,結合sleep
- JDK提供的API,例如:Timer、ScheduledExecutor
- 架構,例如Quartz ,它是一個功能強大的任務排程架構,可以滿足更多更複雜的排程需求
- spring task
入門案例
spring架構中預設就支援了一個任務排程,spring-task
(1)建立一個工程:spring-task-demo
pom檔案
<!-- 繼承Spring boot工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
(2)引導類:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApplication.class,args);
}
}
(3)編寫案例
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class HelloJob {
@Scheduled(cron = "0/5 * * * * ?")
public void eat(){
System.out.println("5秒中吃一次飯,我想成為一個胖子"+new Date());
}
}
測試:啟動項目,每隔5秒中會執行一次eat方法
1.2 cron表達式
0/5 * * * * ?
秒 分 時 日 月 周 年(可選)
* * * * * ? *
//2021 12 14 10:35 觸發一次
0 35 10 14 12 ?2021
//2021 12 月 14日 10點 每分鐘執行一次
0 * 10 14 12 ?2021
//2021 12 月 14日 10點 每隔10秒執行一次
0/10 * 10 14 12 ?2021
cron表達式是一個字元串, 用來設定定時規則, 由七部分組成, 每部分中間用空格隔開, 每部分的含義如下表所示:
組成部分 | 含義 | 取值範圍 |
---|---|---|
第一部分 | Seconds (秒) | 0-59 |
第二部分 | Minutes(分) | 0-59 |
第三部分 | Hours(時) | 0-23 |
第四部分 | Day-of-Month(天) | 1-31 |
第五部分 | Month(月) | 0-11或JAN-DEC |
第六部分 | Day-of-Week(星期) | 1-7(1表示星期日)或SUN-SAT |
第七部分 | Year(年) 可選 | 1970-2099 |
另外, cron表達式還可以包含一些特殊符号來設定更加靈活的定時規則, 如下表所示:
符号 | 含義 |
---|---|
? | 表示不确定的值。當兩個子表達式其中一個被指定了值以後,為了避免沖突,需要将另外一個的值設為“?”。例如:想在每月20日觸發排程,不管20号是星期幾,隻能用如下寫法:0 0 0 20 * ?,其中最後以為隻能用“?” |
* | 代表所有可能的值 |
, | 設定多個值,例如”26,29,33”表示在26分,29分和33分各自運作一次任務 |
- | 設定取值範圍,例如”5-20”,表示從5分到20分鐘每分鐘運作一次任務 |
/ | 設定頻率或間隔,如"1/15"表示從1分開始,每隔15分鐘運作一次任務 |
L | 日 周 用于每月,或每周,表示每月的最後一天,或每個月的最後星期幾,例如"6L"表示"每月的最後一個星期五" |
W | 表示離給定日期最近的工作日,例如"15W"放在每月(day-of-month)上表示"離本月15日最近的工作日" |
# | 表示該月第幾個周X。例如”6#3”表示該月第3個周五 |
為了讓大家更熟悉cron表達式的用法, 接下來我們給大家列舉了一些例子, 如下表所示:
| **cron表達式** | **含義** |
| ------------------ | --------------------------------------- |
| */5 * * * * ? | 每隔5秒運作一次任務 |
| 0 0 23 * * ? | 每天23點運作一次任務 |
| 0 0 1 1 * ? | 每月1号淩晨1點運作一次任務 |
| 0 0 23 L * ? | 每月最後一天23點運作一次任務 |
| 0 26,29,33 * * * ? | 在26分、29分、33分運作一次任務 |
| 0 0/30 9-17 * * ? | 朝九晚五工作時間内每半小時運作一次任務 |
| 0 15 10 ? * 6#3 | 每月的第三個星期五上午10:15運作一次任務 |
1.3 什麼是分布式任務排程
目前軟體的架構已經開始向分布式架構轉變,将單體結構拆分為若幹服務,服務之間通過網絡互動來完成業務處理。在分布式架構下,一個服務往往會部署多個執行個體來運作我們的業務,如果在這種分布式系統環境下運作任務排程,我們稱之為分布式任務排程。
将任務排程程式分布式建構,這樣就可以具有分布式系統的特點,并且提高任務的排程處理能力:
1、并行任務排程
并行任務排程實作靠多線程,如果有大量任務需要排程,此時光靠多線程就會有瓶頸了,因為一台計算機CPU的處理能力是有限的。
如果将任務排程程式分布式部署,每個結點還可以部署為叢集,這樣就可以讓多台計算機共同去完成任務排程,我們可以将任務分割為若幹個分片,由不同的執行個體并行執行,來提高任務排程的處理效率。
2、高可用
若某一個執行個體當機,不影響其他執行個體來執行任務。
3、彈性擴容
當叢集中增加執行個體就可以提高并執行任務的處理效率。
4、任務管理與監測
對系統中存在的所有定時任務進行統一的管理及監測。讓開發人員及運維人員能夠時刻了解任務執行情況,進而做出快速的應急處理響應。
分布式任務排程面臨的問題:
當任務排程以叢集方式部署,同一個任務排程可能會執行多次,例如:電商系統定期發放優惠券,就可能重複發放優惠券,對公司造成損失,信用卡還款提醒就會重複執行多次,給使用者造成煩惱,是以我們需要控制相同的任務在多個運作執行個體上隻執行一次。常見解決方案:
- 分布式鎖,多個執行個體在任務執行前首先需要擷取鎖,如果擷取失敗那麼就證明有其他服務已經在運作,如果擷取成功那麼證明沒有服務在運作定時任務,那麼就可以執行。
- ZooKeeper選舉,利用ZooKeeper對Leader執行個體執行定時任務,執行定時任務的時候判斷自己是否是Leader,如果不是則不執行,如果是則執行業務邏輯,這樣也能達到目的。
1.4 xxl-Job簡介
針對分布式任務排程的需求,市場上出現了很多的産品:
1) TBSchedule:淘寶推出的一款非常優秀的高性能分布式排程架構,目前被應用于阿裡、京東、支付寶、國美等很多網際網路企業的流程排程系統中。但是已經多年未更新,文檔缺失嚴重,缺少維護。
2) XXL-Job:大衆點評的分布式任務排程平台,是一個輕量級分布式任務排程平台, 其核心設計目标是開發迅速、學習簡單、輕量級、易擴充。現已開放源代碼并接入多家公司線上産品線,開箱即用。
3)Elastic-job:當當網借鑒TBSchedule并基于quartz 二次開發的彈性分布式任務排程系統,功能豐富強大,采用zookeeper實作分布式協調,具有任務高可用以及分片功能。
4)Saturn: 唯品會開源的一個分布式任務排程平台,基于Elastic-job,可以全域統一配置,統一監
控,具有任務高可用以及分片功能。
XXL-JOB是一個分布式任務排程平台,其核心設計目标是開發迅速、學習簡單、輕量級、易擴充。現已開放源代碼并接入多家公司線上産品線,開箱即用。
源碼位址:https://gitee.com/xuxueli0323/xxl-job
文檔位址:https://www.xuxueli.com/xxl-job/
特性
-
簡單靈活
提供Web頁面對任務進行管理,管理系統支援使用者管理、權限控制;
支援容器部署;
支援通過通用HTTP提供跨平台任務排程;
-
豐富的任務管理功能
支援頁面對任務CRUD操作;
支援在頁面編寫腳本任務、指令行任務、Java代碼任務并執行;
支援任務級聯編排,父任務執行結束後觸發子任務執行;
支援設定指定任務執行節點路由政策,包括輪詢、随機、廣播、故障轉移、忙碌轉移等;
支援Cron方式、任務依賴、排程中心API接口方式觸發任務執行
-
高性能
任務排程流程全異步化設計實作,如異步排程、異步運作、異步回調等,有效對密集排程進行流量削峰;
-
高可用
任務排程中心、任務執行節點均 叢集部署,支援動态擴充、故障轉移
支援任務配置路由故障轉移政策,執行器節點不可用是自動轉移到其他節點執行
支援任務逾時控制、失敗重試配置
支援任務處理阻塞政策:排程當任務執行節點忙碌時來不及執行任務的處理政策,包括:串行、抛棄、覆寫政策
-
易于監控運維
支援設定任務失敗郵件告警,預留接口支援短信、釘釘告警;
支援實時檢視任務執行運作資料統計圖表、任務進度監控資料、任務完整執行日志;
2. XXL-JOB部署排程中心
2.1. XXL-Job
在分布式架構下,通過XXL-Job實作定時任務
排程中心:負責管理排程資訊,按照排程配置發出排程請求,自身不承擔業務代碼。
任務執行器:負責接收排程請求并執行任務邏輯。
任務:專注于任務的處理。
排程中心會發出排程請求,任務執行器接收到請求之後會去執行任務,任務則專注于任務業務的處理。
2.2 環境搭建
- 導入資料庫腳本
- 修改application.properties檔案中的資料源資訊 修改為自己的資料庫位址、資料庫名 及 賬号和密碼
- 在xxl-job可執行檔案目錄下打開cmd,執行 指令:
java -jar xxl-job-admin-2.3.0.jar
- 登入http://localhost:10086/xxl-job-admin/
- 賬号密碼為 admin 123456
3.xxljob入門案例
3.1 入門案例編寫
3.1.1 配置執行器
任務排程中心,點選進入"執行器管理"界面, 如下圖:
1、此處的AppName,會在建立任務時被選擇,每個任務必然要選擇一個執行器。
2、“執行器清單” 中顯示線上的執行器清單, 支援編輯删除。
以下是執行器的屬性說明:
屬性名稱 | 說明 |
---|---|
AppName | 是每個執行器叢集的唯一标示AppName, 執行器會周期性以AppName為對象進行自動注冊。可通過該配置自動發現注冊成功的執行器, 供任務排程時使用; |
名稱 | 執行器的名稱, 因為AppName限制字母數字等組成,可讀性不強, 名稱為了提高執行器的可讀性; |
排序 | 執行器的排序, 系統中需要執行器的地方,如任務新增, 将會按照該排序讀取可用的執行器清單; |
注冊方式 | 排程中心擷取執行器位址的方式; |
機器位址 | 注冊方式為"手動錄入"時有效,支援人工維護執行器的位址資訊; |
具體操作:
(1)新增執行器:
(2)自動注冊和手動注冊的差別和配置
3.1.2 在排程中心建立任務
在任務管理->建立,填寫以下内容
4.任務詳解
- 執行器:任務的綁定的執行器,任務觸發排程時将會自動發現注冊成功的執行器, 實作任務自動發現功能; 另一方面也可以友善的進行任務分組。每個任務必須綁定一個執行器, 可在 “執行器管理” 進行設定
-
任務描述:任務的描述資訊,便于任務管理
路由政策:當執行器叢集部署時,提供豐富的路由政策,包括
- FIRST(第一個):固定選擇第一個機器;
- LAST(最後一個):固定選擇最後一個機器;
- ROUND(輪詢):
- RANDOM(随機):随機選擇線上的機器;
- CONSISTENT_HASH(一緻性HASH):每個任務按照Hash算法固定選擇某一台機器,且所有任務均勻散列在不同機器上。
- LEAST_FREQUENTLY_USED(最不經常使用):使用頻率最低的機器優先被選舉;
- LEAST_RECENTLY_USED(最近最久未使用):最久為使用的機器優先被選舉;
- FAILOVER(故障轉移):按照順序依次進行心跳檢測,第一個心跳檢測成功的機器標明為目标執行器并發起排程;
- BUSYOVER(忙碌轉移):按照順序依次進行空閑檢測,第一個空閑檢測成功的機器標明為目标執行器并發起排程;
- SHARDING_BROADCAST(分片廣播):廣播觸發對應叢集中所有機器執行一次任務,同時系統自動傳遞分片參數;可根據分片參數開發分片任務;
- Cron:觸發任務執行的Cron表達式
- 運作模式:
- BEAN模式:任務以JobHandler方式維護在執行器端;需要結合 “JobHandler” 屬性比對執行器中任務;
- GLUE模式(Java):任務以源碼方式維護在排程中心;該模式的任務實際上是一段繼承自IJobHandler的Java類代碼并 “groovy” 源碼方式維護,它在執行器項目中運作,可使用@Resource/@Autowire注入執行器裡中的其他服務;
- GLUE模式(Shell):任務以源碼方式維護在排程中心;該模式的任務實際上是一段 “shell” 腳本;
- GLUE模式(Python):任務以源碼方式維護在排程中心;該模式的任務實際上是一段 “python” 腳本;
- GLUE模式(PHP):任務以源碼方式維護在排程中心;該模式的任務實際上是一段 “php” 腳本;
- GLUE模式(NodeJS):任務以源碼方式維護在排程中心;該模式的任務實際上是一段 “nodejs” 腳本;
- GLUE模式(PowerShell):任務以源碼方式維護在排程中心;該模式的任務實際上是一段 “PowerShell” 腳本;
- JobHandler:運作模式為 “BEAN模式” 時生效,對應執行器中新開發的JobHandler類“@JobHandler”注解自定義的value值;
- 阻塞處理政策:排程過于密集執行器來不及處理時的處理政策;
- 單機串行(預設):排程請求進入單機執行器後,排程請求進入FIFO隊列并以串行方式運作;
- 丢棄後續排程:排程請求進入單機執行器後,發現執行器存在運作的排程任務,本次請求将會被丢棄并标記為失敗;
- 覆寫之前排程:排程請求進入單機執行器後,發現執行器存在運作的排程任務,将會終止運作中的排程任務并清空隊列,然後運作本地排程任務;
- 子任務:每個任務都擁有一個唯一的任務ID(任務ID可以從任務清單擷取),當本任務執行結束并且執行成功時,将會觸發子任務ID所對應的任務的一次主動排程。
- 任務逾時時間:支援自定義任務逾時時間,任務運作逾時将會主動中斷任務;
- 失敗重試次數;支援自定義任務失敗重試次數,當任務失敗時将會按照預設的失敗重試次數主動進行重試;
- 報警郵件:任務排程失敗時郵件通知的郵箱位址,支援配置多郵箱位址,配置多個郵箱位址時用逗号分隔;
- 負責人:任務的負責人;
- 執行參數:任務執行所需的參數;
4.1 搭建springboot項目
建立項目:xxl-job-demo
(1)pom檔案
<groupId>com.itheima</groupId>
<artifactId>xxl-job-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 繼承Spring boot工程 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- xxl-job -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.2.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
注意:如果項目中沒有找到
xxl-job-core
這個依賴,需要把這個依賴安裝到本地的maven倉庫
(2)配置有兩個,一個是application.properties,另外一個是日志配置:logback.xml
application.properties
server:
port: 8881
xxl:
job:
admin:
addresses: http://192.168.200.130:8888/xxl-job-admin
executor:
appname: xxl-job-sharding-sample
port: 9999
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="1 seconds">
<contextName>logback</contextName>
<property name="log.path" value="/data/applogs/xxl-job/xxl-job-executor-sample-springboot.log"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>
</configuration>
(3)引導類:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class XxlJobApplication {
public static void main(String[] args) {
SpringApplication.run(XxlJobApplication.class,args);
}
}
4.2 添加xxl-job配置
添加配置類:
這個類主要是建立了任務執行器,參考官方案例編寫,無須改動
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* xxl-job config
*
* @author xuxueli 2017-04-28
*/
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
/**
* 針對多網卡、容器内部署等情況,可借助 "spring-cloud-commons" 提供的 "InetUtils" 元件靈活定制注冊IP;
*
* 1、引入依賴:
* <dependency>
* <groupId>org.springframework.cloud</groupId>
* <artifactId>spring-cloud-commons</artifactId>
* <version>${version}</version>
* </dependency>
*
* 2、配置檔案,或者容器啟動變量
* spring.cloud.inetutils.preferred-networks: 'xxx.xxx.xxx.'
*
* 3、擷取IP
* String ip_ = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
*/
}
4.3 建立任務
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class HelloJob {
@Value("${server.port}")
private String appPort;
@XxlJob("helloJob")
public ReturnT<String> hello(String param) throws Exception {
System.out.println("helloJob:"+ LocalDateTime.now()+",端口号"+appPort);
return ReturnT.SUCCESS;
}
}
@XxlJob("helloJob")
這個一定要與排程中心建立任務的JobHandler的值保持一緻,如下圖:
5. 輪詢測試
(1)首先啟動排程中心
(2)啟動xxl-job-demo項目,為了展示更好的效果,可以同時啟動三個項目,用同一個JobHandler,檢視處理方式。
在啟動多個項目的時候,端口需要切換,連接配接xxl-job的執行器端口不同相同
服務一:預設啟動8801端口,執行器端口為9999
idea中不用其他配置,直接啟動項目即可
服務二:項目端口:8802,執行器端口:9998
idea配置如下:
- 編輯配置,Edit Configurations…
- 選中XxlJobApplication,點選複制 啟動:選中8802啟動項目 服務三:項目端口:8803,執行器端口:9997
(3)測試效果
三個項目啟動後,可以檢視到是輪詢的方式分别去執行目前排程任務。
6.分片廣播
6.1 廣播任務和動态分片
6.1.1 什麼是作業分片
作業分片是指任務的分布式執行,需要将一個任務拆分為多個獨立的任務項,然後由分布式的應用執行個體分别執行某一個或幾個分片項。
6.1.2 XXL-JOB分片
-
廣播任務:
執行器叢集部署,任務路由政策選擇"分片廣播"情況下,一次任務排程将會廣播觸發叢集中所有執行器執行一次任務,系統會自動傳遞分片參數,可根據分片參數開發分片任務;
-
動态分片:
分片廣播任務以執行器為次元進行分片,支援動态擴容執行器叢集,進而動态增加分片數量,協同進行業務處理;在進行大資料量業務操作時可顯著提升任務處理能力和速度。
6.1.3 XXL-JOB支援分片的好處
-
分片項與業務處了解耦
XXL-JOB并不直接提供資料處理的功能,架構隻會将分片項配置設定至各個運作中的作業伺服器,開發者需要自行處理分片項與真實資料的對應關系。
-
最大限度利用資源
基于業務需求配置合理數量的執行器服務,合理設定分片,作業将會最大限度合理的利用分布式資源。
6.1.4 分片廣播案例示範
目标:實作XXL-JOB作業分片的示範
方案分析:規劃一個任務,兩個分片,對應兩個執行器,每個分片處理一部分任務。
實作步驟:
- 建立分片執行器:
-
建立任務
指定剛才建立的分片執行器,在路由政策這一欄選擇分片廣播
-
分片廣播代碼
分片參數屬性說明:
- index:目前分片序号(從0開始),執行器叢集清單中目前執行器的序号;
- total:總分片數,執行器叢集的總機器數量;
目前有一萬條資料,使用兩個分片同時執行
/**
* 2、分片廣播任務
*/
@XxlJob("shardingJobHandler")
public ReturnT<String> shardingJobHandler(String param) throws Exception {
// 分片參數
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
XxlJobLogger.log("分片參數:目前分片序号 = {}, 總分片數 = {}", shardingVO.getIndex(), shardingVO.getTotal());
List<Integer> list = getList();
for (Integer integer : list) {
if(integer % shardingVO.getTotal() == shardingVO.getIndex()){
System.out.println("第"+shardingVO.getIndex()+"分片執行,執行資料為:"+integer);
}
}
return ReturnT.SUCCESS;
}
public static List<Integer> getList(){
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 10000 ; i++) {
list.add(i);
}
return list;
}
結論:通過排程日志可以看出,同一個任務在給定的時間點、給定的時間間隔被多個程序不斷調用,實作了任務的分片,展現了分布式任務排程的并行任務排程的特點。