介绍
XXL-JOB 是一个轻量级分布式任务调度框架,通过中心化的调度器(支持集群)统一管理任务,解决 Spring @Scheduled 在分布式环境下的任务重复执行问题,并提供任务分片、失败重试、可视化监控等功能。
开源地址:https://www.xuxueli.com/xxl-job/
使用
环境准备和初始化
- 源码下载:https://gitee.com/xuxueli0323/xxl-job
- 找到初始化数据库文件并执行

- 修改配置
调度中心配置文件地址:/xxl-job/xxl-job-admin/src/main/resources/application.properties
这里主要修改数据源配置:url、username、password。

- 启动项目

- 访问调度中心
地址: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 accessToken: default_token appName: test 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;
@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
|

修改路由策略为轮询

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

其他策略
- 第一个:直接选任务列表里的第一个机器干活。
- 最后一个:直接选任务列表里的最后一个机器干活。
- 轮询:轮流让每台机器干活,大家排队,一人一次。
- 随机:随便抽一台机器干活,全凭运气。
- 一致性HASH:同样的任务永远找同一台机器干,避免换来换去。
- 最不经常使用:挑平时干活最少的机器去干,雨露均沾。
- 最近最久未使用:挑最闲的机器(好久没干活的)去干。
- 故障转移:如果一台机器挂了,自动换另一台好的继续干。
- 忙碌转移:如果一台机器正忙着,就找其他闲着的机器干。
- 分片广播:让所有机器一起干同一个活,各自分一小块任务。
分片功能
需求:在指定的节假日,给平台所有用户发送祝福短信。
分片广播涉及两个概念:
- 分片总数:一共有多少台机器
- 分片索引:当前这台机器的下标(下标从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
| <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.4</version> </dependency>
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>
<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) + "毫秒"); } }
|
新建任务

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