提示:xxl-job的源码,xxl-job的使用、@Schedule、xxl-job的分片、xxl-job的GLUE、xxl-job的子任务、@Scheduled的缺点
文章目录
前言
由于公司领导的要求,让我在研发组内分享一下xxl-job这个技术栈。所以就准备了一下,后来想了想,反正也准备过了,不如在博客上也同步一份,后期了也方便查阅。本人水平有限,如有误导,欢迎斧正,一起学习,共同进步!
一、基础介绍
1.1、什么是xxl-job
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。上述是摘抄是官方文档,还有一些其他介绍,感兴趣的同学可以自行下载。官方下载地址
1.2、为什么要引入它
随着架构方式的演变,越来越多的项目是微服务架构而不是单体架构,人们需要有一个微服务的定时任务框架来帮助我们更好的开发。简单的说,@Scheduled的坑太多了,所以不用Scheduled,所以需要一个新的框架。
1.3、Scheduled注解的缺点
1.3.1、不支持集群
这点很好理解,因为一个项目单体部署的话,ok没问题,但是如果单个项目到达了瓶颈,想要集群部署的时候,Scheduled注解就歇菜了,还得处理一下。处理的方式可以是加标识,用队列,用mq等,都是为了其中一个节点处理了以后,其他节点不会重复的处理这个定时,不管怎么样,都是需要处理一下的,否则节点1、节点2同事执行了这个定时任务,那幂等性就无法保证了。
1.3.2、多个任务互相阻塞
要知道,Scheduled的默认处理线程是1(可以通过配置核心线程数来解决),如果你不配置的情况下,比如说你一个serviceImpl中有俩个方法使用了两个@Scheduled注解,方法A的定时是1分钟执行一次,方法B的定时是30秒执行一次。一旦你的方法A中的业务逻辑出于某种原因(可能是任务太多处理不过来,可能是在重试,可能是报错等等)阻塞了,那么方法B的定时即使时间到了,也会被阻塞,无法执行定时。(不同的方法会相互阻塞)
1.3.3、一个任务重复消费
这个是说,如果你的定时是每5秒执行一次,但是由于特殊情况(比如说是网络波动,重试次数过多或者数据量多查询变慢等等),导致10秒了还未执行完上次的业务,一旦你配置了核心线程数>1,那么就会导致这次业务未执行完,就开始了下次的业务,就会有一部分数据重复执行(大多数情况下,都是通过扫描来实现的。第一次扫描了10个数据,处理了5条,还未处理5条;第二次的定时又把未处理的5条处理了,后面这5条就会重复处理)。相同的方法会重复消费。
1.3.4、其他缺点
二、原理解析
2.1、xxl-job的设计思想
1、需要有一个独立的服务-调度服务(一定的高可用的)。这样一来,就没有那个重复消费的问题了,任务都是由调度服务去分发到某一个执行器(由此可以看出,调度中心才对外部的数据有管理的权限),然后去由执行器执行具体的定时的业务代码。这样一来,就要求,调度服务一定是高可用的,因为如果你的定时任务没有挂,调度服务挂了的话,还是会导致无法定时任务,因为只有具体的执行业务的逻辑,却没人调度服务去分发给他任务。
2、具体的执行器也要高可用。因为这样的话,就可以动态的监控哪些执行器的好使的,哪些是不好使的,就可以动态的分发给某些执行器。或者一旦某个执行器挂掉了,还可以分发给其他的执行器
2.2、工作原理
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。将任务抽象成分散的 JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的 JobHandler 中业务逻辑。因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性。这点听起来不深刻,举个例子就知道重要性了:
A同学有个定时的需求,如果他将业务代码和定时的代码放在一起的话,一旦后期数据量大了,性能瓶颈了,扩容的时候怎么扩容呢,是业务代码和定时代码一起扩容;
B同学也有一个定时的需求,但是B同学把业务代码和定时的代码拆分了,这样后期数据量大了,性能瓶颈了,扩容的时候就只扩定时代码,而不扩容业务代码,这样一下子就节省了多少资源,所以解耦的好处是非常明显的。
2.3、架构图
手动的将定时任务的项目(执行器)服务器IP和端口号统一存到XXL任务调度注册中心,触发所有的定时任务都先走分布式任务调度中心。b、在任务调度中心创建触发规则d、当事件触发时,在任务调度的注册中心查找执行器集群地址,采用负载均衡算法取出一个地址,使用RPC触发我们的定时任务。
a、这里的注册中心是任务调度平台自己读写的b、任务执行时,可以选择不同的策略,还可以增加参数,执行器可根据参数信息执行不同的数据c、自动注册、任务启动、任务停止、任务信息修改等生效时间都会存在延迟现象,原因:1)、有缓存不能实时刷新
2)、请求时需要先从注册中心拿取执行器地址,也需要耗时。d、任务还支持分片,根据分片执行不同的数据
三、使用步骤(着急的同学直接看3.7)
3.1、下载源码
Git地址是:https://github.com/xuxueli/xxl-job
3.2、模块介绍
3.2.1、调度中心模块
调度中心项目:xxl-job-admin,作用是:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。自身不承担业务代码。
3.2.2、执行器模块
xxl-job-executor-sample-springboot,执行器负责接收“调度中心”的调度并执行;可直接部署执行器,也可以将执行器集成到现有业务项目中。
3.2.3、核心模块
xxl-job-core,调度中心和执行器的模块都依赖了这个核心模块。
3.3、导入db
将doc文件夹下的db文件夹下的 tables_xxl_job.sql 执行即可
3.4、启动
3.4.1、启动调度中心模块
springboot项目,直接运行启动类(XxlJobAdminApplication)即可。
3.4.2、启动执行器模块
springboot项目,直接运行启动类即可,作者提供了多种的方式启动,我此次启动的是springboot的,运行XxlJobExecutorApplication类。
3.4.3、代码中新增一个执行器
@XxlJob("shareJobOneCopy")
public void test1() {
// 调度日志-操作-执行日志
XxlJobHelper.log("shareJobOneCopy copy start ... ");
// 控制台
logger.info("shareJobOneCopy copy test1 start ..");
}
3.5、新建执行器(管理界面)
控制界面中新增一个执行器
(名称都是随便起的,但ip端口一定要是执行器的端口,否则会报错)
执行端口是 xxl.job.executor.port=9999
3.6、新建任务(管理界面)
几个难以理解的属性的解释如下:
3.6.1、运行模式
运行模式。运行模式你可以简单的理解为,GLUE是放在服务器(调度中心)上的,Bean是放在客户端(执行器)上的。
- Bean
- GLUE(java、python、shell、php、nodejs)
3.6.1.1、原理
GLUE:每个 “GLUE模式(Java)” 任务的代码,实际上是“一个继承自 “IJobHandler” 的实现类的类代码”,“执行器”接收到“调度中心”的调度请求时,会通过 Groovy 类加载器加载此代码,实例化成 Java 对象,同时注入此代码中声明的 Spring 服务(请确保 Glue 代码中的服务和类引用在“执行器”项目中存在),然后调用该对象的 execute 方法,执行任务逻辑。
Bean:另外一种方式是你提前把代码写进「执行器」程序中,这样的模式在 XXL-JOB 中叫做「Bean模式」:每个 Bean 模式任务都是一个 Spring 的 Bean 类实例,它被维护在“执行器”项目的 Spring 容器中。任务类需要加 “@JobHandler(value=“名称”)” 注解,因为“执行器”会根据该注解识别 Spring 容器中的任务。任务类需要继承统一接口 “IJobHandler”,任务逻辑在 execute 方法中开发,因为“执行器”在接收到调度中心的调度请求时,将会调用 “IJobHandler” 的 execute 方法,执行任务逻辑
3.6.1.2、GLUE、Bean优缺点
bean方式,每个任务对应一个方法
优点:
每个任务只需要开发一个方法,并添加“@Xxljob”注解即可,更方便、快捷
支持自动扫描任务并注入到执行器容器
缺点:
要求spring容器环境
添加新任务需要重启项目(有时候量大的,一次执行可能几个小时,重启的话,之前的任务的幂等性、顺序等需要考虑)
GLUE模式:任务以源码方式维护在调度中心,不需要本地编写任务代码
优点:
支持通过web ide在线更新,实时编译生效,因此不需要指定JobHandler
不需要重启项目(最大的优点,因为任务的代码是存在数据库里的,是动态的去编译执行的)
缺点:
如果依赖了某个框架,需要在部署之前先依赖到项目中,然后再web ide中才能依赖,否则会报错
3.6.1.3、演示
bean模式
GLUE模式
在这里先新建一个任务,选择GLUE模式,
写具体的脚本
3.6.2、路由策略
这个确实是没啥可说的,负载均衡的策略,开发的小伙伴应该都知道。有以下几种策略
- 第一个
- 最后一个
- 轮询
- 随机
- 一致性哈希
- 最不长使用
- 最近最久未使用
- 故障转移
- 忙碌转移
- 分片广播
大家自己测试的时候,可以先看一下任务的注册地址,如果想修改的话,就修改这个“任务”对应的“执行器”的ip,就可以修改了。具体图片如下:
3.6.2、子任务id
子任务:当父任务执行结束且执行成功后主动触发。多个子任务用逗号分割,适合连贯的任务,框架自带任务的顺序。缺点是不能传递上下文。不过可以通过改库。前面的改了数据库的某个表,后面的任务可以直接看到改过的表。但是不能把前面的任务的参数或者修改的内容传给后面的任务。
别的都一样,只不过在这里加上如果执行成功的话,下一步要执行的任务id。
3.7、业务代码集成(想在自己项目中用xxl-job前面的都可以不看,只看这个)
3.7.1、添加pom
<!-- xxl-job-->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
3.7.2、改配置文件(每个k的解释都有,直接看就行)
xxl.job.admin.addresses 就是调度中心的地址
xxl.job.executor.appname 是对应的执行器的AppName(自动注册的时候使用)
xxl.job.accessToken 和服务器的对不上的话,每30s会报一个错:
14:37:39.438 logback [xxl-job, executor ExecutorRegistryThread] INFO
c.x.j.c.t.ExecutorRegistryThread - >>>>>>>>>>> xxl-job registry fail,
registryParam:RegistryParam{registryGroup=‘EXECUTOR’,
registryKey=‘xxl-job-executor-sample’,
registryValue=‘http://172.16.20.68:9996/’}, registryResult:ReturnT
[code=500, msg=The access token is wrong., content=null]
# web port
server.port=8081
# no web
#spring.main.web-environment=false
# log config
logging.config=classpath:logback.xml
### xxl-job admin address list, such as "http://address" or "http://address01,http://address02"
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### xxl-job, access token
xxl.job.accessToken=
### xxl-job executor appname
# # 是执行器的自动注册的时候用的,可以动态的新增、删除机器(分为自动注册、手动注册,是对应的执行器的AppName)
xxl.job.executor.appname=xxl-job-executor-sample
### xxl-job executor registry-address: default use address to registry , otherwise use ip:port if address is null
xxl.job.executor.address=
### xxl-job executor server-info
## 当你是双网卡或者有虚拟网卡的时候,可以指定一个ip
xxl.job.executor.ip=
# 执行器的ip的端口
xxl.job.executor.port=9999
### xxl-job executor log-path
## 日志存储的路径
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### xxl-job executor log-retention-days
## 日志存储的时间,超过这个时间被清理
xxl.job.executor.logretentiondays=30
3.7.3、加配置类(在你的业务项目中)
package com.zheng.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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
*/
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@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() {
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-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();
*/
}
3.7.4、加一个handler(也就是@XxlJob注解)
package com.zheng.handler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
/**
* @author: ZhengTianLiang
* @date: 2022/05/29 22:04
* @desc: 测试
*/
@Component
public class BusinessJobHandler {
@XxlJob("businessJobHandler")
public void businessJobHandler() {
// 调度日志-操作-执行日志
XxlJobHelper.log("businessJobHandler XxlJobHelper start ... ");
// 控制台
System.out.println("businessJobHandler log start ..");
}
}
3.7.5、启动xxl-job的admin
这个xxl-job的admin是指的xxl-job的服务端,不是你的业务项目。因为它是springboot项目,直接启动(XxlJobAdminApplication)即可。
3.7.6、管理界面中新建一个“执行器”
需要注意的点是,机器的地址是执行器的端口而不是web的端口。是说配置文件中的xxl.job.executor.port=9999 的端口而不是server.port=8081。以我的例子,这个机器地址就是:
http://192.168.64.1:9997 。多个之前用逗号分隔。
3.7.7、管理界面中新建一个“任务”
3.7.8、测试demo,完成集成
四、核心源码
4.1、调度中心(服务器)源码
4.1.1、源码执行顺序
4.1.2、源码解读
从xxljob的源码中就能看到:
1、初始化i18n
2、start定时线程池
3、注册器的启动 (其实就用一个线程去执行registry,就是你定义的registry会30s注册一次,注册的时候就会改xxl_job_registry表的updateTime,如果3次都没注册,就把这个服务剔除,这种操作)
4、失败后的注册器的启动(其实就是失败任务执行了,就会回调任务中心,然后这些失败的数据会做一些处理,比如说重新执行啊之类的)
5、丢失任务的处理的启动(其实就是说,对丢失任务的处理,源码是这么给的注释: 任务结果丢失处理:调度记录停留在 “运行中” 状态超过10min,且对应执行器心跳注册失败不在线,则将本地调度主动标记失败)
6、日志的记录。打开源码就会发现,是一个while(!toStop),就是说,如果xxl-job不停止,就会一直不断的执行日志相关
7、 JobScheduleHelper.getInstance().start(); 是最核心的一段,大概解释如下(详细的可以根据照片自行看源码,图片中的步骤和解释是一一对应的):
1、新建一个线程,停了5s以后,去查数据库(select * from xxl_job_lock where lock_name = ‘schedule_lock’ for update),其实就是一个加锁的操作。
2、当前时间+提前量(默认是5s)去查(xxl_job_info,筛选条件是 trigger_next_time 下次调度时间<= 当前时间+提前量 )数据,并拿到任务数据。(假设我有一个定时器,是说9点整去执行某个东西,我总不能9点整再去从库中查吧,因为我的调度中心是解耦出来的,调度中心查数据库、调度中心将数据给执行器,都是有开销的,所以得提前去查出来任务,这个提前量默认是是5s) 拿到数据以后,将任务放在 ringThread (他是一个ConcurrentHashMap对象)中的。
3、JobScheduleHelper(任务管理器)做的工作:
第一个线程 scheduleThread 扫描任务线程: a、获取任务调度lock
b、查询5s内需要执行的任务
c、构建待执行任务 ringItemData
第二个线程 ringThread (循环处理任务线程):
a、 构建触发器 JobTriggerPoolHelper
b、 triggerPool_.execute
c、 根据路由规则获取调度地址
d、 PRC调用
4.2、执行器(客户端)源码
4.2.2、源码执行顺序
4.2.2、源码解读
1、初始化日志的路径,(日志文件夹)
2、初始化admin远程服务(调度中心)的代理对象,放到一个list中去
3、日志
4、用另一个线程池(TriggerCallbackThread)去拿到第二步(initAdminBizList)的结果(他们是异步的),是一个触发器回调的过程
5、真正的去rpc的调用
4.3、详细流程图
五、我的xxljob分享word
下面这张图是我的分享中截的一张图,感兴趣的同学可以自行下载(内附demo源码压缩包)。
总结
大家都知道,作为开发,是需要不断的学习的。其中一个重要原因就是开发软件的更新迭代速度快。那为什么会不断的出现新工具呢,核心的点就是因为不满足我们的开发需要了。为什么会有xxl-job的出现,不仅仅是轻量级简单好用,更多的原因是@Scheduled在微服务架构集群化部署的大环境下,已经不满足我们的需要了。