前言
前段时间工作中用到了XXL-JOB,所以想着写一篇博客记录一下,比较懒,拖了很久。。。
一、简介
官网地址:https://www.xuxueli.com/xxl-job/
设计思想
将"调度"和"任务"进行解耦。
系统组成
调度模块(调度中心):负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。
架构图
二、如何使用
下载地址
源码地址:https://gitee.com/xuxueli0323/xxl-job/releases
目前最新版本是2.3.0
源码下载下来之后,执行其中的sql文件(xxl-job-master\doc\db\tables_xxl_job.sql),所使用的的数据库是mysql,版本5.7或以上。
简单说说
启动xxl-job-admin模块,在浏览器访问localhost:8080//xxl-job-admin,输入用户密码admin/123456,即可进入页面。
这里一些可视化的功能就不去介绍了。
对照界面中的任务管理和执行器管理以及数据库的表数据,可以看出对应关系。
所以,可以猜到源码中包含可以查询执行器和调度任务的接口,是的,就在这里。
一个栗子
在XXL-JOB上新建一个执行器xxl-job-executor-test,这里的注册方式选择自动注册就好,业务服务在启动时会将自己机器的ip和端口号注册到这个执行器上。
新建一个属于该执行器的执行任务,这里选择“测试执行器”,也就刚才新建的执行器,调度类型选择固定速度,5秒执行一次,运行模式选择bean,执行时会根据注册的执行地址以及JobHandler找到对应的执行方法。
调度任务这里有Cron和固定速度两种,假如使用cron,它会以固定的时间点执行任务,而不是在定时器启动后开始计算时间执行;使用固定速度,那么就会从定时器启动后开始计算下一次执行时间。
新建一个springboot服务,当做是业务服务。
依赖:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
配置文件
server:
port: 8090
tomcat:
uri-encoding: UTF-8
max-connections: 1024
max-threads: 100
xxl:
job:
isConfigure: true #选择性注册,true说明将自己机器的ip和端口号注册进执行器
scheduleConf: 60 #定时任务的执行间隔
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin #xxl-job的地址
userName: admin
password: 123456
executor:
appname: xxl-job-executor-sample #执行器的名字
logpath: /data/applogs/xxl-job/jobhandler/
logretentiondays: -1 #过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
port: 9999 #任务执行地址的端口
向执行器注册执行地址的配置类
@Configuration
@Slf4j
@ConditionalOnProperty(value = "xxl.job.isConfigure", havingValue = "true", matchIfMissing = false)
public class XxlJobConfig {
@Autowired
private XxlJobProperties xxlJobProperties;
@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
log.info(">>>>>>>>>>> xxl-job config init. xxlJobProperties:{}", JSONObject.toJSONString(xxlJobProperties));
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(xxlJobProperties.getAdmin().getAddresses());
xxlJobSpringExecutor.setAppname(xxlJobProperties.getExecutor().getAppname());
xxlJobSpringExecutor.setPort(xxlJobProperties.getExecutor().getPort());
xxlJobSpringExecutor.setLogPath(xxlJobProperties.getExecutor().getLogpath());
xxlJobSpringExecutor.setLogRetentionDays(xxlJobProperties.getExecutor().getLogretentiondays());
return xxlJobSpringExecutor;
}
}
yaml文件属性注入的配置类
@Data
@Component
@ConfigurationProperties(prefix = "xxl.job")
public class XxlJobProperties {
private Admin admin;
private Executor executor;
private String scheduleConf;
private String orderPoolName;
@Data
public static class Admin {
private String addresses;
private String userName;
private String password;
}
@Data
public static class Executor {
private String appname;
private String logpath;
private int logretentiondays;
private int port;
}
}
任务执行逻辑,xxl-job会根据注解@XxlJob(“JobHandler”)找到对应执行方法
@Component
@Slf4j
public class XxlJobHandler {
@XxlJob("JobHandler")
public ReturnT<String> jobHandler() {
String param = XxlJobHelper.getJobParam();
String strDateFormat = "yyyy-MM-dd HH:mm:ss";
SimpleDateFormat sdf = new SimpleDateFormat(strDateFormat);
log.info("time : {} param: {}", sdf.format(new Date()), param);
return ReturnT.SUCCESS;
}
}
启动两个业务服务,要注意的是,除了两个服务的启动端口不同之外,还有注册到XXL-JOB的端口也要不同。
如果上述操作正确,那么在XXL-JOB界面上的执行器中会看到两个注册成功的地址,如下:
自此,这个执行器上的定时任务将会根据这两个注册地址去查找对应的JobHandler并执行任务。
然后启动定时任务,查看业务服务的表现
仔细看时间戳会发现,这个执行策略其实是轮训,因为在创建执行器时,将路由策略选择为轮训
三、动态添加定时任务
有时候我们需要在业务服务中心动态的添加定时任务,之前有说过,XXL-JOB的源码中提供了对外的api,支持对执行器和定时任务的查询和新增。
对XXL-JOB的api的封装:
@Component
@Slf4j
public class XxlJobCommon {
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.admin.userName}")
private String userName;
@Value("${xxl.job.admin.password}")
private String password;
@Value("${xxl.job.executor.appname}")
private String appname;
/**
* 动态添加订单取消的定时器
*
* @param xxlJobInfo
* @return
*/
public int addJob(XxlJobInfo xxlJobInfo) {
log.info("添加定时器入参xxlJobInfo:{}", JSONObject.toJSONString(xxlJobInfo));
int jobId = 0;
getCookie();//获取cookie
try {
String path = adminAddresses + "/jobinfo/add";
String jobgroupPath = "/jobgroup/pageList";
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("jobGroup", getJobGroupId(jobgroupPath));
paramMap.put("jobDesc", xxlJobInfo.getJobDesc());
paramMap.put("executorRouteStrategy", xxlJobInfo.getExecutorRouteStrategy());// 路由策略
paramMap.put("glueType", xxlJobInfo.getGlueType());
paramMap.put("executorHandler", xxlJobInfo.getExecutorHandler()); // 此处hander需提前在项目中定义
paramMap.put("executorBlockStrategy", xxlJobInfo.getExecutorBlockStrategy());
paramMap.put("executorTimeout", xxlJobInfo.getExecutorTimeout());
paramMap.put("executorFailRetryCount", xxlJobInfo.getExecutorFailRetryCount());//执行失败重试
paramMap.put("author", xxlJobInfo.getAuthor());
paramMap.put("scheduleType", xxlJobInfo.getScheduleType());
paramMap.put("scheduleConf", xxlJobInfo.getScheduleConf());
paramMap.put("glueRemark", xxlJobInfo.getGlueRemark());
paramMap.put("triggerStatus", xxlJobInfo.getTriggerStatus()); //调度状态:0-停止,1-运行
paramMap.put("misfireStrategy", xxlJobInfo.getMisfireStrategy());
paramMap.put("executorParam", xxlJobInfo.getExecutorParam());//入参
HttpResponse response = HttpRequest.post(path).form(paramMap).execute();
if (HttpStatus.HTTP_OK != response.getStatus()) {
log.error("订单:{} 定时器创建失败", xxlJobInfo.getExecutorParam());
}
JSONObject jsonObject = JSON.parseObject(response.body());
log.info("--->jsonObject:{}", jsonObject.toJSONString());
jobId = jsonObject.getIntValue("content");
} catch (Exception e) {
log.error("订单:{} 定时器创建失败", xxlJobInfo.getExecutorParam(), e);
}
return jobId;
}
/**
* 获取jogGroup的id
*
* @param jobgroupPath
* @return
* @throws Exception
*/
public int getJobGroupId(String jobgroupPath) throws Exception {
Map<String, Object> jobgroupParamMap = new HashMap<>();
//获取jobGroup的Id
jobgroupParamMap.put("appname", appname);
HttpResponse jobgroupResponse = HttpRequest.post(adminAddresses + jobgroupPath).form(jobgroupParamMap).execute();
log.info("jobgroupResponse : {}", JSON.toJSONString(jobgroupResponse.body()));
Map<String, Object> stringObjectMap = JSON.parseObject(jobgroupResponse.body(), new TypeReference<Map<String, Object>>() {
});
List<XxlJobGroup> data = JSON.parseObject(JSON.toJSONString(stringObjectMap.get("data")), new TypeReference<List<XxlJobGroup>>() {
});
return data.get(0).getId();
}
/**
* 获取XxlJobInfo
*
* @param jobDesc
* @return
*/
public XxlJobInfo getXxlJobInfo(String jobDesc) throws Exception {
String path = adminAddresses + "/jobinfo/pageList";
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("jobDesc", jobDesc);
String jobgroupPath = "/jobgroup/pageList";
paramMap.put("jobGroup", getJobGroupId(jobgroupPath));
paramMap.put("triggerStatus", 1);
HttpResponse response = HttpRequest.post(path).form(paramMap).execute();
log.info("response : {}", JSON.toJSONString(response.body()));
Map<String, Object> stringObjectMap = JSON.parseObject(response.body(), new TypeReference<Map<String, Object>>() {
});
List<XxlJobInfo> data = JSON.parseObject(JSON.toJSONString(stringObjectMap.get("data")), new TypeReference<List<XxlJobInfo>>() {
});
return data.get(0);
}
/**
* 停止定时器
*
* @param jobId
*/
public void stop(int jobId) {
String pathStop = adminAddresses + "/jobinfo/stop";
Map<String, Object> paramMapStop = new HashMap<>();
paramMapStop.put("id", jobId);
HttpRequest.post(pathStop).form(paramMapStop).execute();
}
/**
* 删除定时器
*
* @param jobDesc
*/
public void remove(String jobDesc) throws Exception {
XxlJobInfo xxlJobInfo = getXxlJobInfo(jobDesc);
String pathRemove = adminAddresses + "/jobinfo/remove";
Map<String, Object> paramMapRemove = new HashMap<>();
paramMapRemove.put("id", xxlJobInfo.getId());
HttpRequest.post(pathRemove).form(paramMapRemove).execute();
}
/**
* 获取cookie
*
* @return
*/
public String getCookie() {
String path = adminAddresses + "/login";
Map<String, Object> hashMap = new HashMap();
hashMap.put("userName", userName);
hashMap.put("password", password);
HttpResponse response = HttpRequest.post(path).form(hashMap).execute();
List<HttpCookie> cookies = response.getCookie();
StringBuilder sb = new StringBuilder();
for (HttpCookie cookie : cookies) {
sb.append(cookie.toString());
}
String cookie = sb.toString();
log.info("获取cookie:{}", cookie);
return cookie;
}
}
模拟动态添加定时任务
@RestController
@RequestMapping("/demo")
@Slf4j
public class AddJobController {
@Autowired
private XxlJobCommon xxlJobCommon;
@Autowired
private XxlJobProperties xxlJobProperties;
@GetMapping("/addJob/{jobParam}")
@ResponseBody
public String addJob(@PathVariable("jobParam") String jobParam) {
try{
XxlJobInfo xxlJobInfo = new XxlJobInfo();
xxlJobInfo.setJobDesc(jobParam);
xxlJobInfo.setExecutorRouteStrategy("FAILOVER");// 路由策略
xxlJobInfo.setGlueType("BEAN");
xxlJobInfo.setExecutorHandler("JobHandler");// 此处hander需提前在项目中定义
xxlJobInfo.setExecutorBlockStrategy("SERIAL_EXECUTION");
xxlJobInfo.setExecutorTimeout(0);
xxlJobInfo.setExecutorFailRetryCount(3);//执行失败重试
xxlJobInfo.setAuthor("admin");
xxlJobInfo.setScheduleType("FIX_RATE");
xxlJobInfo.setScheduleConf(xxlJobProperties.getScheduleConf());// 时间间隔
xxlJobInfo.setGlueRemark("GLUE代码初始化");
xxlJobInfo.setTriggerStatus(1); //调度状态:0-停止,1-运行
xxlJobInfo.setMisfireStrategy("DO_NOTHING");
xxlJobInfo.setExecutorParam(jobParam); //执行时的入参
xxlJobCommon.addJob(xxlJobInfo);
}catch (Exception e){
log.error("异常:",e);
}
return "OK";
}
}
请求接口http://localhost:8091/demo/addJob/addJobParam之后,会发现XXL-JOB中多了一个定时任务,这个定时器的各种属性也符合在业务中编写的属性。
也可以看到这个执行任务的执行