zoukankan      html  css  js  c++  java
  • SpringCloud Ribbon和Feign 的使用和源码分析

    1. Ribbon 介绍

    Ribbon 是 Netflix 公司开源的一款 客户端 负载均衡软件,并被SpringCloud集成 作为SpringCloud 负载均衡的工具

    服务端负载均衡 :

    即在服务的消费方和提供方之间使用独立的负载均衡设施,可以是硬件也可以是软件.比如nginx,客户端统一访问nginx 由nginx进行负载均衡并转发到对应的服务,也是平时最常见的方式

    示意图:

    客户端负载均衡 :

    将负载均衡逻辑集成到消费方, 比如ribbon ,它将从注册中心中获取服务列表与地址,并缓存到本地,然后调用时,在本地计算好合适的服务器直接进行访问

    示意图:

    2. 替换默认策略

    Ribbon的基本使用,在我的Eureka那篇文章中几节中已经展示过了,https://www.cnblogs.com/xjwhaha/p/14000370.html

    结合SpringMvc的RestTemplate使用 非常简单,

    	@Bean
        @LoadBalanced
        public RestTemplate initRestTemplate(){
            return new RestTemplate();
        }
    

    只需在注入RestTemplate方法时, 加上@LoadBalanced 注解,就会自动在RestTemplate加入相关的拦截器,加强该类,当使用时 进行负载均衡,默认为 轮询的方式

    如果想要在调用某一个服务时, 使用其他的负载均衡策略 ,也可以单独指定

    定义一个配置类,并注入相关的负载均衡策略类

    /**  随机负载均衡算法
     * @author 1625963331@qq.com
     * @date 2020/8/23
     */
    @Configuration
    public class MySelfRule {
        @Bean
        public IRule myRule(){
            return new RandomRule();
        }
    }
    

    此类的位置不能被主启动类扫描到,不然将会替换默认的策略,即全部的服务调用都使用这个策略,不符合单独指定的要求

    再在主启动类上加上配置,指定服务名 与配置类

    @SpringBootApplication
    //Ribbon访问 该服务的负载均衡算法 使用该自定义配置类
    @RibbonClient(name="CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
    public class OrderMain80 {
    
        public static void main(String[] args) {
            SpringApplication.run(OrderMain80.class, args);
        }
    }
    

    这样 在调用服务名为CLOUD-PAYMENT-SERVICE 时 使用随机算法

    3. 实现一个简单的Ribbon

    Ribbon的实现方式相对简单,模仿其思想 实现一个简单的负载均衡工具

    使用DiscoveryClient 类, 此为Spring-Cloud 的类,可以获取注册信息的相关信息

    LoadBalance接口: 根据服务列表 计算出合适的 具体服务

    public interface LoadBalance {
        ServiceInstance instance(List<ServiceInstance> serviceInstances);
    }
    

    实现: 使用CAS 原子操作类 实现自旋锁自增 ,并对服务列数长度取模得出实际的服务

    @Component
    public class MyLB implements LoadBalance {
        private AtomicInteger atomicInteger = new AtomicInteger(0);
    
        public final int getAndIncrement() {
            int current;
            int next;
    
            /// 
            do {
                current = this.atomicInteger.get();
                next = current >= Integer.MAX_VALUE ? 1 : current + 1;
            } while (!this.atomicInteger.compareAndSet(current, next));
            System.out.println("****next: " + next);
            return next;
        }
    
        @Override
        public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
    
            int index = getAndIncrement()%serviceInstances.size();
    
            return serviceInstances.get(index);
        }
    }
    

    controller调用

    @RestController
    @Slf4j
    public class OrderController {
    
    
        @Resource
        private RestTemplate restTemplate;
    
        @Resource
        private DiscoveryClient discoveryClient;
    
        @Resource
        private LoadBalance loadBalance;
    
        /**
         * 使用自定义的  从注册中心获取服务并实现负载均衡的算法  
         * @return
         */
        @GetMapping("/consumer/payment/lb")
        public String getPaymentLB() {
            //获取CLOUD-PAYMENT-SERVICE 服务列表
            List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
            if (instances == null || instances.size() <= 0) {
                return null;
            }
            ServiceInstance serviceInstance = loadBalance.instance(instances);
            URI uri = serviceInstance.getUri();
    
            return restTemplate.getForObject(uri + "/payment/lb", String.class);
        }
    }
    
    

    4. Feign 与 OpenFeign

    前面在使用Ribbon+RestTemplate 时, 利用 @LoadBalanced 注解 将RestTemplate 类加强,并实现客户端对服务端的调用并负载均衡,但是我们发现,在调用服务端时 必须手动指定其服务名,而客户端往往不止调用一个服务端,这使的Ribbon的使用变得复杂

    Feign 集成了Ribbon,它是一个声明式的客户端工具,可以通过定义一个接口,并通过注解的方式生成代理类,封装了Ribbon的调用,它有一套自己的注解

    OpenFeign是springcloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

    下面通过OpenFeign 对之前的服务端进行调用

    pom(基于前面文章中的工程):

     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-openfeign</artifactId>
     </dependency>
    

    定义接口: 指定服务名 CLOUD-PAYMENT-SERVICE 当调用 getPaymentById 方法时 ,将参数 id替换url中的 id 并进行调用

    @Component
    @FeignClient(value = "CLOUD-PAYMENT-SERVICE")
    public interface PaymentFeignService {
    
        @GetMapping(value = "/payment/get/{id}")
        String getPaymentById(@PathVariable("id") Long id);
    }
    

    服务消费方Controller: 声明注入接口,实际注入的是 Feign的代理类,并进行调用

    @RestController
    @Slf4j
    public class OrderFeignController {
    
        @Resource
        private PaymentFeignService paymentFeignService;
    
        @GetMapping(value = "/consumer/payment/get/{id}")
        public String getPaymentById(@PathVariable("id") Long id){
            return paymentFeignService.getPaymentById(id);
        }
    }
    

    服务提供者Controller, 返回 自己的端口

        @GetMapping(value = "/payment/get/{id}")
        public String getPaymentById(@PathVariable("id") Long id) {
                return "查询成功,serverPort: "+ serverPort;
        }
    

    启动类: @EnableFeignClients 开启 OpenFeign 的 自动配置

    @SpringBootApplication
    @EnableFeignClients
    public class OrderFeignMain80 {
        public static void main(String[] args) {
            SpringApplication.run(OrderFeignMain80.class, args);
        }
    }
    

    调用客户端Controller:http://127.0.0.1/consumer/payment/get/10

    成功返回提供方响应信息:查询成功,serverPort: 8001

    5. Feign 超时时间设置

    当消费方调用服务方时,因为网络或者服务方业务流程过长,将导致消费方读取超时, Feign 最大的等待时间为1秒, 超过一秒,将直接报错超时

    添加服务提供方长流程操作, 操作时间需要两秒

     // 代表 服務提供方 某一个操作很耗时,要消费方设置超时时间
        @GetMapping("/payment/feign/timeout")
        public String paymentFeignTimeOut(){
            try {
                TimeUnit.SECONDS.sleep(2);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            return serverPort;
        }
    

    OpenFeign接口中添加调用该接口的方法

    @Component
    @FeignClient(value = "CLOUD-PAYMENT-SERVICE")
    public interface PaymentFeignService {
    
        @GetMapping(value = "/payment/get/{id}")
        String getPaymentById(@PathVariable("id") Long id);
    
        @GetMapping(value = "/payment/feign/timeout")
        String paymentFeignTimeOut();
    
    }
    

    消费方调用

       @GetMapping(value = "/consumer/payment/feign/timeout")
        public String paymentFeignTimeOut(){
            //客户端默认等待1秒钟
            return paymentFeignService.paymentFeignTimeOut();
        }
    

    浏览器访问http://127.0.0.1/consumer/payment/feign/timeout

    报错页面,显示超时

    我们也可以手动调整这个时间,

    修改yaml

    #设置feign客户端超时时间 (单位:毫秒)
    ribbon:
      #最大读取时间
      ReadTimeout: 5000
      #最大连接时间
      ConnectTimeout: 5000
    

    重启后重新调用,成功返回服务方信息 8001

    6. Ribbon 源码分析

    使用Ribbon非常简单,在之前的代码中,我们仅仅只是 在RestTemplate 类上 加了了注解 ,就自动将RestTemplate类进行加强,可以获取EurekaServer上注册的服务 并进行负载均衡,
    看看SpringCloud如何实现:

    1.RestTemplate如何被加强

    在LoadBalanced 注解包下,有个LoadBalancerAutoConfiguration类,这个类在META-INf/spring-factories 中被声明,在启动过程中被加载 (SpringBoot自动配置原理,详情查看这篇博客:https://www.cnblogs.com/xjwhaha/p/13615288.html )
    源码:

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(RestTemplate.class)
    @ConditionalOnBean(LoadBalancerClient.class)
    @EnableConfigurationProperties(LoadBalancerRetryProperties.class)
    public class LoadBalancerAutoConfiguration {
    
        /**
       	* 容器中被@LoadBalanced 注解修饰的RestTemplate 都会注入到本集合中
       	*/
    	@LoadBalanced
    	@Autowired(required = false)
    	private List<RestTemplate> restTemplates = Collections.emptyList();
    
    	@Autowired(required = false)
    	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
    
        /**
        * 循环集合 将所有的 RestTemplate 类用定制器定制加强
        */
    	@Bean
    	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
    			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
    		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
    			for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
    				for (RestTemplateCustomizer customizer : customizers) {
    					customizer.customize(restTemplate);
    				}
    			}
    		});
    	}
    
        
    	@Bean
    	@ConditionalOnMissingBean
    	public LoadBalancerRequestFactory loadBalancerRequestFactory(
    			LoadBalancerClient loadBalancerClient) {
    		return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    	}
    
        /**
        * 配置类, 根据条件注入到容器中
        */
    	@Configuration(proxyBeanMethods = false)
    	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    	static class LoadBalancerInterceptorConfig {
    
            /*
            * 从容器中接受一个 LoadBalancerClient (主要的工作类)
            * 并作为参数初始化一个拦截器注入到容器中
            */
    		@Bean
    		public LoadBalancerInterceptor ribbonInterceptor(
    				LoadBalancerClient loadBalancerClient,
    				LoadBalancerRequestFactory requestFactory) {
    			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    		}
    
             /*
            * 接受上面那个拦截器,并构建一个 RestTemplate定制器
            * 定制器的内容为 循环 项目中的 RestTemplate类列表,并将拦截器加入到 每个RestTemplate实例的拦		 * 截器链中
            */
    		@Bean
    		@ConditionalOnMissingBean
    		public RestTemplateCustomizer restTemplateCustomizer(
    				final LoadBalancerInterceptor loadBalancerInterceptor) {
    			return restTemplate -> {
    				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
    						restTemplate.getInterceptors());
    				list.add(loadBalancerInterceptor);
    				restTemplate.setInterceptors(list);
    			};
    		}
    
    	}
    
    	/**
    	 * Auto configuration for retry mechanism.
    	 */
    	@Configuration(proxyBeanMethods = false)
    	@ConditionalOnClass(RetryTemplate.class)
    	public static class RetryAutoConfiguration {
    
    		@Bean
    		@ConditionalOnMissingBean
    		public LoadBalancedRetryFactory loadBalancedRetryFactory() {
    			return new LoadBalancedRetryFactory() {
    			};
    		}
    
    	}
    
    	/**
    	 * Auto configuration for retry intercepting mechanism.
    	 */
    	@Configuration(proxyBeanMethods = false)
    	@ConditionalOnClass(RetryTemplate.class)
    	public static class RetryInterceptorAutoConfiguration {
    
    		@Bean
    		@ConditionalOnMissingBean
    		public RetryLoadBalancerInterceptor ribbonInterceptor(
    				LoadBalancerClient loadBalancerClient,
    				LoadBalancerRetryProperties properties,
    				LoadBalancerRequestFactory requestFactory,
    				LoadBalancedRetryFactory loadBalancedRetryFactory) {
    			return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
    					requestFactory, loadBalancedRetryFactory);
    		}
    
    		@Bean
    		@ConditionalOnMissingBean
    		public RestTemplateCustomizer restTemplateCustomizer(
    				final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
    			return restTemplate -> {
    				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
    						restTemplate.getInterceptors());
    				list.add(loadBalancerInterceptor);
    				restTemplate.setInterceptors(list);
    			};
    		}
    
    	}
    
    }
    
    
    • 我们发现 该类有一个属性,为 RestTemplate 集合,并被 @LoadBalanced 修饰,在初始化该类时,会将容器中 被@LoadBalanced 修饰的 RestTemplate,都注入到集合中,这样我们就拿到了我们自己声明的RestTemplate类了

    • 该类中 还根据条件 注入了LoadBalancerInterceptorConfig 配置类,,其中的restTemplateCustomizer方法 接收一个LoadBalancerInterceptor 拦截器, 并返回一个RestTemplateCustomizer 的定制器函数式类,其中的实现为 向原生的 RestTemplate 拦截器链中加入该拦截器.而这个拦截器的初始化就在上方, 接收loadBalancerClient实现类 初始化拦截器,并注入到容器中,

    • 最后由 loadBalancedRestTemplateInitializerDeprecated 方法 将定制器接收 并循环restTemplates 定制加强每个RestTemplate

  • 相关阅读:
    【Java每日一题】20161227
    【Java每日一题】20161226
    【Java每日一题】20161223
    【Java每日一题】20161222
    【Java每日一题】20161221
    【Java每日一题】20161220
    【Java每日一题】20161219
    【Java每日一题】20161216
    【Java每日一题】20161215
    【Java每日一题】20161214
  • 原文地址:https://www.cnblogs.com/xjwhaha/p/14000698.html
Copyright © 2011-2022 走看看