微服务-负载均衡Ribbon

1. 简介

在分布式微服务架构中, 微服务应用多是以集群的方式部署来保证微服务的高可用性. 那么服务消费方是通过什么方式的负载均衡策略调用到集群中的微服务提供方的呢? Spring Cloud给我们提供了解决方案: Ribbon.

2. Ribbon的负载均衡策略

Ribbon提供了轮询, 随机, 权重等多种负载均衡策略供我们使用, 默认是轮询策略.
com.netflix.loadbalancer.*包下面有我们常用的负载均衡策略类.

RoundRobinRule : 轮询策略,默认负载均衡策略
RandomRule : 随机策略
WeightedResponseTimeRule : 权重策略, 根据平均响应时间计算服务权重, 响应时间越快服务权重越大, 被选中概率越高.
ZoneAwareLoadBalancer : 复合判断server所在区域的性能和server的可用性选择服务器.
BestAvailableRule : 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务, 然后选择一个并发量最小的服务
AvailabilityFilteringRule : 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务. 然后对剩余的服务列表按照轮询策略进行访问.
RetryRule : 先按照轮询策略获取服务,如果获取服务失败则在指定时间内会进行重试, 获取可用的服务.

3. 远程调用方案

两种方式:

  • RestTemplate + Ribbon
  • OpenFeign + 内置Ribbon

4. 案例实践

4.1 搭建Eureka服务注册中心

这里省略微服务服务注册中心搭建步骤, 参考博客 : 搭建Eureka-Server集群→.
因为下面还有服务提供方, 服务消费方等多个微服务应用要搭建, 所以将公共的依赖抽取到父工程[microservice-cloud]里面, 下面是依赖的版本管理:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>microservice-cloud</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>microservicecloud-api</module>
        <module>microservicecloud-provider</module>
        <module>microservicecloud-consumer</module>
        <module>microservicecloud-eureka</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.13</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
    </properties>

    <!--springcloud官方仓库-->
    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.0.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.0.4</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.0.31</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.0</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-core</artifactId>
                <version>1.2.3</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 构建,打包插件 -->
    <build>
        <finalName>microservicecloud</finalName>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>${maven.compiler.source}</source>
                        <target>${maven.compiler.target}</target>
                        <encoding>UTF-8</encoding>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <configuration>
                        <delimiters>
                            <!--能够解析 src/main/resources/配置文件中的$占位符获取属性值; Eureka服务列表点击访问指定微服务的暴露端点信息使用-->
                            <delimit>$</delimit>
                        </delimiters>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
    
</project>

4.2 搭建公共微服务

在微服务应用搭建过程中, 可能遇到某些apipojo等资源在服务提供方, 服务消费方多个应用中都会用到, 这里将这部分内容抽取到一个公共应用服务[microservice-cloud]中, 届时以传递依赖jar的方式提供给其他微服务应用使用.

4.2.1 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>microservice-cloud</artifactId>
        <groupId>com.atguigu.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>microservicecloud-api</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.7.RELEASE</version>
        </dependency>
    </dependencies>
</project>

4.2.2 创建pojo

后面多个微服务应用都会用到的pojo类, 抽取到公共服务中实现. com.atguigu.springcloud.entities.Dept

/**
 * 映射到库表 ORM
 * @author 18482
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Builder
//@Accessors(chain = true) // 开启链式编程风格
public class Dept implements Serializable {

    private Integer deptno;
    private String dname;
    private String db_source;
    // lombok在编译时自动生成get, set, toString, equals, hashcode等方法
}

4.2.3 构建发布到本地仓库

[microservice-cloud]构建发布到本地maven仓库, 方便后面应用使用.
在这里插入图片描述在这里插入图片描述

4.3. 搭建服务提供方

创建服务提供方的微服务应用[microservicecloud-provider].

4.3.1 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>microservice-cloud</artifactId>
        <groupId>com.atguigu.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>microservicecloud-provider</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>microservicecloud-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</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-actuator</artifactId>
        </dependency>
    </dependencies>
</project>

4.3.2 编写配置

application.yml配置
server:
  port: 8001
mybatis:
  config-location: classpath:mybatis/mybatis-cfg.xml # mybatis配置文件所在路径
  type-aliases-package: com.atguigu.springcloud.entities # 所有entity别名类所在包
  mapper-locations:
    - classpath:mybatis/mapper/**/*.xml # mapper映射文件

spring:
  application:
    name: microservicecloud-provider
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 数据源类型
    driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
    url: jdbc:mysql://localhost:3306/cloudDB01?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    dbcp2:
      min-idle: 5 # 数据库连接池的最小维持连接数
      initial-size: 5 # 初始化连接数
      max-total: 5 # 最大连接数
      max-wait-millis: 200 # 等待连接获取的最大超时时间
logging:
  level:
    com.atguigu.springcloud.dao: debug

# eureka客户端
eureka:
  client:
    service-url:
      # Eureka注册中心地址
      defaultZone: http://www.eureka01.com:7001/eureka,http://www.eureka02.com:7002/eureka,http://www.eureka03.com:7003/eureka
  instance:
    instance-id: microservicecloud-provider-02 # 自定义微服务名称信息, 默认为主机名
    prefer-ip-address: true # 优先使用id注册,访问路径可以显示IP地址
# 暴露的info端点信息, 需要引入actuator依赖
info:
  app.name: ${spring.application.name}
  company.name: www.atguigu.com
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
整合mybatis配置及mapper映射文件

mybatis-cfg.xml配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

	<!--设置 -->
	<settings>
		<!--延迟加载 -->
		<setting name="lazyLoadingEnabled" value="true" />
		<setting name="aggressiveLazyLoading" value="false" />
		<!--全局映射器启动或禁用缓存 -->
		<setting name="cacheEnabled" value="true" />
		<!--配置自动映射 NONE-不自动映射 PARTIAL-只对没有嵌套结果集自动映射 FULL-全部自动映射 -->
		<setting name="autoMappingBehavior" value="PARTIAL" />
		<!-- 配置驼峰映射 -->
		<setting name="mapUnderscoreToCamelCase" value="false" />
	</settings>
</configuration>

mapper映射文件, mybatis/mapper/DeptMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.springcloud.dao.DeptDao">

    <select id="findById" parameterType="long" resultType="dept">
		select deptno,dname, db_source from dept where deptno=#{deptno}
	</select>

    <select id="findAll" resultType="dept">
		select deptno, dname,db_source  from dept
	</select>

    <insert id="addDept" parameterType="dept">
		insert into dept(dname,db_source) values(#{dname},database())
	</insert>
</mapper>

4.3.3 创建dao层

com.atguigu.springcloud.dao.DeptDao

package com.atguigu.springcloud.dao;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.atguigu.springcloud.entities.Dept;
@Mapper
public interface DeptDao {
    boolean addDept(Dept dept);
    Dept findById(Long id);
    List<Dept> findAll();
}

4.3.4 创建service层

com.atguigu.springcloud.service.impl.DeptServiceImpl

package com.atguigu.springcloud.service.impl;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.atguigu.springcloud.dao.DeptDao;
import com.atguigu.springcloud.entities.Dept;
import com.atguigu.springcloud.service.DeptService;

@Service
public class DeptServiceImpl implements DeptService {

	@Autowired
	private DeptDao deptDao;

	@Override
	public boolean add(Dept dept) {
		return deptDao.addDept(dept);
	}

	@Override
	public Dept get(Long id) {
		return deptDao.findById(id);
	}
	
	@Override
	public List<Dept> findAll() {
		return deptDao.findAll();
	}
}

4.3.5 创建controller层

com.atguigu.springcloud.controller.DeptController

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.Dept;
import com.atguigu.springcloud.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/dept")
public class DeptController {

	@Autowired
	private DeptService deptService;

	@PostMapping(value = "/add")
	public boolean add(@RequestBody Dept dept) {
		return deptService.add(dept);
	}

	@GetMapping(value = "/{id}")
	public Dept get(@PathVariable(value = "id") Long id) {
		return deptService.get(id);
	}

	@GetMapping
	public List<Dept> findAll() {
		return deptService.findAll();
	}
}

4.3.6 创建启动类

服务提供方[microservicecloud-provider]启动类及开启Eureka Client等配置.
com.atguigu.springcloud.ProviderApplication

package com.atguigu.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端配置
@EnableDiscoveryClient // 开启服务发现
public class ProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
}

4.4. 搭建服务消费方

创建服务消费方微服务应用[microservicecloud-consumer].

4.4.1 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>microservice-cloud</artifactId>
        <groupId>com.atguigu.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>microservicecloud-consumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>microservicecloud-api</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
</project>

4.4.2 编写配置

application.yml配置

server:
  port: 8080
spring:
  application:
    name: microservicecloud-consumer

# Eureka客户端配置
eureka:
  client:
    service-url:
      defaultZone: http://www.eureka01.com:7001/eureka,http://www.eureka02.com:7002/eureka,http://www.eureka03.com:7003/eureka
  instance:
    instance-id: microservicecloud-consumer-01
    prefer-ip-address: true # 优先使用ip注册,访问显示ip

# 暴露端点info
info:
  app.name: microservicecloud-consumer
  company.name: www.atguigu.com
  build.artifactId: com.atguigu.springcloud
  build.version: 1.0-SNAPSHOT

4.4.3 创建controller

创建消费方法的controller层 com.atguigu.springcloud.controller.ConsumerController

package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.Dept;
import com.atguigu.springcloud.service.DeptFeignClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;

/**
 * 类描述:消费者api
 * @Author wang_qz
 * @Date 2021/10/23 21:07
 * @Version 1.0
 */
@RestController
@RequestMapping(value = "/consumer")
public class ConsumerController {

    private static final String REST_URL_PREFIX = "http://localhost:8001";

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 服务发现api
     * @see DiscoveryClient
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * Feign声明式调用
     */
    @Autowired
    private DeptFeignClientService feignClientService;

    /**
     * Eureka的负载均衡(Ribbon)
     */
    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @PostMapping(value = "/add")
    public boolean add(Dept dept) {

        return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
    }

    @GetMapping(value = "/{id}")
    public Dept get(@PathVariable(value = "id") Long id) {
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/" + id, Dept.class);
    }

    @GetMapping
    public List<Dept> findAll() {

        return restTemplate.getForObject(REST_URL_PREFIX + "/dept", List.class);
    }

    @GetMapping(value = "/discovery")
    public Object discovery() {
        // 服务发现
        List<String> list = discoveryClient.getServices();
        System.out.println(">>>>list: " + list);
        List<ServiceInstance> instances = discoveryClient.getInstances("microservicecloud-provider");
        for (ServiceInstance instance : instances) {
            System.out.println(instance.getServiceId() + "=" + instance.getHost() + ":" + instance.getPort() + "\t" + instance.getUri());
        }
        return this.discoveryClient;
    }

    /**
     * 使用Eureka的服务发现api调用远程服务提供方
     * @param id
     * @return
     */
    @GetMapping(value = "/findById/{id}")
    public Dept findById(@PathVariable(value = "id") Long id) {
        List<ServiceInstance> instances = discoveryClient.getInstances("microservicecloud-provider");
        ServiceInstance instance = instances.get(0);
        return restTemplate.getForObject("http://" + instance.getHost() + ":" + instance.getPort() + "/dept/" + id, Dept.class);
    }

    /**
     * 使用Eureka的服务发现api调用远程服务提供方(负载均衡Ribbon)
     * @param id
     * @return
     */
    @GetMapping(value = "/findById2/{id}")
    public Dept findById2(@PathVariable(value = "id") Long id) {
        ServiceInstance instance = loadBalancerClient.choose("microservicecloud-provider");
        return restTemplate.getForObject("http://" + instance.getHost() + ":" + instance.getPort() + "/dept/" + id, Dept.class);
    }

    /**
     * 使用Eureka的服务发现api调用远程服务提供方(负载均衡Ribbon-简化版)
     * RestTemplate配置bean需要添加@LoadBalanced注解, RestTemplate才能自动负载均衡
     * @param id
     * @return
     */
    @GetMapping(value = "/findById3/{id}")
    public Dept findById3(@PathVariable(value = "id") Long id) {
        return restTemplate.getForObject("http://microservicecloud-provider" + "/dept/" + id, Dept.class);
    }

    /**
     * 使用Eureka的服务发现api调用远程服务提供方(Feign声明式调用)
     * Feign内置了Ribbon负载均衡, 默认轮询策略
     * @param id
     * @return
     */
    @GetMapping(value = "/findById4/{id}")
    public Dept findById4(@PathVariable(value = "id") Long id) {
        return feignClientService.get(id);
    }
}

4.4.4 编写启动类及配置类

消费方微服务应用的启动类 com.atguigu.springcloud.ConsumerApplication

package com.atguigu.springcloud;

import com.atguigu.myrule.MyRuleConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端配置
@EnableDiscoveryClient // 开启服务发现
// @RibbonClient 在启动微服务时加载自定义的Ribbon配置类
// 这个自定义配置类不能放在@ComponentScan所扫描的当前包下及子包下面,否则自定义配置类会被所有的Ribbon客户端共享, 达不到定制化目的.
// 下面配置解释: 对microservicecloud-provider微服务使用自定义Ribbon策略MyRule
@RibbonClient(name = "microservicecloud-provider", configuration = {MyRuleConfig.class})
@EnableFeignClients // 开启Feign声明式调用的配置
public class ConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}

配置类, 等同于 applicationContext.xml . com.atguigu.springcloud.cfgbeans.ConfigBean

package com.atguigu.springcloud.cfgbeans;

import com.netflix.loadbalancer.*;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * 类描述:Bean配置类
 * @Author wang_qz
 * @Date 2021/10/23 21:00
 * @Version 1.0
 */
@Configuration  // 等同于 applicationContext.xml
public class ConfigBean {

    @Bean
    @LoadBalanced // 负载均衡(Ribbon)
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * 修改Ribbon负载均衡策略, 替代默认的轮询
     * @see RoundRobinRule 轮询策略,默认负载均衡策略
     * @see RandomRule 随机策略
     * @see WeightedResponseTimeRule 权重策略, 根据平均响应时间计算服务权重, 响应时间越快服务权重越大, 被选中概率越高.
     * @see ZoneAwareLoadBalancer 复合判断server所在区域的性能和server的可用性选择服务器
     * @see BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务, 然后选择一个并发量最小的服务
     * @see AvailabilityFilteringRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务. 然后对剩余的服务列表按照轮询策略进行访问
     * @see RetryRule 先按照轮询策略获取服务,如果获取服务失败则在指定时间内会进行重试, 获取可用的服务.
     * @return
     */
    @Bean
    public IRule iRule() {
        return new RandomRule();
    }
}

5. 负载均衡分解

5.1 RestTempate+Ribbon

RestTempate实例化bean的配置方法上添加注解@LoadBalanced, 就使RestTemplate具有负载均衡能力.
实例化远程调用客户端接口RestTempate的配置

@Bean
@LoadBalanced // 负载均衡(Ribbon)
public RestTemplate restTemplate() {
	return new RestTemplate();
}

使用RestTempate远程调用服务提供方, 具备负载均衡.

/**
 * 使用Eureka的服务发现api调用远程服务提供方(负载均衡Ribbon-简化版)
 * RestTemplate配置bean需要添加@LoadBalanced注解, RestTemplate才能自动负载均衡
 * @param id
 * @return
 */
@GetMapping(value = "/findById3/{id}")
public Dept findById3(@PathVariable(value = "id") Long id) {
	return restTemplate.getForObject("http://microservicecloud-provider" + "/dept/" + id, Dept.class);
}

上面的负载均衡原理, 在拦截器LoadBalancerInterceptor#intercept中有体现. 查看源码:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
	// 负载均衡客户端
	private LoadBalancerClient loadBalancer;
	private LoadBalancerRequestFactory requestFactory;
	// .....此处省略部分源码
	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
	}
}

查看LoadBalancerClient#getServer源码

protected Server getServer(ILoadBalancer loadBalancer) {
	if (loadBalancer == null) {
		return null;
	}
	return loadBalancer.chooseServer("default"); // TODO: better handling of key
}

查看BaseLoadBalancer#chooseServer源码

public Server chooseServer(Object key) {
	if (counter == null) {
		counter = createCounter();
	}
	counter.increment();
	if (rule == null) {
		return null;
	} else {
		try {
			return rule.choose(key); // IRule-负载均衡策略
		} catch (Exception e) {
			logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
			return null;
		}
	}
}

IRule接口的choose方法就是负载均衡算法的地方, 需要在不同的负载均衡策略实现类重写这个方法. 下面的自定义负载均衡策略类中会详细介绍.

5.2 OpenFeign+内置Ribbon

open-feign的依赖中内置了Ribbon, 说明Open-Feign已经具备了负载均衡能力. 而Feign的使用也很简单,只需要开发一个接口(会生成动态代理类), 使用@FeignClient注解标注并指定声明调用的服务提供方, 然后在启动类上添加@EnableFeignClients注解开启声明式远程调用的配置功能.
注意: open-feign与feign的区别.
从依赖中可以发现 OpenFeign内置了 Ribbon.
在这里插入图片描述
open-feign的依赖

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

开发Feign接口类, @FeignClient指定调用的服务提供方

/**
 * 类描述:Feign声明式调用的接口
 * @Author wang_qz
 * @Date 2021/10/27 20:44
 * @Version 1.0
 */
@FeignClient(name= "microservicecloud-provider")
public interface DeptFeignClientService {
    @PostMapping(value = "/dept/add")
    boolean add(Dept dept);
    @GetMapping(value = "/dept/{id}")
    Dept get(@PathVariable(value = "id") Long id);
    @GetMapping(value = "/dept")
    List<Dept> findAll();
}

启动类上开启Feign配置.

@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端配置
@EnableDiscoveryClient // 开启服务发现
@EnableFeignClients // 开启Feign声明式调用的配置
public class ConsumerApplication 
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}

Feign声明式调用的使用.

/**
 * Feign声明式调用
 */
@Autowired
private DeptFeignClientService feignClientService;
/**
 * 使用Eureka的服务发现api调用远程服务提供方(Feign声明式调用)
 * Feign内置了Ribbon负载均衡, 默认轮询策略
 * @param id
 * @return
 */
@GetMapping(value = "/findById4/{id}")
public Dept findById4(@PathVariable(value = "id") Long id) {
	return feignClientService.get(id);
}

5.3 修改全局负载均衡策略

Ribbon默认的负载均衡均衡策略是: 轮询(RoundRobinRule). 有两种方式可以修改默认的负载均衡策略.
方式一, 在配置文件中指定全局的负载均衡策略.

microservicecloud-provider:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

方式二, 在启动类或配置类中添加IRule实例化bean的方法.(只要在@ComponentScan扫描下的配置即可.)

/**
 * 修改Ribbon负载均衡策略, 替代默认的轮询
 * @see RoundRobinRule 轮询策略,默认负载均衡策略
 * @see RandomRule 随机策略
 * @see WeightedResponseTimeRule 权重策略, 根据平均响应时间计算服务权重, 响应时间越快服务权重越大, 被选中概率越高.
 * @see ZoneAwareLoadBalancer 复合判断server所在区域的性能和server的可用性选择服务器
 * @see BestAvailableRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务, 然后选择一个并发量最小的服务
 * @see AvailabilityFilteringRule 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,还有并发的连接数量超过阈值的服务. 然后对剩余的服务列表按照轮询策略进行访问
 * @see RetryRule 先按照轮询策略获取服务,如果获取服务失败则在指定时间内会进行重试, 获取可用的服务.
 * @return
 */
@Bean
public IRule iRule() {
	return new RandomRule();
}

5.4 自定义负载均衡策略

有时候,我们想对某些服务提供方的调用进行定制化的负载均衡策略, 而不影响全局其他调用的负载均衡策略. 可以在启动类中使用注解@RibbonClient为指定的服务提供方调用定制化负载均衡策略.

@SpringBootApplication
@EnableEurekaClient // 开启Eureka客户端配置
@EnableDiscoveryClient // 开启服务发现
// @RibbonClient 在启动微服务时加载自定义的Ribbon配置类
// 这个自定义配置类不能放在@ComponentScan所扫描的当前包下及子包下面,否则自定义配置类会被所有的Ribbon客户端共享, 达不到定制化目的.
// 下面配置解释: 对microservicecloud-provider微服务使用自定义Ribbon策略MyRule
@RibbonClient(name = "microservicecloud-provider", configuration = {MyRuleConfig.class})
public class ConsumerApplication {

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

}

自定义负载均衡策略配置类 com.atguigu.myrule.MyRuleConfig .

注意: 为某个服务提供方的调用定制化负载均衡策略的配置类不能放在启动类所在包及其子包下面, 因为会被@ComponentScan注解扫描, 变成全局的负载均衡策略.
启动类所在包: com.atguigu.springcloud
自定义负载均衡策略配置类所在包: com.atguigu.myrule

/**
 * 类描述:自定义Ribbon策略类
 * @Author wang_qz
 * @Date 2021/10/26 22:56
 * @Version 1.0
 */
@Configuration
public class MyRuleConfig {
    @Bean
    public IRule iRule() {
        return new MyRandomRule();
    }
}

自定义的负载均衡策略类, com.atguigu.myrule.MyRandomRule , 可以参考RandomRule随机策略, 需要继承AbstractLoadBalancerRule, 然后重写里面的负载均衡逻辑的方法choose.

package com.atguigu.myrule;

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.List;

/**
 * 类描述:自定义负载均衡策略
 * 要求每台微服务被调用5次, 这样每个微服务被调用5次后轮询
 * @Author wang_qz
 * @Date 2021/10/27 21:18
 * @Version 1.0
 */
public class MyRandomRule extends AbstractLoadBalancerRule {

    /**
     * 每个微服务总共被调用的次数, 目前要求每台微服务被调用5次
     */
    private int total = 0;

    /**
     * 当前提供服务的微服务索引号
     */
    private int currentIndex = 0;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {}

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers();
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                return null;
            }
            
            /**
             * 分析:
             * total==5次以后, 提供服务的微服务索引指针才能继续往下走; 并且total
             * 需要重新置为0, 但是因为已经达到过一个5次, 所以currentIndex此时要+1
             */
            if (total < 5) {
                server = upList.get(currentIndex);
                total++;
            } else {
                total = 0;
                currentIndex++;
                // upList.size为提供服务的微服务个数, 当currentIndex轮询完所有微服务后, 要从第一台重新开始轮询
                if (currentIndex >= upList.size()) {
                    currentIndex = 0;
                }
                server = upList.get(currentIndex);
            }
            if (server == null) {
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }
            server = null;
            Thread.yield();
        }
        return server;
    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }
}

启动各个微服务应用, 查看注册中心服务列表, 已经成功注册到eureka.
在这里插入图片描述
通过服务消费方调用服务提供方, 访问 http://localhost:8080/consumer/findById3/1. 可以发现服务提供方8001, 8002, 8003分别被调用了5次并完成依次轮询的效果.
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

5.5 自定义负载均衡器

自定义负载均衡器接口. com.atguigu.springcloud.lb.LoadBalancer

/**
 * 类描述:自定义负载均衡器接口
 */
public interface LoadBalancer {

    /**
     * 获取负载均衡算法后的服务实例
     * @param serviceInstances
     * @return
     */
    ServiceInstance choose(List<ServiceInstance> serviceInstances);

}

编写自定义负载均衡器接口的实现类. com.atguigu.springcloud.lb.MyLB

/**
 * 类描述:自定义负载均衡器实现
 */
@Component
public class MyLB implements LoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    /**
     * 计算服务被请求的当前次数
     * @return
     */
    public final int getAndIncrement() {
        int current;
        int next;
        do {
            current = this.atomicInteger.get();
            next = (current >= Integer.MAX_VALUE) ? 0 : current + 1;
        } while (!this.atomicInteger.compareAndSet(current, next));
        System.out.println("next: " + next);
        return next;
    }

    /**
     * 返回负载均衡计算后的服务实例
     * @param serviceInstances
     * @return
     */
    @Override
    public ServiceInstance choose(List<ServiceInstance> serviceInstances) {
        if (CollectionUtils.isEmpty(serviceInstances)) return null;
        // 负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标  ,每次服务重启动后rest接口计数从1开始。
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

服务提供者编写api, com.atguigu.springcloud.controller.PaymentController#getPaymentLB

@Value(value = "${server.port}")
private String serverPort;

@GetMapping(value = "/payment/lb")
public String getPaymentLB(){
	return serverPort;
}

服务消费者编写api, com.atguigu.springcloud.controller.OrderController#getPaymentLB

// 服务发现组件
@Resource
private DiscoveryClient discoveryClient;

// http客户端组件
@Resource
private RestTemplate restTemplate;

/**
 * 测试自定义负载均衡器,需要注释掉{@link RestTemplate}实例化方法上的 @LoadBalanced 注解
 * @return
 */
@GetMapping("/consumer/payment/lb")
public String getPaymentLB() {

	List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PROVIDER-PAYMENT");
	ServiceInstance instance = loadBalancer.choose(instances);
	if (instance == null) return "没有可用的支付服务实例.";
	URI uri = instance.getUri();
	return restTemplate.getForObject(uri + "/payment/lb", String.class);
}

RestTemplate实例化方法去掉@LoadBalanced注解, 保证负载均衡实现来自手写的, 而非Ribbon.

@Bean
// @LoadBalanced // 暂且注释掉
public RestTemplate restTemplate() {
	return new RestTemplate();
}

测试: 启动应用后, 访问http://localhost/consumer/payment/lb.
在这里插入图片描述
在这里插入图片描述
查看控制台打印的日志, 当前请求服务的次数确实是逐次增加, 上面截图的返回结果也达到了轮询效果.
在这里插入图片描述

6. 总结

一. 两种负载均衡实现方案, 因为我们习惯面向接口编程, 所以推荐第二种方式.

  • RestTemplate + Ribbon
  • OpenFeign + 内置Ribbon

二. 默认负载均衡策略是轮询, 可以在配置文件或配置类中修改负载均衡策略.
三. 使用@RibbonClient注解可以为指定服务提供方定制化负载均衡策略.

  • 注意自定义负载均衡配置类的包扫描问题.

个人博客

欢迎各位访问我的个人博客: https://www.crystalblog.xyz/

备用地址: https://wang-qz.gitee.io/crystal-blog/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值