Xxl-Job 是一个轻量级分布式任务调度平台
作用:Xxl-Job
是一个任务调度框架,通过引入 Xxl-Job
相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动、执行、中止以及包含了日志记录与查询和任务状态监控。
理解: 如果将 Xxl-Job
形容为一个人的话,每一个引入 Xxl-Job
的微服务就相当于一个独立的人(执行器),而按照相关约定格式撰写的 Handler
为餐桌上的食物,可视化界面则可以决定哪个执行器(人),吃东西或者不吃某个东西(定时任务),在什么时间吃(Corn 表达式控制或者执行或终止);
Xxl-Job 是 2015 年点评员工 许雪里 业余开源制作,2016 点评内部接入 Xxl-Job,据统计,自 2016.01.21 接入至 2017.12.01期间,该系统已调度约 100 万次,表现优异。
经过数十个版本的更新,系统的任务模型、UI 交互模型以及底层调度通讯模型都有了较大的优化和提升,核心功能更加稳定高效。
目前接入登记使用的企业已经有几百家,算上没有登记的企业,几千家至少是有的。接入场景:如电商行业、O2O 业务和大数据作业等。
版本变更:在早期的 Xxl-Job,调度依赖是 Quartz,直到 2019 年 7月发布的 7.27版本才移除 Quartz 依赖。重构代码虽然移除了 Quartz,但是仍然少不了调度器、触发器、任务的设计。
Quartz 的不足: Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz 采用 API 的方式对任务进行管理,这样存在一下问题:
QuartzJobBean
到底层数据表中,系统侵入性相当严重。QuartzJobBean
耦合在同一个项目中,这将导致一个问题,在调度。Quartz | Xxl-Job | |
---|---|---|
依赖 | mysql | mysql、jdk1.7+、maven3.0+ |
集群、弹性扩容 | 多节点部署,通过竞争数据库锁来保证只有一个节点执行任务 | 使用 Quartz 基于数据库的分布式功能,服务器超出一定数量会给数据库造成一定的压力 |
任务分片 | 不支持 | 支持 |
管理界面 | 无 | 有 |
高级功能 | 无 | 弹性扩容,分片广播,故障转移,Rolling 实时日志,Glue(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化 |
缺点 | 没有管理界面,以及不支持任务分片等。不适用于分布式场景 | 调度中心通过获取 DB 锁来保证集群中执行任务的唯一性,如果短任务很多,随着调度中心集群数量增加,那么数据库的锁竞争会比较厉害,性能不好。 |
任务不能重复执行 | 无 | 使用 Quartz 基于数据库的分布式功能 |
并行调度 | 无 | 调度系统多线程(默认10个线程)触发调度运行,确保调度精确执行,不被堵塞。 |
失败处理策略 | 无 | 调度失败时的处理策略,策略包括:失败警告(默认)、失败重试(界面可配置) |
动态分片策略 | 无 | 分片广播任务以执行器为维度进行分片,支持动态扩容执行器集群从而动态增加分片数量,协同进行业务处理;在进行大数据量业务操作时可显著提升任务处理能力和速度。执行器集群部署时,任务路由策略选择“分片广播”情况下,一次任务调度将会广播触发对应集群中所有执行器执行一次任务,通过传递分片参数;可根据分片参数开发分片任务; |
从 github 上将 Xxl-Job 项目 clone 下来
GitHub 地址:https://github.com/xuxueli/xxl-job
Gitee 地址:https://gitee.com/xuxueli0323/xxl-job
doc: 文档资料,包括“调度数据库”建表脚本
xxl-job-core: 公共 jar 依赖
xxl-job-admin:调度中心,项目源码,Spring Boot 项目,可以直接启动
xxl-job-executor-samples:执行器,sample 示例项目,其中的 Spring Boot 工程,可以直接启动。可以在该项目上进行开发,也可以将现有项目改造生成执行器项目。
xxl-job-admin
模块,在 application.properties
中进行后台的配置XxlJobAdminApplication
类,访问 http://localhost:8080/xxl-job-admin
默认账户admin,密码123456定位到 JobHandler 下,新增定时任务
package com.xxl.job.executor.service.jobhandler;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
import static com.xxl.job.core.biz.model.ReturnT.SUCCESS;
/**
* @author zoom
* @desription 编写自己的定时任务handler
* @date 2023/10/27 11:50
*/
@Component
public class MyJobHandler {
/**
* @param param 参数
* @return 成功信息
* @desription 简单任务示例【bean模式】
*/
@XxlJob(value = "myJobHandler", init = "", destroy = "")
public ReturnT<String> demoJobHandler(String param) throws InterruptedException {
// 模拟业务执行
System.out.println("we should fight...");
// 返回执行结果
return SUCCESS;
}
}
自己的项目想要引入 xxl-job,导入以下依赖即可
<dependency>
<groupId>com.xuxueligroupId>
<artifactId>xxl-job-coreartifactId>
<version>2.0.1version>
dependency>
执行器相当于是小组组长,负责任务的具体执行,由它分配线程(组员)执行任务。执行器需要注册到调度中心,这样调度中心才知道怎样选择执行器,或者说做路由。执行器的执行结果,也需要通过回调的方式告诉调度中心
这里选择 Spring Boot 项目用来举例子,从源码中单独拷一个项目出来,如果你是在业务项目里集成的话,也是参考这个
sample
,在项目里面加上xxl-job-core
的依赖,添加配置就可以创建执行器了。
在 xxl-job-executor-samples
下新建自己的执行器模块,例如:xxl-job-executor-myExec
【直接复制 xxl-job-executor-sample-springboot
】,修改一下文件名和模块名
注意: 同时修改执行器的名称,否则后面会报命名冲突
,修改@XxlJob
的value
值即可
修改 sample-myExec
模块的配置,将其修改为集群模式
在D盘中,创建 xxl-log 文件夹
# 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=default_token
### xxl-job executor appname
### xxl-job executor server-info
xxl.job.executor.ip=
### xxl-job executor log-retention-days
xxl.job.executor.logretentiondays=30
### xxl-job executor log-path 日志存放路径
xxl.job.executor.logpath=D:\\xxl-log
### 因为我们要模拟执行器集群部署,打包后单击运行多次,为服务设置随机端口
server.port=${random.int[10000,19999]}
### 执行器的端口
xxl.job.executor.port=${random.int[9000,10000]}
### 集群部署,这两项配置要一致
xxl.job.executor.appname=xxl-job-executor-myExec
xxl.job.executor.address=
修改idea
配置
启动项目,在本地启动多个执行器集群的时候这里要打上勾(或者直接
alt + U
),因为我们配置了随机端口,所以是不会报错的。
启动执行器
启动两个(XxlJobExecutorApplication)
注意点:
新建任务有几个注意点:
- 在 Spring Bean 实例中(@Component 注解),开发 Job 方法。方法格式要求为
public ReturnT
,返回值和参数格式是固定的,这个是不能动的,唯一能动的是方法名。demoJobHandler(String param) - 在方法名上打上
@XxlJob
注解,这里面有几个属性,第一个 value 值对应的是调度中心新建任务的 JobHandler 属性的值。另外的 init 对应 JobHandler 初始化方法,destory 对应 JobHandler 销毁方法。这两个方法要在任务类里面创建。- 执行日志:需要通过
XxlJobLogger.log
打印执行日志,会写到指定的日志文件中。
路由策略是指一个任务可以由多个执行器完成,那具体由哪一个完成呢,这就要看我们指定的路由策略了,
这个参数当执行器做集群部署的时候才有意义
。
Quartz 中只能随机负载。那么这里的第一个,最后一个是按什么顺序来的呢,就是点击查看-注册节点的1,2,3,4,第一个指的就是1,最后一个指的就是4。
运行模式分为两种,一种是 BEAN、一种是 GLUE
Java
类,然后在 JobHandler
里填上 @XxlJob
里面的名字,是在执行器端编写的但是这样会存在一个安全隐患的问题,没有做鉴权。解决方法也很简单,只需要在调度中心和执行器的
application.properties
里加上相同的token
即可。
xxx.job.accessToken=
阻塞处理策略指的是任务的一次运行还没有结束,下一次调度的时间又到了,比如一个任务执行的时间是三分钟,但是设置的频率是每分钟执行一次,这时候第一次还没执行完,第二次怎么办?
一共有三个选项
策略 | 参数值 | 详细含义 |
---|---|---|
单机串行,默认 | SERIAL_EXECUTION | 调度请求进入单机执行器后,调度请求进入 FIFO 队列并以串行方式运行 |
丢弃后续调度 | DISCARD_LATER | 调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败 |
覆盖之前调度 | COVER_EARLY | 调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务 |
当我们要写一个 Job 的时候,任务是相互依赖的。比如下面我要赶这么多事情,A干完了干B,B干完了干C,C干完了干D。
解决这种问题的时候思路有两种:
- 第一种是把这么多逻辑写成一个大 job,串行化
- 第二种就是用子任务,在一个任务末尾触发另一个任务
如果我们需要在本任务执行结束并且执行成功的时候触发另外一个任务,那么就可以把另外的任务作为本任务的子任务执行,因为每个 Job 都有自己的唯一 id,所以只需要在子任务一栏中填上任务 id 即可。
超时:如果在指定时间内没有返回结果,就不再等待结果
假设我们有个任务需要处理 20 万条数据,每条数据的业务逻辑处理要 0.1 秒,对于普通任务单线程来讲,需要处理 5.5 个小时才能处理完成。这时候你想要提高速度,你的第一反应就是开多线程跑嘛,我开三个,差不多就是一个多小时就搞定了,你的思路完全正确!
这时候将会是下面这种情况,在执行器 0 上有三个线程在拼命的工作,但是这样大家觉得好不好?不好吧,我执行器 0 在这累的要死,你执行器 1 和执行器 2 在那休息,旱的旱死,涝的涝死,首先这是一个分配不均匀的问题。其次当执行器 0 三个线程都在工作的时候会浪费它的资源,使之这台服务器的性能也会下降,所以这是一种不好的方式。
这时候就要用到我们的分片任务了,真正好的方案如下,这是一个既科学又合理的方案。三台执行器各自起一个线程来共同把这个任务完成!
这时候有个问题,三台机器大家都执行同一段代码,那岂不是乱套了,这个数据你也执行一遍,我也执行一遍,它也执行一遍。解决的思路很简单,一台执行器处理总数的1/3,大家把需要干的活平均分了嘛,我干1/3,你干1/3,它干1/3,这样也不会产生冲突。
分片任务在运行的时候,调度器会给每个执行器发送一个不同的分片序号,分片的最大序号跟执行器的总数量是一样的,确保每个执行器都会执行到这个任务,比如上图中第一个执行器拿到分片序号0,第二台执行器拿到分片序号1,第三台执行器拿到分片序号2。那现在就好办了,我们只需要把处理得数据进行模3取余,余数为 0 的数据就由执行器 0 干,余数为 1 的数据就由执行器 1 干,余数为 2 的数据就由执行器 2 干。
我们获取数据的 sql 可以这样写:
// count 分片总数, index 当前分片数
select id,name,pssword from student where status = 0 and mod(id,#{count}) = #{index} order by id desc limit 1000;
@Component
public class ShardingJobHandler {
private static Logger logger = LoggerFactory.getLogger(ShardingJobHandler.class);
/**
* 分片广播任务
*/
@XxlJob("shardingJobHandler")
public ReturnT<String> shardingJobHandler(String param) throws Exception {
// 分片参数,包括总分片数(执行器个数),当前分片序号
ShardingUtil.ShardingVO shardingVO = ShardingUtil.getShardingVo();
XxlJobLogger.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardingVO.getIndex(), shardingVo.getTotal());
// sql
String sql = "select id,name,password from student where status = 0 and mod(id,"
+ shardingVO.getTotal() + ")=" + shardingVO.getIndex() + " order by id desc limit 1000;";
// List list = getStudent(sql);
// 获取到这台机器需要执行的数据,执行业务逻辑
return ReturnT.SUCCESS;
}
}
新建任务的时候选择分片广播,填上对应的 JobHandler
即可
最后需要说明一下,分片的数据量不一定是完全均等的数据量,上面的取模只是一个举例,一个思路。我们也可以把0、1、2替换成其他条件去从所有数据中获取部分数据,比如分片序号0 = 机器我查2018年的数据,分片序号1 的机器我查2019年的数据,分片序号2的机器我查2020年的数据。具体怎么分全靠我们的业务来选择。
如果增加或者减少了节点,总分片数和最大分片序号会实时发生变化。
命令任务比较有用,比如我们需要定时重启数据库(service restart
),定时备份数据文件(cp tar rm
),定时清理日志(rm
)。
命令行的使用也很简单,只需要把执行的命令作为参数传递进来即可
@Component
public class CommandJobHandler {
@XxlJob("commandJobHandler")
public ReturnT<String> commandJobHandlerTest() throws IOException{
// 用于获取动态传进来的参数
String jobParam = XxlJobHelper.getJobParam();
Process exec = Runtime.getRuntime().exec(jobParam);
System.out.println("command run success...");
return ReturnT.SUCCESS;
}
}
拓展: 执行 bat 文件
@Component
public class CommandJobHandler2 {
/**
* 命令行任务
*
* @param
* @return
*/
@XxlJob(value = "commandJobHandler2")
public ReturnT<String> commandJobHandlerTest() throws IOException {
//用于获取动态传入进来的参数
String jobParam = XxlJobHelper.getJobParam();
File file = new File(jobParam);
System.out.println(String.valueOf(file));
System.out.println(file.getAbsolutePath());
System.out.println(file.getCanonicalFile());
// Process exec = Runtime.getRuntime().exec("cmd /k start " + "www.baidu.com");
Process exec2 = Runtime.getRuntime().exec("cmd /k start " + file.getCanonicalFile());
System.out.println("command run success...");
return ReturnT.SUCCESS;
}
}
命令
cmd命令执行窗口开闭指令
cmd /c dir 是执行完dir命令后关闭命令窗口。
cmd /k dir 是执行完dir命令后不关闭命令窗口。
cmd /c start dir 会打开一个新窗口后执行dir指令,原窗口会关闭。
cmd /k start dir 会打开一个新窗口后执行dir指令,原窗口不会关闭。
周期性任务就是在任务的开始和销毁的时候执行自定义的方法,做一些自己想做的事
注意: 集群情况下,我们执行任务的时候需要填写执行器地址
执行器地址在执行器管理中查看
Xxl-Job
是一个分布式任务调度平台。有 2 个角色,xxl-job-admin
调度中心和 xxl-job-executor
执行器。调度中心统一管理调度任务,不承担业务逻辑,负责发起调度请求。执行器是我们的业务系统,负责接收调度请求并执行对应的 JobHandler
中的业务逻辑。
特点: