项目由于某些架构原因,要求相同的客户的请求要路由到固定同一个实例里面,主要技术使用的是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);
}
}