路由策略
每一个任务都可以配置一个路由策略
除了分片广播策略,每一种路由策略对用一个java类
什么是路由?
在我们的服务集群部署的情况下,某job到点触发后,调度中心根据某种策略去通知哪些执行器跑该job
从图上可以看出xxljob支持多种路由策略
这里简单讲一讲各种算法的原理,有兴趣的小伙伴可以去看看内部的实现细节
第一个、最后一个、轮询、随机都很简单,没什么好说的
一致性Hash讲起来比较复杂,你可以先看看这篇文章,再去查看Xxl-Job的代码实现
最不经常使用(LFU:Least Frequently Used):Xxl-Job内部会有一个缓存,统计每个任务每个地址的使用次数,每次都选择使用次数最少的地址,这个缓存每隔24小时重置一次
最近最久未使用(LRU:Least Recently Used):将地址存到LinkedHashMap中,它利用LinkedHashMap可以根据元素访问(get/put)顺序来给元素排序的特性,快速找到最近最久未使用(未访问)的节点
故障转移:调度中心都会去请求每个执行器,只要能接收到响应,说明执行器正常,那么任务就会交给这个执行器去执行
忙碌转移:调度中心也会去请求每个执行器,判断执行器是不是正在执行当前需要执行的任务(任务执行时间过长,导致上一次任务还没执行完,下一次又触发了),如果在执行,说明忙碌,不能用,否则就可以用
分片广播:XxlJob给每个执行器分配一个编号,从0开始递增,然后向所有执行器触发任务,告诉每个执行器自己的编号和总共执行器的数据
我们可以通过XxlJobHelper#getShardIndex获取到编号,XxlJobHelper#getShardTotal获取到执行器的总数据量
分片广播就是将任务量分散到各个执行器,每个执行器只执行一部分任务,加快任务的处理
举个例子,比如你现在需要处理30w条数据,有3个执行器,此时使用分片广播,那么此时可将任务分成3分,每份10w条数据,执行器根据自己的编号选择对应的那份10w数据处理
当选择好了具体的执行器实例之后,调用中心就会携带一些触发的参数,发送Http请求,触发任务
路由策略相关源码见下
入口
XxlJobTrigger.trigger()
// 路由策略
ExecutorRouteStrategyEnum routeStrategyEnum = ExecutorRouteStrategyEnum.match(jobInfo.getExecutorRouteStrategy(), null);
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == routeStrategyEnum
&& group.getRegistryList() != null && !group.getRegistryList().isEmpty()
&& shardingParam == null) {
for (int i = 0; i < group.getRegistryList().size(); i++) {
// 分片路由:通知每一个执行器,传给每个执行器的shardIndex不同,shardTotal相同
processTrigger(group, jobInfo, finalFailRetryCount, triggerType, i, group.getRegistryList().size());
}
} else {
if (shardingParam == null) {
shardingParam = new int[]{0, 1};
}
processTrigger(group, jobInfo, finalFailRetryCount, triggerType, shardingParam[0], shardingParam[1]);
}
String address = null;
ReturnT<String> routeAddressResult = null;
if (group.getRegistryList() != null && !group.getRegistryList().isEmpty()) { // 至少有一个执行器可供选择
if (ExecutorRouteStrategyEnum.SHARDING_BROADCAST == executorRouteStrategyEnum) {
// 分片广播
if (index < group.getRegistryList().size()) {
address = group.getRegistryList().get(index);
} else {
address = group.getRegistryList().get(0);
}
} else {
////////////////根据对应的路由策略去选择对应的执行器//////////////////////////////////
routeAddressResult = executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());
if (routeAddressResult.getCode() == ReturnT.SUCCESS_CODE) {
address = routeAddressResult.getContent();
}
}
} else {
routeAddressResult = new ReturnT<String>(ReturnT.FAIL_CODE, I18nUtil.getString("jobconf_trigger_address_empty"));
}
分片广播job实现
package com.shuncom.rulr.config;
import com.xxl.job.core.context.XxlJobContext;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeoutException;
@Component
@Slf4j
public class TestJob2 {
// value对应调度中心任务里的JobHandler
@XxlJob(value = "jobHandlerName2", init = "init", destroy = "destroy")
public void x() {
log.info("xxl-job2 is running");
// 本次job执行的一个上下文,具体原理见JobThread.run()即可,很简单
XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();
// xxl-job也提供了一个工具类XxlJobHelper去获取XxlJobContext中的数据,原理不变
// 这是我们在调度中心创建任务时填的任务参数,调度中心在执行job的时候会传给我们
String jobParam = xxlJobContext.getJobParam();
// 调度中心都把jobId传给我们了,我们可以通过db去查询这个job的任何信息了
long jobId = xxlJobContext.getJobId();
// 以下两个参数用于本次job的执行完成后的情况(成功、失败、超时),需要我们手动设置,用于调度中心xxl-job-admin记录日志
// int handleCode = xxlJobContext.getHandleCode();
// String handleMsg = xxlJobContext.getHandleMsg();
// 当我们这个job所属任务的路由策略是分片广播的时候,以下两个参数才有意义,其他路由策略下这两个参数直接忽略
// 举例说明:当任务在路由策略下,如何处理我们db中的1000个超时的订单
// 当我们的服务集群部署的情况下,假设有3个节点,我们希望这3个节点都可以同时处理我们数据库中的1000个超时订单
// 节点1:shardIndex=0,shardTotal=3 处理第0-334个订单
// 节点2:shardIndex=1,shardTotal=3 处理第334-668个订单
// 节点3:shardIndex=2,shardTotal=3 处理第668-1000个订单
int shardIndex = xxlJobContext.getShardIndex(); // 当前分片
int shardTotal = xxlJobContext.getShardTotal(); // 总的分片
if (shardTotal > 1) { // 路由策略是分片广播的时候才有可能大于1
// 查询数据库中所有未处理的订单数,假设有1000个
int unProcessedOrderCount = selectUnProcessedOrderCount();
// 计算每个执行器处理的订单数
int eachCount = (int) (Math.ceil(((double) unProcessedOrderCount) / ((double) shardTotal)))); // 334
int fromIndex = eachCount * shardIndex;
int pageSize = fromIndex + eachCount;
// 当前执行器根据fromIndex和pageSize去查询自己要处理的订单,这样集群环境下就各个执行器处理的订单不会冲突了
run(jobParam, fromIndex, pageSize);
} else {
run(jobParam, null, null);
}
}
private void run(String jobParam, Integer fromIndex, Integer pageSize) {
XxlJobContext xxlJobContext = XxlJobContext.getXxlJobContext();
try {
// 执行我们的业务代码
// todo
// 200 : success
//500 : fail
//502 : timeout
// 代表本次业务执行成功
xxlJobContext.setHandleCode(XxlJobContext.HANDLE_CODE_SUCCESS);
xxlJobContext.setHandleMsg(null);
} catch (Exception e) {
log.error("Failed to runInternal", e);
if (e instanceof TimeoutException) {
xxlJobContext.setHandleCode(XxlJobContext.HANDLE_CODE_TIMEOUT);
} else {
xxlJobContext.setHandleCode(XxlJobContext.HANDLE_CODE_FAIL);
}
xxlJobContext.setHandleMsg(e.getMessage());
}
}
private int selectUnProcessedOrderCount() {
// 数据库中未处理订单的数量
return 1000;
}
public void init() {
log.info("init2");
}
public void destroy() {
log.info("destroy2");
}
}
部分内容转载自:# 新来个架构师,把xxl-job原理讲的炉火纯青~~