Appearance
XXL-JOB 使用介绍
本文介绍XXL-JOB的基本使用与CRON表达式。
1. 介绍与优势
定时任务是指在特定时间或以特定间隔自动执行的程序或脚本。例如:
- 某电商平台需要在每周一上午10点发放1000张优惠券;
- 某财务系统需要在每天凌晨01:00:00结算前一天的财务数据,进行统计汇总;
在微服务环境下,如果存在多个服务实例,那么定时任务就会执行多次,造成问题。例如,如果该电商平台优惠券服务部署多实例,那么就会造成发放的优惠券不止1000张。
XXL-JOB是一个分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。
官网地址:https://www.xuxueli.com/xxl-job/
分布式任务调度平台可以解决如下问题:
- 防止重复执行:如果有多实例同时运行,会造成定时任务重复执行出现问题,分布式任务调度平台保证每个时间点只有一个实例在执行任务;
- 高可用:单机版的定时任务调度只能在一台机器上运行,如果程序或系统出现问题,就会导致功能不可用;分布式任务调度,可以保证高可用;
- 提升效率:如果现在有大量任务需要处理。分布式任务调度可以并行处理任务,提升效率;
XXL-JOB的架构图如下,可以发现,XXL-JOB主要分为两部分:
调度中心
负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。 调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
执行器
负责接收调度请求并执行任务逻辑。 任务模块专注于任务的执行等操作,开发和维护更加简单和高效; 接收“调度中心”的执行请求、终止请求和日志请求等。

2. 入门案例
2.1 环境准备
本小节介绍XXL-JOB的入门案例,首先下载源码:https://github.com/xuxueli/xxl-job/releases,下载完成后解压并用IDEA打开,项目结构如下:

- xxl-job-admin:调度中心源码,可以启动该服务,即启动调动中心;
- xxl-job-core:执行器源码,可以在自己项目中引入该模块,即可以引入执行器;
- xxl-job-executor-samples:案例代码;
2.2 调度中心的部署
首先,运行XXL-JOB项目下的doc/db/tables_xxl-job.sql脚本文件:

然后,打开xxl-job-admin服务配置文件application.properties,主要关注以下配置:
properties
# 调度中心启动端口
server.port=28080
# 数据库配置
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
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# xxl-job, access token, 主要用于和执行器通信
xxl.job.accessToken=default_token在启动项目时,有关日志路径可能会报文件不存在,需要修改日志路径,resources/logback.xml文件中找到如下一行:
xml
<property name="log.path" value="./data/applogs/xxl-job/xxl-job-admin.log"/>将日志路径修改一下就可以了。
之后启动服务,在浏览器中访问路径:http://localhost:28080/xxl-job-admin,显示登录界面,默认的账号密码为admin/123456:

登录成功后即可进入调度中心主界面:

至此,调度中心成功启动。
2.3 执行器的部署
所谓执行器,就是我们自己的项目,首先在项目中引入依赖:
xml
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>3.1.1</version>
</dependency>然后在配置文件中写入以下有关XXL-JOB的内容:
properties
### 调度中心部署根地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:28080/xxl-job-admin
### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.admin.accessToken=default_token
### 调度中心通讯超时时间[选填],单位秒;默认3s;
xxl.job.admin.timeout=3
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=my-xxl-job-executor
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯使用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=127.0.0.1
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=./data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=30向容器中注入XXL-JOB配置类:
java
@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.admin.accessToken}")
private String accessToken;
@Value("${xxl.job.admin.timeout}")
private int timeout;
@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.setTimeout(timeout);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}最后创建任务类:
java
@Component
public class HelloJob {
private static final Logger logger = Logger.getLogger(HelloJob.class.getName());
@XxlJob(value = "helloJob")
public void execute() {
logger.info("hello job");
}
}使用注解@XxlJob指定任务方法和任务名。
启动执行器项目,然后在调度中心的执行器管理界面,查看注册的执行器:

WARNING
注意,在执行器的配置文件中,如果修改了appName(xxl.job.executor.appname=my-xxl-job-executor),则需要在调度中心新建一个执行器分组,否则看不到。
2.4 任务的执行
当注册上执行器后,我们就可以到调度中心-任务管理界面,新建任务:

- 选择执行器为自己创建的执行器;
- 运行模式选
Bean,JobHandler填写执行器中的任务名称; - Cron表达式设置为5秒执行一次;
保存后,启动任务。
在执行器控制界面,查看任务是否成功执行:

至此,入门案例成功完成。
3. 其他配置
在创建任务时,还有许多配置可以修改,这一小节介绍这些配置。
3.1 任务参数
在配置任务时,可以设置任务参数:

推荐使用JSON格式填写任务参数。
然后,在任务中通过XxlJobHelper获取任务参数:
java
@XxlJob(value = "helloJob")
public void execute() {
String jobParam = XxlJobHelper.getJobParam();
logger.info("jobParam:" + jobParam);
logger.info("hello job");
}结果如下:

可以发现能正确获取到配置的任务参数。
3.2 GLUE模式
在配置任务时,除了BEAN模式,还有GLUE模式。
GLUE模式是指任务以源码方式维护在调度中心,支持通过Web IDE在线更新,实时编译和生效,因此不需要指定JobHandler。
首先,在执行器项目中新增一个任务类:
java
@Component
public class GLUEJob {
private static final Logger logger = Logger.getLogger(GLUEJob.class.getName());
public void method1(){
logger.info("method1");
}
public void method2(){
logger.info("method2");
}
}注意,该任务类只是一个简单的组件,并没有使用@XxlJob标注。
然后,在调度中心新建一个任务,模式设置为GLUE:

保存后在任务列表,选中新创建的任务,操作按钮下拉选型选择GLUE IDE,打开在线源码编辑器:
输入以下代码:
java
package org.example.springbootdemo.job;
import com.xxl.job.core.handler.IJobHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class GLUEJogExecutor extends IJobHandler {
@Autowired
private GLUEJob glueJob;
@Override
public void execute() throws Exception {
glueJob.method1();
}
}
输入版本号后(方便回溯)保存关闭。
然后,启动任务:

可以发现任务顺利运行。
总结,GLUE模式可以让非任务方法实时变为任务。
3.3 服务多实例(路由策略)
由于XXL- JOB是分布式任务调度框架,所以本小节启动两个微服务,注意,由于在同一台机上启动多个微服务,有可能造成端口冲突,所以需要配置为不同的端口(VM参数):
txt
-Dserver.port=8081 -Dxxl.job.executor.port=9998
启动成功后,在执行器界面也能看到两个节点:

回到第一个测试任务helloJob,启动任务,会发现任务只在一个服务实例上运行,这保证了任务不会被重复执行。
在创建任务时,有一个配置路由策略,可以配置当有多个执行器时,选择哪一个执行器执行任务,选项如下:
第一个:总是选择可用执行器列表中的第一个执行器。
最后一个:总是选择可用执行器列表中的最后一个执行器。
轮询:按照顺序依次选择下一个可用执行器,实现轮流执行。
随机:每次从可用执行器列表中随机选择一个执行器。
一致性HASH:根据任务的调度参数(例如,JobId、任务参数等)计算哈希值,然后根据哈希值选择对应的执行器。相同的哈希值总是路由到同一个执行器。
最不经常使用:选择历史调度次数最少的那个执行器来执行任务。
最近最久未使用:选择历史调度时间最久远的那个执行器来执行任务(即,最近没有被调度的执行器)。
故障转移:按照执行器列表顺序依次尝试调度,如果当前执行器失败,则尝试下一个,直到成功。
忙碌转移:当调度中心发现某个执行器正在忙碌(例如,正在执行大量任务,或者某个任务长时间未返回)时,会尝试将任务调度到集群中其他不那么忙碌的执行器上。
分片广播:将一个任务拆分成多个分片,然后将这些分片广播到所有可用的执行器上,每个执行器独立地处理分配给自己的分片。在执行器中,使用如下方式获取总分片和当前分片:
java// 获取总分片 int shardTotal = XxlJobHelper.getShardTotal(); // 获取当前分片 int shardIndex = XxlJobHelper.getShardIndex();例如,在调度中心修改路由策略为分片广播,然后在执行器任务中获取当前分片和总分片:
java@XxlJob(value = "helloJob") public void execute() { // 获取总分片 int shardTotal = XxlJobHelper.getShardTotal(); // 获取当前分片 int shardIndex = XxlJobHelper.getShardIndex(); logger.info(String.format("分片参数:当前分片序号 = %s, 总分片数 = %s", shardIndex, shardTotal)); }结果发现每个执行器节点都会被调用,这样可以并行执行任务。
4. CRON表达式
CRON 表达式 是一种用于配置定时任务的字符串,它指定了任务在什么时候运行。
一个标准的 Cron 表达式由 六个或七个字段 组成,每个字段代表一个时间单位,并用空格分隔。每个字段都有特定的取值范围和含义:
- 秒:取值范围
0-59。 - 分钟:取值范围
0-59。 - 小时:取值范围
0-23(24 小时制)。 - 日期 (月份中的天):取值范围
1-31。 - 月份:取值范围
1-12(或使用JAN,FEB等缩写)。 - 星期几:取值范围
0-7(其中0和7都代表星期日,1代表星期一,以此类推;或使用SUN,MON等缩写)。 - 年份 (可选):取值范围
1970-2099。
大部分情况下,使用的是 六个字段 的 Cron 表达式,省略了年份。

Cron 表达式通过使用一些特殊字符来提供更强大的调度能力:
- ***** (星号):表示“每”或“所有”。例如,在小时字段中使用
*表示每小时。 - ? (问号):表示“不指定”。通常用于日期和星期几字段,因为这两者可能存在冲突。例如,如果指定了特定的日期,就不需要再指定星期几,反之亦然。
- - (连字符):表示范围。例如,在小时字段中使用
9-17表示从上午 9 点到下午 5 点的每个小时。 - , (逗号):表示列表值。例如,在星期几字段中使用
MON,WED,FRI表示每周一、周三和周五。 - / (斜杠):表示步长或增量。例如,在分钟字段中使用
0/15表示从第 0 分钟开始,每 15 分钟执行一次 (即 0, 15, 30, 45 分)。 - L (last):表示“最后”。
- 在日期字段中使用
L表示该月的最后一天。 - 在星期几字段中使用
L表示该月的最后一个星期几(例如,5L表示该月的最后一个星期五)。
- 在日期字段中使用
- W (weekday):表示“最近的工作日”。
- 在日期字段中使用
nW表示离指定日期n最近的工作日。例如,15W表示离该月 15 号最近的工作日。
- 在日期字段中使用
- # (哈希):用于星期几字段,表示该月的第几个星期几。例如,
6#3表示该月的第三个星期六。
一些CRON表达式示例:
0 0 * * * ?:每天午夜 0 点执行。0 15 10 * * ?:每天上午 10:15 执行。0 0/5 14,18 * * ?:每天下午 2 点到 2 点 55 分之间以及下午 6 点到 6 点 55 分之间,每隔 5 分钟执行一次。0 0 10 L * ?:每月最后一天上午 10 点执行。0 0 10 ? * 6#3:每月第三个星期六上午 10 点执行。