【SpringCloud系列】开发环境下重写Loadbalancer实现自定义负载均衡

前言

        spring-cloud-starter-netflix-ribbon已经不再更新了,最新版本是2.2.10.RELEASE,最后更新时间是2021年11月18日,详细信息可以看maven官方仓库:https://search.maven.org/artifact/org.springframework.cloud/spring-cloud-starter-netflix-ribbon,SpringCloud官方推荐使用spring-cloud-starter-loadbalancer进行负载均衡。我们在开发的时候,多人开发同一个微服务,都注册到同一个nacos,前端请求的时候,网关Gateway默认轮训请求注册中心的服务,OpenFeign也会轮询请求注册中心的服务,这样就会导致前端有时会无法请求到我们本地写的接口,而是请求到别人的服务中。所以我们可以重写Loadbalancer默认的负载均衡策略,实现自定义负载均衡策略,不管是Gateway还是OpenFeign都只能请求到我们自己本地的服务。

        我的版本如下:

        <spring-boot.version>2.7.3</spring-boot.version>
        <spring-cloud.version>2021.0.4</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>

一、添加负载方式配置

        1、定义负载均衡方式的枚举

public enum LoadBalancerTypeEnum {

    /**
     * 开发环境,获取自己的服务
     */
    DEV,

    /**
     * 网关,根据请求地址获取对应的服务
     */
    GATEWAY,

    /**
     * 轮循
     */
    ROUND_ROBIN,

    /**
     * 随机
     */
    RANDOM;

}

        2、添加配置类,默认使用轮训方式

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * 负载均衡配置项
 */
@Data
@ConfigurationProperties(prefix = "spring.cloud.loadbalancer")
public class LoadBalanceProperties {

    private LoadBalancerTypeEnum type = LoadBalancerTypeEnum.ROUND_ROBIN;

}

二、参考默认实现自定义

        默认的负载均衡策略是这个类:

org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer

        我们参考这个类实现自己的负载均衡策略即可,RoundRobinLoadBalancer实现了ReactorServiceInstanceLoadBalancer这个接口,实现了choose这个方法,如下图:

        在choose方法中调用了processInstanceResponse方法,processInstanceResponse方法中调用了getInstanceResponse方法,所以我们我们可以复制RoundRobinLoadBalancer整个类,只修改getInstanceResponse这个方法里的内容就可以实现自定义负载均衡策略。

        在自定义的类中,我们实现了四种负载均衡策略

        1、getRoundRobinInstance方法是直接复制的RoundRobinLoadBalancer类中的实现;

        2、getRandomInstance方法参考org.springframework.cloud.loadbalancer.core.RandomLoadBalancer类中的实现;

        3、getDevelopmentInstance方法是返回所有服务中和当前机器ip一致的服务,如果没有,则轮训返回;

        4、getGatewayDevelopmentInstance方法是返回所有服务中和网关请求头中ip一致的服务。

import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.ip.IpUtils;
import com.ruoyi.common.loadbalance.config.LoadBalancerTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestData;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 自定义 SpringCloud 负载均衡算法
 * 负载均衡算法的默认实现是 {@link org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer}
 *
 */
@Slf4j
public class CustomSpringCloudLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;
    private final AtomicInteger position;
    private final LoadBalancerTypeEnum type;
    private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public CustomSpringCloudLoadBalancer(String serviceId,
                                         LoadBalancerTypeEnum type,
                                         ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this(serviceId, new Random().nextInt(1000), type, serviceInstanceListSupplierProvider);
    }

    public CustomSpringCloudLoadBalancer(String serviceId,
                                         int seedPosition,
                                         LoadBalancerTypeEnum type,
                                         ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.serviceId = serviceId;
        this.position = new AtomicInteger(seedPosition);
        this.type = type;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map(serviceInstances -> processInstanceResponse(request, supplier, serviceInstances));
    }

    private Response<ServiceInstance> processInstanceResponse(Request request,
                                                              ServiceInstanceListSupplier supplier,
                                                              List<ServiceInstance> serviceInstances) {
        Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(request, serviceInstances);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
        }
        return serviceInstanceResponse;
    }

    private Response<ServiceInstance> getInstanceResponse(Request request, List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + serviceId);
            }
            return new EmptyResponse();
        }

        if (Objects.equals(type, LoadBalancerTypeEnum.ROUND_ROBIN)){
            return this.getRoundRobinInstance(instances);
        }else if (Objects.equals(type, LoadBalancerTypeEnum.RANDOM)){
            return this.getRandomInstance(instances);
        }else if (Objects.equals(type, LoadBalancerTypeEnum.DEV)){
            return this.getDevelopmentInstance(instances);
        }else if (Objects.equals(type, LoadBalancerTypeEnum.GATEWAY)){
            return this.getGatewayDevelopmentInstance(request, instances);
        }
        return this.getRoundRobinInstance(instances);
    }

    /**
     * 获取网关本机实例
     *
     * @param instances 实例
     * @return {@link Response }<{@link ServiceInstance }>
     * @author : lwq
     * @date : 2022-12-15 14:22:13
     */
    private Response<ServiceInstance> getGatewayDevelopmentInstance(Request request, List<ServiceInstance> instances) {

        //把request转为默认的DefaultRequest,从request中拿到请求的ip信息,再选择ip一样的微服务
        DefaultRequest<RequestDataContext> defaultRequest = Convert.convert(new TypeReference<DefaultRequest<RequestDataContext>>() {}, request);
        RequestDataContext context = defaultRequest.getContext();
        RequestData clientRequest = context.getClientRequest();
        HttpHeaders headers = clientRequest.getHeaders();
        String requestIp = IpUtils.getIpAddressFromHttpHeaders(headers);
        log.debug("客户端请求gateway的ip:{}", requestIp);

        //先取得和本地ip一样的服务,如果没有则按默认来取
        for (ServiceInstance instance : instances) {
            String currentServiceId = instance.getServiceId();
            String host = instance.getHost();
            log.debug("注册服务:{},ip:{}", currentServiceId, host);
            if (StringUtils.isNotEmpty(host) && StringUtils.equals(requestIp, host)) {
                return new DefaultResponse(instance);
            }
        }
        return getRoundRobinInstance(instances);
    }


    /**
     * 获取本机实例
     *
     * @param instances 实例
     * @return {@link Response }<{@link ServiceInstance }>
     * @author : lwq
     * @date : 2022-12-15 14:22:13
     */
    private Response<ServiceInstance> getDevelopmentInstance(List<ServiceInstance> instances) {
        //获取本机ip
        String hostIp = IpUtils.getHostIp();
        log.debug("本机Ip:{}", hostIp);

        //先取得和本地ip一样的服务,如果没有则按默认来取
        for (ServiceInstance instance : instances) {
            String currentServiceId = instance.getServiceId();
            String host = instance.getHost();
            log.debug("注册服务:{},ip:{}", currentServiceId, host);
            if (StringUtils.isNotEmpty(host) && StringUtils.equals(hostIp, host)) {
                return new DefaultResponse(instance);
            }
        }
        return getRoundRobinInstance(instances);
    }

    /**
     * 使用随机算法
     * 参考{link {@link org.springframework.cloud.loadbalancer.core.RandomLoadBalancer}}
     *
     * @param instances 实例
     * @return {@link Response }<{@link ServiceInstance }>
     * @author : lwq
     * @date : 2022-12-15 13:32:11
     */
    private Response<ServiceInstance> getRandomInstance(List<ServiceInstance> instances) {
        int index = ThreadLocalRandom.current().nextInt(instances.size());
        ServiceInstance instance = instances.get(index);
        return new DefaultResponse(instance);
    }

    /**
     * 使用RoundRobin机制获取节点
     *
     * @param instances 实例
     * @return {@link Response }<{@link ServiceInstance }>
     * @author : lwq
     * @date : 2022-12-15 13:28:31
     */
    private Response<ServiceInstance> getRoundRobinInstance(List<ServiceInstance> instances) {
        // 每一次计数器都自动+1,实现轮询的效果
        int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
    }

}

        其中的工具类如下:

import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.URLUtil;
import org.springframework.http.HttpHeaders;

/**
 * 获取IP方法
 */
public class IpUtils{

    /**
     * 获取IP地址
     *
     * @return 本地IP地址
     */
    public static String getHostIp(){
        try{
            return InetAddress.getLocalHost().getHostAddress();
        }catch (UnknownHostException e){
        }
        return "127.0.0.1";
    }

    /**
     * 获取客户端IP
     *
     * @param httpHeaders 请求头
     * @return IP地址
     */
    public static String getIpAddressFromHttpHeaders(HttpHeaders httpHeaders){
        if (httpHeaders == null){
            return "unknown";
        }
        //前端请求自定义请求头,转发到哪个服务
        List<String> ipList = httpHeaders.get("forward-to");
        String ip = CollectionUtil.get(ipList, 0);
        //请求自带的请求头
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ipList = httpHeaders.get("x-forwarded-for");
            ip = CollectionUtil.get(ipList, 0);
        }
        //从referer获取请求的ip地址
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            //从referer中获取请求的ip地址
            List<String> refererList = httpHeaders.get("referer");
            String referer = CollectionUtil.get(refererList, 0);
            URL url = URLUtil.url(referer);
            if (Objects.nonNull(url)){
                ip = url.getHost();
            }else {
                ip = "unknown";
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ipList = httpHeaders.get("Proxy-Client-IP");
            ip = CollectionUtil.get(ipList, 0);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ipList = httpHeaders.get("X-Forwarded-For");
            ip = CollectionUtil.get(ipList, 0);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ipList = httpHeaders.get("WL-Proxy-Client-IP");
            ip = CollectionUtil.get(ipList, 0);
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ipList = httpHeaders.get("X-Real-IP");
            ip = CollectionUtil.get(ipList, 0);
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
    }
}

        getIpAddressFromHttpHeaders方法中,是从请求头总拿到了自定义的请求头forward-to,要想实现此功能,就需要前端发送请求的时候携带这个请求头,例如

        在若依的request.js中可以这么写: config.headers['forward-to'] = '192.168.0.145'

三、配置负载均衡策略

       默认的配置在这里:

org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration

        我们参考这个配置,实现自己的配置。启用上面写好的配置类LoadBalanceProperties,然后传到自定义的负载均衡策略类CustomSpringCloudLoadBalancer,其他的复制就可以。

import com.ruoyi.common.loadbalance.core.CustomSpringCloudLoadBalancer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
 * 自定义负载均衡客户端配置
 *
 */
@SuppressWarnings("all")
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LoadBalanceProperties.class)
public class CustomLoadBalanceClientConfiguration {

    @Bean
    @ConditionalOnBean(LoadBalancerClientFactory.class)
    public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(LoadBalanceProperties loadBalanceProperties,
                                                                   Environment environment,
                                                                   LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new CustomSpringCloudLoadBalancer(name, loadBalanceProperties.getType(),
                loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
    }

}

        然后使用LoadBalancerClients注解加载一下配置

import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;

/**
 * 自定义负载均衡自动配置
 *
 */
@LoadBalancerClients(defaultConfiguration = CustomLoadBalanceClientConfiguration.class)
public class CustomLoadBalanceAutoConfiguration {

}

四、使用

        将以上代码独立成一个模块,然后再其他微服务中的pom文件中引入,然后添加对应的配置就可以实现自定义负载均衡了

        1、在微服务中配置如下即可实现调用其他服务时,调用自己本地开发环境的微服务

    spring.cloud.loadbalancer.type=dev

        2、在网关中配置如下即可实现调用固定某个服务

 spring.cloud.loadbalancer.type=gateway

写在最后的话

        最开始只有想法,但是不知道怎么实现,百度也没找到合适的方案。所以就开始看源码,研究了一下,然后照着源码写,测试了一下真的就实现了。所以,多看看源码还是有好处的。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Windows环境下实现设计模式——职责链模式(JAVA版)

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;今天总结一下Windows环境下如何编程实现职责链模式&#xff08;设计模式&#xff09;。 不知道大家有没有这样的感觉&#xff0c;看了一大堆编程和设计模式的书&#xff0c;却还是很难理解设计模式&#xff…

Maven的进阶操作

系列文章目录 Maven进阶操作的学习 文章目录系列文章目录前言一、分模块开发与设计二、依赖管理1.依赖传递2.可选依赖3.排除依赖三、继承与聚合1.聚合2.继承四、属性1.属性2.版本管理五、多环境配置与应用1.多环境开发2.跳过测试六、私服1.私服简介2.私服仓库分类3.资源上传与…

比GPT-4 Office还炸裂,阿里版GPT全家桶来袭

目录 【新智元导读】 文案、策划、邮件&#xff0c;一键搞定 不用写代码&#xff0c;草稿秒变小程序 聊天记录不用翻&#xff0c;摘要自动生成 会上开小差&#xff1f;不怕&#xff0c;AI替你记了 AI版十万个为什么&#xff0c;有问必答 剁手买买买&#xff0c;连手都不…

Excel技能之数据验证,总有一款适合你

用户填写的内容&#xff0c;是未知的&#xff0c;不可靠的。但是&#xff0c;我们要对数据的规范、格式、条件做出限制&#xff0c;既能保证数据的质量&#xff0c;也能统一每个人的行为。最大限度去避免垃圾数据的录入&#xff0c;眼不见心不烦&#xff0c;让心情美美的。 数…

Python之数据库操作(连接数据库,增删改查操作,易错点理解)

文章目录 前言一、Python之数据库操作二、pymysql 安装三、pymysql 包引入 连接数据库创建游标执行sql数据 - 增删改查要获取查询结果数据关闭游标&#xff0c;关闭数据库连接总结前言 记录&#xff1a;Python操作数据库的步骤&#xff0c;不容易理解的地方。 一、Python之数据…

C++模板基础(九)

完美转发与 lambda 表达式模板 void f(int& input) {std::cout << "void f(int& input)\t" << input << \n; }void f(int&& input) {std::cout << "void f(int&& input)\t" << input << \n;…

uniapp - 全平台兼容的 “多图上传“ 功能,搭配 uview 组件库中的 upload 上传组件(附带详细的示例源码及注释,可直接复制使用或简单修改)

效果图 使用 uniapp 开发,多平台全端兼容的多图上传功能,支持限制个数及移除等。 组件库使用的是 uview 框架,上传组件基于 Upload组件,功能完美无bug。 准备阶段 Upload组件支持手动上传与

Docker安装Elasticsearch详细步骤

1 安装elasticsearch 1.1 拉取镜像 docker pull elasticsearch:7.12.11.2 创建挂载目录 mkdir -p /app/elasticsearch/confecho "http.host: 0.0.0.0" >> /app/elasticsearch/conf/elasticsearch.ymlmkdir -p /app/elasticsearch/datamkdir -p /app/elastic…

GaussDB工作级开发者认证—第三章开发设计建议

一. 数据库对象命名和设计建议 二. 表设计最佳实践 三. SQL查询最佳实践 SQL 最佳实践 - SELECT 避免对大字段执行order by&#xff0c;group by等引起排序的操作避免频繁使用count()获取大表行数慎用通配符字段 “*”避免在select目标列中使用子查询统计表中所有记录数时&…

Leetcode刷题之环形链表

莫等闲&#xff0c;白了少年头&#xff0c;空悲切。 --岳飞 目录 1.环形链表 2.环形链表Ⅱ 1.环形链表 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next …

想制作出专业水准的音视频?掌握H.264编码技巧是关键

H.264编码原理 H.264&#xff0c;也被称为先进视频编码&#xff08;AVC&#xff09;&#xff0c;是目前最流行的视频编码标准之一&#xff0c;其压缩效率很高。H.264编码基于视频编码的原始数据&#xff0c;使用一系列算法和技术以更小的比特率呈现更高质量的视频。以下是H.26…

SpringBoot整合xxl-job详细教程

SrpingBoot整合xxl-job&#xff0c;实现任务调度说明调度中心执行器调试整合SpringBoot说明 Xxl-Job是一个轻量级分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c;开箱即用。Xxl-Job有…

主机发现和端口扫描基本原理和工具选择

发现主机 扫描端口指令sudo nmap -sn ip 实则是封装ping指令 可以找目标靶机 sudo nmap --min-rate 10000 -p- 192.168.10.191 -p端口号 -p-从一开始扫 设置最小速度扫描 -p-指定靶机 10000是较好的速度 在工作中最好扫两遍 UDP扫描 sudo nmap -sU --min-rate 10000 …

Golang每日一练(leetDay0035) 二叉树专题(4)

目录 103. 二叉树的锯齿形层序遍历 Binary Tree Zigzag Level Order Traversal &#x1f31f;&#x1f31f; 104. 二叉树的最大深度 Maximum Depth of Binary-tree] &#x1f31f; 105. 从前序与中序遍历序列构造二叉树 Construct-binary-tree-from-preorder-and-inorder-…

一文弄懂访问者模式

关于设计模式&#xff0c;我们得结合生活中的案例来学习&#xff1b;最近我在网上也看了不少文章&#xff0c;今天想跟大家分享一下关于访问者模式的一些知识&#xff0c;先来看一个简单的案例吧。 相信大家都去过医院&#xff0c;看完病&#xff0c;医生都会给我们开一个处方…

2023最新面试题-Java-6

1. Date API Java 8 在包java.time下包含了一组全新的时间日期API。新的日期API和开源的Joda-Time库差不多&#xff0c;但 又不完全一样&#xff0c;下面的例子展示了这组新API里最重要的一些部分&#xff1a; Clock类提供了访问当前日期和时间的方法&#xff0c;Clock是时区敏…

环境变量概念详解!(4千字长文)

环境变量&#xff01; 文章目录环境变量&#xff01;环境变量PATHexportexport的错误用法定义命令行变量环境变量哪里来的其他各种环境变量HOMEHOSTNAMELOGNAMEHISTSIZEPWD环境变量相关指令echoenvgetenv——相关函数&#xff01;exportsetunset命令行参数argcargvenvpenvironp…

自动化面试题4

1、工业中常见的通信方式都有哪些&#xff0c;各自特点是什么&#xff1f; 2、对于一台新的伺服驱动器来说&#xff0c;需要设置哪几个方面的参数&#xff1f; &#xff08;1&#xff09;参数初始化 &#xff08;2&#xff09;点动测试电机旋转方向 &#xff08;3&#xff09;惯…

Android创建项目

目录 创建Android项目 配置项目结构 创建安卓模拟器 模拟器运行 HelloWorld 应用 真机运行 HelloWorld 应用 创建Android项目 打开 Android studio 工具&#xff0c;选择Project&#xff0c;选择 New Project 由于现在是教程博客&#xff0c;所以我们随便选择 一个 空 Ac…

Java使用elasticjob实现定时任务(v2.1.5)

elastic是一个定时任务库 https://shardingsphere.apache.org/elasticjob/index_zh.html 项目结构 ​依赖 <dependency><groupId>com.dangdang</groupId><artifactId>elastic-job-lite-core</artifactId><version>2.1.5</version>&…