本次试验spring boot
版本2.6.1
配合SpringCloud
版本为Jubilee(2021.0.0)
本来想用Ribbon
做负载均衡,偶然间发现不导入ribbon
也能通过RestTemplate
+@LoadBalance
实现负载均衡,心生好奇
@LoadBalance
注解在之前的springcloud
版本中属于spring-cloud-starter-ribbon
包
但在jubilee
版本好像改成了org.springframework.cloud.client.loadbalancer
后面去查了一下,原来是Ribbon
目前已经停止维护,新版SpringCloud(2021.x.x)
用LoadBalancer
替代了Ribbon
;Spring Cloud
全家桶在Spring Cloud Commons
项目中,添加了Spring cloud Loadbalancer
作为新的负载均衡器,并且做了兼容- 在导入
spring-cloud-starter-netflix-eureka-client
这个包的时候,就已经包含了spring-cloud-starter-loadbalancer
,因此不需要另外导入
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
<version>3.1.0version>
<scope>compilescope>
dependency>
使用之前,先了解一下微服务之间的调用方式
在Spring Cloud
中微服务调用默认是用 http
请求,主要通过一下三种 API:
API | 描述 |
---|---|
RestTemplate | 同步 http API |
WebClient | 异步响应式 http API |
第三方封装 | 如 openfeign |
当项目中导入了 spring-cloud-starter-loadbalancer 依赖,会自动在相关的Bean 中加入负载均衡,对于以上三种请求方式加入负载均衡的方式如下: |
@LoadBalanced
注解修饰的 RestTemplate Bean 增加 Interceptor 拦截器,从而加上了负载均衡的特性。手动注入LoadBalanceClient
,通过choose('生产服务名')
选择一个服务方
根据服务方的host
和port
信息拼接url
,手动new RestTemplate()发送请求获取响应
@RestController
@RequestMapping("/consumer")
public class ConsumeController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@RequestMapping("/interfaceOne")
public String consumeOne(String msg) {
// 第二种调用方式: 通过loadBalancerClient按照一定规则选择服务方(默认为轮询模式)
// 根据服务名(EurekaClient的服务名)获取服务列表,根据算法选取某个服务,并获得这个服务的网络信息。
ServiceInstance serviceInstance = loadBalancerClient.choose("ServiceClient");
String result = new RestTemplate().getForObject("http://" + serviceInstance.getHost()
+ ":" + serviceInstance.getPort()
+ "/clientOne/interfaceOne?msg=" + msg, String.class);
return result;
}
}
方式二是第一种方式的注解简化版.原理相同
因为SpringBoot
不会自动注入RestTemplate
对象,因此需要手动注入
但SpringBoot
自动注入了RestTempalateBuilder
,所以可以用build()
的方式创建restTemplate
对象
同时在restTemplate
对象加上@LoadBalance
注解,叠加自动负载均衡
其实就是用@LoadBalance
注解代替了手动注入loadBalanceClient
对象去实现服务选择
@SpringBootApplication
public class ConsumerApplication {
@Autowired
private RestTemplateBuilder templateBuilder;
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate getTemplate() {
return templateBuilder.build();
}
}
@RestController
@RequestMapping("/consumer")
public class ConsumeController {
@Autowired()
private RestTemplate restTemplate;
@RequestMapping("/interfaceOne")
public String consumeOne(String msg) {
String result = restTemplate.getForObject(
"http://ServiceClient/clientOne/interfaceOne?msg=" + msg, String.class
);
return result;
}
}
以前的Ribbon
有多种负载均衡策略
策略类型 | 类名 |
---|---|
随机 | RandomRule |
轮询 | RoundRobinRule |
重试 | RetryRule |
最低并发 | BestAvailableRule |
可用过滤 | AvailabilityFilteringRule |
响应时间加权重 | ResponseTimeWeightedRule |
区域权重 | ZoneAvoidanceRule |
但LoadBalancer貌似只提供了两种负载均衡器
不指定的时候默认用的是轮询
依据源码:LoadBalancerClientConfiguration类中的默认构造器
@Bean
@ConditionalOnMissingBean // 空Bean(未指定任何负载均衡器)时的默认情况
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty("loadbalancer.client.name");
// 构造器返回的对象是轮询负载均衡器
return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
如果要切换使用随机或者自定义,需要手动配置一下
1.先创建一个LoadBalancerConfig配置类
注意不加@Configuration
注解(只针对单个微服务调用,而不是全局配置)
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.*;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
public class LoadBalanceConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
// name取自@LoadBalancerClient中的name属性指定的服务提供方的服务名
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
依据源码:LoadBalancerClientConfigurationRegistrar类
// 这是个Bean定义器,猜测其作用就是和@LoadBalancerClient注解配合,在注册RestTemplate对象并给其配置负载均衡器的时候,定义负载均衡器的核心属性
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 上来简单明了地获取@LoadBalancerClients的name属性,说明之前没有猜错
Map<String, Object> attrs = metadata.getAnnotationAttributes(LoadBalancerClients.class.getName(), true);
// 这里说如果取name属性没取到,就取value属性,那说明除了name属性外,也可以用过value属性来指定服务提供方的服务名(有待试验)
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[])((AnnotationAttributes[])attrs.get("value"));
AnnotationAttributes[] var5 = clients;
int var6 = clients.length;
for(int var7 = 0; var7 < var6; ++var7) {
AnnotationAttributes client = var5[var7];
// 这里应该是用name和指定的configuration配置去注册一个负载均衡器,先不深究
registerClientConfiguration(registry, getClientName(client), client.get("configuration"));
}
}
// 没有拿到name和configuration等属性的时候,用 default+类名 作为name,加上默认的配置defaultConfiguration去注册负载均衡器
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
} else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name, attrs.get("defaultConfiguration"));
}
// 这里为啥又重走一遍?不太明白
Map<String, Object> client = metadata.getAnnotationAttributes(LoadBalancerClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
// 这个是上面那个方法调用的取值方法~看看就好
private static String getClientName(Map<String, Object> client) {
if (client == null) {
return null;
} else {
String value = (String)client.get("value");
if (!StringUtils.hasText(value)) {
value = (String)client.get("name");
}
if (StringUtils.hasText(value)) {
return value;
} else {
throw new IllegalStateException("Either 'name' or 'value' must be provided in @LoadBalancerClient");
}
}
}
2.再创建一个RestTemplateConfig配置类(直接写在启动类也可以)
通过@LoadBalancerClient
注解为当前的restTemplate
对象指定负载均衡配置
@Configuration
@LoadBalancerClient(name = "provider-one", configuration = LoadBalanceConfig.class)
public class RestemplateConfig {
@Bean
@LoadBalanced
public RestTemplate templateOne() {
return new RestTemplate();
}
}
需要注意:
@LoadBalancerClient
中的name
属性是指服务提供方的服务名(即:spring.application.name),eureka是通过服务名去找对应的微服务的- configuration 则是指定负载均衡器配置类