分布式定时任务系列10:XXL-job源码分析之路由策略

 传送门

分布式定时任务系列1:XXL-job安装

分布式定时任务系列2:XXL-job使用

分布式定时任务系列3:任务执行引擎设计

分布式定时任务系列4:任务执行引擎设计续

分布式定时任务系列5:XXL-job中blockingQueue的应用

分布式定时任务系列6:XXL-job触发日志过大引发的CPU告警

分布式定时任务系列7:XXL-job源码分析之任务触发

分布式定时任务系列8:XXL-job源码分析之远程调用

 分布式定时任务系列9:XXL-job路由策略

 Java并发编程实战1:java中的阻塞队列

不忘初心

好几个月前就打算分析一下XXL-job路由策略的源码,所以有了XXL-job路由策略。不过当时偷懒,只从官网上把介绍贴出来了:

路由策略:当执行器集群部署时,提供丰富的路由策略,包括;

  1. FIRST(第一个):固定选择第一个机器;
  2. LAST(最后一个):固定选择最后一个机器;
  3. ROUND(轮询):;
  4. RANDOM(随机):随机选择在线的机器;
  5. CONSISTENT_HASH(一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。
  6. LEAST_FREQUENTLY_USED(最不经常使用):使用频率最低的机器优先被选举;
  7. LEAST_RECENTLY_USED(最近最久未使用):最久未使用的机器优先被选举;
  8. FAILOVER(故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度;
  9. BUSYOVER(忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度;
  10. SHARDING_BROADCAST(分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务;

再谈前置条件

一般提到路由,可能更多的是理解为对请求做转发时的路由匹配:比如nginx的Location路由,或者SpringCloud组件的Gateway网关上请求Predicate的URL路由匹配。

不过这里说的路由,其实指的在集群条件下对执行器进行的路由选择,是一种负载均衡策略。所以这里假设了一个场景就是,在分布式环境下,有多个执行器组成的集群。这里回顾一下xxl-rpc部署示意图

  •  这里指的路由策略是执行器集群部署
  • 关于调度器集群部署不在此范围暂不讨论,
  • 后面会单开一节具体讨论如何集群部署,达到高性能、高可用目的!

路由策略

这里继续引用XXL-job源码分析之任务触发里面关于代码执行的流程

可以看到路由策略的执行代码类路径在:com.xxl.job.admin.core.trigger.XxlJobTrigger ,方法路径在:com.xxl.job.admin.core.trigger.XxlJobTrigger#processTrigger:

executorRouteStrategyEnum.getRouter().route(triggerParam, group.getRegistryList());

策略定义

XXL-job定义了策略枚举:

public enum ExecutorRouteStrategyEnum {/** FIRST(第一个):固定选择第一个机器; */FIRST(I18nUtil.getString("jobconf_route_first"), new ExecutorRouteFirst()),/** (最后一个):固定选择最后一个机器; */LAST(I18nUtil.getString("jobconf_route_last"), new ExecutorRouteLast()),/** (轮询):; */ROUND(I18nUtil.getString("jobconf_route_round"), new ExecutorRouteRound()),/** (随机):随机选择在线的机器; */RANDOM(I18nUtil.getString("jobconf_route_random"), new ExecutorRouteRandom()),/** (一致性HASH):每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。 */CONSISTENT_HASH(I18nUtil.getString("jobconf_route_consistenthash"), new ExecutorRouteConsistentHash()),/** (最不经常使用):使用频率最低的机器优先被选举; */LEAST_FREQUENTLY_USED(I18nUtil.getString("jobconf_route_lfu"), new ExecutorRouteLFU()),/** (最近最久未使用):最久未使用的机器优先被选举; */LEAST_RECENTLY_USED(I18nUtil.getString("jobconf_route_lru"), new ExecutorRouteLRU()),/** (故障转移):按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度; */FAILOVER(I18nUtil.getString("jobconf_route_failover"), new ExecutorRouteFailover()),/** (忙碌转移):按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度; */BUSYOVER(I18nUtil.getString("jobconf_route_busyover"), new ExecutorRouteBusyover()),/** (分片广播):广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数;可根据分片参数开发分片任务; */SHARDING_BROADCAST(I18nUtil.getString("jobconf_route_shard"), null);ExecutorRouteStrategyEnum(String title, ExecutorRouter router) {this.title = title;this.router = router;}private String title;private ExecutorRouter router;public String getTitle() {return title;}public ExecutorRouter getRouter() {return router;}public static ExecutorRouteStrategyEnum match(String name, ExecutorRouteStrategyEnum defaultItem){if (name != null) {for (ExecutorRouteStrategyEnum item: ExecutorRouteStrategyEnum.values()) {if (item.name().equals(name)) {return item;}}}return defaultItem;}}

其中枚举里面有一个属性router,真正的路由策略实现都在这个接口:com.xxl.job.admin.core.route.ExecutorRouter。

路由接口

看一看路由接口定义代码:

public abstract class ExecutorRouter {protected static Logger logger = LoggerFactory.getLogger(ExecutorRouter.class);/*** route address** @param addressList* @return  ReturnT.content=address*/public abstract ReturnT<String> route(TriggerParam triggerParam, List<String> addressList);}

而上面的各种策略都实现了这个接口:

这种是典型的策略模式应用,这里也可以看出好的代码通过设计模式可以很方便的做到扩展! 

路由策略详解

对于这些路由策略实现,从简单到复杂一个个的来解析。

FIRST(第一个)

此策略的定义是:固定选择第一个机器!意思就是不论执行器有多少个,始终选择执行器列表的第一个进行任务执行。

这个策略的实现也相当简单:

public class ExecutorRouteFirst extends ExecutorRouter {@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList){return new ReturnT<String>(addressList.get(0));}}

这个代码里面就是固定从addressList里面get第一个执行器

LAST(最后一个)

此策略的定义是:固定选择最后一个机器!意思就是不论执行器有多少个,始终选择执行器列表的最后一个进行任务执行。

这个策略的实现也相当简单:

public class ExecutorRouteLast extends ExecutorRouter {@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {return new ReturnT<String>(addressList.get(addressList.size()-1));}}

 这个代码里面就是固定从addressList里面get最后一个个执行器

ROUND(轮询)

此策略的定义是:意思就是不论执行器有多少个,从执行器列表逐个选择进行任务执行。

这个策略的实现如下:

public class ExecutorRouteRound extends ExecutorRouter {private static ConcurrentMap<Integer, AtomicInteger> routeCountEachJob = new ConcurrentHashMap<>();private static long CACHE_VALID_TIME = 0;private static int count(int jobId) {// cache clearif (System.currentTimeMillis() > CACHE_VALID_TIME) {routeCountEachJob.clear();CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;}AtomicInteger count = routeCountEachJob.get(jobId);if (count == null || count.get() > 1000000) {// 初始化时主动Random一次,缓解首次压力count = new AtomicInteger(new Random().nextInt(100));} else {// count++count.addAndGet(1);}routeCountEachJob.put(jobId, count);return count.get();}@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {String address = addressList.get(count(triggerParam.getJobId())%addressList.size());return new ReturnT<String>(address);}}

这个代码稍微复杂一点,为了实现轮询的效果在内存中声明了一个Map来来进行计数,其中的key为任务jobId,value为每个任务的调用次数:

  • 声明一个Map类型字段routeCountEachJob来进行每个任务的调用次数计数,其中为ConcurrentMap、AtomicInteger类型的原因是防止并发
  • count(int jobId)方法的作用对当前触发的任务进行计数,这里AtomicInteger原子类对每次执行后就+1
  • 每24小时后重新开始计数
  • 然后根据当前任务(jobId)的调用次数从addressList里面选择执行器:即count % 执行器个数

这样文字可能理解起来还是不太直接,其实就是类似如下的示例:

RANDOM(随机)

此策略的定义是:意思就是不论执行器有多少个,从执行器列表随机选择在线的机器。

这个策略的实现也比较直观:

public class ExecutorRouteRandom extends ExecutorRouter {private static Random localRandom = new Random();@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {String address = addressList.get(localRandom.nextInt(addressList.size()));return new ReturnT<String>(address);}}

这个代码里面就是随机从addressList里面get一个执行器:

localRandom.nextInt(addressList.size())

LEAST_FREQUENTLY_USED(最不经常使用) 

此策略的定义是:意思就是不论执行器有多少个,使用频率最低的机器优先被选择出来进行任务执行。

这个策略的实现如下:

public class ExecutorRouteLFU extends ExecutorRouter {// 任务调用计算器,其中key为jobId-任务ID,value为HashMap:记录每个实例的调用次数private static ConcurrentMap<Integer, HashMap<String, Integer>> jobLfuMap = new ConcurrentHashMap<Integer, HashMap<String, Integer>>();private static long CACHE_VALID_TIME = 0;public String route(int jobId, List<String> addressList) {// 缓存1天(24小时),然后重新计数// cache clearif (System.currentTimeMillis() > CACHE_VALID_TIME) {jobLfuMap.clear();CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;}// 初始化// lfu item initHashMap<String, Integer> lfuItemMap = jobLfuMap.get(jobId);     // Key排序可以用TreeMap+构造入参Compare;Value排序暂时只能通过ArrayList;if (lfuItemMap == null) {lfuItemMap = new HashMap<String, Integer>();jobLfuMap.putIfAbsent(jobId, lfuItemMap);   // 避免重复覆盖}// put newfor (String address: addressList) {if (!lfuItemMap.containsKey(address) || lfuItemMap.get(address) >1000000 ) {lfuItemMap.put(address, new Random().nextInt(addressList.size()));  // 初始化时主动Random一次,缓解首次压力}}// 这里有一个删除动作,其实是因为实例可能动态上下线,对于下线的节点需要排除// remove oldList<String> delKeys = new ArrayList<>();for (String existKey: lfuItemMap.keySet()) {if (!addressList.contains(existKey)) {delKeys.add(existKey);}}// 移除下线节点,尽量防止调度到下线的节点上导致失败if (delKeys.size() > 0) {for (String delKey: delKeys) {lfuItemMap.remove(delKey);}}// 进行调用次数排序// load least userd count addressList<Map.Entry<String, Integer>> lfuItemList = new ArrayList<Map.Entry<String, Integer>>(lfuItemMap.entrySet());Collections.sort(lfuItemList, new Comparator<Map.Entry<String, Integer>>() {@Overridepublic int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {return o1.getValue().compareTo(o2.getValue());}});// 调用次数+1Map.Entry<String, Integer> addressItem = lfuItemList.get(0);String minAddress = addressItem.getKey();addressItem.setValue(addressItem.getValue() + 1);return addressItem.getKey();}@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {String address = route(triggerParam.getJobId(), addressList);return new ReturnT<String>(address);}}

这个代码比较复杂一点, 不过如果对比缓存的淘汰策略的话,这个其实就是所谓的"LFU":

LFU(The Least Frequently Used)最近不多使用算法,与LRU的区别在于LRU是以时间衡量,LFU是以时间段内的次数

  • 算法:若是一个数据在必定时间内被访问的次数很低,那么被认为在将来被访问的几率也是最低的,当规定空间用尽且需要放入新数据的时候,会优先淘汰时间段内访问次数最低的数据。

  • 优势:LFU也能够有效的保护缓存,相对场景来说,比LRU有更好的缓存命中率。由于是以次数为基准,因此更加准确,天然能有效的保证和提升命中率。

  • 缺点:由于LFU须要记录数据的访问频率,所以需要额外的空间;当访问模式改变的时候,算法命中率会急剧降低,这也是他最大弊端

所以这个策略里面为了实现LFU的效果在内存中声明了一个Map来来进行计数,其中的key为任务jobId,value为每个任务的调用次数:

  • 声明一个Map类型字段jobLfuMap来进行每个任务的调用次数计数,其中为ConcurrentMap原因是防止并发
  • jobLfuMap的value是一个HashMap<String, String>:其中key为任务的实例地址address,value为调用次数。这样设计的目的是为了通过这样的数据结构来达到,记录每一个任务jobId在每一个实例上的调用次数!与ROUND的区别是:ROUND记录的每个任务jobId的所有调用数次,LFU多了一个维度
  • 通过上面的这种数据结构,最终可以对每个实例的调用次数进行排序:所以要依赖一个ArrayList来排序,最终选取调用次数最少的实例来作为任务执行目标机器!

LEAST_RECENTLY_USED(最近最久未使用)

此策略的定义是:意思就是不论执行器有多少个,最久未使用的机器优先被选举。

这个策略的实现如下:


public class ExecutorRouteLRU extends ExecutorRouter {private static ConcurrentMap<Integer, LinkedHashMap<String, String>> jobLRUMap = new ConcurrentHashMap<Integer, LinkedHashMap<String, String>>();private static long CACHE_VALID_TIME = 0;public String route(int jobId, List<String> addressList) {// 缓存1天(24小时),然后重新计数// cache clearif (System.currentTimeMillis() > CACHE_VALID_TIME) {jobLRUMap.clear();CACHE_VALID_TIME = System.currentTimeMillis() + 1000*60*60*24;}// init lruLinkedHashMap<String, String> lruItem = jobLRUMap.get(jobId);if (lruItem == null) {/*** LinkedHashMap*      a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;*      b、removeEldestEntry:新增元素时将会调用,返回true时会删除最老元素;可封装LinkedHashMap并重写该方法,比如定义最大容量,超出是返回true即可实现固定长度的LRU算法;*/lruItem = new LinkedHashMap<String, String>(16, 0.75f, true);jobLRUMap.putIfAbsent(jobId, lruItem);}// 新加入的节点处理:添加到lru中进行统计// put newfor (String address: addressList) {if (!lruItem.containsKey(address)) {lruItem.put(address, address);}}// 这里有一个删除动作,其实是因为实例可能动态上下线,对于下线的节点需要排除// remove oldList<String> delKeys = new ArrayList<>();for (String existKey: lruItem.keySet()) {if (!addressList.contains(existKey)) {delKeys.add(existKey);}}if (delKeys.size() > 0) {for (String delKey: delKeys) {lruItem.remove(delKey);}}// 排序最后一个节点// loadString eldestKey = lruItem.entrySet().iterator().next().getKey();String eldestValue = lruItem.get(eldestKey);return eldestValue;}@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {String address = route(triggerParam.getJobId(), addressList);return new ReturnT<String>(address);}}

 这个代码比较复杂一点, 不过如果对比缓存的淘汰策略的话,这个其实就是所谓的"LRU":

LRU(The Least Recently Used)最近最久未使用算法。相比于FIFO算法智能些。

  • 算法:若是一个数据最近不多被访问到,那么被认为在将来被访问的几率也是最低的,当规定空间用尽且需要放入新数据的时候,会优先淘汰最久未被访问的数据。

  • 优势:LRU能够有效的对访问比较频繁的数据进行保护,也就是针对热点数据的命中率提升有明显的效果。

缺点:对于周期性、偶发性的访问数据,有大几率可能形成缓存污染,也就是置换出去了热点数据,把这些偶发性数据留下了,从而致使LRU的数据命中率急剧降低。

所以这个策略里面为了实现LFU的效果在内存中声明了一个Map来来进行计数,其中的key为任务jobId,value为每个任务的调用次数: 

  • 声明一个Map类型字段jobLRUMap来进行每个任务的调用次数计数,其中为ConcurrentMap原因是防止并发
  • jobLRUMap的value是一个LinkedHashMap<String, String>:其中key为任务的实例地址address,value为address。这里比较巧妙的是直接利用了LinkedHashMap的排序能力
   /*** Constructs an empty <tt>LinkedHashMap</tt> instance with the* specified initial capacity, load factor and ordering mode.** @param  initialCapacity the initial capacity* @param  loadFactor      the load factor* @param  accessOrder     the ordering mode - <tt>true</tt> for*         access-order, <tt>false</tt> for insertion-order* @throws IllegalArgumentException if the initial capacity is negative*         or the load factor is nonpositive*/public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder = accessOrder;}
LinkedHashMap的排序模式

a、accessOrder:true=访问顺序排序(get/put时排序);false=插入顺序排期;

LinkedHashMap中'accessOrder‘字段的用途是什么?

LinkedHashMap是Java中的一个类,它是HashMap的一个子类,具有HashMap的所有特性,并且还保持了插入顺序或访问顺序的特性。

'accessOrder'字段是LinkedHashMap类中的一个布尔类型的属性,用于指定迭代顺序是否基于访问顺序。当accessOrder为true时,表示迭代顺序将基于最近访问顺序,即最近访问的元素将排在迭代顺序的末尾;当accessOrder为false时,表示迭代顺序将基于插入顺序,即元素将按照插入的顺序进行迭代。

使用accessOrder字段可以方便地实现LRU(Least Recently Used,最近最少使用)缓存淘汰算法。通过将accessOrder设置为true,当访问某个元素时,该元素会被移到链表的末尾,这样在需要淘汰元素时,只需要移除链表头部的元素即可。

LinkedHashMap的应用场景包括但不限于:

  1. 缓存系统:通过设置accessOrder为true,可以实现基于访问顺序的缓存淘汰策略。
  2. LRU缓存:通过继承LinkedHashMap并重写removeEldestEntry方法,可以实现固定大小的LRU缓存。
  3. 记录访问顺序:当需要按照访问顺序记录某些数据时,可以使用LinkedHashMap。

FAILOVER(故障转移)

此策略的定义是:按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度。

这个策略的实现如下:

public class ExecutorRouteFailover extends ExecutorRouter {@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {StringBuffer beatResultSB = new StringBuffer();for (String address : addressList) {// beatReturnT<String> beatResult = null;try {ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);beatResult = executorBiz.beat();} catch (Exception e) {logger.error(e.getMessage(), e);beatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );}beatResultSB.append( (beatResultSB.length()>0)?"<br><br>":"").append(I18nUtil.getString("jobconf_beat") + ":").append("<br>address:").append(address).append("<br>code:").append(beatResult.getCode()).append("<br>msg:").append(beatResult.getMsg());// beat successif (beatResult.getCode() == ReturnT.SUCCESS_CODE) {beatResult.setMsg(beatResultSB.toString());beatResult.setContent(address);return beatResult;}}return new ReturnT<String>(ReturnT.FAIL_CODE, beatResultSB.toString());}
}

这个策略的实现也比较直观,根据注册的实例列表依次发起心跳检测,如果成功,则选取为执行节点!代码并不复杂,不过可能对FAILOVER(故障转移)这个术语所震撼,觉得很高大上。

对于FAILOVER这种故障处理策略来说,不同的框架或者场景实现不同,难易程度也不同,而且也不是所有的系统/接口适合故障转移。比如对一个有超时机制的微服务架构来说:

如果链路比较多,一个业务请求需要经过A->B->C3个服务,每个服务有2个节点。假设在A服务调用B的时候,存在网络问题,A的实例A1失败,这里转移到A2成功;A->B的时候,B1失败,B2成功;同理B->C,C1失败,C2成功,虽然最终请求成功,但是整体耗时会增大一倍,早已经进过了网关(整体)响应时长导致timeout了,所以有些专题的FAILOVER并不一定是有益甚至有害的(重试也增加了服务调用次数,服务的压力)。关于这一块会后面单独开一节服务故障模式的讨论

BUSYOVER(忙碌转移)

此策略的定义是:按照顺序依次进行空闲检测,第一个空闲检测成功的机器选定为目标执行器并发起调度。

这个策略的实现如下:

public class ExecutorRouteBusyover extends ExecutorRouter {@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {StringBuffer idleBeatResultSB = new StringBuffer();for (String address : addressList) {// beatReturnT<String> idleBeatResult = null;try {ExecutorBiz executorBiz = XxlJobScheduler.getExecutorBiz(address);idleBeatResult = executorBiz.idleBeat(new IdleBeatParam(triggerParam.getJobId()));} catch (Exception e) {logger.error(e.getMessage(), e);idleBeatResult = new ReturnT<String>(ReturnT.FAIL_CODE, ""+e );}idleBeatResultSB.append( (idleBeatResultSB.length()>0)?"<br><br>":"").append(I18nUtil.getString("jobconf_idleBeat") + ":").append("<br>address:").append(address).append("<br>code:").append(idleBeatResult.getCode()).append("<br>msg:").append(idleBeatResult.getMsg());// beat successif (idleBeatResult.getCode() == ReturnT.SUCCESS_CODE) {idleBeatResult.setMsg(idleBeatResultSB.toString());idleBeatResult.setContent(address);return idleBeatResult;}}return new ReturnT<String>(ReturnT.FAIL_CODE, idleBeatResultSB.toString());}}

 这个策略的实现也比较直观,根据注册的实例列表依次发起空闲检测,如果成功,则选取为执行节点!代码并不复杂,不过可能需要联合起前面XXL-rpc的章节来配合理解

CONSISTENT_HASH(一致性HASH)

此策略的定义是:每个任务按照Hash算法固定选择某一台机器,且所有任务均匀散列在不同机器上。

这个策略的实现如下:

/*** 分组下机器地址相同,不同JOB均匀散列在不同机器上,保证分组下机器分配JOB平均;且每个JOB固定调度其中一台机器;*      a、virtual node:解决不均衡问题*      b、hash method replace hashCode:String的hashCode可能重复,需要进一步扩大hashCode的取值范围* Created by xuxueli on 17/3/10.*/
public class ExecutorRouteConsistentHash extends ExecutorRouter {private static int VIRTUAL_NODE_NUM = 100;/*** get hash code on 2^32 ring (md5散列的方式计算hash值)* @param key* @return*/private static long hash(String key) {// md5 byteMessageDigest md5;try {md5 = MessageDigest.getInstance("MD5");} catch (NoSuchAlgorithmException e) {throw new RuntimeException("MD5 not supported", e);}md5.reset();byte[] keyBytes = null;try {keyBytes = key.getBytes("UTF-8");} catch (UnsupportedEncodingException e) {throw new RuntimeException("Unknown string :" + key, e);}md5.update(keyBytes);byte[] digest = md5.digest();// hash code, Truncate to 32-bitslong hashCode = ((long) (digest[3] & 0xFF) << 24)| ((long) (digest[2] & 0xFF) << 16)| ((long) (digest[1] & 0xFF) << 8)| (digest[0] & 0xFF);long truncateHashCode = hashCode & 0xffffffffL;return truncateHashCode;}public String hashJob(int jobId, List<String> addressList) {// ------A1------A2-------A3------// -----------J1------------------TreeMap<Long, String> addressRing = new TreeMap<Long, String>();for (String address: addressList) {for (int i = 0; i < VIRTUAL_NODE_NUM; i++) {long addressHash = hash("SHARD-" + address + "-NODE-" + i);addressRing.put(addressHash, address);}}long jobHash = hash(String.valueOf(jobId));SortedMap<Long, String> lastRing = addressRing.tailMap(jobHash);if (!lastRing.isEmpty()) {return lastRing.get(lastRing.firstKey());}return addressRing.firstEntry().getValue();}@Overridepublic ReturnT<String> route(TriggerParam triggerParam, List<String> addressList) {String address = hashJob(triggerParam.getJobId(), addressList);return new ReturnT<String>(address);}}

 这个策略的实现也比较直观,这里就不展开讨论了,有兴趣的可以在评论区晒出理解分享给大家!

路由策略的选择

上面介绍了各种路由策略的实现,关于这里面的路由策略的选择就不过多讨论了,建议默认情况采用下轮询!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/32654.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Go语言的诞生背景

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 目录 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌…

Linux操作系统处理器调度基本准则和实现

1&#xff0c;基本概念 在多道程序系统中&#xff0c;进程的数量往往多于处理机的个数&#xff0c;进程争用处理机的情况就在所难免。处理机调度是对处理机进行分配&#xff0c;就是从就绪队列中&#xff0c;按照一定的算法&#xff08;公平、低效&#xff09;选择一个进程并将…

mysql学习——SQL中的DDL和DML

SQL中的DDL和DML DDL数据库操作&#xff1a;表操作 DML添加数据修改数据删除数据 学习黑马MySQL课程&#xff0c;记录笔记&#xff0c;用于复习。 DDL DDL&#xff1a;Data Definition Language&#xff0c;数据定义语言&#xff0c;用来定义数据库对象(数据库&#xff0c;表&…

【CSS】简单实用的calc()函数

calc() 是 CSS 中的一个功能&#xff0c;允许你在属性值中进行基础的数学计算。这是非常有用的&#xff0c;特别是当你需要在不同的上下文或视口大小中动态调整尺寸或位置时。 以下是一些 calc() 函数的简单实用示例&#xff1a; 动态宽度&#xff1a; 假设你希望一个元素的…

C语言入门课程学习笔记8:变量的作用域递归函数宏定义交换变量

C语言入门课程学习笔记8 第36课 - 变量的作用域与生命期&#xff08;上&#xff09;第37课 - 变量的作用域与生命期&#xff08;下&#xff09;实验—局部变量的作用域实验-变量的生命期 第38课 - 函数专题练习第39课 - 递归函数简介实验小结 第40课 - C 语言中的宏定义实验小结…

基于Java的学生成绩管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;Java技术&#xff0c;B/S结构 工具&#xff1a;MyEclipse&#xff0c;MySQL 系统展示 首页 个人中…

基于YOLOv5+pyqt5的跌倒检测系统(含pyqt页面、训练好的模型)

简介 跌倒是老年人和身体不便者常见的意外事故&#xff0c;及时检测和处理跌倒事件对于保障他们的安全至关重要。为了提高对跌倒事件的监控效率&#xff0c;我们开发了一种基于YOLOv5目标检测模型的跌倒检测系统。本报告将详细介绍该系统的实际应用与实现&#xff0c;包括系统…

虚拟机IP地址频繁变化的解决方法

勾八动态分配IP&#xff0c;让我在学习redis集群的时候&#xff0c;配置很多的IP地址&#xff0c;但是由于以下原因导致我IP频繁变动&#xff0c;报错让我烦恼&#xff01;&#xff01;&#xff01;&#xff01; 为什么虚拟机的IP地址会频繁变化&#xff1f; 虚拟机IP地址频繁…

终极解决方案,传统极速方案,下载软件的双雄对决!

在数字资源日益丰富的今天&#xff0c;下载管理器成为了我们日常生活中不可或缺的工具。市场上两款备受欢迎的下载管理软件——Internet Download Manager&#xff08;IDM&#xff09;和迅雷11&#xff0c;它们以各自的特色和优势&#xff0c;满足了不同用户群体的需求。 软件…

Less与Sass的区别

1. 功能和工具&#xff1a; Sass&#xff1a;提供了更多的功能和内置方法&#xff0c;如条件语句、循环、数学函数等。Sass 也支持更复杂的操作和逻辑构建。 Less&#xff1a;功能也很强大&#xff0c;但相比之下&#xff0c;Sass 在功能上更为丰富和成熟。 2、编译环境&…

uniapp使用伪元素实现气泡

uniapp使用伪元素实现气泡 背景实现思路代码实现尾巴 背景 气泡效果在开发中使用是非常常见的&#xff0c;使用场景有提示框&#xff0c;对话框等等&#xff0c;今天我们使用css来实现气泡效果。老规矩&#xff0c;先看下效果图&#xff1a; 实现思路 其实实现这个气泡框的…

自动驾驶规划中使用 OSQP 进行二次规划 代码原理详细解读

目录 1 问题描述 什么是稀疏矩阵 CSC 形式 QP Path Planning 问题 1. Cost function 1.1 The first term: 1.2 The second term: 1.3 The thrid term: 1.4 The forth term: 对 Qx 矩阵公式的验证 整体 Q 矩阵&#xff08;就是 P 矩阵&#xff0c;二次项的权重矩阵&…

java打印菱形和空心菱形

java打印菱形 菱形分上下两个部分。其中上部分同打印金字塔&#xff1b;下部分循环部分i是递减 &#xff08;ps:菱形层数只能为奇数&#xff09; import java.util.Scanner;public class Lingxing{public static void main(String[] args) {Scanner myScanner new Scanner(S…

Android View点击事件分发原理,源码解读

View点击事件分发原理&#xff0c;源码解读 前言1. 原理总结2.1 时序图总结2.2 流程图总结 2. 源码解读2.1 Activity到ViewGroup2.2 ViewGroup事件中断逆序搜索自己处理点击事件ViewGroup总结 2.3 ViewOnTouchListeneronTouchEvent 3. 附录&#xff1a;时序图uml代码 前言 两年…

Nginx Proxy Manager反向代理Jackett

1 说明 最近折腾nas&#xff0c;发现npm反向代理Jackett后出现无法访问的问题&#xff0c;是因为外网访问jackett (例如https://domain.com:7373/jackett/UI/Dashboard)时&#xff0c;url会被重定向到https://domain.com/jackett/UI/Login?ReturnUrl%2Fjackett%2FUI%2FDashbo…

ubuntu链接mysql

C链接mysql 报错 sudo apt-get update sudo apt-get install libmysqlclient-dev 指令编译 g -o mysql_example mysql_example.cpp -I/usr/include/mysql -lmysqlclient g mysql_test.cpp mysql_config --cflags --libs 安装mysql sudo apt updatesudo apt install mysql-…

Java程序之动物声音“模拟器”

题目&#xff1a; 设计一个“动物模拟器”&#xff0c;希望模拟器可以模拟许多动物的叫声和行为&#xff0c;要求如下&#xff1a; 编写接口Animal&#xff0c;该接口有两个抽象方法cry()和getAnimalName()&#xff0c;即要求实现该接口的各种具体的动物类给出自己的叫声和种类…

Flink SQL因类型错误导致MAX和MIN计算错误

背景 最近在做数据分析,用Flink SQL来做分析工具,因数据源的数据存在不太规范的数据格式,因此我需要通过SQL函数把我需要的数据值从VARCHAR类型的字段中把数据提取出来,然后再做MAX、MIN、SUM这些统计。怎料SUM算出来的结果准确无误,而MAX和MIN算出来的结果却始终不正确,…

尹会生:从零开始部署翻译助手【总结】

安装docker安装dify 工具准备 Docker 简介&#xff1a;可以在不同电脑上运行相同的容器&#xff0c;类似于把软件装在便携箱子里&#xff0c;随身携带。 优点&#xff1a;安装Docker可以简化部署过程&#xff0c;避免安装许多依赖性软件。 网址&#xff1a;https://www.docke…

【TOOL】ceres学习笔记(二) —— 自定义函数练习

文章目录 一、曲线方程1. 问题描述2. 实现方案 一、曲线方程 1. 问题描述 现有数学模型为 f ( x ) A e x B s i n ( x ) C x D f(x)Ae^xBsin(x)Cx^D f(x)AexBsin(x)CxD &#xff0c;但不知道 A A A 、 B B B 、 C C C 、 D D D 各参数系数&#xff0c;实验数据中含有噪声…