分佈式定時任務,你瞭解多少?基於 Quartz 實現分佈式任務解決方案!

後臺定時任務系統在應用平臺中的重要性不言而喻,特別是互聯網電商、金融等行業更是離不開定時任務。在任務數量不多、執行頻率不高時,單臺服務器完全能夠滿足。但是隨着業務逐漸增加,定時任務系統必須具備高可用和水平擴展的能力,單臺服務器已經不能滿足需求。因此需要把定時任務系統部署到集羣中,實現分佈式定時任務系統集羣。

分佈式任務調度框架幾乎是每個大型應用必備的工具,下面我們結合項目實踐,對業界普遍使用的開源分佈式任務調度框架的使用進行了探究實踐,並分析了這幾種框架的優劣勢和對自身業務的思考。

一、分佈式定時任務簡介

1. 什麼是分佈式任務?

分佈式定時任務就是把分散的、批量的後臺定時任務納入統一的管理調度平臺,實現任務的集羣管理、調度和分佈式部署管理方式。

2. 分佈式定時任務的特點

實際項目中涉及到分佈式任務的業務場景非常多,這就使得我們的定時任務系統應該集管理、調度、任務分配、監控預警爲一體的綜合調度系統,如何打造一套健壯的、適應不同場景的系統,技術選型尤其重要。針對以上場景我們需要我們的分佈式任務系統具備以下能力:

二、爲什麼需要分佈式定時任務?

定時任務系統在應用平臺中的重要性不言而喻,特別是互聯網電商、金融等行業更是離不開定時任務。在任務數量不多、執行頻率不高時,單臺服務器完全能夠滿足。但是,爲什麼還需要分佈式呢?主要有如下兩點原因:

但我們遇到的問題還不止這些,比如容錯功能、失敗重試、分片功能、路由負載均衡、管理後臺等。這些都是單機的定時任務系統所不具備的,因此需要把定時任務系統部署到集羣中,實現分佈式定時任務系統集羣。

三、常見開源方案

目前,分佈式定時任務框架非常多,而且大部分都已經開源,比較流行的有:xxl-job、elastic-job、quartz 等。

以表列出了幾個代表性的開源分佈式任務框架的:

F6lFd7

四、基於 Quartz 實現分佈式定時任務解決方案

1.Quartz 的集羣解決方案

Quartz 單機版本相比大家應該比較熟悉,它的集羣方案則是在單機的基礎上加上一個公共數據庫。通過在數據庫中配置定時器信息, 以數據庫鎖的方式達到同一個任務始終只有一個節點在運行,集羣架構如下:

通過上面的架構圖可以看到,三個 Quartz 服務節點共享同一個數據庫,如果某一個服務節點失效,那麼 Job 會在其他節點上執行。各個 Quartz 服務器都遵守基於數據庫鎖的調度原則,只有獲取了鎖才能調度後臺任務,從而保證了任務執行的唯一性。同時多個節點的異步運行保證了服務的可靠性。

2. 實現基於 Quartz 的分佈式定時任務

下面就通過示例,演示如何基於 Quartz 實現分佈式定時任務。

  1. 添加 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 數據庫。

  1. 初始化 Quartz 數據庫

分佈式 Quartz 定時任務的配置信息存儲在數據庫中,數據庫初始化腳本可以在官方網站中查找,默認保存在 quartz-2.2.3-distribution\src\org\quartz\impl\jdbcjobstore\tables-mysql.sql 目錄下。首先創建 quartz_jobs 數據庫,然後在數據庫中執行 tables-mysql.sql 初始化腳本。

  1. 配置數據庫和 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 提供數據庫配置信息,如數據庫、數據表的前綴之類。

  1. 定義定時任務

後臺定時任務與普通 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 禁止併發執行,避免同一個任務被多次併發執行。

  1. 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 調度器實例等初始化配置。

  1. 觸發定時任務

配置完成之後,還需要觸發定時任務,創建 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());
        }
    }
}

在上面的示例中,爲了適應分佈式集羣,我們在系統啓動時觸發定時任務,判斷任務是否已經創建、是否正在執行。如果集羣中的其他示例已經創建了任務,則啓動時無須觸發任務。

  1. 驗證測試

配置完成之後,接下來啓動任務,測試分佈式任務配置是否成功。啓動一個實例,可以看到定時任務執行了,然後每 10 秒鐘打印輸出一次,如下圖所示。

接下來,模擬分佈式部署的情況。我們再啓動一個測試程序實例,這樣就有兩個後臺定時任務實例,如下所示。

後臺定時任務實例 1 的日誌輸出:

後臺定時任務實例 2 的日誌輸出:

從上面的日誌中可以看到,Quartz Job 和 Quartz Job2 交替地在兩個任務實例進程中執行,同一時刻同一個任務只有一個進程在執行,這說明已經達到了分佈式後臺定時任務的效果。

接下來,停止任務實例 1,測試任務實例 2 是否會接管所有任務繼續執行。如下圖所示,停止任務實例 1 後,任務實例 2 接管了所有的定時任務。這樣如果集羣中的某個實例異常了,其他實例能夠接管所有的定時任務,確保任務集羣的穩定運行。

最後

以上,就把分佈式後臺任務介紹完了,並通過 Spring Boot + Quartz 實現了基於 Quartz 的分佈式定時任務解決方案!

分佈式任務調度框架幾乎是每個大型應用必備的工具,作爲程序員、架構師必須熟練掌握。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xYSvqgqlDuNcYQdLJPP61Q