上一篇文章中, 详细介绍了 Nacos 注册中心的原理, 相信看完后, 大家应该完全掌握了 Nacos 客户端是如何自动进行服务注册的, 以及 Nacos 客户端是如何订阅服务实例信息的, 以及 Nacos 服务器是如何处理客户端的注册和订阅请求的;
本文承上启下, 在订阅服务实例的基础上, 介绍如何在实例之间进行选择, 实现负载均衡; 并详细介绍了负载均衡组件 LocaBanlancer 和函数式调用组件 OpenFeign 是如何与 Nacos 注册中心进行集成的;
如果在阅读过程中对文中提到的 SpringBoot 启动过程以及扩展机制不太了解, 或者对 @Import 注解不了解, 参考这篇文章 SpringBoot启动流程与配置类处理机制详解, 附源码与思维导图, 强烈建议学习后再来读本文;
LoadBalancer
-
Nacos 1.X 版本自动引入 Ribbon 依赖, 使用 Ribbon 做负载均衡;
-
Nacos 2.X 版本就没有了, 因为 Ribbon 的特性已经不满足 SpringBoot 的要求;
-
2.X 版本可以用 SpringCloud 团队提供的 LoadBalancer 组件来做负载均衡;
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
LoadBalancerClient
Spring Cloud LoadBalancer :: Spring Cloud Commons
-
SpringCloud 定义的接口, 用于负载均衡地选择服务实例, 是SpringCloud制定的负载均衡规范;
-
由具体厂商去实现这个接口, 比如 Ribbon 和 LoadBanlancer 都提供了实现类;
-
两个关键方法,
choose
和execute
-
execute 方法有两个重载, 一个需要传入具体的 ServiceInstance, 一个不需要;
-
LoadBalancerClient 使用, 可以通过直接注入 LoadBanlancerClient对象来使用
@RequestMapping("/order/*")
public class OrderController {
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
RestTemplate restTemplate;
@GetMapping("loadbalancer")
public String test0(){
ServiceInstance instance = loadBalancerClient.choose("stock-service");
String url = instance.getUri() + "/stock/test0";
return restTemplate.getForObject(url, String.class);
}
}
- 或者使用
@LoadBalanced
注解, 注册有复杂均衡功能的 RestTemplate
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("loadbalanced")
public String test1(){
return restTemplate.getForObject("http://stock-service/stock/test0", String.class);
}
负载均衡策略
默认的是轮询策略RoundRobinLoadBalancer
; 也可以选择RandomLoadBalancer
, 这俩都是 SpringCloud 提供的;
@LoadBalancerClients({
@LoadBalancerClient(name = "order-service", configuration = RandomLoadBalancerConfig.class),
@LoadBalancerClient(name = "stock-service", configuration = RandomLoadBalancerConfig.class)
})
// 对所有服务有效
@LoadBalancerClients(defaultConfiguration = RandomLoadBalancerConfig.class)
public class RandomLoadBalancerConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory
.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
OpenFeign
@FeignClient("course-service")
public interface CourseFeignClient {
@GetMapping("/feign/course")
String course();
}
@FeignClient("student-service")
public interface CourseFeignClient {
@GetMapping("/feign/stu")
String course();
}
@EnableFeignClients
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class, args);
}
}
-
@EnableFeignClients
注解通过@Import
注解引入了一个ImportBeanDefinitionRegistrar
; 其注册方法如下:public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // .....一些不重要的代码 this.registerDefaultConfiguration(metadata, registry); this.registerFeignClients(metadata, registry); }
registerFeignClients
-
拿到 @EnableFeignClients 注解的属性;
-
创建一个空 Set, 保存 BeanDefinition;
-
看你的 @EnableFeignClients 注解的
clients
属性是否为空, 如果不为空, 就遍历该属性指定的类, 创建BeanDefinition, 放到 Set 中; -
如果
clients
为空, 就根据value
属性和basePackages
属性和basePackageClasses
属性指定的值去扫描包, 找到有@FeignClient
注解修饰的类, 创建 BeanDefinition, 添加到 Set;如果这些属性都为空, 就扫描启动类所在的包;
-
遍历 Set , 获取 BeanDefinition 的
AnnotationMetadata
, 进而拿到@FeignClient
注解的name
属性( 表示服务名 ); -
调用
registerFeignClient
, 向容器中注册当前BeanDefinition
对应的一个 FactoryBean;
registerFeignClient
- 创建一个
FeignClientFactoryBean
, 在其getObejct
方法中, 封装了创建代理对象的逻辑; - 其
getObject
方法层层调用, 最终调用了ReflectiveFeign
中的一个方法, 该方法使用 JDK 动态代理, 为 @FeignClient 注解修饰的接口, 创建了代理对象; - 将
FeignClientFactoryBean
的 BeanDefinition 添加Spring容器中;
动态代理
- 动态代理使用的
InvocationHandler
是FeignInvocationHandler
(最终是SynchronousMethodHandler
) - Handler 的 invoke 方法中, 又经过层层调用, 最终执行了如下逻辑:
- 获取
@FeignClient 注解的
name
属性, 通过LoadBanlancerClient
负载均衡地获取一个实例; (所以类加载路径下必须有 LoadBalancerClient 的实现类才行, 换言之, 必须引入 Ribbon 或 LoadBanlancer 之类的组件) - 然后根据被调用的方法的 Mapping 注解, 得到请求路径; 和实例的地址进行拼接, 得到最终的请求地址;
- 然后通过 HTTP 工具发送请求;