xxl-job架构原理讲解

1、调度中心

调度中心是一个单独的Web服务,主要是用来触发定时任务的执行

它提供了一些页面操作,我们可以很方便地去管理这些定时任务的触发逻辑

调度中心依赖数据库,所以数据都是存在数据库中的

调度中心也支持集群模式,但是它们所依赖的数据库必须是同一个

所以同一个集群中的调度中心实例之间是没有任何通信的,数据都是通过数据库共享的

2、执行器

执行器是用来执行具体的任务逻辑的

执行器你可以理解为就是平时开发的服务,一个服务实例对应一个执行器实例

每个执行器有自己的名字,为了方便,你可以将执行器的名字设置成服务名

3、任务

一个执行器中也是可以有多个任务的

总的来说,调用中心是用来控制定时任务的触发逻辑,而执行器是具体执行任务的,这是一种任务和触发逻辑分离的设计思想,这种方式的好处就是使任务更加灵活,可以随时被调用,还可以被不同的调度规则触发。

来个Demo

1、搭建调度中心

调度中心搭建很简单,先下载源码

github.com/xuxueli/xxl…

然后改一下数据库连接信息,执行一下在项目源码中的/doc/db下的sql文件

启动可以打成一个jar包,或者本地启动就是可以的

启动完成之后,访问下面这个地址就可以访问到控制台页面了

http://localhost:8080/xxl-job-admin/toLogin

用户名密码默认是 admin/123456

2、执行器和任务添加

添加一个名为sanyou-xxljob-demo执行器

任务添加

执行器选择我们刚刚添加的,指定任务名称为TestJob,corn表达式的意思是每秒执行一次

创建完之后需要启动一下任务,默认是关闭状态,也就不会执行

创建执行器和任务其实就是CRUD,并没有复杂的业务逻辑

按照如上配置的整个Demo的意思就是

每隔1s,执行一次sanyou-xxljob-demo这个执行器中的TestJob任务

3、创建执行器和任务

引入依赖

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>com.xuxueli</groupId><artifactId>xxl-job-core</artifactId><version>2.4.0</version></dependency>
</dependencies>

配置XxlJobSpringExecutor这个Bean

@Configuration
public class XxlJobConfiguration {@Beanpublic XxlJobSpringExecutor xxlJobExecutor() {XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();//设置调用中心的连接地址xxlJobSpringExecutor.setAdminAddresses("http://localhost:8080/xxl-job-admin");//设置执行器的名称xxlJobSpringExecutor.setAppname("sanyou-xxljob-demo");//设置一个端口,后面会讲作用xxlJobSpringExecutor.setPort(9999);//这个token是保证访问安全的,默认是这个,当然可以自定义,// 但需要保证调度中心配置的xxl.job.accessToken属性跟这个token是一样的xxlJobSpringExecutor.setAccessToken("default_token");//任务执行日志存放的目录xxlJobSpringExecutor.setLogPath("./");return xxlJobSpringExecutor;}}

XxlJobSpringExecutor这个类的作用,后面会着重讲

通过@XxlJob指定一个名为TestJob的任务,这个任务名需要跟前面页面配置的对应上

@Component
public class TestJob {private static final Logger logger = LoggerFactory.getLogger(TestJob.class);@XxlJob("TestJob")public void testJob() {logger.info("TestJob任务执行了。。。");}}

所以如果顺利的话,每隔1s钟就会打印一句TestJob任务执行了。。。

启动项目,注意修改一下端口,因为调用中心默认也是8080,本地起会端口冲突

最终执行结果如下,符合预期

讲完概念和使用部分,接下来就来好好讲一讲Xxl-Job核心的实现原理

从执行器启动说起

前面Demo中使用到了一个很重要的一个类

XxlJobSpringExecutor

这个类就是整个执行器启动的入口

这个类实现了SmartInitializingSingleton接口

所以经过Bean的生命周期,一定会调用afterSingletonsInstantiated这个方法的实现

这个方法干了很多初始化的事,这里我挑三个重要的讲,其余的等到具体的功能的时候再提

1、初始化JobHandler

JobHandler是个什么?

所谓的JobHandler其实就是一个定时任务的封装

一个定时任务会对应一个JobHandler对象

当执行器执行任务的时候,就会调用JobHandler的execute方法

JobHandler有三种实现:

  • MethodJobHandler
  • GlueJobHandler
  • ScriptJobHandler

MethodJobHandler是通过反射来调用方法执行任务

所以MethodJobHandler的任务的实现就是一个方法,刚好我们demo中的例子任务其实就是一个方法

所以Demo中的任务最终被封装成一个MethodJobHandler

GlueJobHandler比较有意思,它支持动态修改任务执行的代码

当你在创建任务的时候,需要指定运行模式为GLUE(Java)

之后需要在操作按钮点击GLUE IDE编写Java代码

代码必须得实现IJobHandler接口,之后任务执行的时候就会执行execute方法的实现

如果你需要修改任务的逻辑,只需要重新编辑即可,不需要重启服务

ScriptJobHandler,通过名字也可以看出,是专门处理一些脚本的

运行模式除了BEANGLUE(Java)之外,其余都是脚本模式

而本节的主旨,所谓的初始化JobHandler就是指,执行器启动的时候会去Spring容器中找到加了@XxlJob注解的Bean

解析注解,然后封装成一个MethodJobHandler对象,最终存到XxlJobSpringExecutor成员变量的一个本地的Map缓存中

缓存key就是任务的名字

至于GlueJobHandler和ScriptJobHandler都是任务触发时才会创建

除了上面这几种,你也自己实现JobHandler,手动注册到JobHandler的缓存中,也是可以通过调度中心触发的

2、创建一个Http服务器

除了初始化JobHandler之外,执行器还会创建一个Http服务器

这个服务器端口号就是通过XxlJobSpringExecutor配置的端口,demo中就是设置的是9999,底层是基于Netty实现的

这个Http服务端会接收来自调度中心的请求

当执行器接收到调度中心的请求时,会把请求交给ExecutorBizImpl来处理

这个类非常重要,所有调度中心的请求都是这里处理的

ExecutorBizImpl实现了ExecutorBiz接口

当你翻源码的时候会发现,ExecutorBiz还有一个ExecutorBizClient实现

ExecutorBizClient的实现就是发送http请求,所以这个实现类是在调度中心使用的,用来访问执行器提供的http接口

3、注册到调度中心

当执行器启动的时候,会启动一个注册线程,这个线程会往调度中心注册当前执行器的信息,包括两部分数据

  • 执行器的名字,也就是设置的appname
  • 执行器所在机器的ip和端口,这样调度中心就可以访问到这个执行器提供的Http接口

前面提到每个服务实例都会对应一个执行器实例,所以调用中心会保存每个执行器实例的地址

这里你可以把调度中心的功能类比成注册中心

任务触发原理

弄明白执行器启动时干了哪些事,接下来讲一讲Xxl-Job最最核心的功能,那就是任务触发的原理

任务触发原理我会分下面5个小点来讲解

  • 任务如何触发?
  • 快慢线程池的异步触发任务优化
  • 如何选择执行器实例?
  • 执行器如何去执行任务?
  • 任务执行结果的回调
1、任务如何触发?

调度中心在启动的时候,会开启一个线程,这个线程的作用就是来计算任务触发时机,这里我把这个线程称为调度线程

这个调度线程会去查询xxl_job_info这张表

这张表存了任务的一些基本信息和任务下一次执行的时间

调度线程会去查询下一次执行的时间 <= 当前时间 + 5s的任务

这个5s是XxlJob写死的,被称为预读时间,提前读出来,保证任务能准时触发

举个例子,假设当前时间是2023-11-29 08:00:10,这里的查询就会查出下一次任务执行时间在2023-11-29 08:00:15之前执行的任务

查询到任务之后,调度线程会去将这些任务根据执行时间划分为三个部分:

  • 当前时间已经超过任务下一次执行时间5s以上,也就是需要在2023-11-29 08:00:05(不包括05s)之前的执行的任务
  • 当前时间已经超过任务下一次执行时间,但是但不足5s,也就是在2023-11-29 08:00:052023-11-29 08:00:10(不包括10s)之间执行的任务
  • 还未到触发时间,但是一定是5s内就会触发执行的

对于第一部分的已经超过5s以上时间的任务,会根据任务配置的调度过期策略来选择要不要执行

调度过期策略就两种,就是字面意思

  • 直接忽略这个已经过期的任务
  • 立马执行一次这个过期的任务

对于第二部分的超时时间在5s以内的任务,就直接立马执行一次,之后如果判断任务下一次执行时间就在5s内,会直接放到一个时间轮里面,等待下一次触发执行

对于第三部分任务,由于还没到执行时间,所以不会立马执行,也是直接放到时间轮里面,等待触发执行

当这批任务处理完成之后,不论是前面是什么情况,调度线程都会去重新计算每个任务的下一次触发时间,然后更新xxl_job_info这张表的下一次执行时间

到此,一次调度的计算就算完成了

之后调度线程还会继续重复上面的步骤,查任务,调度任务,更新任务下次执行时间,一直死循环下去,这就实现了任务到了执行时间就会触发的功能

这里在任务触发的时候还有一个很有意思的细节

由于调度中心可以是集群的形式,每个调度中心实例都有调度线程,那么如何保证任务在同一时间只会被其中的一个调度中心触发一次?

我猜你第一时间肯定想到分布式锁,但是怎么加呢?

XxlJob实现就比较有意思了,它是基于八股文中常说的通过数据库来实现的分布式锁的

在调度之前,调度线程会尝试执行下面这句sql

就是这个sql

select * from xxl_job_lock where lock_name = 'schedule_lock' for update

一旦执行成功,说明当前调度中心成功抢到了锁,接下来就可以执行调度任务了

当调度任务执行完之后再去关闭连接,从而释放锁

由于每次执行之前都需要去获取锁,这样就保证在调度中心集群中,同时只有一个调度中心执行调度任务

2、快慢线程池的异步触发任务优化

当任务达到了触发条件,并不是由调度线程直接去触发执行器的任务执行

调度线程会将这个触发的任务交给线程池去执行

所以上图中的最后一部分触发任务执行其实是线程池异步去执行的

那么,为什么要使用线程池异步呢?

主要是因为触发任务,需要通过Http接口调用具体的执行器实例去触发任务

这一过程必然会耗费时间,如果调度线程去做,就会耽误调度的效率

所以就通过异步线程去做,调度线程只负责判断任务是否需要执行

并且,Xxl-Job为了进一步优化任务的触发,将这个触发任务执行的线程池划分成快线程池慢线程池两个线程池

在调用执行器的Http接口触发任务执行的时候,Xxl-Job会去记录每个任务的触发所耗费的时间

注意并不是任务执行时间,只是整个Http请求耗时时间,这是因为执行器执行任务是异步执行的,所以整个时间不包括任务执行时间,这个后面会详细说

当任务一次触发的时间超过500ms,那么这个任务的慢次数就会加1

如果这个任务一分钟内触发的慢次数超过10次,接下来就会将触发任务交给慢线程池去执行

所以快慢线程池就是避免那种频繁触发并且每次触发时间还很长的任务阻塞其它任务的触发的情况发生

3、如何选择执行器实例?

上一节说到,当任务需要触发的时候,调度中心会向执行器发送Http请求,执行器去执行具体的任务

那么问题来了

由于一个执行器会有很多实例,那么应该向哪个实例请求?

这其实就跟任务配置时设置的路由策略有关了

从图上可以看出xxljob支持多种路由策略

除了分片广播,其余的具体的算法实现都是通过ExecutorRouter的实现类来实现的

这里简单讲一讲各种算法的原理,有兴趣的小伙伴可以去看看内部的实现细节

第一个、最后一个、轮询、随机都很简单,没什么好说的

一致性Hash讲起来比较复杂,你可以先看看这篇文章,再去查看Xxl-Job的代码实现

zhuanlan.zhihu.com/p/470368641

最不经常使用(LFU:Least Frequently Used):Xxl-Job内部会有一个缓存,统计每个任务每个地址的使用次数,每次都选择使用次数最少的地址,这个缓存每隔24小时重置一次

最近最久未使用(LRU:Least Recently Used):将地址存到LinkedHashMap中,它利用LinkedHashMap可以根据元素访问(get/put)顺序来给元素排序的特性,快速找到最近最久未使用(未访问)的节点

故障转移:调度中心都会去请求每个执行器,只要能接收到响应,说明执行器正常,那么任务就会交给这个执行器去执行

忙碌转移:调度中心也会去请求每个执行器,判断执行器是不是正在执行当前需要执行的任务(任务执行时间过长,导致上一次任务还没执行完,下一次又触发了),如果在执行,说明忙碌,不能用,否则就可以用

分片广播:XxlJob给每个执行器分配一个编号,从0开始递增,然后向所有执行器触发任务,告诉每个执行器自己的编号和总共执行器的数据

我们可以通过XxlJobHelper#getShardIndex获取到编号,XxlJobHelper#getShardTotal获取到执行器的总数据量

分片广播就是将任务量分散到各个执行器,每个执行器只执行一部分任务,加快任务的处理

举个例子,比如你现在需要处理30w条数据,有3个执行器,此时使用分片广播,那么此时可将任务分成3分,每份10w条数据,执行器根据自己的编号选择对应的那份10w数据处理

当选择好了具体的执行器实例之后,调用中心就会携带一些触发的参数,发送Http请求,触发任务

4、执行器如何去执行任务?

相信你一定记得我前面在说执行器启动是会创建一个Http服务器的时候提到这么一句

当执行器接收到调度中心的请求时,会把请求交给ExecutorBizImpl来处理

所以前面提到的故障转移和忙碌转移请求执行器进行判断,最终执行器也是交给ExecutorBizImpl处理的

执行器处理触发请求是这个ExecutorBizImpl的run方法实现的

当执行器接收到请求,在正常情况下,执行器会去为这个任务创建一个单独的线程,这个线程被称为JobThread

每个任务在触发的时候都有单独的线程去执行,保证不同的任务执行互不影响

之后任务并不是直接交给线程处理的,而是直接放到一个内存队列中,线程直接从队列中获取任务

这里我相信你一定有个疑惑

为什么不直接处理,而是交给队列,从队列中获取任务呢?

那就得讲讲不正常的情况了

如果调度中心选择的执行器实例正在处理定时任务,那么此时该怎么处理呢?**

这时就跟阻塞处理策略有关了

阻塞处理策略总共有三种:

  • 单机串行
  • 丢弃后续调度
  • 覆盖之前调度

单机串行的实现就是将任务放到队列中,由于队列是先进先出的,所以就实现串行,这也是为什么放在队列的原因

丢弃调度的实现就是执行器什么事都不用干就可以了,自然而然任务就丢了

覆盖之前调度的实现就很暴力了,他是直接重新创建一个JobThread来执行任务,并且尝试打断之前的正在处理任务的JobThread,丢弃之前队列中的任务

打断是通过Thread#interrupt方法实现的,所以正在处理的任务还是有可能继续运行,并不是说一打断正在运行的任务就终止了

这里需要注意的一点就是,阻塞处理策略是对于单个执行器上的任务来生效的,不同执行器实例上的同一个任务是互不影响的

比如说,有一个任务有两个执行器A和B,路由策略是轮询

任务第一次触发的时候选择了执行器实例A,由于任务执行时间长,任务第二次触发的时候,执行器的路由到了B,此时A的任务还在执行,但是B感知不到A的任务在执行,所以此时B就直接执行了任务

所以此时你配置的什么阻塞处理策略就没什么用了

如果业务中需要保证定时任务同一时间只有一个能运行,需要把任务路由到同一个执行器上,比如路由策略就选择第一个

5、任务执行结果的回调

当任务处理完成之后,执行器会将任务执行的结果发送给调度中心

如上图所示,这整个过程也是异步化的

  • JobThread会将任务执行的结果发送到一个内存队列中
  • 执行器启动的时候会开启一个处发送任务执行结果的线程:TriggerCallbackThread
  • 这个线程会不停地从队列中获取所有的执行结果,将执行结果批量发送给调度中心
  • 调用中心接收到请求时,会根据执行的结果修改这次任务的执行状态和进行一些后续的事,比如失败了是否需要重试,是否有子任务需要触发等等

到此,一次任务的就算真正处理完成了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/694828.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mybatis数据操作语句

//基于注解 Mapper public interface reboudapt {Select("select * from dept")List<dept> huoqudept();//删除语句Delete("delete from dept where id #{id}")void deletesc(Integer id);//增加语句Insert("insert into dept(name, create_t…

Docker镜像加速

前言 众所周知&#xff0c;我们常用的一些工具或系统的下载源都是国外的&#xff0c;这就会导致我们在下载一些东西时&#xff0c;会导致下载巨慢或者下载失败的情况&#xff0c;下面便是docker换下载源的教程 镜像加速 下面是几个常用的国内的镜像 科大镜像&#xff1a;ht…

LeetCode每日一题 同构字符串(哈希表)

题目描述 给定两个字符串 s 和 t &#xff0c;判断它们是否是同构的。如果 s 中的字符可以按某种映射关系替换得到 t &#xff0c;那么这两个字符串是同构的。每个出现的字符都应当映射到另一个字符&#xff0c;同时不改变字符的顺序。不同字符不能映射到同一个字符上&#xf…

transformer,视觉模型改进论文的讨论

1、efficientVIT efficientformer 模型 快48.9倍的新SAM&#xff01;清华&MIT&英伟达开源EfficientViT-SAM&#xff1a;精度不变&#xff0c;原地起飞 YOLOv8改进 | 2023主干篇 | EfficientViT替换Backbone&#xff08;高效的视觉变换网络&#xff09; - Snu77的文章 -…

BTC系列-系统学习铭文(一)-比特币上的NFT

Ordinals协议概况 开源项目: https://github.com/ordinals/ord铭文浏览器: https://Ordinals.com关于Ordinals的BIP: https://github.com/ordinals/ord/blob/master/bip.mediawiki序数理论手册: https://docs.ordinals.com/overview.html 所需的技术积累 Ordinals NFTs 是基…

容器库(10)-std::unordered_set

unordered_set是以key为元素无序的关联容器&#xff0c;搜索、移除和插入操作是平均常数的时间复杂度。unordered_set在内部没有按任何顺序排列&#xff0c;而是放在桶当中的&#xff0c;放进哪个桶是通过计算key的hash值来决定的。 template<class Key,class Hash std::h…

Wireshark过滤DNS协议包语法实战

背景 现网DNS服务器发现CPU突增&#xff0c;发现有可能是客户恶意发起的随机子域名扫描&#xff0c;对服务器进行抓包分析&#xff0c;记录下当时的操作。 抓包 执行命令 tcpdump -iany port 53 and host $ip -nnv -w $ip.pcap进行抓包导出到本地&#xff0c;使用Wireshark进…

人工智能|深度学习——基于数字图像处理和深度学习的车牌定位

1.研究背景及研究目的和意义 车牌识别Vehicle License Plate Recognition VLPR) 是从一张或一系列数字图片中自动定位车牌区域并提取车牌信息的图像识别技术。车牌识别 以数字图像处理、模式识别、计算机视觉等技术为基础&#xff0c;是现代智能交通系统的重要组成部分&#xf…

前端项目docker部署

以react项目为例&#xff0c;理论上vue项目也支持。 打包。最好在package.json 里加上"homepage": "."&#xff0c;打包好后&#xff0c;会生成build目录将build目录下的所有文件上传到服务器&#xff0c;假设放在/root/service/shop-h5/public 目录下编写…

C#知识点-16(计算器插件开发、事件、递归、XML)

计算器插件开发 1、Calculator.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace Calculator_DLL {//用来明确所有插件开发人员的开发规范public abstract class Calculator{public int N…

2024-02-21 算法: 测试链表是否有环

点击 <C 语言编程核心突破> 快速C语言入门 算法: 测试链表是否有环 前言一、双指针 ( 快慢指针 )二、代码总结 前言 要解决问题: 一道简单的算法题, 测试链表是否含有环. 想到的思路: 哈希表, 将链表指针强制转换为整型, 利用求余法建立哈希函数. 太复杂, 内存效率不高…

MyBatis的注解式开发

MyBatis的注解式开发 一、准备开发环境1.添加依赖2.jdbc.properties3.mybatis-config.xml4.Article实体类5.SqlSessionUtil工具类 二、Insert三、Delete四、Update五、Select mybatis 中也提供了注解式开发⽅式&#xff0c;采⽤注解可以减少 Sql 映射⽂件的配置。使⽤注解式开发…

设计模式浅析(六) ·命令模式

设计模式浅析(六) 命令模式 日常叨逼叨 java设计模式浅析&#xff0c;如果觉得对你有帮助&#xff0c;记得一键三连&#xff0c;谢谢各位观众老爷&#x1f601;&#x1f601; 命令模式 概念 命令模式&#xff08;Command Pattern&#xff09;是一种行为设计模式&#xff0c…

YOLOv5代码解读[02] models/yolov5l.yaml文件解析

文章目录 YOLOv5代码解读[02] models/yolov5l.yaml文件解析yolov5l.yaml文件检测头1--->耦合头检测头2--->解耦头检测头3--->ASFF检测头Model类解析parse_model函数 YOLOv5代码解读[02] models/yolov5l.yaml文件解析 yolov5l.yaml文件 # YOLOv5 &#x1f680; by Ult…

FPGA SERDESE2 (SDR收发仿真)

高速 Serdes 环路测试 高速串行通信优势非常巨大,只需要很少的IO引脚就可以实现高速通信,这也是当今FPGA高速接口的核心 技术。比如XILINX的7代FPGA,GTX可以达到10.3125Gbps,ultrascale FPGA的GTH可以达到16Gbps。目前国产FPGA还难以达到这么高的接口速度。 高速串行通信经…

社区分享|中华保险基于MeterSphere开展接口自动化测试

中华联合保险集团股份有限公司&#xff08;以下简称为“中华保险”&#xff09;始创于1986年&#xff0c;是全国唯一一家以“中华”冠名的国有控股保险公司。截至2022年12月底&#xff0c;中华保险总资产为1006.06亿元&#xff0c;在全国拥有超过2900个营业网点&#xff0c;员工…

Servlet中的请求与响应

Request和Response 1.Request和Response的概述2.Request对象2.1 Request继承体系2.2 Request获取请求数据2.3 解决post请求乱码问题 *2.4 Request请求转发(-&#xff0c;*)2.5 request的生命周期 3.HTTP响应详解(理解)1.使用抓包查看响应报文协议内容2.HTTP响应报文协议介绍 4.…

汇编英文全称

mov move mvn Mov Negative ldr LoaD Register str Store Register lsl Logic Shift Left lsr Logic Shift Right asr Arithmetic Shift Right 算数右移 ror Rotate right 循环右移…

人工智能讲师AI讲师大模型讲师叶梓介绍及大语言模型技术原理与实践提纲

叶梓&#xff0c;上海交通大学计算机专业博士毕业&#xff0c;高级工程师。主研方向&#xff1a;数据挖掘、机器学习、人工智能。历任国内知名上市IT企业的AI技术总监、资深技术专家&#xff0c;市级行业大数据平台技术负责人。 长期负责城市信息化智能平台的建设工作&#xff…

angular-引用本地json文件

angular-引用json文件&#xff0c;本地模拟数据时使用 在assets目录下存放json文件 大佬们的说法是&#xff1a;angular配置限定了资源文件的所在地&#xff08;就是assets的路径&#xff09;&#xff0c;放在其他文件夹中&#xff0c;angular在编译过程中会忽略&#xff0c;会…