分佈式定時任務,你瞭解多少?基於 Quartz 實現分佈式任務解決方案!
後臺定時任務系統在應用平臺中的重要性不言而喻,特別是互聯網電商、金融等行業更是離不開定時任務。在任務數量不多、執行頻率不高時,單臺服務器完全能夠滿足。但是隨着業務逐漸增加,定時任務系統必須具備高可用和水平擴展的能力,單臺服務器已經不能滿足需求。因此需要把定時任務系統部署到集羣中,實現分佈式定時任務系統集羣。
分佈式任務調度框架幾乎是每個大型應用必備的工具,下面我們結合項目實踐,對業界普遍使用的開源分佈式任務調度框架的使用進行了探究實踐,並分析了這幾種框架的優劣勢和對自身業務的思考。
一、分佈式定時任務簡介
1. 什麼是分佈式任務?
分佈式定時任務就是把分散的、批量的後臺定時任務納入統一的管理調度平臺,實現任務的集羣管理、調度和分佈式部署管理方式。
2. 分佈式定時任務的特點
實際項目中涉及到分佈式任務的業務場景非常多,這就使得我們的定時任務系統應該集管理、調度、任務分配、監控預警爲一體的綜合調度系統,如何打造一套健壯的、適應不同場景的系統,技術選型尤其重要。針對以上場景我們需要我們的分佈式任務系統具備以下能力:
-
支持多種任務類型 (shell 任務 / Java 任務 / web 任務)
-
支持 HA,負載均衡和故障轉移
-
支持彈性擴容 (應對開門紅以及促銷活動)
-
支持 Job Timeout 處理
-
支持統一監控和告警
-
支持任務統一配置
-
支持資源隔離和作業隔離
二、爲什麼需要分佈式定時任務?
定時任務系統在應用平臺中的重要性不言而喻,特別是互聯網電商、金融等行業更是離不開定時任務。在任務數量不多、執行頻率不高時,單臺服務器完全能夠滿足。但是,爲什麼還需要分佈式呢?主要有如下兩點原因:
-
高可用:單機版的定時任務調度只能在一臺機器上運行,如果系統出現異常,就會導致整個後臺定時任務不可用。這對於互聯網企業來說是不可接受的。
-
單機處理極限:單機處理的數據,任務數量是有限的。原本 1 分鐘內需要處理 1 萬個訂單,但是現在需要 1 分鐘內處理 10 萬個訂單;原來一個統計需要 1 小時,現在業務方需要 10 分鐘就統計出來。你也許會說,你也可以多線程、單機多進程處理。的確,多線程並行處理可以提高單位時間的處理效率,但是單機能力畢竟有限(主要是 CPU、內存和磁盤),始終會有單機處理不過來的情況。
但我們遇到的問題還不止這些,比如容錯功能、失敗重試、分片功能、路由負載均衡、管理後臺等。這些都是單機的定時任務系統所不具備的,因此需要把定時任務系統部署到集羣中,實現分佈式定時任務系統集羣。
三、常見開源方案
目前,分佈式定時任務框架非常多,而且大部分都已經開源,比較流行的有:xxl-job、elastic-job、quartz 等。
-
elastic-job,是由噹噹網基於 quartz 二次開發之後的分佈式調度解決方案 , 由兩個相對獨立的子項目 Elastic-Job-Lite 和 Elastic-Job-Cloud 組成 。
-
xxl-job,是由個人開源的一個輕量級分佈式任務調度框架 ,主要分爲 調度中心和執行器兩部分 , 調度中心在啓動初始化的時候,會默認生成執行器的 RPC 代理對象(http 協議調用), 執行器項目啓動之後, 調度中心在觸發定時器之後通過 jobHandle 來調用執行器項目裏面的代碼,核心功能和 elastic-job 差不多,同時技術文檔比較完善
-
quartz,是非常流行的開源的作業調度框架,它提供了巨大的靈活性而不犧牲簡單性。你能夠用它來爲執行一個作業而創建簡單的或複雜的調度。同時也提供了基於數據庫的集羣方案,通過在數據庫中配置定時器信息,以數據庫鎖的方式達到同一個任務始終只有一個節點在運行。
以表列出了幾個代表性的開源分佈式任務框架的:
四、基於 Quartz 實現分佈式定時任務解決方案
1.Quartz 的集羣解決方案
Quartz 單機版本相比大家應該比較熟悉,它的集羣方案則是在單機的基礎上加上一個公共數據庫。通過在數據庫中配置定時器信息, 以數據庫鎖的方式達到同一個任務始終只有一個節點在運行,集羣架構如下:
通過上面的架構圖可以看到,三個 Quartz 服務節點共享同一個數據庫,如果某一個服務節點失效,那麼 Job 會在其他節點上執行。各個 Quartz 服務器都遵守基於數據庫鎖的調度原則,只有獲取了鎖才能調度後臺任務,從而保證了任務執行的唯一性。同時多個節點的異步運行保證了服務的可靠性。
2. 實現基於 Quartz 的分佈式定時任務
下面就通過示例,演示如何基於 Quartz 實現分佈式定時任務。
- 添加 Quartz 依賴
由於分佈式的原因,Quartz 中提供分佈式處理的 JAR 包以及數據庫和連接相關的依賴。示例代碼如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- orm -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
在上面的示例中,除了添加 Quartz 依賴外,還需要添加 mysql-connector-java 和 spring-boot-starter-data-jpa 兩個組件,這兩個組件主要用於 JOB 持久化到 MySQL 數據庫。
- 初始化 Quartz 數據庫
分佈式 Quartz 定時任務的配置信息存儲在數據庫中,數據庫初始化腳本可以在官方網站中查找,默認保存在 quartz-2.2.3-distribution\src\org\quartz\impl\jdbcjobstore\tables-mysql.sql 目錄下。首先創建 quartz_jobs 數據庫,然後在數據庫中執行 tables-mysql.sql 初始化腳本。
- 配置數據庫和 Quartz
修改 application.properties 配置文件,配置數據庫與 Quartz。具體操作如下:
# Quartz 數據庫
spring.datasource.url=jdbc:mysql://localhost:3306/quartz_jobs?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.max-active=1000
spring.datasource.max-idle=20
spring.datasource.min-idle=5
spring.datasource.initial-size=10
# 是否使用properties作爲數據存儲
org.quartz.jobStore.useProperties=false
# 數據庫中表的命名前綴
org.quartz.jobStore.tablePrefix=QRTZ_
# 是否是一個集羣,是不是分佈式的任務
org.quartz.jobStore.isClustered=true
# 集羣檢查週期,單位爲毫秒,可以自定義縮短時間。當某一個節點宕機的時候,其他節點等待多久後開始執行任務
org.quartz.jobStore.clusterCheckinInterval=5000
# 單位爲毫秒,集羣中的節點退出後,再次檢查進入的時間間隔
org.quartz.jobStore.misfireThreshold=60000
# 事務隔離級別
org.quartz.jobStore.txIsolationLevelReadCommitted=true
# 存儲的事務管理類型
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
# 使用的Delegate類型
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 集羣的命名,一個集羣要有相同的命名
org.quartz.scheduler.instanceName=ClusterQuartz
# 節點的命名,可以自定義。AUTO代表自動生成
org.quartz.scheduler.instanceId=AUTO
# rmi遠程協議是否發佈
org.quartz.scheduler.rmi.export=false
# rmi遠程協議代理是否創建
org.quartz.scheduler.rmi.proxy=false
# 是否使用用戶控制的事務環境觸發執行任務
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false
上面的配置主要是 Quartz 數據庫和 Quartz 分佈式集羣相關的屬性配置。分佈式定時任務的配置存儲在數據庫中,所以需要配置數據庫連接和 Quartz 配置信息,爲 Quartz 提供數據庫配置信息,如數據庫、數據表的前綴之類。
- 定義定時任務
後臺定時任務與普通 Quartz 任務並無差異,只是增加了 @PersistJobDataAfterExecution 註解和 @DisallowConcurrentExecution 註解。創建 QuartzJob 定時任務類並實現 Quartz 定時任務的具體示例代碼如下:
// 持久化
@PersistJobDataAfterExecution
// 禁止併發執行
@DisallowConcurrentExecution
public class QuartzJob extends QuartzJobBean {
private static final Logger log = LoggerFactory.getLogger(QuartzJob.class);
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
String taskName = context.getJobDetail().getJobDataMap().getString("name");
log.info("---> Quartz job, time:{"+new Date()+"} ,name:{"+taskName+"}<----");
}
}
在上面的示例中,創建了 QuartzJob 定時任務類,使用 @PersistJobDataAfterExecution 註解持久化任務信息。DisallowConcurrentExecution 禁止併發執行,避免同一個任務被多次併發執行。
- SchedulerConfig 配置
創建 SchedulerConfig 配置類,初始化 Quartz 分佈式集羣相關配置,包括集羣設置、數據庫等。示例代碼如下:
@Configuration
public class SchedulerConfig {
@Autowired
private DataSource dataSource;
/**
* 調度器
*
* @return
* @throws Exception
*/
@Bean
public Scheduler scheduler() throws Exception {
Scheduler scheduler = schedulerFactoryBean().getScheduler();
return scheduler;
}
/**
* Scheduler工廠類
*
* @return
* @throws IOException
*/
@Bean
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setSchedulerName("Cluster_Scheduler");
factory.setDataSource(dataSource);
factory.setApplicationContextSchedulerContextKey("applicationContext");
factory.setTaskExecutor(schedulerThreadPool());
//factory.setQuartzProperties(quartzProperties());
factory.setStartupDelay(10);// 延遲10s執行
return factory;
}
/**
* 配置Schedule線程池
*
* @return
*/
@Bean
public Executor schedulerThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors());
executor.setQueueCapacity(Runtime.getRuntime().availableProcessors());
return executor;
}
}
在上面的示例中,主要是配置 Schedule 線程池、配置 Quartz 數據庫、創建 Schedule 調度器實例等初始化配置。
- 觸發定時任務
配置完成之後,還需要觸發定時任務,創建 JobStartupRunner 類以便在系統啓動時觸發所有定時任務。示例代碼如下:
@Component
public class JobStartupRunner implements CommandLineRunner {
@Autowired
SchedulerConfig schedulerConfig;
private static String TRIGGER_GROUP_NAME = "test_trigger";
private static String JOB_GROUP_NAME = "test_job";
@Override
public void run(String... args) throws Exception {
Scheduler scheduler;
try {
scheduler = schedulerConfig.scheduler();
TriggerKey triggerKey = TriggerKey.triggerKey("trigger1", TRIGGER_GROUP_NAME);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (null == trigger) {
Class clazz = QuartzJob.class;
JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity("job1", JOB_GROUP_NAME).usingJobData("name","weiz QuartzJob").build();
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", TRIGGER_GROUP_NAME)
.withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail, trigger);
System.out.println("Quartz 創建了job:...:" + jobDetail.getKey());
} else {
System.out.println("job已存在:{}" + trigger.getKey());
}
TriggerKey triggerKey2 = TriggerKey.triggerKey("trigger2", TRIGGER_GROUP_NAME);
CronTrigger trigger2 = (CronTrigger) scheduler.getTrigger(triggerKey2);
if (null == trigger2) {
Class clazz = QuartzJob2.class;
JobDetail jobDetail2 = JobBuilder.newJob(clazz).withIdentity("job2", JOB_GROUP_NAME).usingJobData("name","weiz QuartzJob2").build();
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ?");
trigger2 = TriggerBuilder.newTrigger().withIdentity("trigger2", TRIGGER_GROUP_NAME)
.withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail2, trigger2);
System.out.println("Quartz 創建了job:...:{}" + jobDetail2.getKey());
} else {
System.out.println("job已存在:{}" + trigger2.getKey());
}
scheduler.start();
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
在上面的示例中,爲了適應分佈式集羣,我們在系統啓動時觸發定時任務,判斷任務是否已經創建、是否正在執行。如果集羣中的其他示例已經創建了任務,則啓動時無須觸發任務。
- 驗證測試
配置完成之後,接下來啓動任務,測試分佈式任務配置是否成功。啓動一個實例,可以看到定時任務執行了,然後每 10 秒鐘打印輸出一次,如下圖所示。
接下來,模擬分佈式部署的情況。我們再啓動一個測試程序實例,這樣就有兩個後臺定時任務實例,如下所示。
後臺定時任務實例 1 的日誌輸出:
後臺定時任務實例 2 的日誌輸出:
從上面的日誌中可以看到,Quartz Job 和 Quartz Job2 交替地在兩個任務實例進程中執行,同一時刻同一個任務只有一個進程在執行,這說明已經達到了分佈式後臺定時任務的效果。
接下來,停止任務實例 1,測試任務實例 2 是否會接管所有任務繼續執行。如下圖所示,停止任務實例 1 後,任務實例 2 接管了所有的定時任務。這樣如果集羣中的某個實例異常了,其他實例能夠接管所有的定時任務,確保任務集羣的穩定運行。
最後
以上,就把分佈式後臺任務介紹完了,並通過 Spring Boot + Quartz 實現了基於 Quartz 的分佈式定時任務解決方案!
分佈式任務調度框架幾乎是每個大型應用必備的工具,作爲程序員、架構師必須熟練掌握。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/xYSvqgqlDuNcYQdLJPP61Q