官方文档:http://www.xuxueli.com/xxl-job。
xxl-job简介
xxl-job是一个分布式任务调度平台,其实现主要包括调度中心、执行器等组件。
- 调度中心:调度中心负责管理调度信息,包括任务信息、执行器信息等,并负责任务的分配和调度。
- 执行器:执行器是任务的具体执行者,负责执行调度中心分配的任务。执行器通过注册到调度中心,使得调度中心能够管理和调度它。
- 调度过程:调度中心根据任务的配置信息、执行器的注册信息等,制定调度计划。当调度时间到来时,调度中心通过内部的调度算法,选择合适的执行器执行任务。
- 通讯协议:调度中心和执行器之间通过HTTP协议通讯。
- 任务注册与发现:执行器启动时向调度中心注册,并保持心跳,以便调度中心发现和管理。
- 任务触发:任务触发主要通过API接口或者界面操作触发。
- 任务分配:调度中心根据任务的配置和执行器的实际情况,将任务分配给合适的执行器。
- 任务执行:执行器获取任务后,执行具体的任务逻辑。
- 执行结果回调:执行器执行完任务后,将结果回调给调度中心。
xxl-job核心代码结构:
- xxl-job-admin:调度中心,管理和调度任务。
- xxl-job-core:公共模块,包含任务调度相关的核心代码。
- xxl-job-executor-sample-spring:执行器示例,展示如何集成xxl-job到Spring项目中。
xxl-job实现原理
xxl-job工作原理
如图所示:
处理流程:
- 1)任务执行器根据配置的调度中心的地址,自动注册到调度中心。
- 2)达到任务触发条件,调度中心下发任务。
- 3)执行器基于线程池执行任务,并把执行结果放入内存队列中、把执行日志写入日志文件中。
- 4)执行器的回调线程消费内存队列中的执行结果,主动上报给调度中心。
- 5)当用户在调度中心查看任务日志,调度中心请求任务执行器,任务执行器读取任务日志文件并返回日志详情。
任务触发
调度中心在启动时会开启一个调度线程来计算任务触发时机,该调度线程通过查询表xxl_job_info中的数据(即:任务的基本信息和任务下一次执行的时间)来获取下一次执行时间 <= 当前时间 + 5s的任务。
注意:5s在XxlJob中固定,称为预读时间(即:提前获取当前时间+5s内的任务,保证任务能准时触发)。
示例:假设当前时间为2024-06-01 10:00:10,调度线程会查出下一次任务执行时间在2024-06-01 10:00:15之前的任务。
如图所示:
调度线程会将查询到的任务根据执行时间划分为三个部分:
- 1)当前时间已经超过任务下一次执行时间5s以上,即:需要在2024-06-01 10:00:05(不含05s)之前执行的任务。
- 2)当前时间已经超过任务下一次执行时间但不足5s,即:需要在2024-06-01 10:00:05和2024-06-01 10:00:10(不含10s)之间执行的任务
- 3)暂未到执行时间,但会在5s内执行的任务。
如图所示:
图中:
- 第一部分已经超时超过5s以上的任务会根据任务配置的调度过期策略来确定是否需要执行。调度过期策略:
- 忽略。
- 立即执行一次。
- 第二部分已经超时但在5s以上的任务会立即执行一次,如果判断任务下一次执行时间在5s内,则直接放到一个时间轮中,等待下一次触发执行。
- 第三部分即将触发的任务由于还未到执行时间,因此不会立马执行,直接放到时间轮中,等待触发执行。
以上批次任务处理完成后,调度线程会去重新计算每个任务的下一次触发时间,并更新xxl_job_info表的下一次执行时间。
调度中心可以以集群的形式存在,每个调度中心实例都存在调度线程,xxl-job通过数据库锁的方式保证任务在同一时间只会被其中的一个调度中心实例触发一次。
语句:
select * from xxl_job_lock where lock_name = 'schedule_lock' for update
任务触发整体流程,如图所示:
快慢线程池异步触发任务优化
当任务达到触发条件时,并不是由调度线程直接去触发执行器的任务执行,调度线程会将这个触发的任务交给线程池去执行(即:上图中触发任务执行是通过线程池异步执行的),原因:触发任务需要通过Http接口调用具体的执行器实例触发任务,这一过程相对耗时,如果通过调度线程触发任务,会影响调度效率。因此,调度线程只负责判断任务是否需要执行。
同时,xxl-Job为了进一步优化任务的触发,将这个触发任务执行的线程池划分成快线程池和慢线程池两个线程池。
在调用执行器的Http接口触发任务执行时,xxl-Job会记录每个任务触发所耗费的时间(注意不是任务执行时间,是整个Http请求耗时时间)。当任务一次触发任务的时间超过500ms时,这个任务的慢次数就会+1,如果这个任务一分钟内触发的慢次数超过10次,就会将触发任务交给慢线程池去执行。因此,快慢线程池是为了避免频繁触发且每次触发时间很长的任务阻塞其它任务的触发。
执行器实例选择
当任务需要触发时,调度中心会向执行器发送Http请求,执行器去执行具体的任务,由于一个执行器会存在多个实例,具体由哪个执行器执行与任务配置时设置的路由策略有关。
路由策略:
- 第一个。
- 最后一个。
- 轮询。
- 随机。
- 一致性Hash。
- 最不经常使用。
- 最近最久未使用。
- 故障转移。
- 忙碌转移。
- 分片广播。
执行器执行任务
当执行器接收到调度中心的Http请求时,会将请求交给ExecutorBizImpl进行处理,具体逻辑在ExecutorBizImpl的run()方法中实现。在run()方法中,执行器会为每个任务创建一个单独的JobThread线程,JobThread线程从任务队列(任务提交时会将任务加入任务队列中)中获取任务并执行。
如图所示:
如果调度中心选择的执行器实例正在处理定时任务,则需要根据任务配置时设置的阻塞处理策略进行处理。
阻塞处理策略:
- 单机串行:执行器按队列的先进先出原则串行处理。
- 丢弃后续调度:执行器不做处理,任务会被丢弃。
- 覆盖之前调度:执行器重新创建一个JobThread来执行任务,并且尝试打断之前的正在处理任务的JobThread,丢弃之前队列中的任务。
注意:阻塞处理策略针对的是单个执行器实例上的任务,不同执行器实例上的同一个任务互不影响。
任务执行结果回调
当任务处理完成后,执行器会将任务的执行结果发送给调度中心。
如图所示:
图中:
- JobThread会将任务的执行结果发送到一个内存队列(执行结果队列)中。
- 执行器启动时会开启一个发送任务执行结果的线程:TriggerCallbackThread,TriggerCallbackThread线程会不停地从内存队列中获取所有的执行结果,并将执行结果批量发送给调度中心。
- 调用中心收到任务执行结果后,会根据执行结果修改本次任务的执行状态及后续逻辑(如:是否失败重试,是否存在子任务需要触发等)。
xxl-job使用示例
以BEAN模式(方法形式)为例。下载xxl-job源码(版本:2.3.1)。
下载地址:https://gitee.com/xuxueli0323/xxl-job/tree/2.3.1/
编译源码:
解压源码,按照maven格式将源码导入IDE, 使用maven进行编译即可。
源码结构如下:
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-executor-sample-jfinal:JFinal版本,通过JFinal管理执行器;
:xxl-job-executor-sample-nutz:Nutz版本,通过Nutz管理执行器;
:xxl-job-executor-sample-jboot:jboot版本,通过jboot管理执行器;
初始化调度数据库
从源码中获取调度数据库脚本,脚本位置:/xxl-job/doc/db/tables_xxl_job.sql。
#
# XXL-JOB v2.3.1
# Copyright (c) 2015-present, xuxueli.
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;
SET NAMES utf8mb4;
CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
`schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
`misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
`address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' );
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
commit;
在数据库中执行以上脚本。
配置并部署调度中心
调度中心项目:xxl-job-admin
作用:统一管理任务调度平台上调度任务,负责触发调度执行,并且提供任务管理平台。
调度中心配置文件地址:
/xxl-job/xxl-job-admin/src/main/resources/application.properties
调度中心配置内容说明:
### 调度中心JDBC连接(配置为创建的调度数据库的地址)
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.jdbc.Driver
### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### 调度中心通讯TOKEN[选填]:非空时启用;
xxl.job.accessToken=
### 调度中心国际化配置[必填]:默认为"zh_CN"/中文简体, 可选范围为"zh_CN"/中文简体, "zh_TC"/中文繁体and"en"/英文;
xxl.job.i18n=zh_CN
## 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### 调度中心日志表数据保存天数[必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30
修改log-back.log文件中日志文件的地址。
<property name="log.path" value="/Users/wuzp/Desktop/learn/log/e-shop/log/xxl-job/xxl-job-admin.log"/>
调度中心配置完成,将项目编译打包部署。
调度中心访问地址:http://localhost:8080/xxl-job-admin (该地址将作为执行器的回调地址)。
调度中心默认账号:admin/123456。
编写执行器及任务
添加依赖
<!-- xxl-job-core -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.1</version>
</dependency>
添加配置
在application.yml文件中添加xxl-job配置信息:
# ----- xxl-job -----
xxl:
job:
admin:
# 调度中心部署跟地址[选填]:如调度中心集群部署存在多个地址则用逗号分隔,执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调",为空则关闭自动注册
addresses: http://127.0.0.1:8080/xxl-job-admin
# 执行器通讯TOKEN[选填]:非空时启用
accessToken: default_token
executor:
# 执行器AppName[选填]:执行器心跳注册分组依据,为空则关闭自动注册
appname: e-shop-user
# 执行器注册[选填]:优先使用该配置作为注册地址,为空时使用内嵌服务”IP:PORT“作为注册地址,从而更灵活的支持容器类型执行器动态IP和动态映射端口问题
address:
# 执行器IP[选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于"执行器注册"和"调度中心请求并触发任务"
ip:
# 执行器端口号[选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口
port: 9999
# 执行器运行日志文件存储磁盘路径[选填]:需要对该路径拥有读写权限,为空则使用默认路径
logpath: /Users/wuzp/Desktop/learn/log/e-shop/log/xxl-job/jobhandler
# 执行器日志文件保存天数[选填]:过期日志自动清理,限制值大于等于3时生效; 否则(如-1),关闭自动清理功能
logretentiondays: 30
添加注解
无
初始化配置
配置文件XxlJobConfig:
@Slf4j
@Configuration
public class XxlJobConfig {
@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() {
log.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;
}
}
编写执行器
@Slf4j
@Component
public class XxlJobExecutorHandler {
/**
* 简单任务示例(Bean模式)
* @param param admin调度界面的任务参数填写的值
* @return ReturnT
* @throws Exception 异常
*/
@XxlJob("demoJobHandler")
public ReturnT<String> demoJobHandler(String param) throws Exception {
log.info("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
log.info("执行demoJobHandler任务:{},任务参数param:{}", i, param);
TimeUnit.SECONDS.sleep(2);
}
return ReturnT.SUCCESS;
}
}
调度中心添加执行器:执行器管理->新增
调度中心添加任务:任务管理->新增
启动任务:操作->启动。