定时任务几乎是每个业务系统必不可少的功能,计算到期时间、过期时间等,定时触发某项任务操作。在使用单体应用时,基本使用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.version>2.3.1xxl-job.version>
......
<dependency>
<groupId>com.xuxueligroupId>
<artifactId>xxl-job-coreartifactId>
<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-pluginartifactId>
<groupId>com.gitegg.cloudgroupId>
<version>1.0.1.RELEASEversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>gitegg-jobartifactId>
<name>${project.artifactId}name>
<packaging>jarpackaging>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<maven.compiler.encoding>UTF-8maven.compiler.encoding>
<maven.compiler.source>1.8maven.compiler.source>
<maven.compiler.target>1.8maven.compiler.target>
<maven.test.skip>truemaven.test.skip>
<netty-all.version>4.1.63.Finalnetty-all.version>
<gson.version>2.9.0gson.version>
<spring.version>5.3.20spring.version>
<spring-boot.version>2.6.7spring-boot.version>
<mybatis-spring-boot-starter.version>2.2.2mybatis-spring-boot-starter.version>
<mysql-connector-java.version>8.0.29mysql-connector-java.version>
<slf4j-api.version>1.7.36slf4j-api.version>
<junit-jupiter.version>5.8.2junit-jupiter.version>
<javax.annotation-api.version>1.3.2javax.annotation-api.version>
<groovy.version>3.0.10groovy.version>
<maven-source-plugin.version>3.2.1maven-source-plugin.version>
<maven-javadoc-plugin.version>3.4.0maven-javadoc-plugin.version>
<maven-gpg-plugin.version>3.0.1maven-gpg-plugin.version>
properties>
<dependencies>
<dependency>
<groupId>com.gitegg.platformgroupId>
<artifactId>gitegg-platform-bootartifactId>
dependency>
<dependency>
<groupId>com.gitegg.platformgroupId>
<artifactId>gitegg-platform-cloudartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>${mybatis-spring-boot-starter.version}version>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-freemarkerartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-mailartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql-connector-java.version}version>
dependency>
<dependency>
<groupId>com.xuxueligroupId>
<artifactId>xxl-job-coreartifactId>
<exclusions>
<exclusion>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>com.google.cloud.toolsgroupId>
<artifactId>jib-maven-pluginartifactId>
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*.jksinclude>
<include>static*.xmlinclude>
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
public class XxlJobConfig {
("${xxl.job.admin.addresses}")
private String adminAddresses;
("${xxl.job.accessToken}")
private String accessToken;
("${xxl.job.executor.appname}")
private String appname;
("${xxl.job.executor.address}")
private String address;
("${xxl.job.executor.ip}")
private String ip;
("${xxl.job.executor.port}")
private int port;
("${xxl.job.executor.logpath}")
private String logPath;
("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
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;
}
}
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;
public class SystemJobHandler {
("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);
}
}
("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整合到了我们的微服务架构中,只需要在有任务调度需求的微服务中实现执行器就可以满足我们的需求了。