xxl-job的使用及源码解析

提示: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在微服务架构集群化部署的大环境下,已经不满足我们的需要了。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值