分布式任务调度平台xxl job应用及源码分析--含本地集群
一、框架分类
常见定时任务框架可按如下区分:
单机:Timer、ScheduledExecutorService、Spring定时框架
分布式:Quartz、SpringBatch、TBSchedule、elastic-job、xxl-job、Saturn
另外有些小众的框架:uncode-schedule、LTS、TBSchedule、Opencron、Antares、sia-task
二、常用框架特点:
- Quartz
Java定时任务标准。但Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行调度的功能,且缺少分片处理功能。
- TBSchedule
阿里早期开源的分布式任务调度系统。代码略陈旧,使用timer而非线程池执行任务调度。众所周知,timer在处理异常状况时是有缺陷的。而且TBSchedule作业类型较为单一,只能是获取/处理数据一种模式。还有就是文档缺失比较严重。
- Saturn
Saturn是唯品会在github开源的一款分布式任务调度产品。它是基于当当elastic-job 1.0版本来开发的,其上完善了一些功能和添加了一些新的feature。支持多语言开发 python、Go、Shell、Java、Php。管理控制台和数据统计分析更加完善,技术文档较少 , 该框架是2016年由唯品会的研发团队基于elastic-job开发而来。
- elastic-job
当当开发的弹性分布式任务调度系统,功能丰富强大,采用zookeeper实现分布式协调,实现任务高可用以及分片,并且可以支持云开发
- xxl-job
是大众点评员工徐雪里于2015年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。文档齐全,教程很多,遇到问题也比较容易找到解决方案.
因为e-job需要至少三台的zookeeper,以及结合项目实际情况,暂时采用从quartz改造成xxl-job管理定时任务,以下将从xxl-job的概述,使用,以及遇到的一些坑进行探索,其他框架暂不做讨论。
官方文档:官方文档
三、本地单机启动
3.0 准备工作
先从官网上将项目down下来:
官网地址:https://github.com/xuxueli/xxl-job
将下载后的zip文件解压,用Idea打开
脚本文件位置如上图所示,在navicat中执行数据库脚本,生成如下所示xxl-job相关表
导入脚本后,在使用项目前,先看下其架构图
如上图所示,xxl-job分为两块:调度中心、执行器。
示例项目结构,如下图所示:
xxl-job-admin:调度中心
xxl-job-core:公共依赖
xxl-job-executor-samples:执行器Sample示例(选择合适的版本执行器,可直接使用,也可以参考其并将现有项目改造成执行器)
- :xxl-job-executor-sample-springboot:Springboot版本,通过Springboot管理执行器,推荐这种方式;
- :xxl-job-executor-sample-spring:Spring版本,通过Spring容器管理执行器,比较通用;
- :xxl-job-executor-sample-frameless:无框架版本;
另外,中间的xxl-job-core在maven上也可以下载其jar包。
3.1 调度中心本地部署
- 环境:Maven3+、Jdk1.8+、Mysql5.7+
- 配置部署“调度中心”
- 调度中心项目:xxl-job-admin
- 作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
3.1.1 步骤一:调度中心配置修改
- 配置文件地位置:
/xxl-job/xxl-job-admin/src/main/resources/application.properties
- 修改配置:
如果仅入门,可仅修改数据库配置,数据库为本地mysql的情况下不用修改,如果为公司测试环境,修改为公司测试环境的数据库配置即可。
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root_pwd
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
3.1.2 步骤二:部署项目:
- 启动
如果已经正确进行上述配置,可将项目编译,在Idea上启动。
- 访问
经过以上两步,即可完成本地单机版调度中心的部署,访问调度中心地址:http://localhost:8080/xxl-job-admin (该地址执行器将会使用到,作为回调地址)
默认登录账号 “admin/123456”, 登录后运行界面如下图所示。
如果看到这个界面,调度中心即部署成功。
3.2 执行器本地部署
3.2.1 SpringBoot部署执行器
“执行器”项目:xxl-job-executor-sample-springboot (提供多种类型执行器供选择,现以 springboot 版本为例,直接使用演示)
作用:负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。
步骤一:maven依赖
确认pom文件中引入了 “xxl-job-core” 的maven依赖,且需与调度中心的xxl-job-core版本保持一致;
步骤二:执行器配置
- 执行器配置,配置文件地址:
/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/resources/application.properties
- 执行器配置:
如无特殊需求,仅需配置调度中心地址以及appname如下所示:
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-executor-sample
注意:其中appname为执行器唯一标识,后面配置执行器时会用到。
步骤三:执行器组件配置
- 执行器组件,配置文件地址
(下载的源码里已存在,使用即可):
/xxl-job/xxl-job-executor-samples/xxl-job-executor-sample-springboot/src/main/java/com/xxl/job/executor/core/config/XxlJobConfig.java
- 配置内容说明:
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
步骤四:部署执行器项目:
如果已经正确进行上述配置,可将执行器项目编译,Idea启动项目(启动SpringBoot项目)。
如项目正常启动,且无报错,则SpringBoot方式的执行器单机部署完毕。
3.2.2 Spring部署执行器
Spring的配置和启动与SpringBoot类似,只是需要用Idea部署到本地tomcat启动,可百度搜索Idea启动war项目。
至此,一套标准,最简单的调度中心和执行器就启动部署完成,接下来进行配置,让job生效执行。
3.2.3 调度中心配置
- 背景
虽然我们已经部署了调度中心和执行器,但是在没有任何配置的情况下,调度中心是不知道执行器的存在的,所以我们首先需要配置对应的执行器。
- 配置执行器
新建执行器,如下图所示进行新增,其中AppName必须与开始我们在执行器项目种配置的appname一致。
- 配置任务
由于GitHub上down下来的代码有Bean模式job任务的示例,下面我们以demoJobHandler作为例子来新建任务。
新建任务,如下图所示
简单使用的话,仅需填写标黄项,执行器选择我们开始配置的”示例执行器",任务描述建议与jobHandler保持一致,方便区分,模式暂时选择Bean模式。
至此,点击任务的操作–启动按钮,就可以让项目协同跑起来了,即:
- 调度中心按配置在规定的时间触发调度任务。
- 执行器收到调度中心的请求后,进行任务的执行以及执行结果的回调等等。
后面讨论的所有问题,基本都是围绕着上面的这些组件进行探讨。
四、本地集群启动
4.1 方案
安装nginx软件、启动两台调度中心、启动三台执行器(已集成xxl-job执行器的项目)
4.2 nginx
4.2.1. 配置
nginx.conf配置文件修改,新增upstream负载均衡,新增location 负载到这两个本地服务。
http {
***
upstream xxlAdmin{
server 127.0.0.1:10086;
server 127.0.0.1:10087;
}
***
server {
***
location /xxl-job-admin {
proxy_pass http://xxlAdmin/xxl-job-admin;
}
***
}
4.2.2. 启动
windows启动nginx(双击启动文件或命令启动)
4.2.3. 访问
访问地址:http://localhost/index.html,看到Welcome to nginx!则nginx启动成功。
4.3 调度中心
4.3.1 启动
启动两台调度中心,其中第一台与单机启动一样,另外一台需重新在新建一个SpringBoot服务配置,如下图所示,新建后除常规设置与单机一致外,还需要设置图中圈出参数指定一个与第一台不同的端口号,且需要保证该端口不被其他线程占用。
4.3.2 访问
若两个服务都正常启动,则访问nginx地址:http://localhost/xxl-job-admin/jobinfo即可通过nginx访问调度中心,达到负载均衡的目的。
4.4 执行器
4.4.1 启动
第一台与单机一致,启动一个后,把配置文件的端口号改成9998,再新增配置一个服务,修改server.port=8083,不要与其他服务冲突,启动第二个服务,9997同理。
4.4.2 查看注册效果
若三个服务都成功启动,且如果我们按appname在调度中心页面配置过对应的执行器,则在1分钟以内能看到三台服务器已经成功注册到了调度中心,点击查看即可显示,如下图所示。
4.4.3 总结
- 至此,已完成本地调度中心的集群(2台),以及执行器的横向扩展(3台)。
- 当我们将任务的路由策略改成轮询时,则会一次在这三台执行器上执行,且每次有且仅有一台会触发执行。
- 另外,如果执行器集成在Spring项目中,可按如下图所示配置三台tomcat,注意修改服务的端口号,三台机器不能冲突,且不能与其他应用冲突,启动一台后修改9998,再启动第二台,9997同理。
- 当然咯,线上的测试环境或者生产环境不会有这种繁琐的端口配置问题,因为每个服务都是在不同机器上部署,就不会造成端口冲突问题。
五、源码解析
以下将简单从源码角度对调度中心和执行器进行探讨
调度中心源码
如下图所示,调度中心项目启动后,如下为配置入口,实现了InitializingBean接口,spring容器在初始化的时候会调用afterPropertiesSet方法,最终会调用XxlJobScheduler的init()方法。
分析XxlJobScheduler.init()
public void init() throws Exception {
// init i18n
initI18n();
// admin trigger pool start
JobTriggerPoolHelper.toStart();
// admin registry monitor run
JobRegistryHelper.getInstance().start();
// admin fail-monitor run
JobFailMonitorHelper.getInstance().start();
// admin lose-monitor run ( depend on JobTriggerPoolHelper )
JobCompleteHelper.getInstance().start();
// admin log report start
JobLogReportHelper.getInstance().start();
// start-schedule ( depend on JobTriggerPoolHelper )
JobScheduleHelper.getInstance().start();
logger.info(">>>>>>>>> init xxl-job admin success.");
}
-
initI18n();
初始化新增任务页面中的阻塞处理策略相关的国际化信息。如果没有配置国际化语言则默认为中文。 -
JobTriggerPoolHelper.toStart();
初始化快慢两个线程池:fastTriggerPool和slowTriggerPool。当任务执行的时候默认使用快线程池fastTriggerPool;当任务执行失败并且重试次数达到阈值的时候就会使用慢线程池slowTriggerPool来执行任务 -
JobRegistryHelper.getInstance().start();
创建一个守护线程管理注册地址(每30秒执行一次) -
JobFailMonitorHelper.getInstance().start();
预警守护线程(每十秒工作一次),可增加自定义的预警类型,默认只有邮件。 -
JobCompleteHelper.getInstance().start();
任务结果丢失处理,另外有些版本是JobLosedMonitorHelper.getInstance().start(),需要注意区分。 -
JobLogReportHelper.getInstance().start();
创建一个日志守护线程(每一分钟工作一次)
刷新前三天的日志相关报表数据
清理N天前的日志记录(N就是我们配置的xxl.job.logretentiondays参数,必须大于7,否则无效) -
JobScheduleHelper.getInstance().start();
创建了两个调度相关守护线程scheduleThread(5秒内)和ringThread(处理耗时较长)
执行器源码
在业务定时任务系统,即实际工作中迭代的项目:
- 引入xxl-job的依赖配置,或导入jar包
- 新增执行器组件配置类XxlJobConfig.java,其中配置了核心类XxlJobSpringExecutor。(注意,如果使用的是Spring4.1以下的,则无法使用XxlJobSpringExecutor,因为较早的Spring版本meiyouSmartInitializingSingleton类,也就无法进行这种扫描的方式管理JobHandler)
Spring4.1以上版本,可使用如下推荐方式生成Excutor:
@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早期版本可使用如下这种方式(缺点:硬编码):
@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobExecutor xxlJobExecutor() {
XxlJobExecutor.registJobHandler("DemoJob1", new DemoJob1());//
XxlJobExecutor.registJobHandler("DemoJob2", new DemoJob2());//
//...
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobExecutor xxlJobExecutor = new XxlJobExecutor();
xxlJobExecutor.setIp(ip);
xxlJobExecutor.setPort(port);
xxlJobExecutor.setAppname(appname);
xxlJobExecutor.setAdminAddresses(addresses);
xxlJobExecutor.setLogPath(logpath);
xxlJobExecutor.setAccessToken(accessToken);
return xxlJobExecutor;
}
- 新增IJobHandler继承类,类中有带 @XxlJob(“xxx”)注解的方法,一个IJobHandler即为一个job任务,其概念与quartz任务类似。
XxlJobSpringExecutor核心类分析
@Override
public void afterSingletonsInstantiated() {
// init JobHandler Repository
/*initJobHandlerRepository(applicationContext);*/
// init JobHandler Repository (for method)
initJobHandlerMethodRepository(applicationContext);
// refresh GlueFactory
GlueFactory.refreshInstance(1);
// super start
try {
super.start();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
查看其继承实现关系,XxlJobSpringExecutor初始化的时候会调用afterSingletonsInstantiated()方法。
-
initJobHandlerRepository(applicationContext);
旧版本中用来注册带有 @JobHandler 注解的bean的Java类, 2.2.1-SNAPSHOT版本及以上已经不支持这种方式; -
initJobHandlerMethodRepository(applicationContext);
利用MethodIntrospector工具类的selectMethods方法和MetadataLookup接口得到Map<Method, XxlJob>。 -
GlueFactory.refreshInstance(1);
刷新GlueFactory为 SpringGlueFactory,在执行 glue 模式的任务时使用 spring 来加载相应实例。 -
super.start();
调用XxlJobExecutor.start() 。
public void start() throws Exception {
// init logpath
XxlJobFileAppender.initLogPath(logPath);
// init invoker, admin-client
initAdminBizList(adminAddresses, accessToken);
// init JobLogFileCleanThread
JobLogFileCleanThread.getInstance().start(logRetentionDays);
// init TriggerCallbackThread
TriggerCallbackThread.getInstance().start();
// init executor-server
initEmbedServer(address, ip, port, appname, accessToken);
}
-
XxlJobFileAppender.initLogPath(logPath);
文件夹不存在的情况下,创建日志文件夹 -
initAdminBizList(adminAddresses, accessToken);
初始化AdminBizClient集合,AdminBizClient提供callback(回调)、registry(注册)以及registryRemove(注册移除)到调度中心的方法。
@Override
public ReturnT<String> callback(List<HandleCallbackParam> callbackParamList) {
return XxlJobRemotingUtil.postBody(addressUrl+"api/callback", accessToken, timeout, callbackParamList, String.class);
}
@Override
public ReturnT<String> registry(RegistryParam registryParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "api/registry", accessToken, timeout, registryParam, String.class);
}
@Override
public ReturnT<String> registryRemove(RegistryParam registryParam) {
return XxlJobRemotingUtil.postBody(addressUrl + "api/registryRemove", accessToken, timeout, registryParam, String.class);
}
-
JobLogFileCleanThread.getInstance().start(logRetentionDays);
初始化日志清除线程,过期日志自动清理(清理N天前的日志文件)。 -
TriggerCallbackThread.getInstance().start();
执行结果回调相关 -
initEmbedServer(address, ip, port, appname, accessToken);
初始化执行器IP 地址与端口,如果IP没配置则默认取当前服务的地址,如果端口没配置则默认为9999
生成服务调用地址"http://{ip_port}/"
调用embedServer.start(address, port, appname, accessToken)方法。
public void start(final String address, final int port, final String appname, final String accessToken) {
executorBiz = new ExecutorBizImpl();
thread = new Thread(new Runnable() {
@Override
public void run() {
// param
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ThreadPoolExecutor bizThreadPool = new ThreadPoolExecutor(
0,
200,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2000),
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "xxl-rpc, EmbedServer bizThreadPool-" + r.hashCode());
}
},
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
}
});
try {
// start server
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline()
.addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL
.addLast(new EmbedHttpServerHandler(executorBiz, accessToken, bizThreadPool));
}
})
.childOption(ChannelOption.SO_KEEPALIVE, true);
// bind
ChannelFuture future = bootstrap.bind(port).sync();
logger.info(">>>>>>>>>>> xxl-job remoting server start success, nettype = {}, port = {}", EmbedServer.class, port);
// start registry
startRegistry(appname, address);
// wait util stop
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
if (e instanceof InterruptedException) {
logger.info(">>>>>>>>>>> xxl-job remoting server stop.");
} else {
logger.error(">>>>>>>>>>> xxl-job remoting server error.", e);
}
} finally {
// stop
try {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
thread.setDaemon(true); // daemon, service jvm, user thread leave >>> daemon leave >>> jvm leave
thread.start();
}
ExecutorBizImpl实现了ExecutorBiz,包含了任务的心跳检测、忙碌检测、触发任务、终止任务以及查看执行日志的方法。
- 使用netty_http启动http 服务,绑定你传入设置执行器的端口。通过 EmbedHttpServerHandler来处理调度中心调度执行器中的任务
- startRegistry注册当前执行器的地址到调度中心 调度中心对执行器的操作源码见:EmbedHttpServerHandler类(此类写在EmbedServer内)。
六、实际项目集成xxl-job
以上的操作与我们本身在开发的项目没有关系,现在大致有两种思路进行xxl-job的应用:
- 将执行器集成到正在开发或已经上线的Spring或SpringBoot项目上
- 直接单独创建SpringBoot或Spring(不推荐)项目,采用类似微服务的方式进行单独部署job服务
优缺点
- 第一种:可以复用以前和数据库交互的service类或其他如redis、mq等功能,缺点是需要附着在原有项目上,可能会造成性能影响,且没有解耦
- 第二种:完全解耦,但需要重新搭建数据库交互、redis、mq等,优点是独立出来方便管理,且解耦,不会对其他正在运行的服务造成任何影响。
6.1 Spring项目集成xxl-job
现在仅以第一种方式为例进行探讨,我们项目是Spring,Bean模式集成方法如下
6.1.1 步骤一
复制xxl.job.properties文件到配置文件夹
上下文引入该文件
<bean class="com.neusoft.system.handler.ConvertPropertyConfigurer">
<property name="locations">
<list>
//*****
<value>WEB-INF/conf/platform/common/xxl.job.properties</value>
//*****
</list>
</property>
</bean>
6.1.2 步骤二
配置XxlJobExecutor实例,我们Spring版本比较低,所以只能采用这种硬编码的方式,有多少job就new多少对应IJobHandler实现类。
@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobExecutor xxlJobExecutor() {
XxlJobExecutor.registJobHandler("DemoJob1", new DemoJob1());//
XxlJobExecutor.registJobHandler("DemoJob2", new DemoJob2());//
//...
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobExecutor xxlJobExecutor = new XxlJobExecutor();
xxlJobExecutor.setIp(ip);
xxlJobExecutor.setPort(port);
xxlJobExecutor.setAppname(appname);
xxlJobExecutor.setAdminAddresses(addresses);
xxlJobExecutor.setLogPath(logpath);
xxlJobExecutor.setAccessToken(accessToken);
return xxlJobExecutor;
}
Spring版本高的可以采用如下方式,可实现注解自动扫描的效果。
@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;
}
6.1.3 步骤三
job改造
public class DemoJob extends IJobHandler
{
private static Logger logger = LoggerFactory.getLogger(UserYearScoreJob.class);
@Override
public ReturnT<String> execute(String arg0) throws Exception {
Calendar cal = Calendar.getInstance();
Date curDt = cal.getTime();
int curYear = cal.get(Calendar.YEAR);
try {
XxlJobLogger.log("DemoJob start...");
logger.info("DemoJob start...");
//handle(curDt, curYear);
logger.info("DemoJob end...");
XxlJobLogger.log("DemoJob end...");
} catch (ParseException e) {
e.printStackTrace();
throw e;
}
return ReturnT.SUCCESS;
}
Spring高版本可用如下方式
@Component
public class SimpleJobHandler {
private static final Logger logger = LoggerFactory.getLogger(SimpleJobHandler.class);
@Autowired
private TaskJob taskJob;
@XxlJob("demoJobHandler")
public ReturnT<String> demoJobHandler(String param) throws Exception {
XxlJobLogger.log("XXL-JOB, demoJobHandlerstart---------------");
try {
taskJob.execute(param);
return ReturnT.SUCCESS;
} catch (Exception exception) {
XxlJobLogger.log("demoJobHandler发生异常:{}", exception.getMessage(), exception);
if (exception instanceof InterruptedException) {
throw exception;
}
return new ReturnT<>(ReturnT.FAIL_CODE, exception.getMessage());
} finally {
XxlJobLogger.log("XXL-JOB, demoJobHandlerend---------------");
}
}
}
至此,已完成实际Spring项目集成xxl-job执行器。
另外,SpringBoot集成类似,完成引入文件、配置类、job改造三个步骤即可完成集成。
需要注意的是,xxl-job的调度中心本身就是一个独立项目,没必要集成在其他项目中。
以上就是我们如何在项目中应用xxl-job的方法。
七、实际遇到的问题及发现的特性
1.调度中心和执行器引用的core包版本不一致
执行器无法完成回调,调度中心在日志中无法显示执行结果
2、阻塞处理策略:调度过于密集执行器来不及处理时的处理策略,策略包括:单机串行(默认)、丢弃后续调度、覆盖之前调度;
其中单机串行,是单个任务在单台机器上的串行,与其他任务无关,与其他机器无关
3、任务失败重试:支持自定义任务失败重试次数,当任务失败时将会按照预设的失败重试次数主动进行重试;其中分片任务支持分片粒度的失败重试;
这里有个坑,如果业务处理逻辑被try catch了则无法捕捉到失败,系统会认为是正常,所以如果有try的去掉try或在catch里抛出来即可。
4、任务失败告警;默认提供邮件方式失败告警,同时预留扩展接口,可方便的扩展短信、钉钉等告警方式;
失败预警可以实现JobAlarm,参照EmailJobAlarm编写如短信、微信等预警模式。
5、路由策略:执行器集群部署时提供丰富的路由策略,包括:第一个、最后一个、轮询、随机、一致性HASH、最不经常使用、最近最久未使用、故障转移、忙碌转移等;
其中故障转移在每次调度的时候会有心跳检测,如果监测的第一台有问题则检测另外一台,直到命中正常的执行器。
其中轮询,是单个任务在集群执行器上轮询,与其他任务无关。
6、分片广播任务:执行器集群部署时,任务路由策略选择”分片广播”情况下,一次任务调度将会广播触发集群中所有执行器执行一次任务。且每台执行器的分片参数index都不一样
可根据分片参数(index、total)开发分片任务,当只有一台机器时,index=0,total=1。
这种分片广播,即为并行执行,另外也可通过同一个jobHandler配置多次并传递参数实现分片,但是其路由无规律,且容易丢失,不建议使用。
注意:分片每次索引一致,且与注册先后无关,按照排序与索引号一一对应,总数不变的情况下,每次分片索引和机器对应关系不变。执行器和调度中心存在心跳,集群中一台机器宕机,不会立马剔除,会导致分片数据未及时更新。
7、GLUE:提供Web IDE,支持在线开发任务逻辑代码,动态发布,实时编译生效,省略部署上线的过程。
这种方式更加灵活,可应对临时突发情况,及时修改代码。
8、任务依赖:支持配置子任务依赖,当父任务执行结束且执行成功后将会主动触发一次子任务的执行, 多个子任务用逗号分隔;
在失败重试时,粒度可到子任务的失败重试。
9.停止任务,日志页面终止任务,调度中心会通知执行器,及时抛出interrupted异常;任务页面 停止调度,不影响本轮次已经触发的任务,且可以正常回调结果。
以上,仅供参考。