XXL-JOB教程

1. 简介

[!tip]

本文全文是按照JDK8,XXL-JOB2.x版本进行说明讲解。

1.1 概述

XXL-Job 是一个分布式任务调度平台,由许雪里(大众点评)开发并开源。它的核心设计目标是:

  • 轻量级、易上手
  • 支持分布式集群部署
  • 提供可视化管理界面
  • 支持弹性扩容与故障转移

参考资料:

  1. 官方中文文档
  2. GitHub官方地址

1.2 为什么选择 XXL-Job

在 Java 生态中,常见的任务调度方案对比如下:

方案 优点 缺点
Spring的Scheduled 极简,无需引入额外组件 不支持分布式,无管理界面,重启丢失状态
Quartz 功能完善,支持持久化 集群配置复杂,无管理界面
Elastic-Job 支持分布式分片 依赖 ZooKeeper,运维成本高
XXL-Job 轻量、可视化、低侵入、开箱即用 功能相对没有 Elastic-Job 丰富

XXL-Job 的核心优势:

  1. 调度与执行分离,解耦彻底。
  2. 提供调度中心 Web 界面,任务管理直观。
  3. 支持 GLUE 模式(在线编辑脚本/代码)。
  4. 支持分片广播、并发执行、任务依赖。
  5. 任务执行失败自动重试与报警。
  6. 执行日志实时查看。

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 应用,它本身不执行任何业务逻辑,只负责三件事:

  1. 管理任务:任务配置存在 MySQL 里,包括 Cron 表达式、路由策略、超时时间、失败重试次数等,通过 Web 界面增删改查。
  2. 触发调度:内部有一个调度线程,每隔一段时间(默认预读 5 秒)从数据库扫描即将触发的任务,到时间后通过 HTTP 请求推送给执行器。
  3. 收集结果:执行器执行完任务后,会主动回调调度中心,上报执行结果(成功/失败)和日志,调度中心将其持久化到数据库,并在失败时触发报警邮件。

[!note]

调度中心主动推送任务给执行器,不是执行器来调度中心拉取。这是"推模型"设计,好处是调度中心能精确控制路由策略。

2.1.2 执行器(Executor)

执行器不是独立部署的服务,而是嵌入到你的业务 Java 应用里的一个组件(引入 xxl-job-core 依赖后,声明一个 XxlJobSpringExecutor Bean 即可)。

执行器启动后会做三件事:

  1. 注册:向调度中心发起 HTTP 注册请求,告知自己的 IP、端口、AppName,此后每 30 秒心跳续约一次。
  2. 监听:在指定端口(默认 9999)启动一个内嵌 HTTP Server,等待调度中心的任务触发请求。
  3. 执行与回调:收到触发请求后,在本地线程池中找到对应的 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

它有两个作用:

  1. 分组隔离:调度中心按 AppName 管理执行器,同一个 AppName 下的所有节点构成一个执行器集群。创建任务时,先选择哪个 AppName(执行器分组),再选择 JobHandler。
  2. 自动注册的依据:执行器向调度中心注册时,上报的就是 AppName + IP + Port,调度中心以此维护每个 AppName 下的在线节点列表。

[!tip]

实际项目里,建议每个业务服务用一个独立的 AppName,比如 user-service-joborder-service-job,便于在调度中心区分管理。

2.1.5 路由策略

当一个 AppName 下有多个执行器节点时,调度中心要决定把任务发给哪一个,这就是路由策略

路由策略

路由策略共有10种,实际常用的主要就是上图中的4种:

  1. 轮询:任务依次发给 A、B、C、A、B、C……简单均衡,适合无状态的普通任务。
  2. 故障转移:调度中心在发送前先做心跳检测,选第一个心跳成功的节点。某节点挂掉时自动跳过,是高可用场景的首选。
  3. 一致性 Hash:对 jobId 做 Hash 运算,保证同一个任务永远打到同一个节点。适合有状态任务,比如任务需要读取本地缓存文件,换节点会找不到。
  4. 分片广播:调度中心同时向所有节点发送触发请求,每个节点收到的参数里包含自己的分片序号,自己决定处理哪部分数据,是大数据量并行处理的利器。
策略名称 说明
FIRST(第一个) 固定选择第一个机器
LAST(最后一个) 固定选择最后一个机器
ROUND(轮询) 任务依次发给不同的执行器节点执行
RANDOM(随机) 随机选择在线的机器
CONSISTENT_HASH(一致性HASH) 每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上
LEAST_FREQUENTLY_USED(最不经常使用) 使用频率最低的机器优先被选举
LEAST_RECENTLY_USED(最近最久未使用) 最久未使用的机器优先被选举
FAILOVER(故障转移) 按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度
BUSYOVER(忙碌转移) 按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度
SHARDING_BROADCAST(分片广播) 广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务

2.1.6 阻塞处理策略

调度中心按 Cron 触发,它不关心上一次任务有没有执行完。如果执行器还在忙,下一次触发到来时该怎么办?这就是阻塞处理策略

也就是当调度过于密集执行器来不及处理时的处理策略

阻塞处理策略有如下几种:

  1. 单机串行(默认):新的触发请求排队等待,等当前执行完再处理下一个(FIFO队列)。适合绝大多数场景,数据不会丢失但可能积压。
  2. 丢弃后续调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,本次请求将会被丢弃并标记为失败。适合对数据实时性要求不高、宁可少跑也不能并发的场景。
  3. 覆盖之前调度:调度请求进入单机执行器后,发现执行器存在运行的调度任务,将会终止运行中的调度任务并清空队列,然后运行本地调度任务,执行最新这次触发。适合"只关心最新状态"的场景,比如刷新配置缓存。

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 与当前执行线程绑定,提供参数获取、日志记录、结果上报等功能。每次任务触发,框架都会在执行前把本次调度的上下文信息塞进去。

下面把它的每个方法都讲清楚:

  1. 获取任务参数

    @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:普通字符串 ----
    }
    
  2. 日志记录(重要)

    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);  // 模拟处理耗时
        }
    }
    
  3. 获取分片参数

    分片广播模式下,每个执行器节点都会收到触发,但参数不同:

    @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
    }
    
  4. 上报执行结果

    @XxlJob("resultDemoHandler")
    public void resultDemoHandler() throws Exception {
    
        try {
            int processedCount = doBusinessLogic();
    
            // 明确标记成功,并附上说明信息
            // 这条信息会显示在调度中心日志的"执行备注"列
            XxlJobHelper.handleSuccess("处理完成,共 " + processedCount + " 条记录");
    
        } catch (BusinessException e) {
            // 明确标记失败,并附上失败原因
            // 触发失败重试和报警邮件
            XxlJobHelper.handleFail("业务异常:" + e.getMessage());
    
        }
        // ⚠️ 如果既没调 handleSuccess 也没调 handleFail:
        //    - 方法正常执行完毕(无异常)→ 框架自动标记为成功
        //    - 方法抛出未捕获的异常  → 框架自动标记为失败
    }
    

BEAN 模式任务从触发到结束的流程

流程中有几个细节值得单独解释:

线程池分 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类,造成类的浪费;不支持自动扫描任务并注入到执行器容器,需要手动注入。

开发步骤:

  1. 继承 IJobHandler 抽象类并重写 execute() 方法。

  2. 手动通过如下方式注入到执行器容器。

    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]

上述端口都是默认端口,你可以根据你自己设置的实际情况进行放行。

当然,如果你的服务都是在内网中访问,那么你不用再单独放行上述端口了。

如果你的服务要求公网可以访问,那么你还得放行防火墙:

  1. CentOS系统放行

    # 放通调度中心端口
    firewall-cmd --permanent --add-port=8080/tcp
    
    # 放通执行器端口
    firewall-cmd --permanent --add-port=9999/tcp
    
    # 重新加载防火墙规则
    firewall-cmd --reload
    
    # 查看已放通的端口
    firewall-cmd --list-ports
    
  2. Ubuntu系统放行

    ufw allow 8080/tcp
    ufw allow 9999/tcp
    ufw reload
    ufw status
    

[!warning]

阿里云 / 腾讯云等云服务器还需要在控制台安全组中额外放通端口,仅靠系统防火墙不够。

4. 调度中心部署

调度中心是 XXL-Job 的大脑,部署方式有两种:源码编译部署Docker(Docker Compose)部署

无论哪种部署方式,我都建议先把源码克隆下来,因为里面有数据库 SQL 文件和配置文件模板。你可以先了解里面的数据库表以及配置参数。

XXL-Job官方源码下载地址

这里我是直接下载2.5.0版本:

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

这个地址后续你的执行器将会使用到,作为回调地址。

调度中心Web界面

默认账号密码如下:

  • 账号:admin
  • 密码:123456

登录后运行界面如下图所示。

调度中心Web页面

至此“调度中心”项目已经部署成功。


这里通过源码编译部署其实是有一个缺陷的,如果你的程序异常退出了,或者服务器重启了,那么你的调度中心并不会立刻重启。所以,生产环境必须配置开机自启,防止服务器重启后调度中心没有自动拉起

# 创建 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部署的好处如下:

  1. 环境隔离。
  2. 一键启动所有服务器,比如mysql,jar。
  3. 异常自动重启。

这里使用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]

  1. 这里数据库我是直接连接我宿主机的数据库,所以这里采用的是host.docker.internal方案。
  2. 这里的敏感信息我都放到了.env文件中去了。
  3. 如果你是用的现成的数据库,那么你需要放出数据库的IP白名单。否则容器是连接不上现成的数据库的

现在Docker部署的目录如图:

Docker部署目录

5. 任务模式详解

新增任务表单

5.1 基础配置

  • 执行器:任务的绑定的执行器,任务触发调度时将会自动发现注册成功的执行器,实现任务自动发现功能;;另一方面也可以方便的进行任务分组。每个任务必须绑定一个执行器。可在 "执行器管理" 进行设置。
  • 任务描述:任务的描述信息,便于任务管理。
  • 负责人:任务的负责人。
  • 报警邮件:任务调度失败时邮件通知的邮箱地址,支持配置多邮箱地址,配置多个邮箱地址时用逗号分隔。

5.2 调度配置

  1. 调度类型
    • 无:该类型不会主动触发调度。
    • CRON:该类型将会通过CRON,触发任务调度。
    • 固定速度:该类型将会以固定速度,触发任务调度;按照固定的间隔时间,周期性触发。
  2. CRON:触发任务执行的Cron表达式。
  3. 固定速度:固定速度的时间间隔,单位为秒。

调度类型分别如下图所示:

调度类型为“无”

调度类型为“Cron”

调度类型为“固定速度”

5.3 任务配置

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

运行模式选项

  • BEAN模式流程图如下:

    BEAN模式流程图

  • GLUE模式流程图如下:

    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 相同,运行模式BEANJobHandler 填写注解中的 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 模式

新增GLUE(Java)模式任务

5.3.4.2 步骤二:开发任务代码

在调度中心任务列表,点击该任务的 GLUE IDE 按钮:

GULE IDE按钮

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

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 服务器上的执行器

新增GLUE(Shell)模式任务

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

Shell脚本

[!caution]

脚本的退出码(exit code)决定任务成败,exit 0 为成功,exit 1 等非零值为失败。

5.3.6 GLUE 模式(Python)

Python 模式下,任务以 Python 脚本形式维护在调度中心,要求执行器所在服务器已安装 Python 环境。

5.3.6.1 步骤一:调度中心,新建调度任务

配置项 说明
运行模式 选择 GLUE(Python)
执行器 选择安装了 Python 的执行器

新增GLUE(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)

Python代码

5.3.7 GLUE 模式(NodeJS)

NodeJS 模式下,任务以 JavaScript 脚本形式维护在调度中心,要求执行器所在服务器已安装 Node.js 环境。

5.3.7.1 步骤一:调度中心,新建调度任务

配置项 说明
运行模式 选择 GLUE(Nodejs)
执行器 选择安装了 Node.js 的执行器

新增GLUE(Nodejs)模式任务

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 失败

Nodejs示例代码

5.3.8 GLUE 模式(PHP)

PHP 模式下,任务以 PHP 脚本形式维护在调度中心,要求执行器所在服务器已安装 PHP 环境(CLI 模式)。

5.3.8.1 步骤一:调度中心,新建调度任务

配置项 说明
运行模式 选择 GLUE(PHP)
执行器 选择安装了PHP 的执行器

新增GLUE(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 失败
?>

PHP示例代码

5.3.9 GLUE 模式(PowerShell)

PowerShell 模式适用于 Windows 服务器环境,任务以 PowerShell 脚本形式维护在调度中心。

5.3.9.1 步骤一:调度中心,新建调度任务

配置项 说明
运行模式 选择 GLUE(PowerShell)
执行器 选择 Windows 服务器的执行器

新增GLUE(PowerShell)模式任务

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 失败

PowerShell示例代码

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();
}