项目由于某些架构原因,要求相同的客户的请求要路由到固定同一个实例里面,主要技术使用的是spring cloud alibaba 2021.1 cloud版本 Hoxton.SR9;

一开始我记得nacos的负载均衡使用的是ribbon,找了半天的ribbon的负载均衡依赖,发现在2021.1版本里面nacos已经把ribbon删除了,那我只能用默认实现load balancer了;

可以看到我们依赖了


spring-cloud-starter-loadbalancer


这个就是spring cloud官方提供的负载均衡依赖;

pom版本

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.crq.demo</groupId>
  <artifactId>demo-gateway</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <properties>
    <java.version>11</java.version>
    <file_encoding>UTF-8</file_encoding>
    <swagger.version>2.9.2</swagger.version>
    <knife4j.version>3.0.2</knife4j.version>
    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
    <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
    <micrometer.version>1.8.0</micrometer.version>

  </properties>

  <dependencies>
    <!--  spring gateway 网关-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!--lettuce 依赖commons-pool-->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
    </dependency>
    <!-- actuator -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>


    <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-loadbalancer</artifactId>
      <version>3.0.2</version>
    </dependency>

    <!--热部署-->

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
    </dependency>


    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.10</version>
    </dependency>

    <dependency>
      <groupId>commons-collections</groupId>
      <artifactId>commons-collections</artifactId>
      <version>3.2.2</version>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
      <scope>provided</scope>
    </dependency>

    <!--jasypt加密-->
    <dependency>
      <groupId>com.github.ulisesbocchio</groupId>
      <artifactId>jasypt-spring-boot-starter</artifactId>
      <version>2.1.0</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
      <exclusions>
        <exclusion>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.restdocs</groupId>
      <artifactId>spring-restdocs-mockmvc</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-registry-prometheus</artifactId>

    </dependency>

    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
    </dependency>

  </dependencies>
  <dependencyManagement>
    <dependencies>
      <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>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>${swagger.version}</version>
        <optional>true</optional>
      </dependency>
      <dependency>
        <groupId>com.github.xiaoymin</groupId>
        <artifactId>knife4j-dependencies</artifactId>
        <version>${knife4j.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>

      <!--<dependency>-->
        <!--<groupId>io.micrometer</groupId>-->
        <!--<artifactId>micrometer-registry-prometheus</artifactId>-->
        <!--<version>${micrometer.version}</version>-->
      <!--</dependency>-->

      <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>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>${spring-cloud-alibaba.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>
  <build>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>2.3.7.RELEASE</version>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.6.1</version>
          <configuration>
            <source>${java.version}</source>
            <target>${java.version}</target>
            <encoding>UTF-8</encoding>
          </configuration>
        </plugin>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.20.1</version>
          <configuration>
            <skipTests>true</skipTests>
          </configuration>
        </plugin>
      </plugins>
    </pluginManagement>

    <resources>
      <resource>
        <directory>src/main/resources</directory>
        <filtering>true</filtering>
        <includes>
          <include>application.yml</include>
          <include>application-${profiles.active}.yml</include>
          <include>bootstrap.yml</include>
          <include>bootstrap-${profiles.active}.yml</include>
          <include>**/*.xml</include>
        </includes>
      </resource>
      <resource>
        <directory>src/main/java</directory>
        <includes>
          <include>**/*.xml</include>
        </includes>
      </resource>
    </resources>
    <finalName>demo-gateway</finalName>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <!-- 指定该Main Class为全局的唯一入口 -->
          <mainClass>com.crq.demo.gateway.GatewayApplication</mainClass>
          <layout>ZIP</layout>
        </configuration>
        <executions>
          <execution>
            <goals>
              <!--可以把依赖的包都打包到生成的Jar包中-->
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

springboot 版本和 cloud 版本; springcloud alibaba 这些依赖的版本要统一对应起来,不然启动报错会抓狂;

配置文件

因为使用到了nacos的配置中心,所以我们需要引入依赖


<!-- 解决bootstrap 不生效--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>


在boot版本2.5.8.RELEASE里面如果没有引入 这个依赖 bootstrap开头的配置文件不会起作用,一个大坑,目前很多更新资料不全,只能靠前辈们趟坑了;我们用的是2.3.7没有这个问题;如果使用boot 2.5.x版本的可以注意一下;

bootstrap.yaml

spring:
  application:
    name: demo-gateway-app
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        server-addr: nacos的地址+端口
        namespace: namespace
      config:
        username: nacos
        password: nacos
        server-addr: nacos的地址+端口
        namespace: namespace

application.yml

spring:
  profiles:
    active: ${profiles.active}

---
spring:
  profiles: dev
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:
        - id: service-1
          uri: lb://service-a
          predicates:
             - Path=/serviceA/**
          filters:
            - StripPrefix= 1

启动类增加注解


@SpringBootApplication @EnableDiscoveryClient @Slf4j @EnableOpenApi @LoadBalancerClient(name = "service-a", configuration = MyLoadBalancerConfig.class)


@LoadBalancerClient 注解 这个是我们自定义的负载均衡实现类,需要我们创建一个MyLoadBalancerConfig.java,这个类注入了两个实例分别是


ReactiveLoadBalancerClientFilter实例


ReactorServiceInstanceLoadBalancer实例


@Configuration
public class MyLoadBalancerConfig {
 @Bean
    @SuppressWarnings("deprecation")
    public ReactorServiceInstanceLoadBalancer reactorServiceInstanceLoadBalancer(
            ObjectProvider<ServiceInstanceListSupplier> serviceInstanceSupplier) {
        return new DemoLoadBalancer(serviceInstanceSupplier);
    }

    @Bean
    public ReactiveLoadBalancerClientFilter reactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,
            LoadBalancerProperties properties) {
        return new NewReactiveLoadBalancerClientFilter(clientFactory, properties);
    }

}

ReactiveLoadBalancerClientFilter源码

/*
 * Copyright 2013-2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.cloud.gateway.filter;

import java.net.URI;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerUriTools;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.client.loadbalancer.reactive.Request;
import org.springframework.cloud.client.loadbalancer.reactive.Response;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl;

/**
 * A {@link GlobalFilter} implementation that routes requests using reactive Spring Cloud
 * LoadBalancer.
 *
 * @author Spencer Gibb
 * @author Tim Ysewyn
 * @author Olga Maciaszek-Sharma
 */
public class ReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {

	private static final Log log = LogFactory
			.getLog(ReactiveLoadBalancerClientFilter.class);

	private static final int LOAD_BALANCER_CLIENT_FILTER_ORDER = 10150;

	private final LoadBalancerClientFactory clientFactory;

	private LoadBalancerProperties properties;

	public ReactiveLoadBalancerClientFilter(LoadBalancerClientFactory clientFactory,
			LoadBalancerProperties properties) {
		this.clientFactory = clientFactory;
		this.properties = properties;
	}

	@Override
	public int getOrder() {
		return LOAD_BALANCER_CLIENT_FILTER_ORDER;
	}

	@Override
	@SuppressWarnings("Duplicates")
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
		String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
		if (url == null
				|| (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
			return chain.filter(exchange);
		}
		// preserve the original url
		addOriginalRequestUrl(exchange, url);

		if (log.isTraceEnabled()) {
			log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName()
					+ " url before: " + url);
		}

		return choose(exchange).doOnNext(response -> {

			if (!response.hasServer()) {
				throw NotFoundException.create(properties.isUse404(),
						"Unable to find instance for " + url.getHost());
			}

			ServiceInstance retrievedInstance = response.getServer();

			URI uri = exchange.getRequest().getURI();

			// if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
			// if the loadbalancer doesn't provide one.
			String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
			if (schemePrefix != null) {
				overrideScheme = url.getScheme();
			}

			DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(
					retrievedInstance, overrideScheme);

			URI requestUrl = reconstructURI(serviceInstance, uri);

			if (log.isTraceEnabled()) {
				log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
			}
			exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
		}).then(chain.filter(exchange));
	}

	protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
		return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
	}

	@SuppressWarnings("deprecation")
     //这个方法就是从服务实例中选择服务的实例;所以我们只需要重写这个方法就可以了,直接创建一个新的类集成本类,重写choose方法
	private Mono<Response<ServiceInstance>> choose(ServerWebExchange exchange) {
		URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
		ReactorLoadBalancer<ServiceInstance> loadBalancer = this.clientFactory
				.getInstance(uri.getHost(), ReactorServiceInstanceLoadBalancer.class);
		if (loadBalancer == null) {
			throw new NotFoundException("No loadbalancer available for " + uri.getHost());
		}
    //这里需要注意我们需要 自己构建一个request;把我们需要的参数传递给loadbalancer ,一般是客户的id之类的,这个request,一般要包含exchange,把exchange作为信息载体
		return loadBalancer.choose(createRequest());
	}

	@SuppressWarnings("deprecation")
	private Request createRequest() {
		return ReactiveLoadBalancer.REQUEST;
	}

}

上面的choose中使用的是


ReactorLoadBalancer来委派真实服务实例,所以我们也需要一个自己的ReactorLoadBalancer实现,也就是上面的DemoLoadBalancer


自定义的request


@SuppressWarnings("deprecation")
public class Demoequest extends org.springframework.cloud.client.loadbalancer.reactive.DefaultRequest {

    public Demoequest (ServerWebExchange context) {
        super(context);
    }

}


@Slf4j
public class DemoLoadBalancer implements ReactorServiceInstanceLoadBalancer {

    // 服务列表
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;



    public DemoLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
     
    }

 @Override
    @SuppressWarnings("deprecation")
    public Mono<Response<ServiceInstance>> choose(Request request) {

        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable();
      //request 强转为自己的request,
        DemoRequest demoRequest = (DemoRequest ) request;
        //根据参数获取
        ServerWebExchange context = (ServerWebExchange) demoRequest.getContext();
        String userId = context.getRequest().getQueryParams().get("userId").get(0);
        //获取userId的 hash值
        int hash = userId.hashCode();
         if(hash>=0){
          return   supplier.get().next().map(i->getInstanceResponse(i,hash));
        }
        
        
        //没有自定义负载均衡的将进行随机规则访问
        return supplier.get().next().map(this::getInstanceResponse);
    }


    //这里就可以计算这个用户的真实的服务实例,hash值然后取余;基本实现了功能
    private  Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, int hash) {
        int index = hash % instances.size();
        return new DefaultResponse(instances.get(index));
    }

    /**
     * 使用随机数获取服务
     * @param instances
     * @return
     */
    @SuppressWarnings("deprecation")
    private Response<ServiceInstance> getInstanceResponse(
            List<ServiceInstance> instances) {

        if (instances.isEmpty()) {
            return new EmptyResponse();
        }
        // 随机算法
        int size = instances.size();
        Random random = new Random();
        ServiceInstance instance = instances.get(random.nextInt(size));

        return new DefaultResponse(instance);
    }

}