XXL - Job 实战:从入门到应对高并发任务
XXL - Job 实战:从入门到应对高并发任务
引言
在微服务架构盛行的今天,定时任务的管理和调度变得尤为重要。XXL - Job 作为一个分布式任务调度平台,以其简单易用、功能强大的特点,受到了众多开发者的青睐。本文将详细介绍如何在 Java 8 环境的微服务项目中,利用 Docker 部署并使用 XXL - Job,同时探讨如何应对大量任务同时执行的场景。
一、XXL - Job 调度中心部署
1.1 准备数据库
XXL - Job 需要 MySQL 数据库来存储任务调度相关的数据。首先创建一个名为 xxl_job
的数据库,然后执行 xxl - job/doc/db/tables_xxl_job.sql 脚本。
1.2 拉取并运行 Docker 镜像
docker pull xuxueli/xxl-job-admin:2.3.1
docker run -e PARAMS="--spring.datasource.url=jdbc:mysql://<your_mysql_host>:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=<your_mysql_username> --spring.datasource.password=<your_mysql_password>" -p 8080:8080 -v /tmp:/data/applogs --name xxl-job-admin -d xuxueli/xxl-job-admin:2.3.1
将 <your_mysql_host>
、<your_mysql_username>
和 <your_mysql_password>
替换为实际的数据库信息。
二、微服务项目引入 XXL - Job 执行器
2.1 添加依赖
在项目的 pom.xml
中添加以下依赖:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.1</version>
</dependency>
2.2 配置执行器
在 application.properties
中添加配置:
xxl.job.admin.addresses=http://<your_xxl_job_admin_host>:8080/xxl-job-admin
xxl.job.accessToken=
xxl.job.executor.appname=your_executor_appname
xxl.job.executor.address=
xxl.job.executor.ip=
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
xxl.job.executor.logretentiondays=30
将 <your_xxl_job_admin_host>
和 your_executor_appname
替换为实际信息。
2.3 创建执行器配置类
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class XxlJobConfig {
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses("http://<your_xxl_job_admin_host>:8080/xxl-job-admin");
xxlJobSpringExecutor.setAppname("your_executor_appname");
xxlJobSpringExecutor.setIp("");
xxlJobSpringExecutor.setPort(9999);
xxlJobSpringExecutor.setAccessToken("");
xxlJobSpringExecutor.setLogPath("/data/applogs/xxl-job/jobhandler");
xxlJobSpringExecutor.setLogRetentionDays(30);
return xxlJobSpringExecutor;
}
}
2.4 创建任务示例
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
@Component
public class SampleXxlJob {
@XxlJob("demoJobHandler")
public void demoJobHandler() throws Exception {
XxlJobHelper.log("XXL-Job sample job start.");
// 业务逻辑
XxlJobHelper.log("XXL-Job sample job end.");
}
}
2.5 打包并部署到 Docker
创建 Dockerfile
:
FROM openjdk:8-jdk-alpine
WORKDIR /app
COPY target/your-service.jar app.jar
EXPOSE 9999
CMD ["java", "-jar", "app.jar"]
构建并运行 Docker 容器:
docker build -t your-service:1.0 .
docker run -p 9999:9999 --name your-service -d your-service:1.0
2.6 在调度中心配置任务
访问 http://<your_xxl_job_admin_host>:8080/xxl-job-admin
,使用默认用户名 admin
和密码 123456
登录。在“执行器管理”中新增执行器,AppName 填写 your_executor_appname
;在“任务管理”中新增任务,选择对应的执行器和任务处理器(如 demoJobHandler
),配置调度策略和执行参数等。
三、常见问题及解决办法
3.1 访问令牌错误
在日志中可能会出现 The access token is wrong.
的错误,这是因为执行器和调度中心的访问令牌不一致。可以通过以下方式解决:
- 确保调度中心和执行器的
accessToken
配置相同。 - 若不想使用访问令牌,可将其置为空。
3.2 大量任务同时执行问题
当有大量任务(如 100 个或 500 个)都在同一时间(如早晨 11 点)执行时,可能会出现任务丢失未执行的情况,原因及解决办法如下:
3.2.1 调度中心负载过高
- 原因:同一时刻触发大量任务,调度中心处理能力达到极限。
- 解决办法:采用集群部署方式,通过负载均衡器将任务调度请求分发到多个调度中心实例上;为调度中心所在服务器分配足够的硬件资源,并调整 JVM 参数。
3.2.2 执行器资源不足
- 原因:执行器的线程池、数据库连接池等资源被迅速耗尽。
- 解决办法:调整执行器线程池大小和队列容量,例如:
同时,增加数据库连接池大小:xxl.job.executor.threadpool.core-size=200 xxl.job.executor.threadpool.max-size=500 xxl.job.executor.threadpool.queue-capacity=1000
spring.datasource.hikari.maximum-pool-size=200 spring.datasource.hikari.minimum-idle=50
3.2.3 网络问题
- 原因:调度中心和执行器之间的网络故障、丢包或延迟过高。
- 解决办法:对网络进行实时监控,使用负载均衡器均衡网络流量。
3.2.4 任务阻塞策略
- 原因:任务的阻塞策略设置不合理,部分任务可能被丢弃。
- 解决办法:选择合适的阻塞策略,如串行执行策略:
createParams.put("executorBlockStrategy", ExecutorBlockStrategyEnum.SERIAL_EXECUTION.name());
3.2.5 任务分散执行
为避免所有任务在同一时刻集中触发,可以将任务的执行时间分散在 11 点前后的一小段时间内:
import java.util.ArrayList;
import java.util.List;
public class CronDistributor {
public static List<String> distributeCron(int taskCount, int startSecond, int endSecond) {
List<String> cronExpressions = new ArrayList<>();
int interval = (endSecond - startSecond) / taskCount;
for (int i = 0; i < taskCount; i++) {
int second = startSecond + i * interval;
cronExpressions.add(second + " 0 11 * * ?");
}
return cronExpressions;
}
}
四、代码创建和管理定时任务
可以通过 XXL - Job 提供的 API 以代码的方式创建和管理定时任务,示例代码如下:
4.1 创建任务处理类
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
@Component
public class FiveSecondJob {
@XxlJob("fiveSecondJobHandler")
public void fiveSecondJobHandler() {
try {
XxlJobHelper.log("Five second job started.");
System.out.println("This job is executed every 5 seconds.");
XxlJobHelper.log("Five second job finished.");
} catch (Exception e) {
XxlJobHelper.log("Error occurred in five second job: {}", e.getMessage());
XxlJobHelper.handleFail("Job execution failed due to an exception.");
}
}
}
4.2 使用 API 创建和启动任务
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.enums.MisfireStrategyEnum;
import com.xxl.job.core.enums.RouteStrategyEnum;
import com.xxl.job.core.util.XxlJobRemotingUtil;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class XxlJobManager {
private static final String XXL_JOB_ADMIN_ADDRESS = "http://<your_xxl_job_admin_host>:8080/xxl-job-admin";
private static final String ACCESS_TOKEN = "";
public void createAndStartJob() {
Map<String, String> createParams = new HashMap<>();
// 配置任务参数
ReturnT<String> createResult = XxlJobRemotingUtil.postBody(XXL_JOB_ADMIN_ADDRESS + "/jobinfo/add", ACCESS_TOKEN, 5000, createParams, String.class);
if (createResult.getCode() == ReturnT.SUCCESS_CODE) {
int jobId = Integer.parseInt(createResult.getContent());
Map<String, Object> startParams = new HashMap<>();
startParams.put("id", jobId);
ReturnT<String> startResult = XxlJobRemotingUtil.postBody(XXL_JOB_ADMIN_ADDRESS + "/jobinfo/start", ACCESS_TOKEN, 5000, startParams, String.class);
if (startResult.getCode() == ReturnT.SUCCESS_CODE) {
System.out.println("Job started successfully.");
} else {
System.out.println("Failed to start job: " + startResult.getMsg());
}
} else {
System.out.println("Failed to create job: " + createResult.getMsg());
}
}
}
4.3 调用创建和启动任务的方法
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class YourApplication implements CommandLineRunner {
@Autowired
private XxlJobManager xxlJobManager;
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
@Override
public void run(String... args) throws Exception {
xxlJobManager.createAndStartJob();
}
}
五、根据页面选择时间创建定时任务
假设需要根据页面选择的开始时间和结束时间,让任务每天早上 11 点执行,可以按照以下步骤实现:
5.1 接收前端传递的时间
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class JobController {
@Autowired
private XxlJobService xxlJobService;
@PostMapping("/createJob")
public String createJob(@RequestBody Map<String, String> request) {
String startTime = request.get("startTime");
String endTime = request.get("endTime");
return xxlJobService.createJob(startTime, endTime);
}
}
5.2 生成 Cron 表达式
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
public class CronGenerator {
public static String generateCron(String startTime, String endTime) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
Date startDate = sdf.parse(startTime);
Date endDate = sdf.parse(endTime);
Calendar startCal = Calendar.getInstance();
startCal.setTime(startDate);
Calendar endCal = Calendar.getInstance();
endCal.setTime(endDate);
int startDay = startCal.get(Calendar.DAY_OF_MONTH);
int endDay = endCal.get(Calendar.DAY_OF_MONTH);
int startMonth = startCal.get(Calendar.MONTH) + 1;
int endMonth = endCal.get(Calendar.MONTH) + 1;
int startYear = startCal.get(Calendar.YEAR);
int endYear = endCal.get(Calendar.YEAR);
return "0 0 11 " + startDay + "-" + endDay + " " + startMonth + "-" + endMonth + " ? " + startYear + "-" + endYear;
} catch (ParseException e) {
e.printStackTrace();
return null;
}
}
}
5.3 使用 XXL - Job API 创建和启动任务
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.enums.ExecutorBlockStrategyEnum;
import com.xxl.job.core.enums.MisfireStrategyEnum;
import com.xxl.job.core.enums.RouteStrategyEnum;
import com.xxl.job.core.util.XxlJobRemotingUtil;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class XxlJobService {
private static final String XXL_JOB_ADMIN_ADDRESS = "http://<your_xxl_job_admin_host>:8080/xxl-job-admin";
private static final String ACCESS_TOKEN = "";
public String createJob(String startTime, String endTime) {
String cron = CronGenerator.generateCron(startTime, endTime);
if (cron == null) {
return "Failed to generate cron expression.";
}
Map<String, String> createParams = new HashMap<>();
// 配置任务参数
ReturnT<String> createResult = XxlJobRemotingUtil.postBody(XXL_JOB_ADMIN_ADDRESS + "/jobinfo/add", ACCESS_TOKEN, 5000, createParams, String.class);
if (createResult.getCode() == ReturnT.SUCCESS_CODE) {
int jobId = Integer.parseInt(createResult.getContent());
Map<String, Object> startParams = new HashMap<>();
startParams.put("id", jobId);
ReturnT<String> startResult = XxlJobRemotingUtil.postBody(XXL_JOB_ADMIN_ADDRESS + "/jobinfo/start", ACCESS_TOKEN, 5000, startParams, String.class);
if (startResult.getCode() == ReturnT.SUCCESS_CODE) {
return "Job created and started successfully.";
} else {
return "Failed to start job: " + startResult.getMsg();
}
} else {
return "Failed to create job: " + createResult.getMsg();
}
}
}
5.4 任务处理类
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
@Component
public class YourJobHandler {
@XxlJob("yourJobHandler")
public void yourJobHandler() {
try {
XxlJobHelper.log("Job started.");
System.out.println("This job is executed at 11:00 AM between selected dates.");
XxlJobHelper.log("Job finished.");
} catch (Exception e) {
XxlJobHelper.log("Error occurred in job: {}", e.getMessage());
XxlJobHelper.handleFail("Job execution failed due to an exception.");
}
}
}
六、总结
通过以上步骤,我们详细介绍了 XXL - Job 的部署、使用方法,以及如何应对大量任务同时执行的场景。在实际应用中,需要根据具体的业务需求和系统资源情况,对 XXL - Job 进行合理的配置和优化。
本作品采用《CC 协议》,转载必须注明作者和本文链接