分布式任务调度平台 XXL-JOB 详解和使用

938 阅读10分钟

什么是任务调度

  • 什么是任务调度
    • 定义: 任务调度是指在计算机系统中,按照一定的策略和规则,在特定时间或条件下执行预定任务的过程。
    • 举例:简单来说,可以类比为闹钟到了具体的时间开始报警
    • 在企业中的需求: 在晚上后台定时进行数据同步和当天的数据统计

时间轮算法: image.png

时间轮可以理解为一个环形的数组,在每一个格子里存放一个任务列表,当前时间指到对应的格子后拿出所有任务并执行。

  1. Netty 使用 HashedWheelTimer 实现高性能定时任务
  2. Kafka 内部用于管理副本同步、心跳检测等
  3. Elastic-Job 使用分片 + 时间轮机制进行任务调度

java中定时器的实现:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
executor.scheduleAtFixedRate(() -> {
    System.out.println("每秒执行一次");
}, 0, 1, TimeUnit.SECONDS);

基于线程池实现,使用 DelayQueue 管理延迟任务;每个任务被包装成 ScheduledFutureTask,包含下次执行时间;线程池中的线程不断从队列中取出到期的任务执行。

为什么选择XXL-JOB

  • 传统定时任务方式的痛点

    • 单机局限性:单机部署的定时任务(如 Java 中使用Timer类或 Spring 的@Scheduled注解),一旦机器故障,任务无法执行。并且在集群环境下,会出现任务重复执行的问题。
    • 缺乏集中管理:多个定时任务分散在不同项目中,难以统一管理和监控任务状态、执行日志等。
    • 功能单一:无法满足复杂的任务调度需求,如任务依赖、任务分片、动态任务修改等。
  • XXL - JOB 解决痛点

    • 高可用性:支持调度中心和执行器集群部署,避免单点故障。
    • 集中管理:通过 Web 界面集中管理所有任务,方便进行任务的创建、修改、删除、启动、停止等操作。
    • 丰富功能:具备任务依赖、任务分片、失败重试、超时控制、实时日志查看等丰富功能,满足各种复杂业务场景。

image.png

XXL_JOB的安装和部署

原本就是java代码编写的,所以直接以项目启动即可,项目结构和配置文件都是可以自定义修改的。因为有项目需要,所以安装和部署主要分为两个步骤,适配人大金仓数据,打包后使用docker部署在linux服务器环境使用。

人大金仓数据库适配

  1. xxl-job对应的版本是2.4.0,从官网下载源码zip包,也可以直接下载源码后打包gitee.com/xuxueli0323…
  2. 打开项目后,在pom文件中增加人大金仓驱动依赖
<!-- 人大金仓数据库驱动 -->
<dependency>
    <groupId>cn.com.kingbase</groupId>
    <artifactId>kingbase8</artifactId>
    <version>8.6.0</version>
</dependency>

3. 在配置文件中更改数据库的连接信息,也可以使用其他数据库的驱动和连接

1747320747899.png

  1. 由于xxl_job原本就使用数据库存放定时器执行数据,所以需要初始化表结构,在源码中存放了mysql版本的初始语句,已经做了对应人大金仓数据库语句的适配,然后导入到准备好的人大金仓数据库

CREATE TABLE xxl_job_info (
  id SERIAL PRIMARY KEY,
  job_group INT NOT NULL COMMENT '执行器主键ID',
  job_desc VARCHAR(255) NOT NULL,
  add_time TIMESTAMP DEFAULT NULL,
  update_time TIMESTAMP DEFAULT NULL,
  author VARCHAR(64) DEFAULT NULL COMMENT '作者',
  alarm_email VARCHAR(255) DEFAULT NULL COMMENT '报警邮件',
  schedule_type VARCHAR(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
  schedule_conf VARCHAR(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
  misfire_strategy VARCHAR(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
  executor_route_strategy VARCHAR(50) DEFAULT NULL COMMENT '执行器路由策略',
  executor_handler VARCHAR(255) DEFAULT NULL COMMENT '执行器任务handler',
  executor_param VARCHAR(512) DEFAULT NULL COMMENT '执行器任务参数',
  executor_block_strategy VARCHAR(50) DEFAULT NULL COMMENT '阻塞处理策略',
  executor_timeout INT NOT NULL DEFAULT 0 COMMENT '任务执行超时时间,单位秒',
  executor_fail_retry_count INT NOT NULL DEFAULT 0 COMMENT '失败重试次数',
  glue_type VARCHAR(50) NOT NULL COMMENT 'GLUE类型',
  glue_source TEXT COMMENT 'GLUE源代码',
  glue_remark VARCHAR(128) DEFAULT NULL COMMENT 'GLUE备注',
  glue_updatetime TIMESTAMP DEFAULT NULL COMMENT 'GLUE更新时间',
  child_jobid VARCHAR(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
  trigger_status SMALLINT NOT NULL DEFAULT 0 COMMENT '调度状态:0-停止,1-运行',
  trigger_last_time BIGINT NOT NULL DEFAULT 0 COMMENT '上次调度时间',
  trigger_next_time BIGINT NOT NULL DEFAULT 0 COMMENT '下次调度时间'
) COMMENT='任务信息表';

CREATE TABLE xxl_job_log (
  id SERIAL PRIMARY KEY,
  job_group INT NOT NULL COMMENT '执行器主键ID',
  job_id INT NOT NULL COMMENT '任务,主键ID',
  executor_address VARCHAR(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
  executor_handler VARCHAR(255) DEFAULT NULL COMMENT '执行器任务handler',
  executor_param VARCHAR(512) DEFAULT NULL COMMENT '执行器任务参数',
  executor_sharding_param VARCHAR(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
  executor_fail_retry_count INT NOT NULL DEFAULT 0 COMMENT '失败重试次数',
  trigger_time TIMESTAMP DEFAULT NULL COMMENT '调度-时间',
  trigger_code INT NOT NULL COMMENT '调度-结果',
  trigger_msg TEXT COMMENT '调度-日志',
  handle_time TIMESTAMP DEFAULT NULL COMMENT '执行-时间',
  handle_code INT NOT NULL COMMENT '执行-状态',
  handle_msg TEXT COMMENT '执行-日志',
  alarm_status SMALLINT NOT NULL DEFAULT 0 COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
  KEY I_trigger_time (trigger_time),
  KEY I_handle_code (handle_code)
) COMMENT='任务日志表';

CREATE TABLE xxl_job_log_report (
  id SERIAL PRIMARY KEY,
  trigger_day TIMESTAMP DEFAULT NULL COMMENT '调度-时间',
  running_count INT NOT NULL DEFAULT 0 COMMENT '运行中-日志数量',
  suc_count INT NOT NULL DEFAULT 0 COMMENT '执行成功-日志数量',
  fail_count INT NOT NULL DEFAULT 0 COMMENT '执行失败-日志数量',
  update_time TIMESTAMP DEFAULT NULL,
  UNIQUE KEY i_trigger_day (trigger_day)
) COMMENT='任务日志报表';

CREATE TABLE xxl_job_logglue (
  id SERIAL PRIMARY KEY,
  job_id INT NOT NULL COMMENT '任务,主键ID',
  glue_type VARCHAR(50) DEFAULT NULL COMMENT 'GLUE类型',
  glue_source TEXT COMMENT 'GLUE源代码',
  glue_remark VARCHAR(128) NOT NULL COMMENT 'GLUE备注',
  add_time TIMESTAMP DEFAULT NULL,
  update_time TIMESTAMP DEFAULT NULL
) COMMENT='任务GLUE日志';

CREATE TABLE xxl_job_registry (
  id SERIAL PRIMARY KEY,
  registry_group VARCHAR(50) NOT NULL,
  registry_key VARCHAR(255) NOT NULL,
  registry_value VARCHAR(255) NOT NULL,
  update_time TIMESTAMP DEFAULT NULL,
  KEY i_g_k_v (registry_group, registry_key, registry_value)
) COMMENT='执行器注册表';

CREATE TABLE xxl_job_group (
  id SERIAL PRIMARY KEY,
  app_name VARCHAR(64) NOT NULL COMMENT '执行器AppName',
  title VARCHAR(12) NOT NULL COMMENT '执行器名称',
  address_type SMALLINT NOT NULL DEFAULT 0 COMMENT '执行器地址类型:0=自动注册、1=手动录入',
  address_list TEXT COMMENT '执行器地址列表,多地址逗号分隔',
  update_time TIMESTAMP DEFAULT NULL
) COMMENT='执行器分组表';

CREATE TABLE xxl_job_user (
  id SERIAL PRIMARY KEY,
  username VARCHAR(50) NOT NULL COMMENT '账号',
  password VARCHAR(50) NOT NULL COMMENT '密码',
  role SMALLINT NOT NULL COMMENT '角色:0-普通用户、1-管理员',
  permission VARCHAR(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
  UNIQUE KEY i_username (username)
) COMMENT='用户表';

CREATE TABLE xxl_job_lock (
  lock_name VARCHAR(50) NOT NULL COMMENT '锁名称',
  PRIMARY KEY (lock_name)
) COMMENT='锁表';

INSERT INTO xxl_job.xxl_job_group (app_name, title, address_type, address_list, update_time) 
VALUES ('xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31');

INSERT INTO xxl_job.xxl_job_info (job_group, job_desc, add_time, update_time, author, alarm_email, schedule_type, schedule_conf, misfire_strategy, executor_route_strategy, executor_handler, executor_param, executor_block_strategy, executor_timeout, executor_fail_retry_count, glue_type, glue_source, glue_remark, glue_updatetime, child_jobid) 
VALUES (1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');

INSERT INTO xxl_job.xxl_job_user (username, password, role, permission) 
VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);

INSERT INTO xxl_job.xxl_job_lock (lock_name) 
VALUES ('schedule_lock');

  1. 尝试启动后,发现有一些报错,这些报错原因是因为数据库语句的对应,大体来说需要更改更改对应mapper的xml文件中的sql语句和mapper查询参数的一些问题

1747321215287.png

1747321363633.png

1747321401051.png

DATE_ADD(#{nowTime},- INTERVAL '${timeout} second')
  • 未选择模式问题 xxl_job.‘表名’
  • 字段单引号问题,直接去掉
  • 函数问题 人大金仓默认不支持DATE_ADD 函数
  • 分页问题 分页语句更改为 LIMIT #{pagesize} OFFSET #{offset}

修改后,本地启动测试成功,下一步开始docker部署

docker部署

  1. xxl-job-admin下自带dockerfiles文件
FROM openjdk:8-jre-slim
MAINTAINER xuxueli

ENV PARAMS=""

ENV TZ=PRC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

ADD xxl-job-admin-*.jar /app.jar

ENTRYPOINT ["sh","-c","java -jar $JAVA_OPTS /app.jar $PARAMS"]

2. 构建docker镜像

docker build -t xxl-job:latest . 

3. 启动容器

docker run -d --name xxl-job -p 8988:8080 -v /opt/xxl-job/logs:/data/applogs xxl-job:latest

完成启动后,就可以登陆使用了 http://ip:8988/xxl-job-admin/user

默认用户名和密码:admin/123456

XXL_JOB的使用

部署好之后,就可以在对应的平台应用中使用xxl_job实现定时任务

应用服务中使用示例

  1. 增加对应版本xxl_job的pom依赖
<!-- 定时任务xxl-job -->
<dependency>
    <groupId>com.xuxueli</groupId>
    <artifactId>xxl-job-core</artifactId>
    <version>2.4.0</version>
</dependency>

2. 增加配置类管理XXL_JOB的自定义配置

package com.starlinkdt.hrm.config;


import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.springframework.beans.factory.annotation.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class XxlJobConfig {
    @Value("${xxl.job.admin.addresses}")
    private String adminAddresses;
    @Value("${xxl.job.accessToken}")
    private String accessToken;
    @Value("${xxl.job.executor.appname}")
    private String appname;
    @Value("${xxl.job.executor.address}")
    private String address;
    @Value("${xxl.job.executor.ip}")
    private String ip;
    @Value("${xxl.job.executor.port}")
    private int port;
    @Value("${xxl.job.executor.logpath}")
    private String logPath;
    @Value("${xxl.job.executor.logretentiondays}")
    private int logRetentionDays;
    @Bean
    public XxlJobSpringExecutor xxlJobExecutor() {
        log.info(">>>>>>>>>>> start 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;
    }
}

3. 配置文件中加入xxl_job的配置信息

xxl:
  job:
    accessToken: default_token  # 默认的
    admin:
      addresses: http://ip:8988/xxl-job-admin  # 任务调度中心console的访问地址
    executor:
      appname: hrm-executor-product  # 每个项目注册的别名,对应调度中心的执行器
      address: http://${HOST_IP}:9899  # 填充地址,假设执行器运行在本地
      ip: ${HOST_IP}  # 填充本地IP,确保它与address配置一致
      port: 9899
      logpath: /data/applogs/xxl-job/jobhandler
      logretentiondays: -1
      retry-count: 3  # 重试次数
      retry-interval: 60000  # 重试间隔(毫秒)
      task-timeout: 300000  # 任务超时时间(毫秒)

4. 创建一个简单的定时任务

/**
 * TestJobHandler
 */
@Component
public class TestHandler {

    @Value("${server.port}")
    private String port;

    @XxlJob("testJobHandler")
    public ReturnT helloJob() {
        System.out.println("一个简单的定时任务执行了。。。" + port);
        return  ReturnT.SUCCESS;
    }
}

对应调度任务平台的配置

  1. 执行器管理:在网页上对应的执行器管理中新增客户端对应配置的执行器,自动注册和手动注册控制注册应用实例的ip地址管理

image.png

  1. 任务管理:在任务管理页面可以新增任务,选择刚才配置好的执行器,填写好任务的基本业务信息

image.png

  1. 可以配置好任务的报警邮件,下图是对应的xxl_job的邮件配置

image.png 4. 定时频率的配置,可以使用时间配置框代替cron表达式的编写,最下面有运行时间的示例

image.png

  1. 对应任务配置

image.png

  1. 任务的高级策略配置:路由策略、阻塞处理策略、调度过期策略、任务超时时间、失败重试次数

image.png

  1. 调度日志

image.png

  1. 运行报表

1747356949347.png

XXL_JOB的核心组件

1747358789511.png

上图是xxl_job官方的架构图,从中其实可以抽象出最重要的三个组件部分: 调度中心、执行器、调度任务

  1. 调度中心: 注册线程处理应用平台的注册、任务的发布、日志管理、监控运维
  2. 执行器: 执行任务、返回任务执行结果、记录任务日志
  3. 调度任务:待执行的任务使用队列存放,使用对应的jobHadler执行

1747359372241.png

案例

用一个常见的数据同步的需求来举例,实现数据同步有一些常见的解决方案 1747324229507.png

  • API 接口调用

    通过 RESTful API 接口调用实现两个平台之间的数据交互,可以使用微服务中的 Feign、Ribbon 等组件进行服务间的通信,直接HTTP请求调用对方平台的接口也可以。接口调用的方式由于实时性和性能的原因,一般都使用定时任务来进行全量和增量的数据同步。就是利用 XXL-JOB定期执行任务,查询源平台数据并保存到同步平台。
  • 消息队列

    利用消息中间件(如 RabbitMQ、Kafka、RocketMQ)实现异步数据同步。源平台数据变更时发送事件变动消息到消息队列,同步平台消费消息并处理数据同步。
  • 数据库同步

    使用数据库复制技术(如 MySQL 主从复制、GoldenDB、DataX)直接同步两个平台的数据库,也可以结合定时任务框架XXL-JOB触发同步作业。
  • 文件导入导出

    将数据导出为文件(如 CSV、JSON、XML),再通过接口或脚本导入到同步平台。
  • ETL 工具

    使用 ETL 工具(如 Kettle、Informatica)进行源平台的数据抽取、转换和加载,然后写入到同步平台。

定时任务 接口调用

image.png

上图是简单数据同步方案的示意图,第一次同步全量的数据,如果对方的数据表数据量小可以直接全量同步,如果数据量大不允许全量同步,就只能后续增量同步

异常解决方案

  1. 引入幂等性机制 对每条数据增加唯一标识(如 UUID),防止重复插入。
  2. 日志记录与监控 记录每次同步的开始时间、结束时间、成功/失败数量。
  3. 失败重试机制 使用 XXL-JOB 内置的重试功能。
  4. 断点续传 若同步中断,下次从上次失败位置继续执行。
  5. 数据校验机制 同步完成后,对关键字段做一致性校验。
  6. 性能问题 可以使用多线程分片拉取数据