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

原文地址:https://www.cnblogs.com/xjwhaha/p/14000698.html