SpringCloud微服務實戰——搭建企業級開發框架(四十二):整合分散式任務排程平臺XXL-JOB,實現定時任務功能

2022-06-10 15:01:00

  定時任務幾乎是每個業務系統必不可少的功能,計算到期時間、過期時間等,定時觸發某項任務操作。在使用單體應用時,基本使用Spring提供的註解即可實現定時任務,而在使用微服務叢集時,這種方式就要考慮新增分散式鎖來防止多個微服務同時執行定時任務而導致同一個任務重複執行。
  除了使用註解,現在還有一種方式,就是搭建分散式任務平臺,所有的微服務註冊到分散式任務平臺,由分散式任務平臺統一排程,這樣避免了同一任務被重複執行。這裡我們選擇使用XXL-JOB作為分散式任務排程平臺,XXL-JOB核心設計目標是開發迅速、學習簡單、輕量級、易擴充套件。
  使用分散式任務排程平臺的優點除了避免同一任務重複執行外,還有使用簡單,可以手動執行、有詳細的排程紀錄檔檢視任務具體執行情況等優點。
  XXL-JOB官方架構設計圖:

  下面我們按照步驟來介紹,如何結合我們的微服務平臺將分散式任務排程平臺XXL-JOB整合進來,實現我們需要的定時任務功能。

一、微服務架構整合xxl-job-admin

1、XXL-JOB開源網站下載原始碼,下載地址 https://github.com/xuxueli/xxl-job/releases ,下載下來的原始碼如下:
xxl-job-admin:排程中心
xxl-job-core:公共依賴
xxl-job-executor-samples:執行器Sample範例(選擇合適的版本執行器,可直接使用,也可以參考其並將現有專案改造成執行器)
    :xxl-job-executor-sample-springboot:Springboot版本,通過Springboot管理執行器,推薦這種方式;
    :xxl-job-executor-sample-frameless:無框架版本;

  下載下來的開源包有三個目錄:xxl-job-admin、xxl-job-core和xxl-job-executor-samples,顧名思義,xxl-job-admin是分散式任務平臺的伺服器端兼管理臺,我們需要部署的也是這個工程,我們可以把整個工程整合到我們的微服務中,統一打包部署;xxl-job-core是公共依賴包,我們其他需要實現定時任務的微服務需要引入這個包來實現定時任務執行器。xxl-job-executor-samples為定時任務執行器的範例程式碼。

2、在基礎平臺gitegg-platform工程gitegg-platform-bom中引入xxl-job-core核心包,統一版本管理。
......
        <!--分散式任務排程平臺XXL-JOB核心包-->
        <xxl-job.version>2.3.1</xxl-job.version>
......
            <!--分散式任務排程平臺XXL-JOB核心包-->
            <dependency>
                <groupId>com.xuxueli</groupId>
                <artifactId>xxl-job-core</artifactId>
                <version>${xxl-job.version}</version>
            </dependency>
3、將xxl-job-admin整合到微服務工程中,方便統一打包部署

  根據我們的微服務架構設計,gitegg-plugin作為我們系統的外掛工程,裡面放置我們需要的外掛服務。有些外掛是必須的,而有些外掛可能會用不到,此時我們就可以根據自己的業務需求去選擇部署業務外掛。
  為和我們的微服務深度整合就不是解耦的特性,我們需要對xxl-job-admin的組態檔進行適當的修改:

  • 首先修改pom.xml,保持各依賴庫版本一致,修改parent標籤,使其參照GitEgg工程的基礎jar包和微服務設定註冊功能,同時排除logback,使用log4j2記錄紀錄檔
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>gitegg-plugin</artifactId>
        <groupId>com.gitegg.cloud</groupId>
        <version>1.0.1.RELEASE</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    
    <artifactId>gitegg-job</artifactId>
    <name>${project.artifactId}</name>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.test.skip>true</maven.test.skip>

        <netty-all.version>4.1.63.Final</netty-all.version>
        <gson.version>2.9.0</gson.version>

        <spring.version>5.3.20</spring.version>
        <spring-boot.version>2.6.7</spring-boot.version>

        <mybatis-spring-boot-starter.version>2.2.2</mybatis-spring-boot-starter.version>
        <mysql-connector-java.version>8.0.29</mysql-connector-java.version>

        <slf4j-api.version>1.7.36</slf4j-api.version>
        <junit-jupiter.version>5.8.2</junit-jupiter.version>
        <javax.annotation-api.version>1.3.2</javax.annotation-api.version>

        <groovy.version>3.0.10</groovy.version>

        <maven-source-plugin.version>3.2.1</maven-source-plugin.version>
        <maven-javadoc-plugin.version>3.4.0</maven-javadoc-plugin.version>
        <maven-gpg-plugin.version>3.0.1</maven-gpg-plugin.version>
    </properties>
    
    <dependencies>
        <!-- gitegg Spring Boot自定義及擴充套件 -->
        <dependency>
            <groupId>com.gitegg.platform</groupId>
            <artifactId>gitegg-platform-boot</artifactId>
        </dependency>
        <!-- gitegg Spring Cloud自定義及擴充套件 -->
        <dependency>
            <groupId>com.gitegg.platform</groupId>
            <artifactId>gitegg-platform-cloud</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot-starter.version}</version>
            <!-- 去除springboot預設的logback設定-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <!-- 去除springboot預設的logback設定-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- freemarker-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
            <!-- 去除springboot預設的logback設定-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- mail-starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
            <!-- 去除springboot預設的logback設定-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
            <!-- 去除springboot預設的logback設定-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql-connector-java.version}</version>
        </dependency>
        <!--分散式任務排程平臺XXL-JOB核心包-->
        <dependency>
            <groupId>com.xuxueli</groupId>
            <artifactId>xxl-job-core</artifactId>
            <!-- 去除衝突的slf4j設定-->
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>com.google.cloud.tools</groupId>
                <artifactId>jib-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  • 修改application.properties ,根據我們系統的規範,新增bootstrap.yml、bootstrap-dev.yml、bootstrap-prod.yml、bootstrap-test.yml檔案。將application.properties部分設定,移到bootstrap.yml設定中。因xxl-job-admin單獨資料庫,且其預設使用的是Hikari資料庫連線池,這裡我們不打算改動,仍然使其保持原有的資料庫設定,我們將可設定的內容放置在Nacos微服務設定中心上,同時在bootstrap.yml中新增多yaml檔案設定(請注意,在我們本地使用的是yml結尾的檔案,Nacos服務註冊中心上使用的是yaml結尾的檔案,兩者是一樣的,只是擴充套件名的不同)。

bootstrap.yml設定:

server:
  port: 8007
spring:
  profiles:
    active: '@spring.profiles.active@'
  application:
    name: '@artifactId@'
  cloud:
    inetutils:
      ignored-interfaces: docker0
    nacos:
      discovery:
        server-addr: ${spring.nacos.addr}
      config:
        server-addr: ${spring.nacos.addr}
        file-extension: yaml
        extension-configs:
          # 必須帶副檔名,此時 file-extension 的設定對自定義擴充套件設定的 Data Id 副檔名沒有影響
          - data-id: ${spring.nacos.config.prefix}.yaml
            group: ${spring.nacos.config.group}
            refresh: true
          - data-id: ${spring.nacos.config.prefix}-xxl-job.yaml
            group: ${spring.nacos.config.group}
            refresh: true
  ### xxl-job-admin config
  mvc:
    servlet:
      load-on-startup: 0
    static-path-pattern: /static/**
  resources:
    static-locations: classpath:/static/
  ### freemarker
  freemarker:
    templateLoaderPath: classpath:/templates/
    suffix: .ftl
    charset: UTF-8
    request-context-attribute: request
    settings.number_format: 0.##########
### actuator
management:
  server:
    servlet:
      context-path: /actuator
  health:
    mail:
      enabled: false
### mybatis
mybatis:
  mapper-locations: classpath:/mybatis-mapper/*Mapper.xml


Nacos上gitegg-cloud-config-xxl-job.yaml設定:

server:
  servlet:
    context-path: /xxl-job-admin
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1/xxl_job?useSSL=false&zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&serverTimezone=GMT%2B8
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    ### datasource-pool
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 10
      maximum-pool-size: 30
      auto-commit: true
      idle-timeout: 30000
      pool-name: HikariCP
      max-lifetime: 900000
      connection-timeout: 10000
      connection-test-query: SELECT 1
      validation-timeout: 1000
  ### email
  mail:
    host: smtp.qq.com
    port: 25
    username: [email protected]
    from: [email protected]
    password: xxx
    properties:
      mail:
        smtp:
          auth: true
          starttls:
            enable: true
            required: true
          socketFactory:
            class: javax.net.ssl.SSLSocketFactory
### xxl-job, access token
xxl:
  job:
    accessToken: default_token
    ### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
    i18n: zh_CN
    ## xxl-job, triggerpool max size
    triggerpool: 
      fast: 
        max: 200
      slow:
        max: 100
    ### xxl-job, log retention days
    logretentiondays: 30
4、初始化xxl-job-admin需要的資料庫指令碼

  初始化指令碼存放在下載的包目錄的\xxl-job-2.3.1\doc\db\tables_xxl_job.sql中,一共需要8張表。我們將xxl-job-admin的資料庫和業務資料庫分開,設定不同的資料來源,在Nacos設定單獨的xxl-job-admin組態檔。

  • 新建xxl_job資料庫

  • 開啟資料庫執行建表語句

5、在GitEgg工程的父級pom.xml下新增靜態檔案過濾

  xxl-job-admin是SpringMVC專案,其前端頁面由ftl檔案和靜態檔案組成,預設情況下maven啟用分環境讀取設定時,會對resource目錄下的@進行替換,導致靜態檔案下的字型檔案不能用,所以,這裡需要進行和jks檔案一樣的過濾設定:

        <resources>
            <!-- 增加分環境讀取設定 -->
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>**/*.jks</exclude>
                    <exclude>static/**</exclude>
                </excludes>
            </resource>
            <!-- 解決jks被過濾掉的問題 -->
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
                <includes>
                    <include>**/*.jks</include>
                    <include>static/**</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
6、在Gateway新增xxl-job-admin路由轉發

  xxl-job-admin路由轉發需要新增兩方面內容,一個是xxl-job-admin註冊到Nacos註冊中心上的gitegg-job服務,一個是xxl-job-admin前端頁面請求的靜態檔案轉發。第一個是為了和我們整體微服務保持一致,第二個是為了解決xxl-job-admin前端ftl頁面在請求靜態檔案時,請求的是/xxl-job-admin根路徑。新增Gateway路由轉發設定如下:

        - id: gitegg-job
          uri: lb://gitegg-job
          predicates:
            - Path=/gitegg-job/**
          filters:
            - StripPrefix=1
        - id: xxl-job-admin
          uri: lb://gitegg-job
          predicates:
            - Path=/xxl-job-admin/**
          filters:
            - StripPrefix=0
7、增加xxl-job-admin存取白名單

  xxl-job-admin有自己的許可權存取控制,我們不在閘道器對其進行鑑權,所以在Nacos設定中,增加白名單設定:

# 閘道器放行設定 1、whiteUrls不需要鑑權的公共url,白名單,設定白名單路徑 2、authUrls需要鑑權的公共url
oauth-list:
......
  whiteUrls:
......
    - "/gitegg-job/**"
    - "/xxl-job-admin/**"
......
8、啟動xxl-job-admin微服務,檢視是否啟動成功,預設使用者名稱密碼: admin/123456

二、測試XXL-JOB定時任務功能

  我們在上面的第一步中,完成了xxl-job-admin的整合和啟動,xxl-job-admin可以看做是分散式任務的服務註冊中心和管理臺,如果我們需要實現定時任務,還需要具體實現執行器讓xxl-job-admin呼叫執行。
  XXL-JOB支援多種方式的定時任務呼叫,可以將定時任務執行器寫在業務程式碼中,也可以寫在xxl-job-admin伺服器端:

  • BEAN模式(類形式): Bean模式任務,支援基於類的開發方式,每個任務對應一個Java類。
  • BEAN模式(方法形式): Bean模式任務,支援基於方法的開發方式,每個任務對應一個方法。
  • GLUE模式(Java/Shell/Python/NodeJS/PHP/PowerShell) :任務以原始碼方式維護在排程中心,支援通過Web IDE線上更新,實時編譯和生效,因此不需要指定JobHandler。
1、增加xxl-job通用設定

  新增gitegg-platform-xxl-job工程,增加通用設定XxlJobConfig.java通用設定,這樣在需要使用定時任務的微服務中,只需要引入一次即可,不需要重複設定。

XxlJobConfig.java:

package com.gitegg.platform.xxl.job.config;

import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
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
 */
@Slf4j
@Configuration
public class XxlJobConfig {

    @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() {
        log.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();
     */
}

Nacos設定中心:

xxl:
  job:
    admin:
      addresses: http://127.0.0.1/xxl-job-admin
    accessToken: 'default_token'
    executor:
      appname: ${spring.application.name}
      address:
      ip:
      port: 9999
      logpath: D:\\log4j2_nacos\\xxl-job\\jobhandler
      logretentiondays: 30
2、實現定時任務測試程式碼

  我們在gitegg-service-system中測試定時任務執行器,先在pom.xml中新增gitegg-platform-xxl-job依賴,然後新增SystemJobHandler.java測試類

SystemJobHandler.java:

package com.gitegg.service.system.jobhandler;

import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 定時任務範例程式碼,其他更多範例請檢視
 * https://www.xuxueli.com/xxl-job
 * @author GitEgg
 */
@Slf4j
@Component
public class SystemJobHandler {
    
    /**
     * 1、簡單任務範例(Bean模式)不帶返回值
     */
    @XxlJob("systemJobHandler")
    public void systemJobHandler() throws Exception {
        
        XxlJobHelper.log("不帶返回值:XXL-JOB, Hello World.");
        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
    }
    
    /**
     * 2、簡單任務範例(Bean模式)帶成功或失敗返回值
     */
    @XxlJob("userJobHandler")
    public ReturnT<String> userJobHandler() throws Exception {
        
        XxlJobHelper.log("帶返回值:XXL-JOB, Hello World.");
        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("beat at:" + i);
            TimeUnit.SECONDS.sleep(2);
        }
        return ReturnT.SUCCESS;
    }

}

3、設定xxl-job-admin新增執行器
  • 新增時:

  • gitegg-service-system服務啟動後,自動註冊:

4、新增xxl-job-admin任務

  執行器可以看做是一組微服務,而任務是微服務具體執行的方法。任務新增後,預設是STOP狀態,需要手動啟動,當列表顯示RUNNING時,表示該任務是執行狀態,會根據設定的時間執行。

5、檢視執行器是否執行

  在本地開發環境檢視任務執行的方式有多種,直接Debug也可以,生產環境我們可以檢視xxl-job紀錄檔,在測試程式碼中記錄的log,在xxl-job-admin管理臺都可以詳細檢視。

  通過以上操作步驟,我們將xxl-job和xxl-job-admin整合到了我們的微服務架構中,只需要在有任務排程需求的微服務中實現執行器就可以滿足我們的需求了。

原始碼地址:

Gitee: https://gitee.com/wmz1930/GitEgg

GitHub: https://github.com/wmz1930/GitEgg