一、Ribbon简介

1、Ribbon介绍

Ribbon是由Netflix公司推出的开源软件,是基于HTTP和TCP协议的,其主要功能是实现客户端软件的负载均衡算法。
Spring Cloud中Ribbon就是基于Netflix公司的Ribbon实现的。它不需要单独部署,但是却存在于整个微服务中。Eureka里面有Ribbon,OpenFeign也是基于Ribbon实现的。

2、Ribbon原理

Netfilx Ribbon负载均衡工具 - 图1
内部基于ILoadBalancer实现的(代码层面)。
继承关系如下:
Netfilx Ribbon负载均衡工具 - 图2
使用Ribbon工作原理:
所有的项目都会注册到Eureka中,Eureka允许不同项目的spring.application.name是相同。当相同时会认为这些项目一个集群。所以同一个项目部署多次时都是设置应用程序名相同。
Application Client会从Eureka中根据spring.application.name加载Application Service的列表。根据设定的负载均衡算法,从列表中取出一个URL,到此Ribbon的事情结束了。剩下的事情由程序员自己进行技术选型,选择一个HTTP协议工具,通过这个URL调用Application Service。
注意:以下事情和Ribbon没有关系的

  • Application Service注册到Eureka过程。这是Eureka的功能。
  • Application Client从Eureka取出注册列表。这是Eureka的功能。
  • Application Client 通过URL访问Application Service。具体实现可以自己进行选择使用哪个HTTP工具。
  • 只有Application Client从Eureka中取出列表后进行负载均衡算法的过程和Ribbon有关系。

    二、负载均衡解决方案分类及特征

    业界主流的负载均衡解决方案有:集中式负载均衡和线程内负载均衡。
    Netfilx Ribbon负载均衡工具 - 图3

    1.1 集中式负载均衡

    即在客户端和服务端之间使用独立的负载均衡设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发至服务端。
    也叫做:服务器端负载均衡。
    Netfilx Ribbon负载均衡工具 - 图4

    1.2 进程内负载均衡

    将负载均衡逻辑集成到客户端组件中,客户端组件从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务端发起请求。Ribbon就是一个进程内的负载均衡实现。
    也叫做:客户端负载均衡。
    Netfilx Ribbon负载均衡工具 - 图5

    三、微服务架构中Ribbon

    在Application Client中配置Ribbon。
    微服务架构既然把项目拆分成多个项目,一定会出现项目调用另外一个项目的情况。
    在加上Eureka支持多项目同项目名(spring.application.name)所以这个时候就可以使用Ribbon。
    项目B和项目C实际上是一个项目。项目A调用项目B和项目C,那么Ribbon应该配置在项目A中(Application Client中)

    四、搭建Application Service集群

    前提:已经配置了单机版Eureka。端口为8761
    新建项目ApplicationServiceDemo

    1、添加依赖

    新建项目后,在pom.xml中添加依赖

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-web</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>org.springframework.cloud</groupId>
    7. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    8. </dependency>
    9. <dependency>
    10. <groupId>org.springframework.boot</groupId>
    11. <artifactId>spring-boot-starter-test</artifactId>
    12. <scope>test</scope>
    13. </dependency>

2、编写配置文件

在resources下新建application.yml.

spring:
  application:
    name: applicationservice

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka/
server:
  port: 8081

3、编写控制器

@RestController
public class MyController {

    @RequestMapping("/demo01")
      public   String  demo01(){
          return "bjsxt111";
      }

    @RequestMapping("/demo02")
    public   String  demo02(String name,String pwd){
        System.out.println(name+"--"+pwd);
        return "bjsxt";
    }

    @RequestMapping("/demo03")
    public   String  demo03(@RequestBody User user){
        System.out.println(user);
        return "bjsxt";
    }

    @RequestMapping("/demo04/{name}/{pwd}")
    public   String  demo04(@PathVariable String name,@PathVariable String pwd){
        System.out.println(name+"--"+pwd);
        return "bjsxt";
    }

    @PostMapping("/selectAll")
    public List<User>  selectAll(){
          List<User>  list=new ArrayList<>();
          list.add(new User("zs","123"));
          list.add(new User("lisi","123"));
          list.add(new User("sxt","123"));

          return list;
    }

    @RequestMapping("/selectMore")
    public List<String> selectMore(@RequestBody List<Integer> id){
        List<String> list = new ArrayList<>();
        for (Integer s : id) {
            list.add("sxt:"+s);
        }
        return list;
    }

    @RequestMapping("/selectA")
    public  List<String>  selectA(@RequestBody List<String> list){
        System.out.println(list);
        List<String>  list1=new ArrayList<>();

        for (String s:list) {
            list1.add(s+"查询数据");
        }
        return list1;
    }

}

4、编写启动类

@SpringBootApplication
@EnableEurekaClient
public class EurekaApplicationserverApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplicationserverApplication.class, args);
    }

}

5、启动项目(启动三个)

修改启动配置文件
添加 -Dserver.port=8082
添加 -Dserver.port=8083
观察Eureka管理页面会发现已经注册了三个Provider
Netfilx Ribbon负载均衡工具 - 图6

五、基于Ribbon测试负载均衡

1、添加依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2、编写配置文件

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka/
server:
  port: 9999

spring:
  application:
    name: application-client

3、编写service

新建com.bjsxt.service.ClientService及实现类。

public interface ClientService {
        /**@return 返回值类型不是必须和调用的控制器方法返回值一样,需要看页面要什么。*/
        String client();
}
@Service
public class ClientServiceImpl implements ClientService {
    @Autowired
    private LoadBalancerClient loadBalancerClient;
    @Override
    public String client() {
        ServiceInstance applicationservice = loadBalancerClient.choose("applicationservice");
        //获取URL地址
        System.out.println(applicationservice.getUri());
/*        // 获取Application Service IP
        System.out.println(applicationservice.getHost());
        // 获取Ip及端口。
        System.out.println(applicationservice.getInstanceId());
        // 获取额外声明信息.{management.port=xxxx}
        System.out.println(applicationservice.getMetadata());
        // 端口 8082
        System.out.println(applicationservice.getPort());
        // 模式 null
        System.out.println(applicationservice.getScheme());
        // 应用程序名 applicationservice
        System.out.println(applicationservice.getServiceId());*/
        return null;
    }
}

4、编写控制器
新建控制器com.bjsxt.controller.ClientController

@RestController
public class ClientController {
    @Autowired
    private ClientService clientService;
    @RequestMapping("/client")
    public String client(){
        clientService.client();
        return "aa";
    }

}

5、编写启动类

新建com.bjsxt.ClientApplication。

@SpringBootApplication
@EnableEurekaClient
public class EurekaApplicationclientApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplicationclientApplication.class, args);
    }

}

6、运行

运行程序,通过浏览器多次访问项目ApplicationClientDemo中/client控制器,观察IDEA控制器中输出结果。
会发现访问是轮询访问的。

六、Ribbon支持的负载均衡算法

ribbon的负载均衡策略是通过不同的类型来实现的,下表详细介绍一些常用负载均衡策略及对应的Ribbon策略类。

id 策略名称 策略对应的类名 实现原理
1 轮询策略(默认) RoundRobinRule 轮询策略表示每次都按照顺序取下一个application service,比如一共有5个application service,第1次取第1个,第2次取第2个,第3次取第3个,以此类推
2 权重轮询策略(常用,中小型项目使用) WeightedResponseTimeRule 1.根据每个application service的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性越低。2.原理:一开始为轮询策略,并开启一个计时器,每30秒收集一次每个application service的平均响应时间,当信息足够时,给每个application service附上一个权重,并按权重随机选择application service,权重越高的application service会被高概率选中。
3 随机策略(不推荐,测试使用,开发一般不使用) RandomRule 从application service列表中随机选择一个
4 最少并发数策略(应用在硬件软件环境一致的情况下,中小型项目使用) BestAvailableRule 选择正在请求中的并发数最小的application service,除非这个application service在熔断中。
5 重试策略。在“选定的负载均衡策略”基础上进行重试机制 RetryRule 1.“选定的负载均衡策略”这个策略是轮询策略RoundRobinRule
2.该重试策略先设定一个阈值时间段,如果在这个阈值时间段内当选择application service不成功,则一直尝试采用“选定的负载均衡策略:轮询策略”最后选择一个可用的application service
6 可用性敏感策略(一般在同区域内服务集群环境中使用) AvailabilityFilteringRule 过滤性能差的application service,有2种:
第一种:过滤掉在eureka中处于一直连接失败application service第二种:过滤掉高并发的application service
7 区域敏感性策略(应用在大型的,物理隔离分布式环境中) ZoneAvoidanceRule 1.以一个区域为单位考察可用性,对于不可用的区域整个丢弃,从剩下区域中选可用的application service
2.如果这个ip区域内有一个或多个实例不可达或响应变慢,都会降低该ip区域内其他ip被选中的权重。

七、 指定负载均衡策略

1、添加bean

在application client的配置类中添加。配置类中指定哪个负载均衡策略默认使用哪种策略。不允许配置多个负载均衡策略的实例。
com.bjsxt.config.ApplicationConfig

@Configuration
public class ApplicationConfig {

    @Bean
    public RandomRule getRandomRule(){
        return new RandomRule();
    }
}

2、在ApplicationService观察输出语句

测试是否随机

八、RestTemplate

RestTemplate是spring-web-xxx.jar包中提供的Http协议实现类。也就是说导入spring-boot-starter-web的项目可以直接使用RestTemplate类,就是基于模板方法设计模式的,封装了所有需要使用的API
在该类中主要针对6类请求方式封装的方法。
Netfilx Ribbon负载均衡工具 - 图7

1、说明

get方式提供了两个方法:
两个方法都是发送get请求并处理响应。区别:
getForObject:把响应体直接转换为对象。该方法返回值为特定类类型。舍弃了Response Header的东西,但是用起来比getForEntity方便。如果只需要获取响应体中内容(调用控制器方法的返回值)使用此方法。
getForEntity:返回值包含响应头和响应体。用起来比getForObject稍微麻烦一些。
注意:
如果方法返回值是String或基本数据类型时,建议给定produces设置响应结果类型,否则使用浏览器测试和使用RestTemplate获取的ContentType类型可能不一致。

2、get方式

@SpringBootTest
class EurekaApplicationclientApplicationTests {

    //get/post/通用
    @Test
    void contextLoads() {

        RestTemplate rp=new RestTemplate();
        //发送get请求
        //rp.getForEntity() rp.getForObject()这两个方法都可以发送get请求
        //rp.getForEntity()返回结果中包含了响应头响应体等信息
        //rp.getForObject()返回结果中只包含了响应的结果

        System.out.println("------getForEntity()和rp.getForObject()区别-------------");
        //String forObject = rp.getForObject("http://192.168.237.1:8081/demo01", String.class);
        //System.out.println(forObject);
        //ResponseEntity<String> forEntity = rp.getForEntity("http://192.168.237.1:8081/demo01", String.class);
        //System.out.println(forEntity.getBody());
        //System.out.println(forEntity.getHeaders());

        System.out.println("------------普通数据发送-----------------");
        String name="zs";
        String pwd="123";
        //String forObject = rp.getForObject("http://192.168.237.1:8081/demo02?zs=" + name + "&pwd=" + pwd, String.class);
        //String forObject = rp.getForObject("http://192.168.237.1:8081/demo02?zs={1}&pwd={2}", String.class,name,pwd);

        System.out.println("------------对象数据发送 注意:get请求不支持对象,传递的时候需要把对象拆分-----------------");
        User user=new User("lisi","123");
        //rp.getForObject("http://192.168.237.1:8081/demo03?name={1}&pwd={2}",String.class,user.getName(),user.getPwd());

        System.out.println("------------Rest风格方式-----------------");
        //rp.getForObject("http://192.168.237.1:8081/demo04/{1}/{2}",String.class,user.getName(),user.getPwd());

        System.out.println("------------Map类型数据传输-----------------");
        Map<String,String> map =new HashMap<>();
        map.put("a","value-a");
        map.put("b","value-b");
        rp.getForObject("http://192.168.237.1:8081/demo04/{a}/{b}",String.class,map);
    }
}

3、post方式

@SpringBootTest
class EurekaApplicationclientApplicationTests {
    //发送Post请求
    @Test
    void   demoPost(){
        RestTemplate rp=new RestTemplate();

        //相比get而言post中多了一个rp.postForLocation()方法 这个方法返回值是一个URL对象 这个对象处理的时候比较麻烦 几乎不用
        System.out.println("---------普通类型数据传递-------------");
        String name="zs";
        String pwd="123";
        //rp.postForObject("http://192.168.237.1:8081/demo02?zs={1}&pwd={2}",null,String.class,name,pwd);

        System.out.println("------------post传递请求体数据也就是可以直接传递对象---------------");
        //发送是请求体数据 再applicationservice 中进行数据接收时候必须使用@RequestBody才可以
        //发送请求的时候即可以传递普通数据又可以传递请求体数据
        User  user=new User("lisi","123");
        rp.postForObject("http://192.168.237.1:8081/demo03",user,String.class);
    }
}

4、exchange

当请求的控制器返回值类型为List、Set等带有泛型类型的类型时,就不能使用getForObject、getForEntity、postForObject等,因为它们都是只能设置返回值类型,而不能设置类型的泛型。这时就需要使用exchange方法。
除此以外,如果需要设置请求头参数情况也需要使用exchange方法。

@SpringBootTest
class EurekaApplicationclientApplicationTests {
    //【A】其他的请求如果发送
    //【B】接收集合类型返回值 数据没有办法遍历
    @Test
    void   getExchange(){

        RestTemplate rp=new RestTemplate();
        //List list = rp.getForObject("http://192.168.237.1:8081/selectAll", List.class);
        //System.out.println(((User)list.get(0)).getName());

        //设置请求体数据
        User  user=new User("zs","123");
        HttpEntity<User> httpEntity = new HttpEntity<>(user);

        //设置响应数据类型
        ParameterizedTypeReference<List<User>> pt=new ParameterizedTypeReference<List<User>>(){};

        ResponseEntity<List<User>> exchange = rp.exchange("http://192.168.237.1:8081/selectAll", HttpMethod.GET, httpEntity, pt);
        //返回响应体数据 就是我们需要的类型
        List<User> list = exchange.getBody();

        for (User u:list) {
            System.out.println(u);
        }

    }
}

九、实现Application Client调用Application Service

上面的例子中是LoadBalance调用Provider的测试。现在实现调用Provider的控制器接口方法。
在Java代码中调用另一个控制器接口,可以使用之前学习的HttpClient实现,在今天的课程中换一种技术实现,基于RestTemplate完成的。

1、新建配置类

新建com.bjsxt.config.RibbonConfig。
注意方法上面要有@LoadBalanced注解。否则Ribbon不生效。

@Configuration
public class RibbonConfig {

    @Bean
    public RandomRule getRandomRule(){
        return new RandomRule();
    }


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

2、修改service实现类

注意:
无论使用RestTemplate的哪个方法,方法中URL参数必须使用spring.application.name应用程序名(ServerID)替换原来URL中host(主机)和port(端口)。因为底层Ribbon是根据应用程序名获取注册列表的。

@Service
public class ClientServiceImpl implements ClientService {
    @Autowired
    private RestTemplate restTemplate;
    @Override
    public String client() {
        String forObject = restTemplate.getForObject("http://applicationservice/demo01", String.class);
        System.out.println("结果为:"+forObject);
        return null;
    }

3、运行

controller代码

@RestController
public class ClientController {
    @Autowired
    private ClientService clientService;
    @RequestMapping("/client")
    public String client(){
        clientService.client();
        return "aa";
    }
}

运行项目,访问/client控制器,观察IDEA控制台打印的内容。