什么是任务调度
- 什么是任务调度:
- 定义: 任务调度是指在计算机系统中,按照一定的策略和规则,在特定时间或条件下执行预定任务的过程。
- 举例:简单来说,可以类比为闹钟到了具体的时间开始报警
- 在企业中的需求: 在晚上后台定时进行数据同步和当天的数据统计
时间轮算法:
时间轮可以理解为一个环形的数组,在每一个格子里存放一个任务列表,当前时间指到对应的格子后拿出所有任务并执行。
- Netty 使用 HashedWheelTimer 实现高性能定时任务
- Kafka 内部用于管理副本同步、心跳检测等
- 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注解),一旦机器故障,任务无法执行。并且在集群环境下,会出现任务重复执行的问题。 - 缺乏集中管理:多个定时任务分散在不同项目中,难以统一管理和监控任务状态、执行日志等。
- 功能单一:无法满足复杂的任务调度需求,如任务依赖、任务分片、动态任务修改等。
- 单机局限性:单机部署的定时任务(如 Java 中使用
-
XXL - JOB 解决痛点:
- 高可用性:支持调度中心和执行器集群部署,避免单点故障。
- 集中管理:通过 Web 界面集中管理所有任务,方便进行任务的创建、修改、删除、启动、停止等操作。
- 丰富功能:具备任务依赖、任务分片、失败重试、超时控制、实时日志查看等丰富功能,满足各种复杂业务场景。
XXL_JOB的安装和部署
原本就是java代码编写的,所以直接以项目启动即可,项目结构和配置文件都是可以自定义修改的。因为有项目需要,所以安装和部署主要分为两个步骤,适配人大金仓数据,打包后使用docker部署在linux服务器环境使用。
人大金仓数据库适配
- xxl-job对应的版本是2.4.0,从官网下载源码zip包,也可以直接下载源码后打包gitee.com/xuxueli0323…
- 打开项目后,在pom文件中增加人大金仓驱动依赖
<!-- 人大金仓数据库驱动 -->
<dependency>
<groupId>cn.com.kingbase</groupId>
<artifactId>kingbase8</artifactId>
<version>8.6.0</version>
</dependency>
3. 在配置文件中更改数据库的连接信息,也可以使用其他数据库的驱动和连接
- 由于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');
- 尝试启动后,发现有一些报错,这些报错原因是因为数据库语句的对应,大体来说需要更改更改对应mapper的xml文件中的sql语句和mapper查询参数的一些问题
DATE_ADD(#{nowTime},- INTERVAL '${timeout} second')
- 未选择模式问题 xxl_job.‘表名’
- 字段单引号问题,直接去掉
- 函数问题 人大金仓默认不支持DATE_ADD 函数
- 分页问题 分页语句更改为 LIMIT #{pagesize} OFFSET #{offset}
修改后,本地启动测试成功,下一步开始docker部署
docker部署
- 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实现定时任务
应用服务中使用示例
- 增加对应版本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;
}
}
对应调度任务平台的配置
- 执行器管理:在网页上对应的执行器管理中新增客户端对应配置的执行器,自动注册和手动注册控制注册应用实例的ip地址管理
- 任务管理:在任务管理页面可以新增任务,选择刚才配置好的执行器,填写好任务的基本业务信息
- 可以配置好任务的报警邮件,下图是对应的xxl_job的邮件配置
4. 定时频率的配置,可以使用时间配置框代替cron表达式的编写,最下面有运行时间的示例
- 对应任务配置
- 任务的高级策略配置:路由策略、阻塞处理策略、调度过期策略、任务超时时间、失败重试次数
- 调度日志
- 运行报表
XXL_JOB的核心组件
上图是xxl_job官方的架构图,从中其实可以抽象出最重要的三个组件部分: 调度中心、执行器、调度任务
- 调度中心: 注册线程处理应用平台的注册、任务的发布、日志管理、监控运维
- 执行器: 执行任务、返回任务执行结果、记录任务日志
- 调度任务:待执行的任务使用队列存放,使用对应的jobHadler执行
案例
用一个常见的数据同步的需求来举例,实现数据同步有一些常见的解决方案
-
API 接口调用
通过 RESTful API 接口调用实现两个平台之间的数据交互,可以使用微服务中的 Feign、Ribbon 等组件进行服务间的通信,直接HTTP请求调用对方平台的接口也可以。接口调用的方式由于实时性和性能的原因,一般都使用定时任务来进行全量和增量的数据同步。就是利用 XXL-JOB定期执行任务,查询源平台数据并保存到同步平台。 -
消息队列
利用消息中间件(如 RabbitMQ、Kafka、RocketMQ)实现异步数据同步。源平台数据变更时发送事件变动消息到消息队列,同步平台消费消息并处理数据同步。 -
数据库同步
使用数据库复制技术(如 MySQL 主从复制、GoldenDB、DataX)直接同步两个平台的数据库,也可以结合定时任务框架XXL-JOB触发同步作业。 -
文件导入导出
将数据导出为文件(如 CSV、JSON、XML),再通过接口或脚本导入到同步平台。 -
ETL 工具
使用 ETL 工具(如 Kettle、Informatica)进行源平台的数据抽取、转换和加载,然后写入到同步平台。
定时任务 接口调用
上图是简单数据同步方案的示意图,第一次同步全量的数据,如果对方的数据表数据量小可以直接全量同步,如果数据量大不允许全量同步,就只能后续增量同步
异常解决方案
- 引入幂等性机制 对每条数据增加唯一标识(如 UUID),防止重复插入。
- 日志记录与监控 记录每次同步的开始时间、结束时间、成功/失败数量。
- 失败重试机制 使用 XXL-JOB 内置的重试功能。
- 断点续传 若同步中断,下次从上次失败位置继续执行。
- 数据校验机制 同步完成后,对关键字段做一致性校验。
- 性能问题 可以使用多线程分片拉取数据