Java实现权重随机算法详解

目录
  • 应用场景
  • 本文目标
  • 算法详解
  • 权重比例
  • Java 实现
  • 参考

应用场景

客户端负载均衡,例如 Nacos 提供的客户端负载均衡就是使用了该算法
游戏抽奖(普通道具的权重很高,稀有道具的权重很低)

本文目标

Java 实现权重随机算法

算法详解

比如我们现在有三台 Server,权重分别为1,3,2。现在想对三台 Server 做负载均衡

Server1     Server2     Server3

 weight      weight      weight
   1           3          2

权重比例

我们算出每台 Server 的权重比例,权重比例 = 自己的权重 / 总权重

server1     server2     server3

 weight      weight      weight
   1           3          2

 radio       radio       radio
  1/6         3/6         2/6

根据权重比例计算覆盖区域

      server1               server2                  server3
         ^                     ^                        ^
    |---------||---------|---------|---------||---------|---------||
    0         1/6                            4/6                  6/6
               ^                              ^                    ^
          0.16666667                      0.66666667              1.0

根据权重负载均衡

如步骤2所示,每个 server 都有自己的范围,把每一个格子作为单位来看的话

  • server1 (0,1]
  • server2 (1,4]
  • server3 (4,6]

使用随机数函数,取 (0,6] 之间的随机数,根据随机数落在哪个范围决定如何选择。例如随机数为 2,处于 (1,4] 范围,那么就选择 server2。

思路大概就是这样,落实到代码上,用一个数组 [0.16666667, 0.66666667, 1] 来表示这三个 server 的覆盖范围,使用 ThreadLocalRandom 或者 Random 获取 [0,1) 内的随机数。然后使用二分查找法快速定位随机数处于哪个区间

Java 实现

代码基本上与 com.alibaba.nacos.client.naming.utils.Chooser 一致,在可读性方面做了下优化。

import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;

public class WeightRandom<T> {

    private final List<T> items = new ArrayList<>();
    private double[] weights;

    public WeightRandom(List<ItemWithWeight<T>> itemsWithWeight) {
        this.calWeights(itemsWithWeight);
    }

    /**
     * 计算权重,初始化或者重新定义权重时使用
     *
     */
    public void calWeights(List<ItemWithWeight<T>> itemsWithWeight) {
        items.clear();

        // 计算权重总和
        double originWeightSum = 0;
        for (ItemWithWeight<T> itemWithWeight : itemsWithWeight) {
            double weight = itemWithWeight.getWeight();
            if (weight <= 0) {
                continue;
            }

            items.add(itemWithWeight.getItem());
            if (Double.isInfinite(weight)) {
                weight = 10000.0D;
            }
            if (Double.isNaN(weight)) {
                weight = 1.0D;
            }
            originWeightSum += weight;
        }

        // 计算每个item的实际权重比例
        double[] actualWeightRatios = new double[items.size()];
        int index = 0;
        for (ItemWithWeight<T> itemWithWeight : itemsWithWeight) {
            double weight = itemWithWeight.getWeight();
            if (weight <= 0) {
                continue;
            }
            actualWeightRatios[index++] = weight / originWeightSum;
        }

        // 计算每个item的权重范围
        // 权重范围起始位置
        weights = new double[items.size()];
        double weightRangeStartPos = 0;
        for (int i = 0; i < index; i++) {
            weights[i] = weightRangeStartPos + actualWeightRatios[i];
            weightRangeStartPos += actualWeightRatios[i];
        }
    }

    /**
     * 基于权重随机算法选择
     *
     */
    public T choose() {
        double random = ThreadLocalRandom.current().nextDouble();
        int index = Arrays.binarySearch(weights, random);
        if (index < 0) {
            index = -index - 1;
        } else {
            return items.get(index);
        }

        if (index < weights.length && random < weights[index]) {
            return items.get(index);
        }

        // 通常不会走到这里,为了保证能得到正确的返回,这里随便返回一个
        return items.get(0);
    }

    public static class ItemWithWeight<T> {
        T item;
        double weight;

        public ItemWithWeight() {
        }

        public ItemWithWeight(T item, double weight) {
            this.item = item;
            this.weight = weight;
        }

        public T getItem() {
            return item;
        }

        public void setItem(T item) {
            this.item = item;
        }

        public double getWeight() {
            return weight;
        }

        public void setWeight(double weight) {
            this.weight = weight;
        }
    }

    public static void main(String[] args) {
        // for test
        int sampleCount = 1_000_000;

        ItemWithWeight<String> server1 = new ItemWithWeight<>("server1", 1.0);
        ItemWithWeight<String> server2 = new ItemWithWeight<>("server2", 3.0);
        ItemWithWeight<String> server3 = new ItemWithWeight<>("server3", 2.0);

        WeightRandom<String> weightRandom = new WeightRandom<>(Arrays.asList(server1, server2, server3));

        // 统计 (这里用 AtomicInteger 仅仅是因为写起来比较方便,这是一个单线程测试)
        Map<String, AtomicInteger> statistics = new HashMap<>();

        for (int i = 0; i < sampleCount; i++) {
            statistics
                    .computeIfAbsent(weightRandom.choose(), (k) -> new AtomicInteger())
                    .incrementAndGet();
        }

        statistics.forEach((k, v) -> {
            double hit = (double) v.get() / sampleCount;
            System.out.println(k + ", hit:" + hit);
        });
    }
}

这里重点说一下 Arrays.binarySearch(weights, random),这个 API 我之前没有用过导致我在读 Nacos 源码时,对这块的操作十分费解

来看一下 java API 文档对该方法返回值的解释

Returns:
index of the search key, if it is contained in the array; otherwise, (-(insertion point) - 1). The insertion point is defined as the point at which the key would be inserted into the array: the index of the first element greater than the key, or a.length if all elements in the array are less than the specified key. Note that this guarantees that the return value will be >= 0 if and only if the key is found.

解释下,首先该方法的作用是通过指定的 key 搜索数组。(前提条件是要保证数组的顺序是从小到大排序过的)

  • 如果数组中包含该 key,则返回对应的索引
  • 如果不包含该 key,则返回该 key 的 (-(insertion point)-1)

insertion point(插入点):该 key 应该在数组的哪个位置。举个例子,数组 [1,3,5],我的搜索 key 为 2,按照顺序排的话 2 应该在数组的 index = 1 的位置,所以此时 insertion point = 1。

(这里 jdk 将能查到 key 和 查不到 key 两种情况做了区分。为了将未找到的情况全部返回负数,所以做了 (-(insertion point)-1) 这样的操作)

看到这,我们就懂了,insertion point 就是我们需要的,现在我们用小学数学来推导一下如何计算 insertion point

// 小学数学推导一下 insertion point 如何计算
returnValue = (- (insertionPoint) - 1)
insertionPoint = (- (returnValue + 1) )

// 所以就有了上边代码中的
if (index < 0) {
 index = -index - 1;
}

参考

https://github.com/alibaba/nacos/blob/develop/client/src/main/java/com/alibaba/nacos/client/naming/utils/Chooser.java

到此这篇关于Java实现权重随机算法详解的文章就介绍到这了,更多相关Java 权重随机内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java实现按权重随机数

    一.问题定义: 问下有一个数组,这些数组中的值都有自己的权重,怎样设计才能高效的优先取出权重高的数?? 例如: 复制代码 代码如下: 权重: 8  2  11  79 权重返回的值: 0  1  2   3 二.分析问题: 思路一:创建一个数组数组大小为权重和的大小,如值0的权重是8,则放入8个0值,值1的权重是2,则放入2个1值,依次类推. 然后用用一个权重和大小的随机数,产生随机数,即可.缺点要占用过多的内存. 思路二: 权重和数组 w[i]存储的是[0,i]元素的所有元素的权重和  时间复

  • Java权重随机的实现方法

    本文实例讲述了Java权重随机的实现方法.分享给大家供大家参考.具体分析如下: 权重随机在项目中经常用到,所以我把它抽象到一个工具类中. 一般实现随机权重有两种方式: 1. 使用一个数组存放权重对应的实际目标,比如A的权重是2,B的权重是3,那么数组长度为5, 数组前两个存放A,后三个存放B. 然后随机一个[0-数据长度)的数字,直接取数组对应下标的值就可以了. 优点:数据结构简单,算法高效,实现简单 缺点:当权重值比较大同时数据又比较多的时候,会浪费内存 2. 使用区间算法,从前到后依次叠加权

  • java语言实现权重随机算法完整实例

    前言 现在app就是雨后春笋,嗖嗖的往外冒啊,有经验的.没经验的.有资历的.没资历的都想着创业,创业的90%以上都要做一个app出来,好像成了创业的标配. 做了app就得推广啊,怎么推,发券送钱是最多用的被不可少的了,现在好多产品或者运营都要求能够随机出优惠券的金额,但是呢又不能过于随机,送出去的券都是钱吗,投资人的钱,是吧. 所以,在随机生成的金额中就要求,小额度的几率要大,大额度的几率要小,比如说3元的70%,5块的25%,10块的5%,这个样子的概率去生成优惠券,这个怎么办呢? 对于上述的

  • Java实现权重随机算法详解

    目录 应用场景 本文目标 算法详解 权重比例 Java 实现 参考 应用场景 客户端负载均衡,例如 Nacos 提供的客户端负载均衡就是使用了该算法 游戏抽奖(普通道具的权重很高,稀有道具的权重很低) 本文目标 Java 实现权重随机算法 算法详解 比如我们现在有三台 Server,权重分别为1,3,2.现在想对三台 Server 做负载均衡 Server1 Server2 Server3 weight weight weight 1 3 2 权重比例 我们算出每台 Server 的权重比例,权

  • Java垃圾回收机制算法详解

    概述 Java GC(Garbage Collection,垃圾回收)机制,是Java与C++/C的主要区别之一,作为Java开发者,一般不需要专门编写内存回收和垃圾清理代码,对内存泄露和溢出的问题,也不需要像C程序员那样战战兢兢.这是因为在Java虚拟机中,存在自动内存管理和垃圾清扫机制.概括地说,该机制对JVM中的内存进行标记,并确定哪些内存需要回收,根据一定的回收策略,自动的回收内存,永不停息的保证JVM中的内存空间,防止出现内存泄露和溢出问题. 在真实工作中的项目中,时不时的会发生内存溢

  • Java数据结构之KMP算法详解以及代码实现

    目录 暴力匹配算法(Brute-Force,BF) 概念和原理 next数组 KMP匹配 KMP全匹配 总结 我们此前学了前缀树Trie的实现原理以及Java代码的实现.Trie树很好,但是它只能基于前缀匹配实现功能.但是如果我们的需求是:一个已知字符串中查找子串,并且子串并不一定符合前缀匹配,那么此时Trie树就无能为力了. 实际上这种字符串匹配的需求,在开发中非常常见,例如判断一个字符串是否包括某些子串,然后进行分别的处理. 暴力匹配算法(Brute-Force,BF) 这是最常见的算法字符

  • Java实现最小生成树算法详解

    目录 定义 带权图的实现 Kruskal 算法 二叉堆 并查集 实现算法 Prim 算法 定义 在一幅无向图G=(V,E) 中,(u,v) 为连接顶点u和顶点v的边,w(u,v)为边的权重,若存在边的子集T⊆E且(V,T) 为树,使得 最小,这称 T 为图 G 的最小生成树. 说的通俗点,最小生成树就是带权无向图中权值和最小的树.下图中黑色边所标识的就是一棵最小生成树(图片来自<算法第四版>),对于权值各不相同的连通图来说最小生成树只会有一棵: 带权图的实现 在 <如何在 Java 中实

  • Java求最小生成树的两种算法详解

    目录 1 最小生成树的概述 2 普里姆算法(Prim) 2.1 原理 2.2 案例分析 3 克鲁斯卡尔算法(Kruskal) 3.1 原理 3.2 案例分析 4 邻接矩阵加权图实现 5 邻接表加权图实现 6 总结 介绍了图的最小生成树的概念,然后介绍了求最小生成树的两种算法:Prim算法和Kruskal算法的原理,最后提供了基于邻接矩阵和邻接链表的图对两种算法的Java实现. 阅读本文需要一定的图的基础,如果对于图不是太明白的可以看看这篇文章:Java数据结构之图的原理与实现. 1 最小生成树的

  • java 中归并排序算法详解

    java 中归并排序算法详解 归并排序算法,顾名思义,是一种先分再合的算法,其算法思想是将要排序的数组分解为单个的元素,每个元素就是一个单个的个体,然后将相邻的两个元素进行从小到大或从大到小的顺序排序组成一个整体,每个整体包含一到两个元素,然后对相邻的整体继续"合"并,因为每个整体都是排过序的,因而可以采用一定的算法对其进行合并,合并之后每个整体包含三到四个元素,继续对相邻的整体进行合并,直到所有的整体都合并为一个整体,最终得到的整体就是将原数组进行排序之后的结果. 对于相邻的整体,其

  • Java语言实现快速幂取模算法详解

    快速幂取模算法的引入是从大数的小数取模的朴素算法的局限性所提出的,在朴素的方法中我们计算一个数比如5^1003%31是非常消耗我们的计算资源的,在整个计算过程中最麻烦的就是我们的5^1003这个过程 缺点1:在我们在之后计算指数的过程中,计算的数字不都拿得增大,非常的占用我们的计算资源(主要是时间,还有空间) 缺点2:我们计算的中间过程数字大的恐怖,我们现有的计算机是没有办法记录这么长的数据的,所以说我们必须要想一个更加高效的方法来解决这个问题 当我们计算AB%C的时候,最便捷的方法就是调用Ma

  • Java垃圾回收之分代收集算法详解

    概述 这种算法,根据对象的存活周期的不同将内存划分成几块,新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法.可以用抓重点的思路来理解这个算法. 新生代对象朝生夕死,对象数量多,只要重点扫描这个区域,那么就可以大大提高垃圾收集的效率.另外老年代对象存储久,无需经常扫描老年代,避免扫描导致的开销. 新生代 在新生代,每次垃圾收集器都发现有大批对象死去,只有少量存活,采用复制算法,只需要付出少量存活对象的复制成本就可以完成收集:可以参看我之前写的Java垃圾回收之复制算法详解 老年代

  • Java垃圾回收之复制算法详解

    之前的Java垃圾回收之标记清除算法详解 会导致内存碎片.下文的介绍的coping算法可以解决内存碎片问题. 概述 如果jvm使用了coping算法,一开始就会将可用内存分为两块,from域和to域, 每次只是使用from域,to域则空闲着.当from域内存不够了,开始执行GC操作,这个时候,会把from域存活的对象拷贝到to域,然后直接把from域进行内存清理. 应用场景 coping算法一般是使用在新生代中,因为新生代中的对象一般都是朝生夕死的,存活对象的数量并不多,这样使用coping算法

随机推荐