介绍

XXL-JOB 是一个轻量级分布式任务调度框架,通过中心化的调度器(支持集群)统一管理任务,解决 Spring @Scheduled 在分布式环境下的任务重复执行问题,并提供任务分片、失败重试、可视化监控等功能。

开源地址:https://www.xuxueli.com/xxl-job/

使用

环境准备和初始化

  1. 源码下载:https://gitee.com/xuxueli0323/xxl-job
  2. 找到初始化数据库文件并执行

  1. 修改配置

调度中心配置文件地址:/xxl-job/xxl-job-admin/src/main/resources/application.properties

这里主要修改数据源配置:url、username、password。

  1. 启动项目

  1. 访问调度中心

地址:http://localhost:8080,端口号可自己在配置文件中指定

账号:admin

密码:123456

配置部署"执行器项目"

新建项目

添加依赖并配置

注意版本尽量和你下载的源码的版本一致

1
2
3
4
5
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>3.0.0</version>
</dependency>

配置

1
2
3
4
5
6
7
8
9
xxl:
job:
adminAddresses: http://127.0.0.1:8080 # xxl-job 调度中心部署根地址,如调度中心集群部署存在多个地址则用逗号分隔
accessToken: default_token # 需要和 xxl-job 上创建的 token 保持一致
appName: test # 需要和 xxl-job 上创建的执行器名称保持一致
ip: # 执行器 ip, 为空表示自动获取 ip
port: 10001 # 执行器端口号
logPath: /data/applogs/xxl-job/jobhandler # 执行器日志存放路径
logRetentionDays: 30 # 日志保留时间(单位:天)

新建执行器和任务

新建config文件夹:读取配置并注入使用

配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.zzh.xxxjobdemo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = XxlJobProperties.PREFIX)
@Component
@Data
public class XxlJobProperties {

public static final String PREFIX = "xxl.job";
private String adminAddresses;
private String accessToken;
private String appName;
private String ip;
private int port;
private String logPath;
private int logRetentionDays = 30;
}

注入使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.zzh.xxxjobdemo.config;

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


@Configuration
@Slf4j
public class XxlJobConfig {

@Resource
private XxlJobProperties xxlJobProperties;

/**
* 初始化执行器
* @return
*/
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdminAddresses());
xxlJobSpringExecutor.setAppname(xxlJobProperties.getAppName());
xxlJobSpringExecutor.setIp(xxlJobProperties.getIp());
xxlJobSpringExecutor.setPort(xxlJobProperties.getPort());
xxlJobSpringExecutor.setAccessToken(xxlJobProperties.getAccessToken());
xxlJobSpringExecutor.setLogPath(xxlJobProperties.getLogPath());
xxlJobSpringExecutor.setLogRetentionDays(xxlJobProperties.getLogRetentionDays());
return xxlJobSpringExecutor;
}
}

新建一个任务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.zzh.xxxjobdemo.job;

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

import java.util.Date;

@Component
public class SimpleXxlJob {
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception{
System.out.println("执行定时任务,执行时间:" + new Date());
}
}

@XxlJob 中的 demoJobHandler 要和创建任务时填写的 JobHandler 保持一致。

执行

在任务管理界面选中任务并选择执行一次

日志

GLUE 模式使用

先在 demo 项目中添加一个 service 类,然后重启项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.zzh.xxxjobdemo.service;

import org.springframework.stereotype.Service;

@Service
public class HelloService {


public void doSome(){
System.out.println("do some!");
}
public void doOther(){
System.out.println("do other!");
}
}

新建一个任务,选择 GLUE 模式

编辑代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xxl.job.service.handler;

import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
import org.springframework.beans.factory.annotation.Autowired;
import com.zzh.xxxjobdemo.service.HelloService;

public class DemoGlueJobHandler extends IJobHandler {

// 注入业务实现类
@Autowired
private HelloService helloService;

@Override
public void execute() throws Exception {
helloService.doSome();
}

}

点击执行任务即可

路由策略

在IDE中模拟集群

修改配置,添加 VM Options 选项,配置实例启动端口和执行器监听端口

1
-Dserver.port=8088 -Dxxl.job.port=10010

修改路由策略为轮询

在后台管理界面执行两次任务,可以观察到每一个实例都执行了一次

其他策略

  1. 第一个:直接选任务列表里的第一个机器干活。
  2. 最后一个:直接选任务列表里的最后一个机器干活。
  3. 轮询:轮流让每台机器干活,大家排队,一人一次。
  4. 随机:随便抽一台机器干活,全凭运气。
  5. 一致性HASH:同样的任务永远找同一台机器干,避免换来换去。
  6. 最不经常使用:挑平时干活最少的机器去干,雨露均沾。
  7. 最近最久未使用:挑最闲的机器(好久没干活的)去干。
  8. 故障转移:如果一台机器挂了,自动换另一台好的继续干。
  9. 忙碌转移:如果一台机器正忙着,就找其他闲着的机器干。
  10. 分片广播:让所有机器一起干同一个活,各自分一小块任务。

分片功能

需求:在指定的节假日,给平台所有用户发送祝福短信。

分片广播涉及两个概念:

  1. 分片总数:一共有多少台机器
  2. 分片索引:当前这台机器的下标(下标从0开始)

准备

数据库文件生成提示词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
你是一名 MySQL 数据库专家和数据生成专家。请帮我编写一个脚本,用于生成包含建表 SQL 以及 5000 条模拟数据的 SQL 文件。

### 一、 表结构要求 (DDL)
- **数据库/ Schema**: `xxl_job`
- **表名**: `t_user_mobile_plan`
- **Engine/字符集**: `InnoDB`, `utf8mb4`, 排序规则 `utf8mb4_0900_ai_ci`
- **字段定义**:
1. `id`: bigint, NOT NULL, AUTO_INCREMENT, 主键, 注释 '主键'
2. `username`: varchar(50), NOT NULL, 注释 '用户名'
3. `nickname`: varchar(50), NULL DEFAULT NULL, 注释 '昵称'
4. `phone`: varchar(20), NOT NULL, 注释 '手机号码'
5. `info`: varchar(500), NULL DEFAULT NULL, 注释 '备注'
- **其他**: 包含 `DROP TABLE IF EXISTS `t_user_mobile_plan`;` 语句,并在脚本开头和结尾分别加上 `SET FOREIGN_KEY_CHECKS = 0;` 和 `SET FOREIGN_KEY_CHECKS = 1;`。设置建表后的 `AUTO_INCREMENT = 5001`。

### 二、 数据生成规则 (5000条,ID 从 1 到 5000)
1. **`id`**: 从 1 递增至 5000。
2. **`username`**: 格式为 `user_{id}`(如 `user_1`, `user_2`)。
3. **`nickname`**: 格式为 `用户{id}`(如 `用户1`, `用户2`)。
4. **`phone`**: 生成符合中国大陆手机号规则的数据(如 131, 151, 181 开头,按 ID 规律递增或合理伪随机生成 11 位号码)。
5. **`info`**: 根据 `id % 5` 的余数按如下规律循环填入:
- `id % 5 == 1`: `"普通用户,创建于批次{batch}"` (其中 batch 计算公式为: (id - 1) // 100 + 1,即 1~100 为批次1,101~200 为批次2,以此类推)
- `id % 5 == 2`: `"测试账号,序号:{id}"`
- `id % 5 == 3`: `"无特殊备注"`
- `id % 5 == 4`: `"手机套餐用户,编号:{id}"`
- `id % 5 == 0`: `"VIP客户,第{id}号用户"`

### 三、 输出要求
由于 5000 条 SQL 超过了单次回答的字数限制,请提供以下 3 种实现方式:
1. **Python 生成脚本**:提供一段完整的 Python 代码,运行后可直接生成目标 `.sql` 文件。
2. **MySQL 存储过程**:提供一段可以在 MySQL 中直接执行的存储过程代码,调用后自动创建表并插入这 5000 条数据。
3. **SQL 示例**:展示前 10 条和最后 5 条生成的 `INSERT INTO` SQL 语句样例。

添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!--lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

数据库配置

1
2
3
4
5
6
7
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxl_job_demo
username: root
password: xxxxxx

启动类上添加扫描类

1
2
3
4
5
6
7
@MapperScan(basePackages = "com.zzh.xxxjobdemo.mapper")
@SpringBootApplication
public class XxxJobDemoApplication {
public static void main(String[] args) {
SpringApplication.run(XxxJobDemoApplication.class, args);
}
}

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class UserMobilePlan {
// 主键
private Long id;
// 用户名
private String username;
// 昵称
private String nickname;
// 手机号码
private String phone;
// 备注
private String info;
}

mapper 类

1
2
3
4
5
6
7
8
public interface UserMobilePlanMapper {

@Select("select * from t_user_mobile_plan")
List<UserMobilePlan> selectAll();

@Select("select * from t_user_mobile_plan where mod(id, #{shardTotal}) = #{shardIndex}")
List<UserMobilePlan> selectByMod(@Param("shardIndex") int shardIndex, @Param("shardTotal") int shardTotal);
}

Job 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component
public class SendJob {

@Resource
private UserMobilePlanMapper userMobilePlanMapper;

@XxlJob("sendMsgHandler")
public void sendMsgHandler(){
// 分片总数
int shardTotal = XxlJobHelper.getShardTotal();
// 分片索引
int shardIndex = XxlJobHelper.getShardIndex();

// 查询数据
List<UserMobilePlan> userMobilePlans = null;
if(shardTotal == 1) {
userMobilePlans = userMobilePlanMapper.selectAll();
}else{
userMobilePlans = userMobilePlanMapper.selectByMod(shardIndex, shardTotal);
}

// 处理数据
System.out.println("开始执行任务时间:" + new Date() + ",需要处理的数据总量:" + userMobilePlans.size());
long begin = System.currentTimeMillis();
userMobilePlans.forEach(item -> {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println("结束执行任务时间:" + new Date());
long end = System.currentTimeMillis();
System.out.println("处理任务总耗时" + (end - begin) + "毫秒");
}
}

新建任务

启动执行

启动两个实例,并启动任务,观察输出