SpringCloud自定义loadbalancer实现标签路由的详细方案

 更新时间:2025年02月15日 08:50:13   作者:烟味i  
本文介绍了通过标签路由解决前端开发环境接口调用慢的问题,实现方案包括在本地服务注册元数据、自定义负载均衡器、以及网关配置等步骤,通过环境变量设置标签,网关根据请求头中的标签进行路由,从而实现前后端互不干扰的开发调试,感兴趣的朋友一起看看吧

一、背景

  最近前端反应开发环境有时候调接口会很慢,原因是有开发图方便将本地服务注册到开发环境,请求路由到开发本地导致,

为了解决该问题想到可以通过标签路由的方式避免该问题,实现前端联调和开发自测互不干扰。

  该方案除了用于本地调试,还可以用于用户灰度发布。

二、实现方案

  关于负载均衡,低版本的SpringCloud用的是Spring Cloud Ribbon,高版本用Spring Cloud LoadBalancer替代了,

Ribbon可以通过实现IRlue接口实现,这里只介绍高版本的实现方案。

实现方案:

  • idea在环境变量中设置tag,本地服务启动时读取环境变量将tag注册到nacos的元数据

  • 重写网关的负载均衡算法,从请求头中获取到的request-tag和服务实例的元数据进行匹配,如果匹配到则返回对应的

    服务实例,否则提示服务未找到。

三、编码实现

3.1 order服务

新建一个SpringCloud服务order-service,注册元数据很简单,只需要排除掉NacosDiscoveryClientConfiguration,再写一个自己的NacosDiscoveryClientConfiguration配置类即可。

创建MyNacosDiscoveryClientConfiguration

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2025/2/12
 */
@Configuration(
        proxyBeanMethods = false
)
@ConditionalOnDiscoveryEnabled
@ConditionalOnBlockingDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
@AutoConfigureBefore({SimpleDiscoveryClientAutoConfiguration.class, CommonsClientAutoConfiguration.class})
@AutoConfigureAfter({NacosDiscoveryAutoConfiguration.class})
public class MyNacosDiscoveryClientConfiguration {
    @Bean
    public DiscoveryClient nacosDiscoveryClient(NacosServiceDiscovery nacosServiceDiscovery) {
        return new NacosDiscoveryClient(nacosServiceDiscovery);
    }
    @Bean
    @ConditionalOnProperty(
            value = {"spring.cloud.nacos.discovery.watch.enabled"},
            matchIfMissing = true
    )
    public NacosWatch nacosWatch(NacosServiceManager nacosServiceManager, NacosDiscoveryProperties nacosDiscoveryProperties,
                                 ObjectProvider<ThreadPoolTaskScheduler> taskExecutorObjectProvider, Environment environment) {
        // 环境变量读取标签
        String tag = environment.getProperty("tag");
        nacosDiscoveryProperties.getMetadata().put("request-tag", tag);
        return new NacosWatch(nacosServiceManager, nacosDiscoveryProperties, taskExecutorObjectProvider);
    }
}

这里代码基本与NacosDiscoveryClientConfiguration一致,只是加上了设置元数据的逻辑。

@SpringBootApplication(exclude = NacosDiscoveryClientConfiguration.class)
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

启动类上需要排除默认的NacosDiscoveryClientConfiguration,不然启动会报bean重复注册的错误,或者配置添加spring.main.allow-bean-definition-overriding=true允许重复注册也行。

写一个测试接口,方便后面测试

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2025/2/12
 */
@RequestMapping("test")
@RestController
public class TestController {
    @GetMapping("")
    public String sayHello(){
        return "hello";
    }
}

3.2 gateway服务

新建一个网关服务,pom文件如下:

 <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2020.0.3</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
        <spring-boot.version>2.5.1</spring-boot.version>
        <maven-compiler-plugin.version>3.1</maven-compiler-plugin.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>${spring-boot.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

Spring-Cloud-loadBalancer默认使用轮询的算法,即org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer类实现,因此可以参考RoundRobinLoadBalancer实现一个TagLoadBalancer,代码如下:

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2025/2/12
 */
public class TagLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final String TAG_HEADER = "request-tag";
    private static final Log log = LogFactory.getLog(TagLoadBalancer.class);
    final AtomicInteger position;
    final String serviceId;
    ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        this(serviceInstanceListSupplierProvider, serviceId, (new Random()).nextInt(1000));
    }
    public TagLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.position = new AtomicInteger(seedPosition);
    }
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier) this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> {
            return this.processInstanceResponse(supplier, serviceInstances, request);
        });
    }
    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) {
        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances, request);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback) supplier).selectedServiceInstance((ServiceInstance) serviceInstanceResponse.getServer());
        }
        return serviceInstanceResponse;
    }
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }
            return new EmptyResponse();
        }
        if (request instanceof DefaultRequest) {
            DefaultRequest<RequestDataContext> defaultRequest = (DefaultRequest) request;
            // 上下文获取请求头
            HttpHeaders headers = defaultRequest.getContext().getClientRequest().getHeaders();
            List<String> list = headers.get(TAG_HEADER);
            if (!CollectionUtils.isEmpty(list)) {
                String requestTag = list.get(0);
                for (ServiceInstance instance : instances) {
                    String str = instance.getMetadata().getOrDefault(TAG_HEADER, "");
                    if (requestTag.equals(str)) {
                        return new DefaultResponse(instance);
                    }
                }
                log.error(String.format("No servers available for service:%s,tag:%s ", this.serviceId, requestTag));
                return new EmptyResponse();
            }
        }
        int pos = Math.abs(this.position.incrementAndGet());
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
    }
}

这里需要实现ReactorServiceInstanceLoadBalancer接口,如果请求头带有标签则根据标签路由,否则使用默认的轮询算法。

还要把TagLoadBalancer用起来,所以需要定义一个配置类TagLoadBalancerConfig,并通过@LoadBalancerClients注解添加默认配置,代码如下:

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2025/2/12
 */
public class TagLoadBalancerConfig {
    @Bean
    public ReactorLoadBalancer reactorTagLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new TagLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}
@LoadBalancerClients(defaultConfiguration = {TagLoadBalancerConfig.class})
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

最后在application.yml文件添加网关路由配置

spring:
  application:
    name: gateway
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        namespace: dev
        group: DEFAULT_GROUP
      discovery:
        server-addr: 127.0.0.1:8848
        namespace: dev
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            - StripPrefix=1
server:
  port: 9000

3.3 代码测试

本地启动nacos后启动order(注意需要在idea设置环境变量tag=ship)和gateway服务,可以看到order服务已经成功注册了元数据

  • 然后用Postman请求网关http://localhost:9000/order/test

可以看到请求成功路由到了order服务,说明根据tag路由成功了。

去掉环境变量tag后重新启动Order服务,再次请求响应报文如下:

{
    "timestamp": "2025-02-14T12:10:44.294+00:00",
    "path": "/order/test",
    "status": 503,
    "error": "Service Unavailable",
    "requestId": "41651188-4"
}

说明根据requst-tag找不到对应的服务实例,代码逻辑生效了。

四、总结

  聪明的人已经发现了,本文只实现了网关路由到下游服务这部分的标签路由,下游服务A调服务B的标签路由并未实现,其实现方案也不难,只需要通过上下文传递+feign拦截器就可以做到全链路的标签路由,有兴趣的可以自己试试。

  本文代码已上传github,顺便推广下前段时间写的idea插件CodeFaster(快速生成常用流操作的代码,Marketplace搜索下载即可体验)😂。

到此这篇关于SpringCloud自定义loadbalancer实现标签路由的详细方案的文章就介绍到这了,更多相关SpringCloud标签路由内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 浅谈springboot 属性定义

    浅谈springboot 属性定义

    本篇文章主要介绍了浅谈springboot 属性定义,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • MyBatis-Plus联表查询(Mybatis-Plus-Join)的功能实现

    MyBatis-Plus联表查询(Mybatis-Plus-Join)的功能实现

    mybatis-plus作为mybatis的增强工具,简化了开发中的数据库操作,这篇文章主要介绍了MyBatis-Plus联表查询(Mybatis-Plus-Join),需要的朋友可以参考下
    2022-08-08
  • 不知道面试会不会问Lambda怎么用(推荐)

    不知道面试会不会问Lambda怎么用(推荐)

    这篇文章主要介绍了Lambda表达式用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • Java实现视频自定义裁剪功能

    Java实现视频自定义裁剪功能

    这篇文章主要介绍了如何通过java实现视频裁剪,可以将视频按照自定义尺寸进行裁剪,文中的示例代码简洁易懂,感兴趣的可以了解一下
    2022-01-01
  • 继承WebMvcConfigurationSupport后自动配置不生效及如何配置拦截器

    继承WebMvcConfigurationSupport后自动配置不生效及如何配置拦截器

    这篇文章主要介绍了继承WebMvcConfigurationSupport后自动配置不生效及如何配置拦截器,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • SpringBoot如何使用自定义注解实现接口限流

    SpringBoot如何使用自定义注解实现接口限流

    这篇文章主要介绍了SpringBoot如何使用自定义注解实现接口限流,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • 谈谈对Java多态性的一点理解

    谈谈对Java多态性的一点理解

    多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定
    2017-08-08
  • Java基础知识之BufferedReader流的使用

    Java基础知识之BufferedReader流的使用

    这篇文章主要介绍了Java基础知识之BufferedReader流的使用,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • 浅谈关于Java正则和转义中\\和\\\\的理解

    浅谈关于Java正则和转义中\\和\\\\的理解

    这篇文章主要介绍了浅谈关于Java正则和转义中\\和\\\\的理解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-08-08
  • jvm crash的崩溃日志详细分析及注意点

    jvm crash的崩溃日志详细分析及注意点

    本篇文章主要介绍了jvm crash的崩溃日志详细分析及注意点。具有很好的参考价值,下面跟着小编一起来看下吧
    2017-04-04

最新评论