使用Java实现6种常见负载均衡算法

 更新时间:2023年06月12日 08:43:30   作者:Cosolar  
Java负载均衡算法也是分布式系统中的重要组成部分,用于将来自客户端的请求分配到不同的后端服务器上,本文将介绍常见的Java负载均衡算法,轮询法、加权随机法……一次性让你了解 6 种常见负载均衡算法

负载均衡是指将来自客户端的请求分摊到多个服务器上进行处理,从而有效地提高系统性能、可用性和可扩展性。常见的负载均衡算法包括轮询法、加权轮询法、随机法、加权随机法、源地址哈希法和最小连接数法等。

在实际应用中有很多工具和框架使用了这些算法来解决服务器负载均衡的问题。下面我整理出了一些常见的工具和框架:

  • Nginx:Nginx 是一款高性能的 Web 服务器,同时也是一款反向代理服务器。Nginx 的负载均衡模块支持多种算法,包括轮询法、加权轮询法、IP_HASH 等。

  • Apache:Apache 是一款流行的 Web 服务器。Apache 也提供了负载均衡模块,支持多种算法,包括轮询法、加权轮询法、最小连接数法等。

  • HAProxy:HAProxy 是一款高性能的 TCP/HTTP 负载均衡器,支持多种负载均衡算法,包括轮询法、加权轮询法、IP_HASH、最少连接数法、最短响应时间法等。

  • Spring Cloud Ribbon:Ribbon 是一款基于 HTTP 和 TCP 客户端的负载均衡器,是 Spring Cloud 生态系统中的一员。Ribbon 支持多种负载均衡算法,包括轮询法、加权轮询法、随机法等。

  • ZooKeeper:ZooKeeper 是一款分布式协调服务,在分布式系统中广泛应用。ZooKeeper 的客户端库 Curator 提供了一种基于 ZooKeeper 的负载均衡算法,称为 Dynamic Server List Load Balancing。

这些工具和框架都应用了负载均衡算法来实现服务器的负载均衡,它们的实现方式也各不相同,但是尽管如此,我们可以从中学习到很多有价值的经验和思路。

划重点 Java 架构师必备技能-我们要深入学习各类负载均衡算法实现方式

Java负载均衡算法也是分布式系统中的重要组成部分,用于将来自客户端的请求分配到不同的后端服务器上,以达到提高系统吞吐量、减轻服务器负担、提高系统可用性等目的。本文将介绍常见的Java负载均衡算法,轮询法、加权随机法……一次性让你了解 6 种常见负载均衡算法。

一、轮询法(Round Robin)

轮询法是最简单、最常见的负载均衡算法之一,其实现思路也非常简单:按照事先规定的顺序依次将请求转发至后端服务器。例如,若有3台服务器,则第1个请求会被分配到第1台服务器上,第2个请求会被分配到第2台服务器上,第3个请求会被分配到第3台服务器上,第4个请求又会被分配到第1台服务器上,以此类推。

这种算法的优点是实现简单、可靠性高,但是它并没有考虑服务器的实际负载情况,导致某些服务器可能会承受过多的负载,而其他服务器则处于空闲状态。

轮询法(Round Robin)写个简单的实现代码让你感觉一下:

//定义一个全局计数器,每次调用累加
private static AtomicInteger atomicInteger = new AtomicInteger(0);
//定义服务器列表
private static List<String> serverList = new ArrayList<>();
public static String roundRobin() {
    //获取服务器数量
    int serverCount = serverList.size();
    //获取当前请求应该转发到哪台服务器
    int currentServerIndex = atomicInteger.incrementAndGet() % serverCount;
    //返回对应的服务器地址
    return serverList.get(currentServerIndex);
}

二、加权轮询法(Weight Round Robin)

加权轮询法是在轮询法的基础上进行改进,其思路是在服务器的选择中,根据服务器的处理能力或负载情况分配不同的权重,以使处理能力较强或负载较轻的服务器获得更多的请求。例如,若存在2台服务器,其中第1台服务器负载比较重,则应当将更多的请求分配给第2台服务器。

举个例子,如果服务器A的权重是2,服务器B的权重是1,那么在两个请求中,有一个请求会被发送到服务器A,另一个请求将被发送到服务器B。

这种算法的优点是可以根据服务器的实际负载情况来分配请求,但是还是存在服务器负载不均衡的问题,因为它只是根据权值进行分配,并没有考虑服务器的实际负载情况。

加权轮询法按自己的思路写一下如下示例:

//定义一个全局计数器,每次调用累加
private static AtomicInteger atomicInteger = new AtomicInteger(0);
//定义服务器列表及服务器权重值
private static Map<String, Integer> serverMap = new ConcurrentHashMap<>();
//记录服务器权重总和
private static int totalWeight = 0;
public static String weightRoundRobin() {
    //获取服务器数量
    int serverCount = serverMap.size();
    //如果没有可用的服务器返回null
    if (serverCount == 0) {
        return null;
    }
    //在此处为避免多线程并发操作造成错误,在方法内部进行锁操作
    synchronized (serverMap) {
        //计算服务器权重总和
        for (Map.Entry<String, Integer> entry : serverMap.entrySet()) {
            totalWeight += entry.getValue();
        }
        //获取当前请求应该转发到哪台服务器
        int currentServerIndex = atomicInteger.incrementAndGet() % totalWeight;
        //遍历服务器列表,根据服务器权重值选择对应地址
        for (Map.Entry<String, Integer> entry : serverMap.entrySet()) {
            String serverAddress = entry.getKey();
            Integer weight = entry.getValue();
            currentServerIndex -= weight;
            if (currentServerIndex < 0) {
                return serverAddress;
            }
        }
    }
    //默认返回null
    return null;
}

这是源码中的一个小小的摘抄,供你观赏一下:

public class WeightRoundRobinLoadBalancer implements LoadBalancer {
    private List<String> servers = new ArrayList<>();
    private Map<String, Integer> weightMap = new HashMap<>();
    private int currentWeightIndex = -1;
    public WeightRoundRobinLoadBalancer(Map<String, Integer> servers) {
        this.servers.addAll(servers.keySet());
        for (String server : servers.keySet()) {
            int weight = servers.get(server);
            weightMap.put(server, weight);
        }
    }
    @Override
    public synchronized String chooseServer() {
        int weightSum = weightMap.values().stream().reduce(Integer::sum).orElse(0);
        while (true) {
            currentWeightIndex = (currentWeightIndex + 1) % servers.size();
            String server = servers.get(currentWeightIndex);
            int weight = weightMap.get(server);
            if (weight >= weightSum) {
                return server;
            }
            weightSum -= weight;
        }
    }
}

三、随机法(Random)

随机法是指将请求随机分配至后端服务器的负载均衡算法。该算法实现简单,但分配效果不可控,难以保证后端服务器的负载均衡。因此,随机法通常被用作测试或压力测试等临时场景下的负载均衡算法。

随机法实现代码如下:

// 1、思路参考:-----------------------------------------------
//定义服务器列表
private static List<String> serverList = new ArrayList<>();
public static String random() {
    //获取服务器数量
    int serverCount = serverList.size();
    //如果没有可用的服务器返回null
    if (serverCount == 0) {
        return null;
    }
    //生成一个随机数
    int randomIndex = new Random().nextInt(serverCount);
    //返回对应的服务器地址
    return serverList.get(randomIndex);
}
// 2、源码参考:-----------------------------------------------
public class RandomLoadBalancer implements LoadBalancer {
    private List<String> servers = new ArrayList<>();
    public RandomLoadBalancer(List<String> servers) {
        this.servers = servers;
    }
    @Override
    public String chooseServer() {
        int randomIndex = ThreadLocalRandom.current().nextInt(servers.size());
        return servers.get(randomIndex);
    }
}

四、加权随机法(Weight Random)

加权随机法是在随机法的基础上进行改进,其思路是在服务器的选择中,根据服务器的处理能力或负载情况分配不同的权重,以使处理能力较强或负载较轻的服务器获得更多的请求。

加权随机法实现代码如下:

// 1、思路参考:-----------------------------------------------------------
//定义服务器列表及服务器权重值
private static Map<String, Integer> serverMap = new ConcurrentHashMap<>();
//记录服务器权重总和
private static int totalWeight = 0;
public static String weightRandom() {
    //获取服务器数量
    int serverCount = serverMap.size();
    //如果没有可用的服务器返回null
    if (serverCount == 0) {
        return null;
    }
    //在此处为避免多线程并发操作造成错误,在方法内部进行锁操作
    synchronized (serverMap) {
        //计算服务器权重总和
        for (Map.Entry<String, Integer> entry : serverMap.entrySet()) {
            totalWeight += entry.getValue();
        }
        //生成一个随机数
        int randomWeight = new Random().nextInt(totalWeight);
        //遍历服务器列表,根据服务器权重值选择对应地址
        for (Map.Entry<String, Integer> entry : serverMap.entrySet()) {
            String serverAddress = entry.getKey();
            Integer weight = entry.getValue();
            randomWeight -= weight;
            if (randomWeight < 0) {
                return serverAddress;
            }
        }
    }
    //默认返回null
    return null;
}
// 2、源码参考:-----------------------------------------------------------
public class WeightRandomLoadBalancer implements LoadBalancer {
    private List<String> servers = new ArrayList<>();
    private Map<String, Integer> weightMap = new HashMap<>();
    public WeightRandomLoadBalancer(Map<String, Integer> servers) {
        this.servers.addAll(servers.keySet());
        for (String server : servers.keySet()) {
            int weight = servers.get(server);
            weightMap.put(server, weight);
        }
    }
    @Override
    public String chooseServer() {
        int weightSum = weightMap.values().stream().reduce(Integer::sum).orElse(0);
        int randomWeight = ThreadLocalRandom.current().nextInt(weightSum) + 1;
        for (String server : servers) {
            int weight = weightMap.get(server);
            if (randomWeight <= weight) {
                return server;
            }
            randomWeight -= weight;
        }
        return null;
    }
}

五、源地址哈希法(Hash)

源地址哈希法是一种基于请求源IP地址的负载均衡算法,其思路是将每个请求的源IP地址通过哈希函数计算出一个值,然后根据该值与可用服务器总数取模的结果来确定该请求应当转发到哪台服务器上。

换言之,源地址哈希算法就是使用客户端 IP 地址作为哈希键。负载均衡器将哈希值映射到可用服务器中的一个,然后将请求发送到这个服务器处理。如果客户端 IP 地址发生改变(比如重启后重新分配 IP 地址),那么将会被分配到其他服务器上。

这种算法的优点是可以避免某些客户端被重定向到不同的服务器,对于同一IP地址的请求,总是会被分配到同一台服务器上,因此可以在一定程度上提高缓存命中率等性能指标,但是它也有一些缺点。例如,如果有很多请求来自相同的 IP 地址,那么可能会导致某个服务器负载过高。另外,由于服务器数量的变化,哈希值映射也会发生变化,这可能会导致缓存无效,并且需要重新分配所有请求。

源地址哈希法实现代码示例如下:

// 1、思路参考:-----------------------------------------------------------
//定义服务器列表
private static List<String> serverList = new ArrayList<>();
public static String hash(String clientIP) {
    //获取服务器数量
    int serverCount = serverList.size();
    //如果没有可用的服务器返回null
    if (serverCount == 0) {
        return null;
    }
    //将客户端IP地址进行哈希计算
    int hashCode = clientIP.hashCode();
    //根据哈希值计算需要转发到哪台服务器上
    int serverIndex = hashCode % serverCount;
    //返回对应的服务器地址
    return serverList.get(serverIndex);
}
// 2、源码参考:-----------------------------------------------------------
public class HashLoadBalancer implements LoadBalancer {
    private List<String> servers = new ArrayList<>();
    public HashLoadBalancer(List<String> servers) {
        this.servers = servers;
    }
    @Override
    public String chooseServer() {
        String clientIp = getClientIp();
        int hashCode = Math.abs(clientIp.hashCode());
        return servers.get(hashCode % servers.size());
    }
    private String getClientIp() {
        // 获取客户端IP地址的代码省略
        return "1.1.1.1";
    }
}

六、最小连接数法(Least Connections)

最小连接数法是一种动态调整的负载均衡算法,其思路是尽可能地将请求分配给当前空闲连接数最少的后端服务器,以达到负载均衡的效果。在实现过程中,通常需要定期检测各个服务器的连接数并进行动态调整。

最小连接数算法是根据当前连接数来选择一个可用服务器。负载均衡器会查询可用服务器的连接数,然后选择一个连接数最小的服务器。这种算法保证了服务器不会被过度负载,并且还允许负载均衡器根据实际情况动态分配请求。

需要注意的是,如果服务器挂掉了或者网络链路中断了,那么负载均衡器就需要重新计算服务器的连接数,这将延长响应时间并且影响性能。

最小连接数法实现代码示例如下:

// 1、思路参考:-----------------------------------------------------------
//定义服务器列表
private static List<String> serverList = new ArrayList<>();
//记录每个服务器的连接数
private static Map<String, Integer> connectionsMap = new ConcurrentHashMap<>();
public static String leastConnections() {
    //获取服务器数量
    int serverCount = serverList.size();
    //如果没有可用的服务器返回null
    if (serverCount == 0) {
        return null;
    }
    //默认选择第一个服务器
    String selectedServerAddress = serverList.get(0);
    //获取第一个服务器的连接数
    int minConnections = connectionsMap.getOrDefault(selectedServerAddress, 0);
    //遍历服务器列表,寻找连接数最少的服务器
    for (int i = 1; i < serverCount; i++) {
        String serverAddress = serverList.get(i);
        int connections = connectionsMap.getOrDefault(serverAddress, 0);
        if (connections < minConnections) {
            selectedServerAddress = serverAddress;
            minConnections = connections;
        }
    }
    //返回连接数最少的服务器地址
    return selectedServerAddress;
}
// 2、源码参考:-----------------------------------------------------------
public class LeastConnectionsLoadBalancer implements LoadBalancer {
    private List<String> servers = new ArrayList<>();
    private Map<String, Integer> connectionsMap = new HashMap<>();
    public LeastConnectionsLoadBalancer(List<String> servers) {
        this.servers = servers;
        for (String server : servers) {
            connectionsMap.put(server, 0);
        }
    }
    @Override
    public synchronized String chooseServer() {
        int minConnections = Integer.MAX_VALUE;
        String targetServer = null;
        for (String server : servers) {
            int connections = connectionsMap.get(server);
            if (connections < minConnections) {
                minConnections = connections;
                targetServer = server;
            }
        }
        connectionsMap.put(targetServer, connectionsMap.get(targetServer) + 1);
        return targetServer;
    }
    public void releaseConnection(String server) {
        connectionsMap.put(server, connectionsMap.get(server) - 1);
    }
}

以上便是常见的Java负载均衡算法,这些算法都有其自身的优缺点和适用场景。

七、小结一下

Java 架构师面临的挑战越来越大,我们需要在不断发展的技术中保持敏锐的触觉,并掌握越来越广泛的知识。而在当前互联网架构中,负载均衡算法是一个至关重要的领域。它是实现服务的高可用性和可伸缩性的重要手段。因此,Java 架构师必须深入学习各类负载均衡算法的实现方式,并且理解它们的优劣之处,以便为公司设计出更好的网络架构。

如果你想成为一名 Java 架构师或者网络工程师,那么希望你多多了解底层知识对你将大有益处。让我们一起努力吧!

以上就是使用Java实现6种常见负载均衡算法的详细内容,更多关于Java 负载均衡的资料请关注脚本之家其它相关文章!

相关文章

  • 详细解读spring中的@Resource注解

    详细解读spring中的@Resource注解

    这篇文章主要介绍了详细解读spring中的@Resource注解,此注解来源于JSR规范(Java Specification Requests),其作用是找到依赖的组件注入到应用来,它利用了JNDI技术查找所需的资源,需要的朋友可以参考下
    2023-10-10
  • Java对象以Hash结构存入Redis详解

    Java对象以Hash结构存入Redis详解

    这篇文章主要介绍了Java对象以Hash结构存入Redis详解,和Java中的对象非常相似,却不能按照Java对象的结构直接存储进Redis的hash中,因为Java对象中的field是可以嵌套的,而Redis的Hash结构不支持嵌套结构,需要的朋友可以参考下
    2023-08-08
  • 优化SpringBoot程序启动速度的实现

    优化SpringBoot程序启动速度的实现

    本文主要介绍了优化SpringBoot程序启动速度的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-01-01
  • SpringMVC4.3解析器HandlerMethodArgumentResolver接口源码

    SpringMVC4.3解析器HandlerMethodArgumentResolver接口源码

    这篇文章主要为大家介绍了SpringMVC4.3解析器HandlerMethodArgumentResolver接口源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-09-09
  • Java实现员工管理系统

    Java实现员工管理系统

    这篇文章主要为大家详细介绍了Java实现员工管理系统,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-01-01
  • java启动jar包设置启动参数的实现

    java启动jar包设置启动参数的实现

    本文主要介绍了java启动jar包设置启动参数的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • java模拟实现微信红包算法

    java模拟实现微信红包算法

    这篇文章主要为大家详细介绍了java实现模拟微信红包算法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-11-11
  • Java创建对象的四种方式详解

    Java创建对象的四种方式详解

    这篇文章主要介绍了Java创建对象的四种方式详解,如果我们不想利用默认构造器来创建java对象,而想利用指定的构造器来创建java对象,则需要利用Construtor对象,每个Construtor对应一个构造器,需要的朋友可以参考下
    2023-11-11
  • Spring存储与读取Bean对象方法

    Spring存储与读取Bean对象方法

    在Spring中,要想更简单的存储和读取对象的核心是使用注解,这篇文章主要给大家介绍了关于Spring如何通过注解存储和读取对象的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • 总结Java常用到的六个加密技术和代码

    总结Java常用到的六个加密技术和代码

    大家要记住现代密码学最重要的原则柯克霍夫原则:数据的安全基于密钥而不是算法的保密。也就是说即使密码系统的任何细节已为人悉知,只要密匙未洩漏,它也应是安全的。这篇文章给大家介绍了6个常用的加密技术和代码。
    2016-07-07

最新评论