Ribbon简介

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。
Ribbon服务调用及负载均衡示例。

  1. package com.atguigu.springcloud.controller;
  2. import com.atguigu.springcloud.entities.CommonResult;
  3. import com.atguigu.springcloud.entities.Payment;
  4. import com.atguigu.springcloud.lb.LoadBalancer;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.cloud.client.ServiceInstance;
  7. import org.springframework.cloud.client.discovery.DiscoveryClient;
  8. import org.springframework.web.bind.annotation.GetMapping;
  9. import org.springframework.web.bind.annotation.PathVariable;
  10. import org.springframework.web.bind.annotation.RestController;
  11. import org.springframework.web.client.RestTemplate;
  12. import javax.annotation.Resource;
  13. import java.net.URI;
  14. import java.util.List;
  15. @RestController
  16. @Slf4j
  17. public class ConsumerController {
  18. // private static final String PAYMENT_URL="http://localhost:8001";
  19. private static final String PAYMENT_URL="http://CLOUD-PAYMENT-SERVICE";
  20. @Resource
  21. private RestTemplate restTemplate;
  22. @Resource
  23. private LoadBalancer loadBalancer;
  24. @Resource
  25. private DiscoveryClient discoveryClient;
  26. @GetMapping("/consumer/payment/create")
  27. public CommonResult<Payment> create(Payment payment){
  28. return restTemplate.postForObject(PAYMENT_URL+"/payment",payment,CommonResult.class);
  29. }
  30. @GetMapping("/consumer/payment/get/{id}")
  31. public CommonResult<Payment> getPaymentById(@PathVariable Long id){
  32. return restTemplate.getForObject(PAYMENT_URL+"/payment/"+id,CommonResult.class);
  33. }
  34. }

GET请求

在RestTemplate中,对GET请求可以通过如下两个方法进行调用实现。
第一种:getForEntity函数。该方法返回的是ResponseEntity,该对象是Spring对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatus(也就是我们常说的404、500这些错误码)、在它的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象。

getForEntity(String url, Class responseType,Object... urlVariables);

该方法提供了三个参数,其中url为请求的地址,responseType为请求响应体body的包装类型,urlVariables为url中的参数绑定。GET请求的参数绑定通过使用url中拼接的方式,比如http://USER-SERVICE/user?name=didi,我们可以像这样自己将参数拼接到 url中,但更好的方法是在url中使用占位符并配合urlVariables参数实现GET请求的参数绑定,比如url定义为:getForEntity(“http://USER-SERVICE/user?name={1}“, String.class, “didi”),其中第三个参数didi会替换掉url中的{1}站位符。这里需要注意的是,由于urlVariables参数是一个数组,所以它的顺序会对应url中占位符定义的数字顺序

getForEntity(String url, Class responseType, Map urlVariables);

该方法提供的参数重,只有urlVariables的参数类型与上面的方法不同。这里使用了Map类型,所以使用该方法进行参数绑定时需要再占位符中指定Map中的参数的key值,比如url定义为http://USER-SERVICE/user?name={name},在Map类型的urlVariables中,我们就需要put一个key为name的参数来绑定url中{name}占位符的值,比如:
🌲 Ribbon基础入门2 - 图1

getForEntity(URI url, Class responseType)

该方法使用uri对象来代替之前url和urlVariables参数来指定访问地址和参数绑定。URI是JDK java.net包下单一个类,它表示一个统一资源标识符(Uniform Resource Identifier)引用,比如下面的例子:
🌲 Ribbon基础入门2 - 图2
更多关于如何定义一个URI的方法可以参见JDK文档,这里不做详细说明。
第二种:getForObject函数。该方法可以理解为对getForEntity的进一步封装,它通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。比如:

RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(uri, String.class);

当body是一个User对象时,可以直接这样实现:

RestTemplate restTemplate = new RestTemplate();
User result = restTemplate.getForObject(uri, User.class);

当不需要关注请求响应除body外的其他内容时,该函数就非常好用,可以少一个从Response中获取body的步骤。它与getForEntity函数类似,也提供了三种不同的重载实现。

getForObject(String url, Class responseType, Object ... urlVariables)
getForObject(String url, Class responseType, Map urlVariables)
getForObject(URI url, Class responseType)

POST请求

在RestTemplate中,对POST请求时可以通过如下三个方法调用实现。
▪️第一种:postForEntity函数。

该方法同GET请求中的getForEntity类似,会在调用后返回ResponseEntity对象,其中T为请求响应的body类型。比如下面这个例子,使用postForEntity提交POST请求到USER-SERVICE服务的/user接口,提交的body内容为user对象,请求响应返回的body类型为String。

🌲 Ribbon基础入门2 - 图3

postForEntity函数

postForEntity函数也实现了三种不同的重载方法。

三种不同的重载方法

▪️postForEntity(String url, Object request, Class responseType, Object… uriVariables)

▪️postForEntity(String url, Object request, Class responseType, Map uriVariables)

▪️postForEntity(URI url, Object request, Class responseType)

这些函数中的参数用法大部分与getForEntity一致,比如,第一个重载函数和第二个重载函数中的uriVariables参数都用来对url中的参数进行绑定使用;responseType参数是对请求响应的body内容的类型定义。这里需要注意的是新增加的request参数,该参数可以是一个普通对象,也可以是一个HttpEntity对象。如果是普通对象,而非HttpEntity对象的时候,RestTemplate会将请求对象转换为一个HttpEntity对象来处理;其中Object就是request的类型,request内容会呗视作完整的body来处理;而如果request是一个HttpEntity对象,那么就会被当作一个完成的HTTP请求对象来处理,这个request中不仅包含了body的内容,也包含了header的内容。

▪️第二种:postForObject函数。

该方法也跟getForObject的类型类似,它的作用就是简化postForEntity的后续处理。通过直接将请求响应的body内容包装成对象来返回使用,比如下面的例子:

🌲 Ribbon基础入门2 - 图4

postForObject函数

postForObject函数也实现了三种不同的重载方法:

postForObject的重载方法

▪️postForObject(String url, Object request, Class responseType, Object… uriVariables)

▪️postForObject(String url, Object request, Class responseType, Map uriVariables)

▪️postForObject(URI url, Object request, Class responseType)

这三个函数除了返回的对象类型不同,函数的传入参数均与postForEntity一致,因此可参考之前postForEntity的说明。

▪️第三种:postForLocation函数。

该方法实现了以POST请求提交资源,并返回新的资源的URI,比如下面的例子:

🌲 Ribbon基础入门2 - 图5

postForLocation函数

postForLocation函数也实现了三种不同的重载方法:

🌲 Ribbon基础入门2 - 图6

postForLocation函数的重载方法

▪️postForLocation(String url, Object request, Object… uriVariables)

▪️postForLocation(String url, Object request, Map uriVariables)

▪️postForLocation(URI url, Object request)

由于postForLocation函数会返回新资源的URI,该URI就相当于指定了返回类型,所以此方法实现的POST请求不需要像postForEntity和postForObject那样指定responseType。其他的参数用法相同。

Ribbon自带负载规则

com.netflix.loadbalancer.RoundRobinRule 轮询
com.netflix.loadbalancer.RandomRule 随机
com.netflix.loadbalancer.RetryRule 先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
WeightResponseTimRule 对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
BestAvaliableRule 会像过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
AvaliabilityFilteringRule 先过滤掉故障实例,再选择并发较小的实例
ZoneAvoidanceRule 默认规则,复合判断server所在区域的性能喝server的可用性选择服务器

Ribbon替换负载规则

创建其他负载规则配置文件:官方文档明确给出了警告,这个自定义配置类不能放在@ComponentScan所扫描的当前包及子包下面,否则我们自定义的配置类就会被所有的RIbbon客户端所共享,达不到特殊化定制的目的了

package com.atguigu.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

public interface LoadBalancer {
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

启动类配置自动规则注解

package com.atguigu.springcloud;

import com.atguigu.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name="CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

手写Ribbon轮询负载规则

创建获取服务实例接口

package com.atguigu.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

public interface LoadBalancer {
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

创建实现类实现轮询算法

package com.atguigu.springcloud.lb;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Component
public class MyLB implements LoadBalancer {
    private AtomicInteger atomicInteger = new AtomicInteger(0);
    public final int getAndIncrement(){
        int current;
        int next;
        do{
            current = this.atomicInteger.get();
            next = current>=3 ? 0: current+1;
        }while (!this.atomicInteger.compareAndSet(current,next));
        System.out.println("*********第几次访问,次数next: "+next);
        return next;

    }
    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

controller实现调用

    @GetMapping(value="consumer/payment/lb")
    public String getPaymentLB(){
        List<ServiceInstance> instanceList =  discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        ServiceInstance instance = loadBalancer.instances(instanceList);
        URI uri = instance.getUri();
        return restTemplate.getForObject(uri+"/payment/lb",String.class) ;
    }