XXL-Job 实现分布式定时任务

在微服务架构盛行的今天,定时任务的调度与管理已成为分布式系统中的核心挑战。本文将从架构原理、部署实践、Spring Boot 集成到生产优化,系统性地介绍如何使用 XXL-Job 构建高可用的分布式任务调度平台。


一、为什么选择 XXL-Job?

在分布式系统中,传统的单机定时任务方案(如 Spring @Scheduled、Quartz)面临着诸多挑战:

  • 单点故障:调度节点宕机导致任务无法执行
  • 扩展困难:无法动态增减执行节点
  • 管理不便:缺少统一的任务管理界面
  • 监控缺失:任务执行状态难以追踪

XXL-Job 作为一款轻量级分布式任务调度框架,通过"调度中心 + 执行器"的架构设计,优雅地解决了上述问题:

特性 说明
轻量级 核心包体积小,接入成本低(仅需引入依赖 + 配置)
功能完善 支持分片、重试、阻塞策略、日志追踪等全量特性
易扩展 执行器支持自定义 JobHandler、路由策略、通信方式
可视化 提供完善的管理界面,支持任务配置、执行日志、监控告警
高可用 支持调度中心集群部署、执行器故障转移

二、核心架构与工作原理

2.1 架构设计

XXL-Job 采用中心化架构,整体分为两大核心角色:

复制代码
┌─────────────────────────────────────────────────────────┐
│                    调度中心(Admin)                      │
│  负责任务管理、触发调度、状态管理、监控告警               │
│              支持集群部署(数据库行锁保证一致性)          │
└─────────────────────────────────────────────────────────┘
                            ↓ HTTP 调度指令
┌─────────────────────────────────────────────────────────┐
│                    执行器集群(Executor)                  │
│  嵌入业务服务,接收调度指令并执行具体任务逻辑             │
│              支持水平扩展、自动注册与发现                 │
└─────────────────────────────────────────────────────────┘

2.2 核心工作流程

XXL-Job 的任务执行完整流程如下:

  1. 任务扫描 :调度中心通过 JobScheduleHelper 线程,每隔 1 秒扫描数据库中"待触发"的任务(根据 cron 表达式计算下次执行时间)
  2. 触发调度:通过数据库行锁竞争,保证同一任务只被一个调度节点触发
  3. 路由选择:根据配置的路由策略(轮询、随机、分片广播等)选择目标执行器
  4. HTTP 调度:调度中心通过 RESTful 接口向执行器发送调度请求
  5. 异步执行:执行器接收任务后,放入线程池异步执行
  6. 结果回调 :执行完成后,执行器调用调度中心 /callback 接口上报结果
  7. 状态更新:调度中心记录执行日志,并根据结果进行重试或告警

2.3 关键技术点

  • 定时触发:基于 Quartz 的 cron 表达式解析,结合时间轮算法实现精准调度
  • 分布式锁 :通过数据库行锁(SELECT ... FOR UPDATE)解决调度中心集群部署时的并发调度问题
  • 通信机制:基于 OkHttp 实现 HTTP 通信,轻量且高效
  • 日志处理:执行器本地存储 + 调度中心远程读取,支持实时日志查看

三、部署实施:调度中心搭建

3.1 数据库初始化

从 XXL-Job 官方仓库下载源码,找到数据库初始化脚本:

bash 复制代码
git clone https://github.com/xuxueli/xxl-job.git
cd xxl-job/doc/db

执行 SQL 脚本 tables_xxl_job.sql,初始化核心表结构:

表名 说明
xxl_job_group 执行器信息表
xxl_job_info 任务配置表
xxl_job_log 调度日志表
xxl_job_registry 执行器注册表

3.2 调度中心配置

修改 xxl-job-admin/src/main/resources/application.properties

properties 复制代码
### 数据源配置(必改)
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=your_password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

### 调度中心端口(可选,默认8080)
server.port=8080

### 报警邮箱(可选)
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true

### 调度线程池配置
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100

### 日志保留天数(过期日志自动清理)
xxl.job.logretentiondays=30

### 通信令牌(建议生产环境设置)
xxl.job.accessToken=your_token

### 国际化配置
xxl.job.i18n=zh_CN

3.3 启动调度中心

bash 复制代码
# 编译打包
mvn clean package -Dmaven.test.skip=true

# 启动
java -jar xxl-job-admin/target/xxl-job-admin-2.4.0.jar

启动成功后,访问调度中心管理界面:

  • 访问地址:http://localhost:8080/xxl-job-admin
  • 默认账号:admin
  • 默认密码:123456

四、Spring Boot 集成执行器

4.1 引入核心依赖

在 Spring Boot 项目的 pom.xml 中添加依赖:

xml 复制代码
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.4.0</version>
</dependency>

4.2 配置执行器参数

application.yml(或 application.properties)中添加配置:

yaml 复制代码
xxl:
  job:
    admin:
      # 调度中心地址(多个地址用逗号分隔,支持集群)
      addresses: http://localhost:8080/xxl-job-admin
    executor:
      # 执行器应用名称(需与调度中心配置的执行器名称一致)
      appname: my-app-executor
      # 执行器地址(为空时自动获取本机IP:端口)
      address:
      # 执行器IP(为空时自动获取,多网卡时可手动设置)
      ip:
      # 执行器端口号(默认9999,若端口被占用可修改)
      port: 9999
      # 任务执行日志存储路径
      logpath: /data/applogs/xxl-job/jobhandler
      # 日志保留天数(默认30天)
      logretentiondays: 30
    # 通信令牌(与调度中心配置一致)
    accessToken:

4.3 初始化执行器 Bean

创建配置类 XxlJobConfig

java 复制代码
package com.example.xxljob.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;

@Configuration
public class XxlJobConfig {

    private 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.setAppname(appname);
        xxlJobSpringExecutor.setAddress(address);
        xxlJobSpringExecutor.setIp(ip);
        xxlJobSpringExecutor.setPort(port);
        xxlJobSpringExecutor.setAccessToken(accessToken);
        xxlJobSpringExecutor.setLogPath(logPath);
        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
        return xxlJobSpringExecutor;
    }
}

4.4 编写任务处理器

场景一:简单定时任务

java 复制代码
package com.example.xxljob.handler;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;

@Component
public class SimpleJobHandler {

    @XxlJob("simpleTask")
    public void executeSimpleTask() throws Exception {
        XxlJobHelper.log("========== 任务开始执行 ==========");

        // 获取任务参数
        String param = XxlJobHelper.getJobParam();
        XxlJobHelper.log("任务参数:{}", param);

        // 模拟业务逻辑
        for (int i = 0; i < 5; i++) {
            XxlJobHelper.log("执行进度:{}/5", i + 1);
            Thread.sleep(1000);
        }

        XxlJobHelper.log("========== 任务执行完成 ==========");
    }
}

场景二:分片广播任务(大数据量并行处理)

核心思想:将大数据量任务拆分为多个分片,分配到不同的执行器节点并行执行。

java 复制代码
package com.example.xxljob.handler;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class ShardingJobHandler {

    @XxlJob("shardingTask")
    public void executeShardingTask() throws Exception {
        // 获取分片参数
        int shardIndex = XxlJobHelper.getShardIndex();  // 当前分片序号(从0开始)
        int shardTotal = XxlJobHelper.getShardTotal();  // 总分片数(等于在线执行器数量)

        XxlJobHelper.log("========== 分片任务开始执行 ==========");
        XxlJobHelper.log("分片参数:当前分片={}, 总分片={}", shardIndex, shardTotal);

        // 业务场景:按ID取模分配数据
        List<Data> dataList = fetchDataFromDB();  // 假设有10万条数据
        int processedCount = 0;

        for (Data data : dataList) {
            // 根据ID取模,每个执行器处理 1/shardTotal 的数据
            if (data.getId() % shardTotal == shardIndex) {
                processData(data);
                processedCount++;
            }
        }

        XxlJobHelper.log("本分片共处理数据:{} 条", processedCount);
        XxlJobHelper.log("========== 分片任务执行完成 ==========");
    }

    // 模拟数据查询
    private List<Data> fetchDataFromDB() {
        // 实际业务中从数据库查询待处理数据
        return List.of(
            new Data(1, "数据1"),
            new Data(2, "数据2"),
            new Data(3, "数据3"),
            new Data(4, "数据4"),
            new Data(5, "数据5")
        );
    }

    // 模拟数据处理
    private void processData(Data data) {
        // 实际业务逻辑
        XxlJobHelper.log("处理数据:id={}, name={}", data.getId(), data.getName());
    }

    // 数据实体类
    private static class Data {
        private int id;
        private String name;

        public Data(int id, String name) {
            this.id = id;
            this.name = name;
        }

        public int getId() {
            return id;
        }

        public String getName() {
            return name;
        }
    }
}

分片广播的优势

  • 并行处理:多个节点同时执行,效率提升 N 倍(N 为执行器数量)
  • 动态扩容:新增执行器后,下次调度自动增加分片数量
  • 容错能力:某个分片失败不影响其他分片

场景三:任务参数传递与幂等性处理

java 复制代码
package com.example.xxljob.handler;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;

@Component
public class ParamJobHandler {

    @XxlJob("paramTask")
    public void executeParamTask() throws Exception {
        // 获取任务参数
        String param = XxlJobHelper.getJobParam();

        XxlJobHelper.log("任务参数:{}", param);

        // 解析参数(假设传递的是 JSON 格式)
        if (param != null && !param.isEmpty()) {
            // 实际业务中可以使用 JSON 库解析
            String[] parts = param.split(",");
            for (String part : parts) {
                XxlJobHelper.log("处理参数:{}", part);
            }
        }

        // 幂等性处理:记录任务执行记录,避免重复执行
        String jobId = XxlJobHelper.getJobId();
        Date triggerTime = new Date(XxlJobHelper.getJobTime());
        String logId = jobId + "_" + triggerTime.getTime();

        // 实际业务中可以将 logId 存入 Redis 或数据库
        if (isProcessed(logId)) {
            XxlJobHelper.log("任务已执行过,跳过本次执行");
            return;
        }

        // 执行业务逻辑
        doBusinessLogic();

        // 标记任务已执行
        markAsProcessed(logId);

        XxlJobHelper.log("任务执行完成");
    }

    private boolean isProcessed(String logId) {
        // 实际业务中从 Redis 或数据库查询
        return false;
    }

    private void markAsProcessed(String logId) {
        // 实际业务中写入 Redis 或数据库
    }

    private void doBusinessLogic() {
        // 实际业务逻辑
    }
}

4.5 启动应用并注册执行器

启动 Spring Boot 应用后,执行器会自动向调度中心注册。

登录调度中心管理界面,进入"执行器管理"页面,可以看到刚启动的执行器状态为"在线"。


五、调度中心配置任务

5.1 创建执行器

进入"执行器管理" → 点击"新增":

配置项 说明 示例
AppName 执行器应用名称(与配置文件一致) my-app-executor
名称 执行器描述名称 我的执行器
注册方式 自动注册 / 手动录入 自动注册

5.2 创建任务

进入"任务管理" → 点击"新增":

配置项 说明 示例
执行器 选择刚创建的执行器 my-app-executor
任务描述 任务的描述信息 每日数据同步任务
调度类型 CRON / 固定速率 / 手动触发 CRON
Cron表达式 定时规则 0 0 2 * * ? (每天凌晨2点)
运行模式 BEAN / GLUE BEAN
JobHandler 任务处理器名称(与@XxlJob注解的value一致) simpleTask
任务参数 传递给任务的自定义参数 {"type":"sync","date":"2024-01-01"}
路由策略 轮询 / 随机 / 分片广播等 轮询
阻塞策略 单机串行 / 丢弃后续 / 覆盖之前 单机串行
失败重试次数 任务失败后重试次数 3
超时时间 单位:秒 600
报警邮件 任务失败时的邮件通知 admin@example.com

5.3 Cron 表达式参考

表达式 说明
0 0 2 * * ? 每天凌晨 2 点执行
0 0/5 * * * ? 每 5 分钟执行一次
0 0 12 * * ? 每天中午 12 点执行
0 0 10 ? * MON-FRI 周一到周五每天上午 10 点执行
0 0,30 8-10 * * ? 每天 8 点、8 点半、9 点、9 点半、10 点、10 点半执行

六、核心特性详解

6.1 路由策略

XXL-Job 提供多种路由策略,根据业务场景选择合适的策略:

策略 说明 适用场景
第一个 固定选择第一个执行器 单机测试
最后一个 固定选择最后一个执行器 单机测试
轮询 依次选择执行器 无状态任务,均匀分配负载
随机 随机选择执行器 简单场景
一致性哈希 根据 JobHandler 参数选择执行器 有状态任务,相同数据分配到同一节点
最不经常使用(LFU) 选择使用频率最低的执行器 负载均衡
最近最久未使用(LRU) 选择最久未使用的执行器 负载均衡
故障转移 优先选择健康节点,失败后重试其他节点 高可用场景
忙碌转移 优先选择空闲节点 负载均衡
分片广播 广播到所有执行器,携带分片参数 大数据量并行处理

6.2 阻塞策略

当调度频率过高,执行器来不及消费时,XXL-Job 提供三种阻塞策略:

策略 说明 适用场景
单机串行(默认) 任务进入内存队列,依次执行,前一个未完成则后续任务等待 任务需严格串行,避免并发导致数据竞争
丢弃后续调度 当执行器正在执行任务时,丢弃新的调度请求 实时性要求高,宁可丢弃也不要堆积
覆盖之前调度 当执行器正在执行任务时,终止旧任务,执行新任务 配置刷新、数据同步场景

注意事项

  • 单机串行策略可能导致队列堆积,需监控队列深度,避免 OOM
  • 选择合适的阻塞策略对系统稳定性至关重要

6.3 失败重试

XXL-Job 支持任务级别的失败重试:

  • 重试次数:可在任务配置中设置(如 3 次)
  • 重试间隔:固定间隔或指数退避(如 30s、60s、120s)
  • 重试触发条件 :任务执行返回 ReturnT.FAIL 或抛出异常
java 复制代码
@XxlJob("retryTask")
public void executeRetryTask() throws Exception {
    try {
        // 业务逻辑
        doBusinessLogic();
        // 执行成功,返回默认的 SUCCESS
    } catch (Exception e) {
        XxlJobHelper.log("任务执行异常:{}", e.getMessage());
        // 抛出异常触发重试
        throw e;
    }
}

6.4 任务超时控制

  • 在任务配置中设置超时时间(如 600 秒)
  • 执行时间超过阈值后,任务会被中断
  • 适用于防止任务长时间挂起

七、生产环境最佳实践

7.1 数据库隔离

问题:xxl-job 的调度依赖数据库行锁,频繁的数据库扫描可能影响业务库性能。

解决方案:建议将 xxl-job 数据库与业务数据库分离,使用独立的 MySQL 实例。

复制代码
业务库 ──────┐
            ├─ 不同的 MySQL 实例(推荐)
xxl-job库 ──┘

7.2 调度中心高可用

  • 集群部署:部署多个调度中心实例(如 3 个节点)
  • 数据库行锁:保证同一任务只被一个节点调度
  • 负载均衡:通过 Nginx 或 SLB 进行流量分发
bash 复制代码
# 示例:部署 3 个调度中心节点
java -jar xxl-job-admin.jar --server.port=8080 &
java -jar xxl-job-admin.jar --server.port=8081 &
java -jar xxl-job-admin.jar --server.port=8082 &

7.3 执行器集群部署

  • 多实例部署:同一 appName 的执行器可部署多个实例
  • 端口隔离:每个实例的 port 需配置不同的端口
  • 自动注册:新节点上线后自动被调度中心发现
yaml 复制代码
# 节点1
xxl.job.executor.port: 9999

# 节点2
xxl.job.executor.port: 9999

# 节点3
xxl.job.executor.port: 9999

7.4 日志治理

日志保留策略

  • 调度中心:xxl.job.logretentiondays 建议设置为 30 天
  • 执行器:xxl.job.executor.logretentiondays 建议设置为 30 天

日志监控

  • 定期检查日志文件大小,避免磁盘撑爆
  • 设置日志告警,当日志文件过大时及时通知

7.5 告警配置

告警场景

  • 任务执行失败
  • 任务执行超时
  • 执行器离线

告警方式

  • 邮件通知
  • 钉钉机器人
  • 企业微信机器人
  • 自定义 Webhook

钉钉告警示例

在调度中心配置钉钉告警:

  1. 创建钉钉机器人,获取 Webhook 地址
  2. 在任务配置中填写钉钉 Webhook
  3. 设置告警触发条件(失败重试 3 次后告警)

7.6 安全性配置

设置 AccessToken

properties 复制代码
# 调度中心配置
xxl.job.accessToken=your_secure_token

# 执行器配置
xxl.job.accessToken=your_secure_token

注意事项

  • 生产环境务必设置 AccessToken
  • 不要使用默认空值或弱密码

八、常见问题排查

8.1 执行器注册失败

症状:执行器启动后,调度中心看不到执行器在线。

排查步骤

  1. 检查执行器 appname 与调度中心配置是否一致
  2. 检查网络连通性:执行器能否访问调度中心地址
  3. 检查 accessToken 是否一致
  4. 查看执行器日志,寻找注册失败的原因
bash 复制代码
# 查看执行器日志
tail -f /data/applogs/xxl-job/jobhandler/xxl-job-executor.log

8.2 任务执行超时

症状:任务一直处于"运行中"状态,超时后中断。

排查步骤

  1. 检查任务执行时间是否超过配置的超时时间
  2. 检查是否存在慢 SQL、网络超时等问题
  3. 检查线程池是否耗尽(查看执行器日志)
  4. 考虑将任务拆分为多个子任务

8.3 分片任务数据重复

症状:分片广播模式下,同一条数据被多个节点处理。

排查步骤

  1. 检查分片算法是否正确(如取模、范围划分)
  2. 检查分片总数是否与执行器数量匹配
  3. 避免在分片逻辑中出现数据范围重叠
  4. 添加日志输出分片参数,验证分配逻辑
java 复制代码
XxlJobHelper.log("分片参数:index={}, total={}", shardIndex, shardTotal);

8.4 任务执行失败但不重试

症状:任务失败后没有触发重试。

排查步骤

  1. 检查任务配置中是否启用了失败重试
  2. 检查任务代码是否抛出异常或返回 ReturnT.FAIL
  3. 检查任务是否配置了"子任务",父任务失败可能不触发重试

8.5 调度中心日志文件过大

症状xxl-job.log 文件持续增长,占用大量磁盘空间。

解决方案

  1. 设置日志保留天数:xxl.job.logretentiondays=30
  2. 定期清理历史日志(可编写脚本定时清理)
  3. 考虑日志轮转配置(Logback 配置)

九、与其他框架对比

特性 Spring @Scheduled Quartz Elastic Job xxl-job
架构 单机 单机/集群(依赖数据库锁) 无中心化(依赖注册中心) 中心化调度 + 分布式执行
管理界面 有(较简陋) 完善(功能强大)
分片能力 强(自动分片) 强(手动配置分片)
弹性扩容 困难 困难 自动 半自动(需配置)
学习成本
适用场景 简单定时任务 中小规模 超大规模任务 企业级应用

选型建议

  • Spring @Scheduled:适合简单、无需高可用的定时任务
  • Quartz:适合传统应用,需要一定分布式能力的场景
  • Elastic Job:适合超大规模任务调度,需要自动分片和弹性扩容
  • xxl-job:适合企业级应用,需要完善的监控和管理功能

十、总结

XXL-Job 作为一款轻量级分布式任务调度框架,凭借其简单易用、高性能、高可用性、灵活调度、监控与告警等优点,在分布式系统开发中发挥着重要作用。

核心价值

  • 让分布式任务调度变得"可视化+可观测+可扩展"
  • 实现了 Quartz 难以做到的"高可用+弹性+异步执行"
  • 以极低学习成本支撑中大型系统的定时与分布式任务场景

参考资料

相关推荐
小北方城市网13 小时前
Redis 分布式锁高可用实现:从原理到生产级落地
java·前端·javascript·spring boot·redis·分布式·wpf
小尘要自信19 小时前
高级网络爬虫实战:动态渲染、反爬对抗与分布式架构
分布式·爬虫·架构
小程故事多_801 天前
深度解析Kafka重平衡,触发机制、执行流程与副本的核心关联
分布式·kafka
2501_948120151 天前
基于HBase的分布式列式存储
数据库·分布式·hbase
小北方城市网1 天前
MyBatis-Plus 生产级深度优化:从性能到安全的全维度方案
开发语言·redis·分布式·python·缓存·性能优化·mybatis
【赫兹威客】浩哥1 天前
【赫兹威客】伪分布式Kafka测试教程
分布式·kafka
【赫兹威客】浩哥1 天前
【赫兹威客】伪分布式Spark测试教程
大数据·分布式·spark
what丶k1 天前
MySQL读写分离部署配置全解析(从原理到落地)
数据库·分布式·mysql
rustfs1 天前
RustFS 配置 Cloudflare Tunnel 实现安全访问的详细教程!
分布式·安全·docker·rust·开源