欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

程序员文章站 2022-03-14 09:57:54
...

负载均衡

负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。

一个没有负载均衡的 web 架构类似下面这样:

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

在这里用户直连 web 服务器,如果这个服务器宕机了,那么用户自然也就没办法访问了。另外,如果同时有很多用户试图访问服务器,超过了其能处理的极限,就会出现加载速度缓慢或根本无法连接的情况。

而通过在后端引入一个负载均衡器和至少一个额外的 web 服务器,可以缓解这个故障。通常情况下,所有的后端服务器会保证提供相同的内容,以便用户无论哪个服务器响应,都能收到一致的内容。

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Spring Cloud 入门 ---- Ribbon 负载均衡组件

简介

Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡工具。简单的说,Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件中列出 Load Balancer(简称LB)后面所有的机器,Ribbon 会自动的帮助你基于某种规则(如:简单轮询,随机连接等)去连接这些机器。我们很容易使用 Ribbon 实现自定义的负载均衡算法。

官网:https://github.com/Netflix/ribbon/wiki

概念:

LB 负载均衡 (Load Balance) 是什么

简单的所就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件 F5等。

Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡的区别

Nginx 是服务端负载均衡,客户端的所有请求都会交给 nginx,然后由 nginx 实现转发请求。即负载是由服务端实现的。

Ribbon 本地负载均衡,在调用微服务接口时,会在注册中心上获取服务的注册信息列表之后缓存到 JVM 本地【即将服务别名 以及对应的提供服务的机器信息缓存到本地,当调用服务时根据负载均衡策略选择一个提供服务的机器并调用它】,从而在本地实现 RPC 远程服务调用技术。

LB分类:

  • 集中式LB

    即在服务的消费方和提供方之间使用独立的 LB 设施(可以是硬件,如F5,也可以是软件,如 nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;

  • 进程内LB

    将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,他只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

总结:

Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。

Eureka + Ribbon 架构图:

以 EurekaServer 作为注册中心,当服务提供者启动时,会将自身服务信息【IP,port等】以别名的方式注册到 EurekaServer【同一种服务别名一样】,服务消费者会从 EurekaServer 查询服务列表【服务别名与其下的节点信息】并缓存到本地,当消费者需要调用服务提供者的服务时,会根据负载均衡策略选择一个节点并调用它;

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Ribbon 在工作时分为两步:

第一步:选择EurekaServer,它优先选择在同一个区域内负载较少的server。

第二步:在根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权等。

创建演示项目

为了方便我们介绍和演示 Ribbon 负载均衡的效果,我们需要创建 服务消费者 与 服务提供者模块【集群-两个节点】,至于注册中心就用之前的 Eureka 即可。

创建服务提供者

引入 pom 依赖,由于这次使用了数据库,所以需要引入 db 相关依赖

<!-- 引入 eureka client -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

创建 yaml 配置文件,由于要演示负载均衡效果,为了启动两个服务,我们这里需要配置两个配置文件,由于 application-one.ymlapplication-two.yml 配置差不多一样,我们给出 application-one.yml 完整的配置,而 application-two.yml 只给出不一样的配置。

application-one.yml

server:
  port: 8010

spring:
  application:
    name: ribbon-payment-provider
  security:
    # 配置spring security登录用户名和密码
    user:
      name: akieay
      password: 1qaz2wsx
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3600/cloud?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root
    #   数据源其他配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true
    #   配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
    filters: stat,wall
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500


eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      #      defaultZone: http://localhost:7001/eureka
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: payment8010
    ## 当调用getHostname获取实例的hostname时,返回ip而不是host名
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
    lease-renewal-interval-in-seconds: 10
    # Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
    lease-expiration-duration-in-seconds: 30


#mybatis-plus
mybatis-plus:
  mapper-locations: classpath*:mapper/*.xml
  #实体扫描,多个package用逗号或者分号分隔
  typeAliasesPackage: com.akieay.cloud.rbpayment.entity;
  configuration:
    #是否开启驼峰命名自动映射
    map-underscore-to-camel-case: true
    #全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。
    cache-enabled: false
    #指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法
    call-setters-on-nulls: true
    #指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

application-two.yml

server:
  port: 8011
  
eureka:
 instance:
    instance-id: payment8011

主启动

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@MapperScan(value = "com.akieay.cloud.rbpayment.mapper")
public class RibbonPaymentApplication {

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

业务类,这里只介绍主要相关的业务类,其它细节请自行补充,或直接去 gitee 上拉取代码查阅。

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {

    @Override
    public int create(UserEntity userEntity) {
        return baseMapper.insert(userEntity);
    }

    @Override
    public UserEntity getById(int id) {
        return baseMapper.selectById(id);
    }
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("user")
public class UserEntity implements Serializable {

    private static final long serialVersionUID=1L;

    /**
     * 用户ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;
    /**
     * 用户名
     */
    private String username;
    /**
     * 密码
     */
    private String password;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 创建时间
     */
    private Date createTime;
}
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

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

    @PostMapping(value = "/create")
    public CommonResult create(@RequestBody UserEntity userEntity) {
        int result = userService.create(userEntity);
        log.info("*****插入结果:"+result);

        if (result > 0) {
            return new CommonResult(200, "插入数据库成功, serverPort:"+serverPort, result);
        } else {
            return new CommonResult(444, "插入数据库失败");
        }
    }

    @GetMapping(value = "/get/{id}")
    public CommonResult<UserEntity> getUserById(@PathVariable("id") Long id) {
        UserEntity userEntity = userService.getById(id);
        log.info("*****查询结果:"+userEntity);

        if (null != userEntity) {
            return new CommonResult(200, "查询数据成功, serverPort:"+serverPort, userEntity);
        } else {
            return new CommonResult(400, "没有对应的记录,查询ID:"+id);
        }
    }
}

创建两个启动服务,具体细节已经在前面的注册中心模块做了介绍,这里就不介绍了

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

创建成功后启动服务,打开 Eureka 注册中心,输入账号密码登录上去,即可看到 提供者模块已经注册进入了 Eureka 注册中心,如下图:

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

至此 服务提供者创建完成

创建服务消费者

引入 pom 依赖,由于该版本的 eureka 自带了 ribbon 依赖,所以我们不需要单独引入 ribbon 依赖。

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

 <!-- 引入 eureka client -->
<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-netflix-ribbon</artifactId>
</dependency>

创建 application.yml 配置文件

server:
  port: 80
spring:
  application:
    name: ribbon-order-consumer
  security:
    # 配置spring security登录用户名和密码
    user:
      name: akieay
      password: 1qaz2wsx

eureka:
  client:
    #表示是否将自己注册进 Eureka Server服务 默认为true
    register-with-eureka: true
    #f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
      defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
  instance:
    instance-id: order80
    prefer-ip-address: true

主启动

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class RibbonOrderApplication {

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

配置类

@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced  //赋予默认的负载均衡规则
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

业务类

@RestController
@Slf4j
public class UserConcumerController {

    /**
     * payment-provider:服务别名
     */
    public static final String PAYMENT_URL = "http://RIBBON-PAYMENT-PROVIDER";

    @Resource
    private RestTemplate restTemplate;

    /**
     * getForObject
     * @param id
     * @return
     */
    @GetMapping("/consumer/user/getObject/{id}")
    public CommonResult getObject(@PathVariable("id") Long id) {
        return restTemplate.getForObject(PAYMENT_URL + "/user/get/" + id, CommonResult.class);
    }

}

至此,服务消费者基础模块完成,启动服务,打开 Eureka 注册中心,即可看到服务已经注册到了其中,并且访问:http://localhost//consumer/user/getObject/1 可以发现使用默认的 轮询 负载均衡规则轮流调用服务提供者 8010 与 8011。

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

至此,演示模块的基础创建完成。

Ribbon 负载均衡与 RestTemplate 调用

由于我们该模块是使用的 Ribbon + RestTemplate 来实现的负载均衡与服务调用,所以这里先介绍一下 RestTemplate ;RestTemplate 是一个 HTTP 客户端工具,主要为我们调用 Http 接口提供方便的,支持 GET、POST、PUT、DELEFT …等方法,我们这里主要介绍 GET 与 POST。

GET 请求

主要是 getForObject 与 getForEntity 方法,两者的区别为:

  • getForObject 方法:返回对象为响应体中数据转换成的对象,基本可以理解为Json【在前面的案例中我们已经使用过了,具体的返回类型根据我们给定的 class 类型确定】。
  • getForEntity 方法:返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如:响应头、响应状态码、响应体等。

演示:

修改服务消费者的业务类:UserConcumerController,由于之前已经存在了 getForObject 的调用,我们这只需要添加 getForEntity 方法的调用即可。

/**
 * getForEntity
 * @param id
 * @return
 */
@GetMapping("/consumer/user/getEntity/{id}")
public CommonResult getEntity(@PathVariable("id") Long id) {
    ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/user/get/" + id, CommonResult.class);
    // 打印响应code 与 响应头信息
    log.info(entity.getStatusCode() + "  ----   " + entity.getHeaders());
    if (entity.getStatusCode().is2xxSuccessful()) {
        return entity.getBody();
    } else {
        return new CommonResult(444, "操作失败");
    }
}

重启服务,并分别调用 http://localhost//consumer/user/getObject/1 与 http://localhost//consumer/user/getEntity/1 可以发现两个返回给客户端的响应结果一致,也就是说 getForEntity 的响应实体内容 entity.getBody() 与 getForObject 返回的内容一致。这就验证了我们之前所说的 getForEntity 与 getForObject 的不同之处在于,getForEntity 不仅会返回相应的实体数据,还会返回响应状态码,响应头信息等,如下面我们控制台打印的这条信息。

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

POST 请求

主要是 postForObject 与 postForEntity 方法,两者区别为: postForEntity 方法可以设置 请求的 header 属性,而 postForObject 不能指定请求的 header,当需要指定 header 的属性值的时候,使用postForEntity方法。

演示:

新增 insertObjectinsertEntity 方法,我们在 insertEntity 中设置了请求头信息

@PostMapping("/consumer/user/insertObject")
public CommonResult insertObject(@RequestBody Map<String, Object> user) {
    return restTemplate.postForObject(PAYMENT_URL + "/user/create", user, CommonResult.class);
}

@PostMapping("/consumer/user/insertEntity")
public CommonResult insertEntity(@RequestBody Map<String, Object> user) {
    HttpHeaders headers = new HttpHeaders();
    //添加请求头参数 认证token
    headers.add("Authorization", "Bearer 05ce151b-8574-4411-a9c2-baaa4cd17e59");
    headers.add("Content-Type", "application/json");

    HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(user,headers);
    ResponseEntity<CommonResult> responseEntity = restTemplate.postForEntity(PAYMENT_URL + "/user/create", httpEntity, CommonResult.class);

    log.info(responseEntity.getStatusCode() + "  ----   " + responseEntity.getStatusCodeValue() + "  ----   " + responseEntity.getHeaders());
    return responseEntity.getBody();
}

重启服务,并测试这两个接口;可以看到返回结果差不多;postForEntitypostForObject 的不同之处除了可以设置 headers 外,返回的结果也不一样,postForObject 只会返回响应实体部分,而 postForEntity 会返回 响应码、响应头、响应体等信息;

postForObject

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

postForEntity

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

至此关于RestTemplate 的介绍结束,至于其它的请求方法的介绍请自行查阅。

Ribbon自带的负载均衡规则
Ribbon的核心组件IRule

根据特定算法从服务器列表中选取一个要访问的服务

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

自带的负载均衡算法
  • RoundRobinRule: 轮询获取服务实例。
  • RandomRule: 随机获取服务实例。
  • RetryRule: 先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。
  • WeightedResponseTimeRule: 对于 RoundRobinRule 的扩展,响应速度越快的实例选择权重越大,越容易被选择。
  • BestAvailableRule: 会先过滤由于多次访问故障而处于熔断跳闸状态的服务,然后选择一个并发量最小的服务。
  • AvailabilityFilteringRule: 先过滤掉故障实例,再选择并发较小的实例。
  • ZoneAvoidanceRule: 采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。
替换负载均衡规则

替换负载均衡规则需要我们自定义一个负载均衡规则,这个规则的放置的位置是有限制的;官方文档明确给出警告:这个自定义配置类不能放在 @ComponentScan 所扫描的当前包以及其子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了。即替换的负载均衡规则如果放置在启动类包或其子包下时,这个规则对所有服务生效。

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

正确的放法如下:

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

自定义负载均衡规则:

@Configuration
public class MySelfRule {
    
    /**
     * 随机
     * @return
     */
    @Bean
    public IRule myRule(){
        return new RandomRule();
    }
}

修改主启动,添加注解 @RibbonClient

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@RibbonClient(name = "RIBBON-PAYMENT-PROVIDER", configuration = MySelfRule.class)
public class RibbonOrderApplication {

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

name:服务别名 configuration:替换的负载均衡规则;重启服务,访问:http://localhost/consumer/user/getObject/1 多次访问,通过返回消息中的端口,我们可用看到访问规则变成了 随机,有的时候 8010 经常出现,有的时候 8011 经常出现访问完全随机。至于其它的负载均衡策略这里不做介绍,可自行修改测试。

Ribbon 默认负载均衡策略【RoundRobinRule】原理

rest 接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次重启服务后,rest 接口计数重置为1。

List<ServiceInstance> instances = discoveryClient.getInstances("RIBBON-PAYMENT-PROVIDER")

如:List[0] instanceA = 127.0.0.1:8011 List[1] instanceB = 127.0.0.1:8010

8010 + 8011 组合成为集群,它们共计 2 台机器,集群总数为 2,按轮询算法原理:

当请求总数为 1 时: 1 % 2 = 1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8010

当请求总数为 2 时: 2 % 2 = 0 对应下标位置为 1,则获得服务地址为 127.0.0.1:8011

当请求总数为 3 时: 3 % 2 = 1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8010

当请求总数为 4 时: 4 % 2 = 0 对应下标位置为 1,则获得服务地址为 127.0.0.1:8011

如此类推… 服务重启后重置为 1。

RoundRobinRule 源码分析

public class RoundRobinRule extends AbstractLoadBalancerRule {

    private AtomicInteger nextServerCyclicCounter;
    private static final boolean AVAILABLE_ONLY_SERVERS = true;
    private static final boolean ALL_SERVERS = false;

    private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);

    public RoundRobinRule() {
        nextServerCyclicCounter = new AtomicInteger(0);
    }

    public RoundRobinRule(ILoadBalancer lb) {
        this();
        setLoadBalancer(lb);
    }

    public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
            //获取可达的机器
            List<Server> reachableServers = lb.getReachableServers();
            //获取所有机器
            List<Server> allServers = lb.getAllServers();
            //可达的机器数量
            int upCount = reachableServers.size();
            //总机器数量
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }

            //获取下一次调用服务器的下标[CAS + 自旋锁]
            int nextServerIndex = incrementAndGetModulo(serverCount);
            //获取下一次调用的服务机器
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            int next = (current + 1) % modulo;
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

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

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}


nextServerCyclicCounter:记录接口第几次请求。

以上为 RoundRobinRule 负载均衡的源码,其核心是 choose 方法用来选择提供服务的机器;该方法会首先判断 lb 【负载均衡器】是否为空,若为空抛出异常,然后 while 循环 10 次获取 server 若获取到不为空的 server 则跳出循环,返回 server,否则循环 10 次后,抛出没有可用服务器的异常;

获取 server 的流程:获取可达【正常可用】的服务机器列表,获取总服务机器列表;若两者有一方为空,则抛出 没有可用服务 的异常;通过 CAS + 自旋锁 获取下一个可用的服务节点的下标【轮询算法是通过 接口请求次数 % 服务节点数量 = 访问的节点下标】,通过下标获取下一个可用的服务节点,若服务节点能够正常使用则返回,否则将其置空 进入下一次循环判断。

Ribbon—手写轮询算法

服务提供者 新增业务,然后重启服务

/**
* 自定义轮询负载均衡规则测试
* @return
*/
@GetMapping("/lb")
public String getUserLb(){
    return "self RoundRobinRule 访问端口:" + serverPort;
}

服务消费者 手写轮询算法

首先为了避免原有的负载均衡规则的影响;需要注释掉原有的 负载均衡配置

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

自定义负载均衡规则

public interface LoadBalancer {

    /**
     * 从服务主机列表根据 负载均衡规则 获取主机
     * @param serviceInstances
     * @return
     */
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

@Component
public class SelfRoundRobinRule implements LoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    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);
        return next;
    }

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

@Resource
private SelfRoundRobinRule selfRoundRobinRule;
@Resource
private DiscoveryClient discoveryClient;

@GetMapping("/consumer/user/lb")
public String getUserLb() {
    //获取指定服务的节点列表
    List<ServiceInstance> instances = discoveryClient.getInstances("RIBBON-PAYMENT-PROVIDER");
    if (null == instances || instances.size() <= 0) {
        return null;
    }

    ServiceInstance instance = selfRoundRobinRule.instances(instances);
    return restTemplate.getForObject(instance.getUri() + "/user/lb", String.class);
}

重启服务,测试访问:http://localhost/consumer/user/lb ,可以看到 8010 与 8011 轮流调用;

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

Spring Cloud 入门 ---- Ribbon 负载均衡组件【随笔】

相关标签: spring cloud