XXL-JOB教程
1. 简介
[!tip]
本文全文是按照JDK8,XXL-JOB2.x版本进行说明讲解。
1.1 概述
XXL-Job 是一个分布式任务调度平台,由许雪里(大众点评)开发并开源。它的核心设计目标是:
- 轻量级、易上手
- 支持分布式集群部署
- 提供可视化管理界面
- 支持弹性扩容与故障转移
参考资料:
1.2 为什么选择 XXL-Job
在 Java 生态中,常见的任务调度方案对比如下:
| 方案 | 优点 | 缺点 |
|---|---|---|
| Spring的Scheduled | 极简,无需引入额外组件 | 不支持分布式,无管理界面,重启丢失状态 |
| Quartz | 功能完善,支持持久化 | 集群配置复杂,无管理界面 |
| Elastic-Job | 支持分布式分片 | 依赖 ZooKeeper,运维成本高 |
| XXL-Job | 轻量、可视化、低侵入、开箱即用 | 功能相对没有 Elastic-Job 丰富 |
XXL-Job 的核心优势:
- 调度与执行分离,解耦彻底。
- 提供调度中心 Web 界面,任务管理直观。
- 支持 GLUE 模式(在线编辑脚本/代码)。
- 支持分片广播、并发执行、任务依赖。
- 任务执行失败自动重试与报警。
- 执行日志实时查看。
1.3 特性

1.4 环境说明
- Maven:3+。
- JDK:17+ (说明:版本3.x及以上要求JDK17+;版本2.x及以下支持JDK1.8)。
- Mysql:8.0+。
2. 核心概念与架构
2.1 整体架构


上图展示了 XXL-Job 的整体架构,接下来我们逐层拆解每一个核心概念。
XXL-Job 最核心的设计哲学是调度与执行彻底分离。这一点非常关键,理解了这个,整个架构就通了。
想象一下对比方案:用 Spring 的 @Scheduled,调度逻辑和业务逻辑写在同一个进程里。一旦业务服务重启,任务就停了;多部署几台机器,任务就会重复跑。XXL-Job 把"什么时候触发"和"具体干什么"拆成了两个独立角色,各司其职。
2.1 核心术语
2.1.1 调度中心(xxl-job-admin)
调度中心是一个独立部署的 Spring Boot 应用,它本身不执行任何业务逻辑,只负责三件事:
- 管理任务:任务配置存在 MySQL 里,包括 Cron 表达式、路由策略、超时时间、失败重试次数等,通过 Web 界面增删改查。
- 触发调度:内部有一个调度线程,每隔一段时间(默认预读 5 秒)从数据库扫描即将触发的任务,到时间后通过 HTTP 请求推送给执行器。
- 收集结果:执行器执行完任务后,会主动回调调度中心,上报执行结果(成功/失败)和日志,调度中心将其持久化到数据库,并在失败时触发报警邮件。
[!note]
调度中心主动推送任务给执行器,不是执行器来调度中心拉取。这是"推模型"设计,好处是调度中心能精确控制路由策略。
2.1.2 执行器(Executor)
执行器不是独立部署的服务,而是嵌入到你的业务 Java 应用里的一个组件(引入 xxl-job-core 依赖后,声明一个 XxlJobSpringExecutor Bean 即可)。
执行器启动后会做三件事:
- 注册:向调度中心发起 HTTP 注册请求,告知自己的 IP、端口、
AppName,此后每 30 秒心跳续约一次。 - 监听:在指定端口(默认 9999)启动一个内嵌 HTTP Server,等待调度中心的任务触发请求。
- 执行与回调:收到触发请求后,在本地线程池中找到对应的
JobHandler执行,执行完成后主动 HTTP 回调调度中心上报结果。
[!important]
一个业务应用(不管它部署了几个实例)对应一个
AppName,所有实例构成这个 AppName 下的执行器集群。

上图是一次完整调度的时序,可以看到五个关键步骤:扫描→触发→执行→回调→记录。
2.1.3 JobHandler(任务处理器)
JobHandler 是任务执行逻辑的最小单元,就是你用 @XxlJob("名字") 注解的那个方法。
需要理解几个关键点:
命名注册机制:@XxlJob("syncOrderHandler") 中的字符串是这个 Handler 在调度中心的唯一标识。执行器启动时,会扫描所有带该注解的方法,把它们注册到一张本地 Map 里,Key 是字符串名称,Value 是方法引用。调度中心发来触发请求时,携带 executor_handler 字段(就是这个字符串),执行器从 Map 里找到对应方法并调用。
线程隔离:每个 JobHandler 在执行时都运行在执行器内部的线程池里,与主业务线程完全隔离,互不干扰。
同一个执行器可以注册多个 JobHandler,它们可以对应调度中心里的不同任务,例如:
@XxlJob("syncOrderHandler") // 同步订单任务
public void syncOrder() { ... }
@XxlJob("clearCacheHandler") // 清理缓存任务
public void clearCache() { ... }
2.1.4 AppName(执行器标识)
AppName 是执行器的唯一业务身份,字符串类型,例如 order-service-job。
它有两个作用:
- 分组隔离:调度中心按 AppName 管理执行器,同一个 AppName 下的所有节点构成一个执行器集群。创建任务时,先选择哪个 AppName(执行器分组),再选择 JobHandler。
- 自动注册的依据:执行器向调度中心注册时,上报的就是
AppName + IP + Port,调度中心以此维护每个 AppName 下的在线节点列表。
[!tip]
实际项目里,建议每个业务服务用一个独立的 AppName,比如
user-service-job、order-service-job,便于在调度中心区分管理。
2.1.5 路由策略
当一个 AppName 下有多个执行器节点时,调度中心要决定把任务发给哪一个,这就是路由策略。

路由策略共有10种,实际常用的主要就是上图中的4种:
- 轮询:任务依次发给 A、B、C、A、B、C……简单均衡,适合无状态的普通任务。
- 故障转移:调度中心在发送前先做心跳检测,选第一个心跳成功的节点。某节点挂掉时自动跳过,是高可用场景的首选。
- 一致性 Hash:对
jobId做 Hash 运算,保证同一个任务永远打到同一个节点。适合有状态任务,比如任务需要读取本地缓存文件,换节点会找不到。 - 分片广播:调度中心同时向所有节点发送触发请求,每个节点收到的参数里包含自己的分片序号,自己决定处理哪部分数据,是大数据量并行处理的利器。
| 策略名称 | 说明 |
|---|---|
| FIRST(第一个) | 固定选择第一个机器 |
| LAST(最后一个) | 固定选择最后一个机器 |
| ROUND(轮询) | 任务依次发给不同的执行器节点执行 |
| RANDOM(随机) | 随机选择在线的机器 |
| CONSISTENT_HASH(一致性HASH) | 每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上 |
| LEAST_FREQUENTLY_USED(最不经常使用) | 使用频率最低的机器优先被选举 |
| LEAST_RECENTLY_USED(最近最久未使用) | 最久未使用的机器优先被选举 |
| FAILOVER(故障转移) | 按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度 |
| BUSYOVER(忙碌转移) | 按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度 |
| SHARDING_BROADCAST(分片广播) | 广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务 |
2.1.6 阻塞处理策略
调度中心按 Cron 触发,它不关心上一次任务有没有执行完。如果执行器还在忙,下一次触发到来时该怎么办?这就是阻塞处理策略。
也就是当调度过于密集执行器来不及处理时的处理策略。
阻塞处理策略有如下几种:
- 单机串行(默认):新的触发请求排队等待,等当前执行完再处理下一个(FIFO队列)。适合绝大多数场景,数据不会丢失但可能积压。
- 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败。适合对数据实时性要求不高、宁可少跑也不能并发的场景。
- 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务,执行最新这次触发。适合"只关心最新状态"的场景,比如刷新配置缓存。
2.1.7 BEAN模式
2.1.7.1 方法形式
BEAN 模式的方法形式本质是:把一个普通的 Spring Bean 方法,通过 @XxlJob 注解声明为可被调度中心调用的任务处理器。"BEAN" 这个名字的来源,就是这个 Handler 必须是 Spring 容器管理的 Bean(加了 @Component、@Service 等注解),框架通过 Spring 上下文来查找和调用它。
先看整体的内部机制,再逐层拆解:

从上图可以看到整个执行器内部的四个核心部件:Handler 注册表、任务执行线程池、内嵌 HTTP Server,以及你写的业务 Bean。接下来逐层讲清楚。
对于BEAN模式的方法形式,主要是@XxlJob 注解在驱动,这是 BEAN 模式的入口,也是最需要理解透彻的部分。
注解定义:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface XxlJob {
// Handler 名称,全局唯一,调度中心用它来找到对应方法
String value();
// 执行器初始化时的回调方法名(可选)
String init() default "";
// 执行器销毁时的回调方法名(可选)
String destroy() default "";
}
@XxlJob 只能加在方法上(@Target(ElementType.METHOD)),且方法所在的类必须是 Spring Bean。
@XxlJob 注解支持两个可选参数,声明 Handler 的初始化和销毁回调:
@Component
public class ResourceJobHandler {
private SomeResource resource;
/**
* 声明 init 和 destroy 方法
* init:执行器启动时,首次使用该 Handler 之前调用一次
* destroy:执行器关闭时调用
*/
@XxlJob(value = "resourceJobHandler", init = "initResource", destroy = "destroyResource")
public void resourceJobHandler() throws Exception {
XxlJobHelper.log("任务执行,使用已初始化的资源:{}", resource);
// 正常业务逻辑
}
/**
* 初始化方法:签名必须是 public void xxx(),无参数
* 常见用途:预热连接池、加载静态数据、初始化第三方 SDK
*/
public void initResource() {
log.info("初始化资源...");
this.resource = new SomeResource();
this.resource.connect();
}
/**
* 销毁方法:签名同上
* 常见用途:关闭连接、释放文件句柄、清理临时数据
*/
public void destroyResource() {
log.info("销毁资源...");
if (resource != null) {
resource.close();
}
}
}
这两个回调的实际使用场景不多,大多数任务直接在 Spring Bean 的 @PostConstruct / @PreDestroy 里管理资源即可。init/destroy 更多用于对 Handler 自身隔离资源生命周期的场景。
注册时机与过程:应用启动时,XxlJobSpringExecutor 实现了 SmartInitializingSingleton 接口,会在所有 Spring Bean 初始化完毕之后,自动扫描整个 Spring 上下文,找出所有标注了 @XxlJob 的方法,把它们包装成 MethodJobHandler 对象,存入一个全局 ConcurrentHashMap:
// 内部实现简化示意(非源码原文)
// Key = @XxlJob("value") 中的字符串
// Value = MethodJobHandler(封装了 Bean 实例 + Method 反射对象)
Map<String, IJobHandler> jobHandlerRepository = new ConcurrentHashMap<>();
注册过程用反射来调用:发现一个带 @XxlJob("syncOrder") 的方法时,框架存储的是 bean 对象引用 + method 对象,触发时调用 method.invoke(bean) 来执行。这意味着:
- 方法的 Bean 实例是单例(Spring 默认),每次触发复用同一个实例。
- 方法本身需要是
public,且返回类型为void(2.x 版本)。 - 方法可以声明
throws Exception,框架会捕获异常并标记失败。
完整的Handler写法:
package com.example.job;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 订单相关的定时任务集合
*
* 最佳实践:一个业务域的任务放在同一个 Handler 类里,方便管理
* 不需要每个 Handler 都单独一个类
*/
@Component // 必须是 Spring Bean!缺少这个注解,@XxlJob 不会生效
public class OrderJobHandler {
private static final Logger log = LoggerFactory.getLogger(OrderJobHandler.class);
// 可以注入任何 Spring Bean
// @Autowired
// private OrderService orderService;
/**
* 任务1:同步订单状态
* value = Handler 名称,必须与调度中心配置的 JobHandler 字段完全一致(区分大小写)
*/
@XxlJob("syncOrderHandler")
public void syncOrderHandler() throws Exception {
XxlJobHelper.log("syncOrderHandler 开始执行");
// 业务逻辑...
XxlJobHelper.log("syncOrderHandler 执行完成");
}
/**
* 任务2:清理过期订单
*/
@XxlJob("cleanExpiredOrderHandler")
public void cleanExpiredOrderHandler() throws Exception {
XxlJobHelper.log("cleanExpiredOrderHandler 开始执行");
// 业务逻辑...
}
}
这里有一个特殊的类需要简单介绍一下:XxlJobHelper(任务执行上下文)。
XxlJobHelper 是任务执行时的核心工具类,通过 ThreadLocal 与当前执行线程绑定,提供参数获取、日志记录、结果上报等功能。每次任务触发,框架都会在执行前把本次调度的上下文信息塞进去。
下面把它的每个方法都讲清楚:
-
获取任务参数
@XxlJob("paramDemoHandler") public void paramDemoHandler() throws Exception { // 获取调度中心在"任务参数"字段填写的字符串 // 如果没有配置参数,返回空字符串 "",不是 null String jobParam = XxlJobHelper.getJobParam(); XxlJobHelper.log("收到参数:{}", jobParam); // ---- 示例1:简单 key=value 格式 ---- // 调度中心填写:type=daily if ("type=daily".equals(jobParam)) { processDailyData(); } // ---- 示例2:JSON 格式(推荐,更灵活)---- // 调度中心填写:{"type":"daily","limit":500,"date":"2024-01-15"} if (jobParam != null && jobParam.startsWith("{")) { // 用 Jackson 或 FastJSON 解析 // ObjectMapper mapper = new ObjectMapper(); // Map<String, Object> params = mapper.readValue(jobParam, Map.class); // String type = (String) params.get("type"); // int limit = (int) params.get("limit"); } // ---- 示例3:普通字符串 ---- } -
日志记录(重要)
XxlJobHelper.log()写入的内容,会在调度中心"调度日志 → 执行日志"界面实时展示,这是任务排查问题的第一手资料。@XxlJob("logDemoHandler") public void logDemoHandler() throws Exception { // 支持 {} 占位符,语法与 Slf4j 一致 XxlJobHelper.log("任务开始,时间:{}", System.currentTimeMillis()); XxlJobHelper.log("当前参数:{}", XxlJobHelper.getJobParam()); // ⚠️ 注意:XxlJobHelper.log() 的内容只出现在调度中心日志界面 // 普通的 log.info() / log.error() 只写到应用本地日志文件 // 两者互不影响,建议关键节点两处都记 log.info("[syncOrderHandler] 任务开始"); XxlJobHelper.log("[syncOrderHandler] 任务开始"); // 每次 XxlJobHelper.log() 都是追加,不是覆盖 // 最终在界面上会看到本次执行的完整日志流 for (int i = 0; i < 5; i++) { XxlJobHelper.log("处理第 {} 批数据", i + 1); Thread.sleep(200); // 模拟处理耗时 } } -
获取分片参数
分片广播模式下,每个执行器节点都会收到触发,但参数不同:
@XxlJob("shardingDemoHandler") public void shardingDemoHandler() throws Exception { // 当前节点是第几片(从 0 开始) int shardIndex = XxlJobHelper.getShardIndex(); // 总共几个节点参与 int shardTotal = XxlJobHelper.getShardTotal(); XxlJobHelper.log("分片参数:{}/{}", shardIndex, shardTotal); // 例如 3 个节点时: // 节点A收到 0/3,节点B收到 1/3,节点C收到 2/3 } -
上报执行结果
@XxlJob("resultDemoHandler") public void resultDemoHandler() throws Exception { try { int processedCount = doBusinessLogic(); // 明确标记成功,并附上说明信息 // 这条信息会显示在调度中心日志的"执行备注"列 XxlJobHelper.handleSuccess("处理完成,共 " + processedCount + " 条记录"); } catch (BusinessException e) { // 明确标记失败,并附上失败原因 // 触发失败重试和报警邮件 XxlJobHelper.handleFail("业务异常:" + e.getMessage()); } // ⚠️ 如果既没调 handleSuccess 也没调 handleFail: // - 方法正常执行完毕(无异常)→ 框架自动标记为成功 // - 方法抛出未捕获的异常 → 框架自动标记为失败 }

流程中有几个细节值得单独解释:
线程池分 Fast/Slow 两个:执行器内部维护两个线程池。任务触发时默认进 Fast pool,1分钟窗口期内任务耗时达500ms超过10次,该窗口期内判定为慢任务,慢任务自动降级进入”Slow”线程池,避免耗尽调度线程,提高系统稳定性。如果上次触发时该任务消耗的时间超过阈值(默认 500ms),下次触发会被打入 Slow pool,防止慢任务拖垮快任务的执行能力。
ThreadLocal 绑定上下文:每次触发都会创建一个 XxlJobContext 对象,塞入当前线程的 ThreadLocal,所以在 Handler 方法里调用 XxlJobHelper.getJobParam() 和 XxlJobHelper.log() 都是线程安全的,不同任务的日志不会串台。
最后把代码和调度中心的配置对应关系总结成一张图:

一张图把代码和配置的对应关系说清楚了:appname 对应调度中心的执行器分组,@XxlJob("name") 里的字符串对应任务表单里的 JobHandler 字段,是唯一绑定关系。
BEAN 模式的核心可以用四句话概括:
第一,@Component 是前提,没有 Spring 管理的 Bean,@XxlJob 什么都不是。
第二,@XxlJob("name") 里的字符串是任务的唯一 ID,代码里和调度中心里必须完全一致。
第三,XxlJobHelper 是与调度系统通信的唯一入口,通过 ThreadLocal 保证线程安全,在 Handler 里获取参数、写日志、上报结果全靠它。
第四,方法正常返回 = 成功,抛出异常 = 失败,也可以用 handleSuccess/handleFail 显式控制,失败会触发调度中心的重试和报警。
2.1.7.2 类形式
Bean模式任务,还支持基于类的开发方式,每个任务对应一个Java类。
- 优点:不限制项目环境,兼容性好。即使是无框架项目,如main方法直接启动的项目也可以提供支持,可以参考示例项目 “xxl-job-executor-sample-frameless”;
- 缺点:每个任务需要占用一个Java类,造成类的浪费;不支持自动扫描任务并注入到执行器容器,需要手动注入。
开发步骤:
-
继承
IJobHandler抽象类并重写execute()方法。 -
手动通过如下方式注入到执行器容器。
XxlJobExecutor.registJobHandler("demoJobHandler", new DemoJobHandler());
这是 XXL-Job 1.x 的老写法,2.x 依然兼容保留。
2.1.7.2 两种方式对比
| 对比维度 | 方法形式(2.x 推荐) | 类形式(1.x 遗留) |
|---|---|---|
| 核心注解 | @XxlJob("handlerName") |
@JobHandler("handlerName") |
| 注解位置 | 加在方法上 | 加在类上 |
| 是否需要继承 | 不需要 | 必须 extends IJobHandler |
| 是否需要实现方法 | 不需要 | 必须重写 execute() |
| 一个类多个 Handler | 支持,写多个方法即可 | 不支持,一个类只能是一个 Handler |
| init / destroy 回调 | 支持,通过注解参数声明 | 支持,重写父类 init() / destroy() |
| 方法签名要求 | public void xxx() throws Exception |
public void execute() throws Exception(固定) |
| 获取参数方式 | XxlJobHelper.getJobParam() |
XxlJobHelper.getJobParam()(相同) |
| 写日志方式 | XxlJobHelper.log() |
XxlJobHelper.log()(相同) |
| 上报结果方式 | XxlJobHelper.handleSuccess/Fail() |
XxlJobHelper.handleSuccess/Fail()(相同) |
| 框架侵入性 | 低,业务类无需继承框架类 | 高,业务类强依赖框架基类 |
| 代码聚合度 | 高,同一业务域的任务放一个类 | 低,每个 Handler 单独一个类 |
| 2.x 推荐程度 | 推荐,官方首选 | 兼容保留,不建议新项目使用 |
| 适用场景 | 所有新项目 | 从 1.x 迁移的老项目 |
3. 环境准备
在正式安装之前,先明确 XXL-Job 的部署涉及哪些角色,每个角色需要什么环境:
| 角色 | 说明 | 部署位置 |
|---|---|---|
| 调度中心(xxl-job-admin) | 独立 Spring Boot 应用 | 独立服务器 / Docker |
| MySQL 数据库 | 存储任务配置、执行日志 | 数据库服务器 |
| 执行器(业务应用) | 嵌入你的 Spring Boot 项目 | 业务服务器 |
3.1 服务器配置要求
3.1.1 调度中心服务器
| 配置项 | 最低配置(开发/测试) | 推荐配置(生产) |
|---|---|---|
| CPU | 1 核 | 2 核及以上 |
| 内存 | 1 GB | 4 GB 及以上 |
| 磁盘 | 20 GB | 50 GB 及以上(日志存储) |
| 操作系统 | CentOS 7+ / Ubuntu 18.04+ | CentOS 7+ / Ubuntu 20.04+ |
| 网络 | 与执行器、MySQL 网络互通 | 内网专线,低延迟 |
[!tip]
调度中心本身很轻量,主要消耗在调度线程和日志写入,生产环境 2 核 4G 足以支撑数百个任务的调度。
3.1.2 MySQL 服务器
| 配置项 | 最低配置 | 推荐配置 |
|---|---|---|
| CPU | 2 核 | 4 核及以上 |
| 内存 | 2 GB | 8 GB 及以上 |
| 磁盘 | 50 GB | 200 GB 及以上 |
| MySQL 版本 | 5.7 | 5.7 / 8.0 |
3.1.3 执行器服务器(业务服务器)
执行器是嵌入在你的业务应用里的,不需要额外服务器,和业务应用共享资源。唯一要求是:执行器所在服务器的 IP 和端口(默认 9999)必须对调度中心网络可达。
3.2 软件环境要求
- XXL-JOB2.x版本,我这里使用2.5.0版本,这是2.x的最后一个版本。
- JDK1.8,如果你是JDK1.8,那么你只能使用XXL-JOB2.x版本,如果你是JDK17,那么你就可以使用XXL-JOB3.x版本。
- Maven3.x+
- MySQL5.7/8.0,如果你是XXL-JOB3.x版本,最好使用8.0。
3.3 其他注意事项
3.3.1 服务器时区需统一
XXL-Job 是基于时间调度的系统,如果调度中心服务器、执行器服务器、MySQL 三者时区不一致,会导致任务在错误的时间触发,是非常隐蔽的 bug。
查看服务器时区:
timedatectl

如果服务器时区不是你想要的,比如不是Asia/Shanghai,那么你可以修改为你想要的时区:
timedatectl set-timezone Asia/Shanghai
之后验证时区:
timedatectl
date
同时,你的JVM时区也要调整为和服务器一致。你可以在启动 Java 应用时,建议显式指定时区参数,避免读取系统时区出错:
java -Duser.timezone=Asia/Shanghai -jar your-app.jar
或者在 Spring Boot 应用的启动类中强制设置:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
// 强制设置 JVM 时区,放在最前面
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
SpringApplication.run(Application.class, args);
}
}
最后就是数据库的时区,你也要保持一致:
-- 查看 MySQL 当前时区
SHOW VARIABLES LIKE '%time_zone%';
-- 输出:
-- system_time_zone | CST
-- time_zone | +08:00
-- 如果 time_zone 不是 +08:00,在 my.cnf 中设置:
-- default-time-zone='+8:00'
3.3.2 端口与防火墙放行
XXL-Job 涉及以下几个端口需要放通:
| 端口 | 用途 | 需要放通的方向 |
|---|---|---|
| 8080 | 调度中心 Web 界面及 API | 浏览器 → 调度中心;执行器 → 调度中心 |
| 3306 | MySQL 数据库 | 调度中心 → MySQL |
| 9999 | 执行器内嵌 HTTP Server | 调度中心 → 执行器 |
[!tip]
上述端口都是默认端口,你可以根据你自己设置的实际情况进行放行。
当然,如果你的服务都是在内网中访问,那么你不用再单独放行上述端口了。
如果你的服务要求公网可以访问,那么你还得放行防火墙:
-
CentOS系统放行
# 放通调度中心端口 firewall-cmd --permanent --add-port=8080/tcp # 放通执行器端口 firewall-cmd --permanent --add-port=9999/tcp # 重新加载防火墙规则 firewall-cmd --reload # 查看已放通的端口 firewall-cmd --list-ports -
Ubuntu系统放行
ufw allow 8080/tcp ufw allow 9999/tcp ufw reload ufw status
[!warning]
阿里云 / 腾讯云等云服务器还需要在控制台安全组中额外放通端口,仅靠系统防火墙不够。
4. 调度中心部署
调度中心是 XXL-Job 的大脑,部署方式有两种:源码编译部署、Docker(Docker Compose)部署。
无论哪种部署方式,我都建议先把源码克隆下来,因为里面有数据库 SQL 文件和配置文件模板。你可以先了解里面的数据库表以及配置参数。
这里我是直接下载2.5.0版本:

将源码下载下来,解压后打开大致目录结构如下:
xxl-job/
├── doc/
│ └── db/
│ └── tables_xxl_job.sql # 数据库建表 SQL(第三章已用到)
├── xxl-job-admin/ # 调度中心模块(我们要部署的)
│ └── src/main/resources/
│ └── application.properties # 核心配置文件
├── xxl-job-core/ # 核心依赖包(执行器引入的 SDK)
├── xxl-job-executor-samples/ # 官方示例执行器(学习参考用)
│ ├── xxl-job-executor-sample-springboot/
│ └── xxl-job-executor-sample-frameless/
└── pom.xml

4.1 源码编译部署
4.1.1 修改调度中心配置文件
### web
# 调度中心启动端口
server.port=8080
# WEBUI界面访问路径前缀(不建议修改)
server.servlet.context-path=/xxl-job-admin
### actuator
management.server.base-path=/actuator
management.health.mail.enabled=false
### resources
spring.mvc.servlet.load-on-startup=0
spring.mvc.static-path-pattern=/static/**
spring.web.resources.static-locations=classpath:/static/
### freemarker
spring.freemarker.templateLoaderPath=classpath:/templates/
spring.freemarker.suffix=.ftl
spring.freemarker.charset=UTF-8
spring.freemarker.request-context-attribute=request
spring.freemarker.settings.number_format=0.##########
spring.freemarker.settings.new_builtin_class_resolver=safer
### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
### 数据库连接池配置
# 连接池类型,使用 HikariCP(Spring Boot 默认,性能最好)
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=1000
### xxl-job, datasource
# 调度中心数据库配置,这里替换为你自己实际的数据库信息
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.cj.jdbc.Driver
### xxl-job, email
# 邮件相关配置,告警时可以用
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.from=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
### xxl-job, access token
# 调度中心与执行器通讯的令牌
# 执行器配置中的 accessToken 必须与这里完全一致
# 建议生产环境设置一个复杂的随机字符串
xxl.job.accessToken=default_token
### xxl-job, access token
xxl.job.timeout=3
### xxl-job, i18n (default is zh_CN, and you can choose "zh_CN", "zh_TC" and "en")
# 国际化,zh_CN=简体中文,en=英文,默认是简体中文
xxl.job.i18n=zh_CN
## xxl-job, triggerpool max size
# fast pool:处理响应时间短的任务
xxl.job.triggerpool.fast.max=200
# slow pool:处理响应时间长(超过 500ms)的任务
xxl.job.triggerpool.slow.max=100
### xxl-job, log retention days
# 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=30
将上述调度中心配置文件修改为符合你自己环境的数据。比如里面的数据库连接信息,线程池的配置,邮件的配置等。
之后,将数据库初始化文件导入你的数据库中。
然后将打包好的jar包上传到服务器中,通过jar命令启动程序:
nohup java \
-Duser.timezone=Asia/Shanghai \
-Xms512m \
-Xmx512m \
-Xss512k \
-XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/software/xxl_job/v2.5.0/logs/heap-dump.hprof \
-jar /data/software/xxl_job/v2.5.0/xxl-job-admin-2.5.0.jar \
> /data/software/xxl_job/v2.5.0/logs/admin.log 2>&1 &
当你的程序成功启动之后,你可以访问调度中心访问地址:
http://IP:端口/xxl-job-admin
这个地址后续你的执行器将会使用到,作为回调地址。

默认账号密码如下:
- 账号:admin
- 密码:123456
登录后运行界面如下图所示。

至此“调度中心”项目已经部署成功。
这里通过源码编译部署其实是有一个缺陷的,如果你的程序异常退出了,或者服务器重启了,那么你的调度中心并不会立刻重启。所以,生产环境必须配置开机自启,防止服务器重启后调度中心没有自动拉起。
# 创建 systemd 服务文件
vim /etc/systemd/system/xxl-job-admin.service
文件内容:
[Unit]
Description=XXL-Job Admin Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/data/software/xxl_job/v2.5.0
ExecStart=/usr/bin/java \
-Duser.timezone=Asia/Shanghai \
-Xms512m \
-Xmx512m \
-Xss512k \
-XX:+UseG1GC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/data/software/xxl_job/v2.5.0/logs/heap-dump.hprof \
-jar /data/software/xxl_job/v2.5.0/xxl-job-admin-2.5.0.jar
# 日志输出(替代 nohup 重定向)
StandardOutput=append:/data/software/xxl_job/v2.5.0/logs/admin.log
StandardError=append:/data/software/xxl_job/v2.5.0/logs/admin.log
# 出现异常 5秒自动重启
Restart=always
RestartSec=5
# 限制资源
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
# 重新加载 systemd 配置
systemctl daemon-reload
# 启动服务
systemctl start xxl-job-admin
# 设置开机自启
systemctl enable xxl-job-admin
# 查看运行状态
systemctl status xxl-job-admin
# 查看实时日志
journalctl -u xxl-job-admin -f
4.2 Docker部署(推荐生产使用)
调度中心还支持Docker部署,Docker部署的好处如下:
- 环境隔离。
- 一键启动所有服务器,比如mysql,jar。
- 异常自动重启。
这里使用Docker方式部署时,我推荐使用Docker Compose的方式来部署,这样更好维护:
services:
xxl-job-admin:
# 使用官方镜像,版本号与调度中心保持一致
image: xuxueli/xxl-job-admin:2.5.0
# 容器名称,docker ps / docker logs 时用这个名字识别
container_name: xxl-job-admin
# 退出后自动重启:always = 无论何种原因退出都重启(包括 docker restart 宿主机)
restart: always
ports:
# 端口映射格式:宿主机端口:容器内端口
# 左边 8250 是外部访问端口,右边 8250 是容器内 server.port 对应的端口,两者必须一致
- "8250:8250"
extra_hosts:
# Linux 环境下,让容器内能通过 host.docker.internal 域名访问宿主机
# host-gateway 是 Docker 自动解析宿主机网关 IP 的关键字,Docker 20.10+ 支持
# Mac / Windows 的 Docker Desktop 不需要这一行,已内置支持
- "host.docker.internal:host-gateway"
environment:
# 容器时区,必须与宿主机、MySQL 保持一致,否则 Cron 触发时间会错乱
TZ: Asia/Shanghai
# JVM 启动参数,通过 JAVA_OPTS 环境变量传入容器内的启动脚本
JAVA_OPTS: >-
-Xms256m
-Xmx256m
-XX:+UseG1GC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/applogs/xxl-job/heap-dump.hprof
# XXL-Job 调度中心的 Spring Boot 配置参数
# 以 -- 开头的参数会覆盖 application.properties 中的同名配置
PARAMS: >-
--server.port=8250
--server.servlet.context-path=/xxl-job-admin
--spring.datasource.url=jdbc:mysql://host.docker.internal:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
--spring.datasource.username=${MYSQL_USERNAME}
--spring.datasource.password=${MYSQL_PASSWORD}
--xxl.job.accessToken=${XXL_JOB_ACCESS_TOKEN}
--xxl.job.i18n=zh_CN
--xxl.job.triggerpool.fast.max=200
--xxl.job.triggerpool.slow.max=100
--xxl.job.logretentiondays=30
--logging.file.path=/data/applogs/xxl-job
--logging.file.name=/data/applogs/xxl-job/xxl-job-admin.log
--logging.level.root=INFO
volumes:
# 挂载日志目录:宿主机路径:容器内路径
# 容器删除或重建后,日志文件依然保留在宿主机上
- /data/www/dk_project/dk_app/dk_XxlJob/logs:/data/applogs/xxl-job
healthcheck:
# 健康检查命令:用 curl 请求调度中心的健康端点
# -f 参数表示 HTTP 响应码非 2xx 时直接返回失败
test: ["CMD", "curl", "-f", "http://localhost:8250/xxl-job-admin/actuator/health"]
# 每隔 30 秒检查一次
interval: 30s
# 单次检查超过 10 秒未响应则判定为失败
timeout: 10s
# 连续失败 3 次才将容器标记为 unhealthy
retries: 3
# 容器启动后等待 60 秒再开始第一次健康检查(留出 Spring Boot 启动时间)
start_period: 60s
[!caution]
- 这里数据库我是直接连接我宿主机的数据库,所以这里采用的是
host.docker.internal方案。- 这里的敏感信息我都放到了
.env文件中去了。- 如果你是用的现成的数据库,那么你需要放出数据库的IP白名单。否则容器是连接不上现成的数据库的。
现在Docker部署的目录如图:

5. 任务模式详解

5.1 基础配置
- 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器,实现任务自动发现功能;;另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器。可在 "执行器管理" 进行设置。
- 任务描述:任务的描述信息,便于任务管理。
- 负责人:任务的负责人。
- 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔。
5.2 调度配置
- 调度类型:
- 无:该类型不会主动触发调度。
- CRON:该类型将会通过CRON,触发任务调度。
- 固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发。
- CRON:触发任务执行的Cron表达式。
- 固定速度:固定速度的时间间隔,单位为秒。
调度类型分别如下图所示:



5.3 任务配置
XXL-Job 提供了多种任务执行模式,适应不同的业务场景。主要分为两大类:BEAN 模式 和 GLUE 模式。

-
BEAN模式流程图如下:

-
GLUE模式流程图如下:

两种模式对比选型对比如图:

5.3.1 配置属性说明
在新建调度任务时,"运行模式"是核心配置项,决定了任务代码的组织和执行方式。下拉可选项如下:

| 运行模式 | 说明 |
|---|---|
| BEAN | 任务以 Spring Bean 方式维护在执行器项目中 |
| GLUE(Java) | 任务以源码方式维护在调度中心,动态编译运行(Java) |
| GLUE(Shell) | 任务以 Shell 脚本方式维护在调度中心 |
| GLUE(Python) | 任务以 Python 脚本方式维护在调度中心 |
| GLUE(PHP) | 任务以 PHP 脚本方式维护在调度中心 |
| GLUE(Nodejs) | 任务以 Node.js 脚本方式维护在调度中心 |
| GLUE(PowerShell) | 任务以 PowerShell 脚本方式维护在调度中心 |
JobHandler:运行模式为 "BEAN模式" 时生效,对应执行器中新开发的JobHandler类“@XxlJob”注解自定义的value值。
执行参数:任务执行所需的参数。
5.3.2 BEAN 模式(类形式)
BEAN 模式是最常用的任务开发方式,任务逻辑以 Spring Bean 的形式存在于执行器项目中,由调度中心触发执行。
5.3.2.1 步骤一:执行器项目中,开发 Job 类
在执行器项目中,新建一个 Java 类,继承 IJobHandler,并使用 @XxlJob 注解进行标注。
import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.core.context.XxlJobHelper;
import org.springframework.stereotype.Component;
@Component
public class SampleXxlJob {
/**
* 1. 简单任务示例(Bean模式)
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobHelper.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
// default success
}
}
[!caution]
- 类需要被 Spring 容器管理(加
@Component)。@XxlJob("demoJobHandler")中的 value 就是任务的 JobHandler 名称,需与调度中心配置保持一致。- 使用
XxlJobHelper.log()打印日志,日志可在调度中心执行日志中查看。- 任务执行成功默认 return,失败可调用
XxlJobHelper.handleFail("失败原因")。
5.3.2.2 步骤二:调度中心,新建调度任务
在 XXL-Job 调度中心后台,新建任务时填写如下关键配置:
| 配置项 | 说明 |
|---|---|
| 执行器 | 选择对应的执行器 AppName |
| 任务描述 | 任务的说明信息 |
| 运行模式 | 选择 BEAN |
| JobHandler | 填写 @XxlJob 注解中定义的 value,如 demoJobHandler |
| Cron | 配置触发时间,如 0 0 1 * * ? 每天凌晨1点执行 |
| 任务参数 | 可选,执行时传入的参数,在代码中通过 XxlJobHelper.getJobParam() 获取 |
5.3.3 BEAN 模式(方法形式)
方法形式是 BEAN 模式的另一种用法,无需继承基类,直接在任意 Spring Bean 的方法上使用 @XxlJob 注解即可,是推荐的开发方式。
5.3.3.1 步骤一:执行器项目中,开发 Job 方法
import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.core.context.XxlJobHelper;
import org.springframework.stereotype.Component;
@Component
public class MyJobService {
/**
* 方法形式的 Bean 任务
*/
@XxlJob("myJobHandler")
public void myJobHandler() {
// 获取任务参数
String param = XxlJobHelper.getJobParam();
XxlJobHelper.log("任务参数:" + param);
// 业务逻辑
doSomeBusiness();
XxlJobHelper.log("任务执行完成");
}
private void doSomeBusiness() {
// 具体业务逻辑
}
}
[!note]
方法形式与类形式的对比:
- 类形式需要继承
IJobHandler,方法形式不需要。- 方法形式更灵活,可以在任意 Bean 中定义,便于与业务 Service 集成。
- 推荐使用方法形式。
5.3.3.2 步骤二:调度中心,新建调度任务
配置方式与 3.1 相同,运行模式选 BEAN,JobHandler 填写注解中的 value。
5.3.3.3 原生内置 Bean 模式任务(通用执行器)
XXL-Job 执行器默认内置了若干通用任务处理器,无需开发即可使用:
| JobHandler | 说明 |
|---|---|
demoJobHandler |
示例任务,打印 Hello World |
shardingJobHandler |
分片广播任务示例 |
httpJobHandler |
通用 HTTP 任务,通过任务参数传入 URL 发起 HTTP 请求 |
commandJobHandler |
通用命令行任务,执行本机命令 |
以 httpJobHandler 为例,任务参数格式如下:
url: http://www.xxx.com/test
method: get
data: version=1&name=xuxueli
5.3.4 GLUE 模式(Java)
GLUE 模式下,任务源码直接维护在调度中心。该模式的任务实际上是一段继承自IJobHandler的Java类代码并以 "groovy" 源码方式维护,它在执行器项目中运行,可使用@Resource/@Autowire注入执行器中的其他服务;支持在线编辑、动态编译、实时生效,无需重启执行器。
5.3.4.1 步骤一:调度中心,新建调度任务
| 配置项 | 说明 |
|---|---|
| 运行模式 | 选择 GLUE(Java) |
| JobHandler | 无需填写(GLUE 模式不依赖执行器中的 Bean) |
| 其他配置 | 同 BEAN 模式 |

5.3.4.2 步骤二:开发任务代码
在调度中心任务列表,点击该任务的 GLUE IDE 按钮:

进入在线代码编辑器,编写 Java 代码:

package com.xxl.job.service.handler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
@Override
public void execute() throws Exception {
XxlJobHelper.log("GLUE(Java) 任务开始执行");
// 编写业务逻辑
String param = XxlJobHelper.getJobParam();
XxlJobHelper.log("接收到参数:" + param);
XxlJobHelper.log("GLUE(Java) 任务执行完成");
}
}
[!note]
GLUE(Java) 特点:
- 代码托管在调度中心数据库,修改后立即生效,无需重启
- 支持 30 个版本的历史回溯
- 适合需要频繁修改逻辑的任务
- 执行器需要能访问调度中心(拉取最新代码)
5.3.5 GLUE 模式(Shell)
Shell 模式下,任务以 Shell 脚本形式维护在调度中心,适合运维类操作、文件处理、服务器脚本等场景。
5.3.5.1 步骤一:调度中心,新建调度任务
| 配置项 | 说明 |
|---|---|
| 运行模式 | 选择 GLUE(Shell) |
| 执行器 | 选择部署在 Linux/Mac 服务器上的执行器 |

5.3.4.2 步骤二:开发任务代码
在 GLUE IDE 中编写 Shell 脚本:
#!/bin/bash
echo "GLUE(Shell) 任务开始"
# 获取任务参数(通过环境变量 XXL_JOB_PARAM 传入)
echo "任务参数:$1"
# 业务逻辑
DATE=$(date +%Y%m%d)
echo "当前日期:$DATE"
# 示例:清理7天前的日志
# find /data/logs -mtime +7 -name "*.log" -exec rm -rf {} \;
echo "GLUE(Shell) 任务结束"
# 脚本退出码 0 表示成功,非 0 表示失败
exit 0

[!caution]
脚本的退出码(exit code)决定任务成败,
exit 0为成功,exit 1等非零值为失败。
5.3.6 GLUE 模式(Python)
Python 模式下,任务以 Python 脚本形式维护在调度中心,要求执行器所在服务器已安装 Python 环境。
5.3.6.1 步骤一:调度中心,新建调度任务
| 配置项 | 说明 |
|---|---|
| 运行模式 | 选择 GLUE(Python) |
| 执行器 | 选择安装了 Python 的执行器 |

5.3.6.2 步骤二:开发任务代码
在 GLUE IDE 中编写 Python 脚本:
import sys
import datetime
print("GLUE(Python) 任务开始")
# 获取任务参数
if len(sys.argv) > 1:
param = sys.argv[1]
print(f"任务参数: {param}")
# 业务逻辑
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"当前时间: {now}")
# 执行结果
print("GLUE(Python) 任务结束")
# sys.exit(0) 成功,sys.exit(1) 失败
sys.exit(0)

5.3.7 GLUE 模式(NodeJS)
NodeJS 模式下,任务以 JavaScript 脚本形式维护在调度中心,要求执行器所在服务器已安装 Node.js 环境。
5.3.7.1 步骤一:调度中心,新建调度任务
| 配置项 | 说明 |
|---|---|
| 运行模式 | 选择 GLUE(Nodejs) |
| 执行器 | 选择安装了 Node.js 的执行器 |

5.3.7.2 步骤二:开发任务代码
在 GLUE IDE 中编写 Node.js 脚本:
const process = require('process');
console.log("GLUE(NodeJS) 任务开始");
// 获取任务参数
const args = process.argv.slice(2);
if (args.length > 0) {
console.log("任务参数:" + args[0]);
}
// 业务逻辑
const now = new Date().toLocaleString();
console.log("当前时间:" + now);
// 示例:调用 HTTP 接口
// const https = require('https');
// https.get('https://api.example.com/data', (res) => { ... });
console.log("GLUE(NodeJS) 任务结束");
process.exit(0); // 0 成功,非0 失败

5.3.8 GLUE 模式(PHP)
PHP 模式下,任务以 PHP 脚本形式维护在调度中心,要求执行器所在服务器已安装 PHP 环境(CLI 模式)。
5.3.8.1 步骤一:调度中心,新建调度任务
| 配置项 | 说明 |
|---|---|
| 运行模式 | 选择 GLUE(PHP) |
| 执行器 | 选择安装了PHP 的执行器 |

5.3.8.2 步骤二:开发任务代码
在 GLUE IDE 中编写 PHP 脚本:
<?php
echo "GLUE(PHP) 任务开始\n";
// 获取任务参数
$param = isset($argv[1]) ? $argv[1] : '';
echo "任务参数:" . $param . "\n";
// 业务逻辑
$date = date('Y-m-d H:i:s');
echo "当前时间:" . $date . "\n";
echo "GLUE(PHP) 任务结束\n";
exit(0); // 0 成功,非0 失败
?>

5.3.9 GLUE 模式(PowerShell)
PowerShell 模式适用于 Windows 服务器环境,任务以 PowerShell 脚本形式维护在调度中心。
5.3.9.1 步骤一:调度中心,新建调度任务
| 配置项 | 说明 |
|---|---|
| 运行模式 | 选择 GLUE(PowerShell) |
| 执行器 | 选择 Windows 服务器的执行器 |

5.3.9.2 步骤二:开发任务代码
在 GLUE IDE 中编写 PowerShell 脚本:
# GLUE(PowerShell) 示例
Write-Host "GLUE(PowerShell) 任务开始"
# 获取任务参数
param (
[string]$JobParam = ""
)
Write-Host "任务参数:$JobParam"
# 业务逻辑
$date = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "当前时间:$date"
# 示例:检查服务状态
# $service = Get-Service -Name "Spooler"
# Write-Host "服务状态:$($service.Status)"
Write-Host "GLUE(PowerShell) 任务结束"
exit 0 # 0 成功,非0 失败

6. 执行器集成
执行器(Executor)是 XXL-JOB 架构中实际承载并执行定时任务的一侧。它以 Spring Boot 应用的形式运行,内嵌一个轻量级 HTTP Server,负责接受调度中心下发的任务请求、执行业务逻辑、上报执行结果以及回写任务日志。
在之前下载的源码包里面,官方也提供了执行器的两个示例,一个是Spring Boot 应用形式的,一个是非框架版本,任何普通Java程序都能够使用的,如下图:

6.1. 执行器与调度中心的通信流程
执行器启动后会向调度中心发起注册请求,此后每隔 30 秒发送心跳以维持在线状态。调度中心在触发某个 JobHandler 时,会通过 HTTP 协议将任务参数推送到执行器的内嵌 Server(默认端口 9999),执行器线程池接收任务后异步执行,执行完成后将结果回调给调度中心。

[!caution]
执行器与调度中心之间的通信为双向 HTTP,因此两侧必须互通网络。在部署于 Kubernetes 或 Docker 中时,需特别注意网络策略与端口映射。
6.2 执行器内部结构
一个执行器实例由以下核心组件构成:
| 组件 | 职责 |
|---|---|
| EmbedServer | 基于 Netty 的内嵌 HTTP 服务器,接收调度中心下发的任务触发请求 |
| ExecutorRegistryThread | 后台线程,负责注册与心跳维持 |
| TriggerCallbackThread | 后台线程,批量将执行结果回调给调度中心 |
| JobThread | 每个 JobHandler 对应独立的工作线程,管理任务队列与执行生命周期 |
| XxlJobFileAppender | 将 XxlJobHelper.log() 写入本地日志文件 |

6.3 执行器Spring Boot集成
[!tip]
由于我之前部署的调度中心版本为v2.5.0,所以这里执行器的版本也最好保持一致,防止版本差异导致的兼容问题。
这里你可以新开一个新的Spring Boot应用,或者使用现成的Spring Boot应用都可以。
6.3.1 新增Maven依赖
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.5.0</version>
</dependency>
6.3.2 配置文件
# XXL-JOB 执行器配置
xxl:
job:
# 配置调度中心信息
admin:
# 调度中心部署的地址,选填项,如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
addresses: http://192.168.3.69:8250/xxl-job-admin
# 通讯令牌,必须与调度中心配置一致,选填项,非空时启用;执行器注册、心跳、任务结果传输时需要携带该Token;
accessToken: Xh3HlBzwLIeNNeCyXjvMQW5bH9nfs1tOkxNZg0tBe1
# 调度中心通信超时时间,单位秒,选填项,默认为3秒;
timeout: 3
# 配置执行器信息
executor:
# 执行器AppName,选填项,执行器心跳注册分组依据;为空则关闭自动注册;必须与调度中心执行器管理中的名称一致
appname: xxl-job-demo
# 执行器注册地址,选填项,优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。(留空则自动获取本机 IP)
address:
# 执行器IP,选填项,默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯使用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";(留空则自动获取)
ip:
# 执行器端口号,选填项,小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
port: 8989
# 执行器运行日志文件存储磁盘路径,选填项,需要对该路径拥有读写权限;为空则使用默认路径;
logpath: ./logs/xxl-job/jobhandler
# 执行器日志文件保存天数,选项填,过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
logretentiondays: 7
6.3.3 执行器核心Bean配置
创建配置类,将 application.yml 中的参数注入 XxlJobSpringExecutor Bean:
package com.example.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 执行器配置类
*/
@Configuration
public class XxlJobConfig {
private static final 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.setAccessToken(accessToken);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}
这里你也可以使用propertis的写法来完成:
package cn.yunrain.xxljobdemo.config.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @author nianxinzhuo
* @date 2026/5/9 09:25
* @description XXL-JOB配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "xxl.job")
public class XxlJobProperties {
/**
* 调度中心配置
*/
private Admin admin;
/**
* 执行器配置
*/
private Executor executor;
@Data
public static class Admin {
/**
* 调度中心地址列表,多个地址用逗号分隔
*/
private String addresses;
/**
* 通讯令牌
*/
private String accessToken;
/**
* 调度中心与执行器通讯时间限制,单位秒
*/
private int timeout;
}
@Data
public static class Executor {
/**
* 执行器AppName
*/
private String appName;
/**
* 执行器地址,默认为空表示自动获取本机IP和端口
*/
private String address;
/**
* 执行器IP地址,默认为空表示自动获取本机IP
*/
private String ip;
/**
* 执行器端口,默认为9999
*/
private int port = 9999;
/**
* 执行器日志路径,默认为 "logs/xxl-job/jobhandler"
*/
private String logPath = "logs/xxl-job/jobhandler";
/**
* 执行器日志保留天数,默认为30天
*/
private int logRetentionDays = 30;
}
}
package cn.yunrain.xxljobdemo.config;
import cn.yunrain.xxljobdemo.config.properties.XxlJobProperties;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author nianxinzhuo
* @date 2026/5/9 09:23
* @description 执行器核心Bean配置
*/
@Configuration
@RequiredArgsConstructor
@Slf4j
public class XxlJobConfig {
private final XxlJobProperties properties;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
executor.setAdminAddresses(properties.getAdmin().getAddresses());
executor.setAccessToken(properties.getAdmin().getAccessToken());
executor.setAppname(properties.getExecutor().getAppName());
executor.setAddress(properties.getExecutor().getAddress());
executor.setIp(properties.getExecutor().getIp());
executor.setPort(properties.getExecutor().getPort());
executor.setLogPath(properties.getExecutor().getLogPath());
executor.setLogRetentionDays(properties.getExecutor().getLogRetentionDays());
return executor;
}
}
6.3.4 调度中心新增执行器
由于我们在执行器的配置文件中配置的AppName为xxl-job-demo,所以,在调度中心新增执行器的时候,你的AppName也得配置为这个,如下图所示:


之后我启动执行器项目:


6.4 编写JobHandler
到这,你的执行器其实就已经部署并且集成完成了。这里的编写JobHandler实际上就是真实的定时任务逻辑了,这里我不过多编写,直接把官方的示例拿过来:
package cn.yunrain.xxljobdemo.service.jobhandler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* XxlJob开发示例(Bean模式)
*
* 开发步骤:
* 1、任务开发:在Spring Bean实例中,开发Job方法;
* 2、注解配置:为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
* 3、执行日志:需要通过 "XxlJobHelper.log" 打印执行日志;
* 4、任务结果:默认任务结果为 "成功" 状态,不需要主动设置;如有诉求,比如设置任务结果为失败,可以通过 "XxlJobHelper.handleFail/handleSuccess" 自主设置任务结果;
*
* @author xuxueli 2019-12-11 21:52:51
*/
@Component
public class SampleXxlJob {
private static Logger logger = LoggerFactory.getLogger(SampleXxlJob.class);
/**
* 1、简单任务示例(Bean模式)
*/
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
for (int i = 0; i < 5; i++) {
XxlJobHelper.log("beat at:" + i);
TimeUnit.SECONDS.sleep(2);
}
// default success
}
/**
* 2、分片广播任务
*/
@XxlJob("shardingJobHandler")
public void shardingJobHandler() throws Exception {
// 分片参数
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);
// 业务逻辑
for (int i = 0; i < shardTotal; i++) {
if (i == shardIndex) {
XxlJobHelper.log("第 {} 片, 命中分片开始处理", i);
} else {
XxlJobHelper.log("第 {} 片, 忽略", i);
}
}
}
/**
* 3、命令行任务
*/
@XxlJob("commandJobHandler")
public void commandJobHandler() throws Exception {
String command = XxlJobHelper.getJobParam();
int exitValue = -1;
BufferedReader bufferedReader = null;
try {
// command process
ProcessBuilder processBuilder = new ProcessBuilder();
processBuilder.command(command);
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
//Process process = Runtime.getRuntime().exec(command);
BufferedInputStream bufferedInputStream = new BufferedInputStream(process.getInputStream());
bufferedReader = new BufferedReader(new InputStreamReader(bufferedInputStream));
// command log
String line;
while ((line = bufferedReader.readLine()) != null) {
XxlJobHelper.log(line);
}
// command exit
process.waitFor();
exitValue = process.exitValue();
} catch (Exception e) {
XxlJobHelper.log(e);
} finally {
if (bufferedReader != null) {
bufferedReader.close();
}
}
if (exitValue == 0) {
// default success
} else {
XxlJobHelper.handleFail("command exit value("+exitValue+") is failed");
}
}
/**
* 4、跨平台Http任务
* 参数示例:
* "url: http://www.baidu.com\n" +
* "method: get\n" +
* "data: content\n";
*/
@XxlJob("httpJobHandler")
public void httpJobHandler() throws Exception {
// param parse
String param = XxlJobHelper.getJobParam();
if (param==null || param.trim().length()==0) {
XxlJobHelper.log("param["+ param +"] invalid.");
XxlJobHelper.handleFail();
return;
}
String[] httpParams = param.split("\n");
String url = null;
String method = null;
String data = null;
for (String httpParam: httpParams) {
if (httpParam.startsWith("url:")) {
url = httpParam.substring(httpParam.indexOf("url:") + 4).trim();
}
if (httpParam.startsWith("method:")) {
method = httpParam.substring(httpParam.indexOf("method:") + 7).trim().toUpperCase();
}
if (httpParam.startsWith("data:")) {
data = httpParam.substring(httpParam.indexOf("data:") + 5).trim();
}
}
// param valid
if (url==null || url.trim().length()==0) {
XxlJobHelper.log("url["+ url +"] invalid.");
XxlJobHelper.handleFail();
return;
}
if (method==null || !Arrays.asList("GET", "POST").contains(method)) {
XxlJobHelper.log("method["+ method +"] invalid.");
XxlJobHelper.handleFail();
return;
}
boolean isPostMethod = method.equals("POST");
// request
HttpURLConnection connection = null;
BufferedReader bufferedReader = null;
try {
// connection
URL realUrl = new URL(url);
connection = (HttpURLConnection) realUrl.openConnection();
// connection setting
connection.setRequestMethod(method);
connection.setDoOutput(isPostMethod);
connection.setDoInput(true);
connection.setUseCaches(false);
connection.setReadTimeout(5 * 1000);
connection.setConnectTimeout(3 * 1000);
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8");
// do connection
connection.connect();
// data
if (isPostMethod && data!=null && data.trim().length()>0) {
DataOutputStream dataOutputStream = new DataOutputStream(connection.getOutputStream());
dataOutputStream.write(data.getBytes("UTF-8"));
dataOutputStream.flush();
dataOutputStream.close();
}
// valid StatusCode
int statusCode = connection.getResponseCode();
if (statusCode != 200) {
throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid.");
}
// result
bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
StringBuilder result = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
result.append(line);
}
String responseMsg = result.toString();
XxlJobHelper.log(responseMsg);
return;
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail();
return;
} finally {
try {
if (bufferedReader != null) {
bufferedReader.close();
}
if (connection != null) {
connection.disconnect();
}
} catch (Exception e2) {
XxlJobHelper.log(e2);
}
}
}
/**
* 5、生命周期任务示例:任务初始化与销毁时,支持自定义相关逻辑;
*/
@XxlJob(value = "demoJobHandler2", init = "init", destroy = "destroy")
public void demoJobHandler2() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
}
public void init(){
logger.info("init");
}
public void destroy(){
logger.info("destroy");
}
}
之后再重新启动程序,并且配置其中一个jobhandler到调度中心去:


手动执行一次,查看日志:

可见,确实是如下任务触发的:

7. 参数传递
XXL-JOB 的参数传递分为两个维度:
按参数来源分类:
| 来源 | 获取方式 | 说明 |
|---|---|---|
| 调度中心配置的固定参数 | XxlJobHelper.getJobParam() |
在任务编辑页填写,每次触发都携带 |
| 手动触发时的动态参数 | XxlJobHelper.getJobParam() |
"执行一次"时临时填写,覆盖固定参数 |
| 分片广播参数 | XxlJobHelper.getShardIndex() / getShardTotal() |
路由策略为"分片广播"时自动注入 |
| 任务运行上下文 | XxlJobHelper.getJobId() 等 |
任务 ID、日志 ID 等元数据 |
按参数格式分类:
| 格式 | 示例 | 适用场景 |
|---|---|---|
| 普通字符串 | Beijing |
简单单值传递 |
| K=V 键值对 | city=Beijing&date=2024-01-01 |
多值传递,类似 URL QueryString |
| JSON 字符串 | {"city":"Beijing","limit":100} |
复杂对象传递,推荐方式 |
| 空参数 | (留空) | 执行器内部逻辑自己决定参数 |
[!tip]
XXL-JOB 不限制参数格式,
getJobParam()始终返回原始字符串,解析逻辑完全由执行器代码自行实现。推荐统一使用 JSON 格式,便于扩展和维护。
7.1 基础参数传递
7.1.1 维护任务时配置持久化参数
进入调度中心 → 任务管理 → 新增 / 编辑任务,在 "任务参数" 字段填写参数内容。

7.1.2 手动触发时临时参数
在调度中心任务列表点击 "执行一次" 按钮,弹窗中可填写本次专用参数,此参数会覆盖任务的固定参数,仅对当次执行生效。


[!caution]
手动触发参数只覆盖当次执行,不会修改任务的持久化配置。下一次按 CRON 自动触发时,仍使用任务配置中的固定参数。
7.1.3 执行器中读取参数
所有的读取参数都是通过XxlJobHelper.getJobParam()这个API:
@XxlJob("dataExportHandler")
public void dataExportHandler() {
// 获取原始参数字符串
String param = XxlJobHelper.getJobParam();
XxlJobHelper.log("收到参数:{}", param);
if (param == null || param.trim().isEmpty()) {
XxlJobHelper.handleFail("参数不能为空");
return;
}
// 执行业务逻辑
XxlJobHelper.log("开始导出城市 {} 的数据", param);
// ... doExport(param)
XxlJobHelper.handleSuccess();
}
7.2 JSON参数传递(推荐方案)
推荐为每个任务定义专属的参数对象,结构清晰,易于文档化:
// 任务参数 POJO(可复用 Jackson / Gson 反序列化)
public class DataExportParam {
private String city; // 目标城市
private int pageSize; // 每页数据量
private String startDate; // 起始日期,格式 yyyy-MM-dd
private String endDate; // 结束日期
// getter / setter / toString 省略
}
使用Jackson解析:
@Component
public class DataExportHandler {
@Autowired
private ObjectMapper objectMapper; // Spring Boot 自动配置
@XxlJob("dataExportHandler")
public void execute() throws Exception {
String param = XxlJobHelper.getJobParam();
// 参数校验
if (param == null || param.isBlank()) {
XxlJobHelper.handleFail("任务参数为空,请在调度中心配置 JSON 参数");
return;
}
// 反序列化
DataExportParam exportParam;
try {
exportParam = objectMapper.readValue(param, DataExportParam.class);
} catch (JsonProcessingException e) {
XxlJobHelper.log("参数解析失败,原始参数:{},错误:{}", param, e.getMessage());
XxlJobHelper.handleFail("参数 JSON 格式错误:" + e.getMessage());
return;
}
XxlJobHelper.log("解析参数成功:city={}, pageSize={}, startDate={}",
exportParam.getCity(), exportParam.getPageSize(), exportParam.getStartDate());
// 执行业务逻辑
doExport(exportParam);
XxlJobHelper.handleSuccess("导出完成");
}
private void doExport(DataExportParam param) {
// 业务逻辑实现
}
}
当然,你也可以用Gson解析:
// 引入 Gson 依赖(Spring Boot 项目一般已包含)
Gson gson = new Gson();
DataExportParam exportParam = gson.fromJson(param, DataExportParam.class);
[!tip]
一般我们在遇到参数很多的时候才会选择定义对象参数,如果参数比较少的时候,你也可以直接用JsonNode/JsonObject等方式来获取。
7.3 KV格式参数传递
对于简单场景,可使用 key=value&key2=value2 的 QueryString 风格:
@XxlJob("simpleHandler")
public void simpleHandler() {
String param = XxlJobHelper.getJobParam();
// 示例参数:city=Beijing&limit=500
Map<String, String> paramMap = parseKV(param);
String city = paramMap.getOrDefault("city", "Shanghai");
int limit = Integer.parseInt(paramMap.getOrDefault("limit", "100"));
XxlJobHelper.log("city={}, limit={}", city, limit);
XxlJobHelper.handleSuccess();
}
/**
* 解析 K=V 格式参数
*/
private Map<String, String> parseKV(String param) {
Map<String, String> map = new HashMap<>();
if (param == null || param.isBlank()) return map;
for (String pair : param.split("&")) {
String[] kv = pair.split("=", 2);
if (kv.length == 2) {
map.put(kv[0].trim(), kv[1].trim());
}
}
return map;
}
[!caution]
K=V 格式不支持嵌套结构,若参数中包含特殊字符(
&、=),需提前 URL 编码,否则解析结果将出错。复杂场景请改用 JSON 格式。
7.4 分片参数传递
当调度中心任务的路由策略选择 "分片广播" 时,所有在线执行器实例同时收到触发,每个实例会携带唯一的分片索引与总分片数。
分片参数API:
| 方法 | 返回类型 | 说明 |
|---|---|---|
XxlJobHelper.getShardIndex() |
int | 当前实例的分片索引,从 0 开始 |
XxlJobHelper.getShardTotal() |
int | 分片总数,等于当前在线执行器实例数 |
7.4.1 按主键取模分片
最常见的分片策略:按数据 ID 对分片总数取模,每个实例处理自己负责的那一份数据:
@XxlJob("shardingBroadcastHandler")
public void shardingBroadcastHandler() {
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
XxlJobHelper.log("分片参数:index={}, total={}", shardIndex, shardTotal);
// 查询属于本分片的数据:id % shardTotal == shardIndex
List<Long> ids = userService.queryIdsByMod(shardIndex, shardTotal);
XxlJobHelper.log("本分片需处理 {} 条数据", ids.size());
for (Long id : ids) {
try {
userService.processUser(id);
} catch (Exception e) {
XxlJobHelper.log("处理 userId={} 失败:{}", id, e.getMessage());
}
}
XxlJobHelper.handleSuccess(String.format("分片 %d/%d 完成,处理 %d 条",
shardIndex + 1, shardTotal, ids.size()));
}
对应的 SQL 写法:
-- 查询属于当前分片的用户 ID
SELECT id FROM t_user
WHERE id % #{shardTotal} = #{shardIndex}
AND status = 'ACTIVE'
ORDER BY id
LIMIT 1000
7.4.2 按日期范围分片
另一种常见策略:将数据日期范围均分给各个实例:
@XxlJob("dateRangeShardHandler")
public void dateRangeShardHandler() {
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
// 获取总日期范围(从任务参数中读取)
String param = XxlJobHelper.getJobParam();
// 示例参数:{"startDate":"2024-01-01","endDate":"2024-12-31"}
DateRangeParam rangeParam = parseJson(param, DateRangeParam.class);
// 计算本分片负责的日期子范围
long totalDays = ChronoUnit.DAYS.between(
LocalDate.parse(rangeParam.getStartDate()),
LocalDate.parse(rangeParam.getEndDate()));
long daysPerShard = totalDays / shardTotal;
LocalDate myStart = LocalDate.parse(rangeParam.getStartDate()).plusDays(shardIndex * daysPerShard);
LocalDate myEnd = (shardIndex == shardTotal - 1)
? LocalDate.parse(rangeParam.getEndDate())
: myStart.plusDays(daysPerShard - 1);
XxlJobHelper.log("分片 {}/{} 负责日期:{} ~ {}", shardIndex + 1, shardTotal, myStart, myEnd);
// 执行本分片的数据处理
dataService.processDateRange(myStart, myEnd);
XxlJobHelper.handleSuccess();
}
7.4.3 分片参数与任务参数结合
分片参数和任务参数可以同时使用,互不影响:
@XxlJob("combinedShardHandler")
public void combinedShardHandler() {
// 任务参数(来自调度中心配置)
String jobParam = XxlJobHelper.getJobParam();
ConfigParam config = parseJson(jobParam, ConfigParam.class);
// 分片参数(来自路由策略)
int shardIndex = XxlJobHelper.getShardIndex();
int shardTotal = XxlJobHelper.getShardTotal();
XxlJobHelper.log("业务参数:batchSize={},分片:{}/{}",
config.getBatchSize(), shardIndex + 1, shardTotal);
// 结合两者执行:按业务配置的批次大小,处理本分片数据
dataService.processBatch(config.getBatchSize(), shardIndex, shardTotal);
XxlJobHelper.handleSuccess();
}
[!warning]
分片广播模式下,调度中心会同时将任务下发给所有在线执行器实例。若某个实例下线,其分片数据将无人处理(不会自动补偿)。对数据完整性要求高的场景,建议在任务完成后设置校验逻辑。
7.5 上下文参数(任务元数据)
除业务参数外,XxlJobHelper 还提供任务运行时的元数据,可用于日志追踪、幂等控制等场景:
7.5.1 常用上下文API
| 方法 | 返回类型 | 说明 |
|---|---|---|
XxlJobHelper.getJobId() |
long | 任务 ID(调度中心中唯一) |
XxlJobHelper.getJobLogId() |
long | 本次调度日志 ID,用于幂等键 |
XxlJobHelper.getJobParam() |
String | 业务参数字符串 |
XxlJobHelper.getShardIndex() |
int | 分片索引 |
XxlJobHelper.getShardTotal() |
int | 分片总数 |
7.5.2 使用LogId实现幂等
利用本次调度的唯一 logId 作为幂等键,防止网络重试导致任务重复执行:
@XxlJob("idempotentHandler")
public void idempotentHandler() {
long logId = XxlJobHelper.getJobLogId();
String idempotentKey = "xxl:task:log:" + logId;
// 检查 Redis 中是否已执行过本次调度
Boolean isNew = redisTemplate.opsForValue().setIfAbsent(
idempotentKey, "1", 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(isNew)) {
XxlJobHelper.log("任务已执行(logId={}),跳过重复触发", logId);
XxlJobHelper.handleSuccess("已幂等跳过");
return;
}
// 执行实际业务逻辑
XxlJobHelper.log("执行任务,logId={}", logId);
doBusinessLogic();
XxlJobHelper.handleSuccess();
}
7.5.3 使用JobId实现任务级别配置
jobId 在任务整个生命周期内固定不变,可用于从外部配置中心加载任务专属配置:
@XxlJob("configurableHandler")
public void configurableHandler() {
long jobId = XxlJobHelper.getJobId();
// 从配置中心按 jobId 加载运行时配置(如动态调整并发数、超时时间等)
JobRuntimeConfig config = configService.loadByJobId(jobId);
XxlJobHelper.log("任务 {} 加载运行时配置:{}", jobId, config);
// 按配置执行
executeWithConfig(config);
XxlJobHelper.handleSuccess();
}
7.6 父子任务参数传递
XXL-JOB 支持父任务完成后自动触发子任务,子任务同样可以通过 getJobParam() 获取其自身在调度中心配置的参数。
7.6.1 配置子任务
在调度中心任务编辑页,"子任务 ID" 字段填写子任务的任务 ID(多个子任务用逗号分隔)。父任务执行成功后,调度中心会自动触发对应子任务。
[!caution]
子任务触发时使用的是子任务自身配置的参数,父任务无法直接将运行结果传递给子任务。如需传递运行时数据,建议通过数据库、Redis 或消息队列中间件进行中转。
7.6.2 通过 Redis 中转传递参数
// 父任务:执行完成后将结果写入 Redis
@XxlJob("parentJobHandler")
public void parentJobHandler() {
XxlJobHelper.log("父任务开始执行");
// 执行父任务业务逻辑,产生结果
List<Long> processedIds = parentService.execute();
// 将结果存入 Redis,key 包含 jobId 保证唯一性
String resultKey = "xxl:parent:result:" + XxlJobHelper.getJobId();
redisTemplate.opsForValue().set(
resultKey,
JSON.toJSONString(processedIds),
10, TimeUnit.MINUTES);
XxlJobHelper.log("父任务完成,处理 {} 条,结果已写入 Redis key={}", processedIds.size(), resultKey);
XxlJobHelper.handleSuccess();
// 父任务成功后,调度中心自动触发子任务 ID
}
// 子任务:从 Redis 读取父任务结果
@XxlJob("childJobHandler")
public void childJobHandler() {
// 子任务参数中配置父任务的 jobId(在调度中心任务参数字段填写)
String param = XxlJobHelper.getJobParam();
long parentJobId = Long.parseLong(param.trim());
String resultKey = "xxl:parent:result:" + parentJobId;
String resultJson = redisTemplate.opsForValue().get(resultKey);
if (resultJson == null) {
XxlJobHelper.handleFail("未找到父任务结果,key=" + resultKey);
return;
}
List<Long> ids = JSON.parseArray(resultJson, Long.class);
XxlJobHelper.log("子任务读取到 {} 条父任务结果,开始处理", ids.size());
childService.process(ids);
XxlJobHelper.handleSuccess();
}
Comments