写作时间:2020-02-16
Spring Cloud: Greenwich, Spring Boot: 2.1, JDK: 1.8, IDE: IntelliJ IDEA
说明
Spring Cloud Ribbon 是Spring Cloud Netflix 子项目的核心组件之一,主要给服务间调用及API网关转发提供负载均衡的功能,本文将对其用法进行详细介绍。
Ribbon简介
在微服务架构中,很多服务都会部署多个,其他服务去调用该服务的时候,如何保证负载均衡是个不得不去考虑的问题。负载均衡可以增加系统的可用性和扩展性,当我们使用RestTemplate来调用其他服务时,Ribbon可以很方便的实现负载均衡功能。
1. 创建Eureka Server工程
拷贝来自文章【易实战】Spring Cloud Greenwich Eureka:服务注册与发现 的项目EurekaServer。
Run Dashboard
运行完成后访问地址http://localhost:8001/可以看到Eureka注册中心的界面
2. 创建UserServiceClient工程
首先创建一个UserServiceClient,用于给Ribbon提供服务调用。
2.1 在pom.xml
中添加依赖
<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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
2.2 在application.yml
进行配置
server:
port: 8201
spring:
application:
name: user-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8001/eureka/
2.3 添加User
类
package com.zgpeace.cloud.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
@AllArgsConstructor
public class User {
private Long id;
private String userName;
private String password;
}
2.4 添加CommonResult
类
package com.zgpeace.cloud.domain;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class CommonResult<T> {
private T data;
private String message;
private Integer code;
public CommonResult(T data, String message, Integer code) {
this.data = data;
this.message = message;
this.code = code;
}
public CommonResult(String message, Integer code) {
this(null, message, code);
}
public CommonResult(T data) {
this(data, "操作成功", 200);
}
}
2.5 添加UserService
接口
package com.zgpeace.cloud.service;
import com.zgpeace.cloud.domain.User;
import java.util.List;
public interface UserService {
void create(User user);
User getUser(Long id);
void update(User user);
void delete(Long id);
User getByUserName(String userName);
List<User> getUserByIds(List<Long> ids);
}
2.6 添加service
实现类UserServiceImpl
package com.zgpeace.cloud.service.impl;
import com.zgpeace.cloud.domain.User;
import com.zgpeace.cloud.service.UserService;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
private List<User> userList;
@Override
public void create(User user) {
userList.add(user);
}
@Override
public User getUser(Long id) {
List<User> findUserList = userList.stream()
.filter(userItem -> userItem.getId().equals(id))
.collect(Collectors.toList());
if (!CollectionUtils.isEmpty(findUserList)) {
return findUserList.get(0);
}
return null;
}
@Override
public void update(User user) {
userList.stream().filter(userItem -> userItem.getId().equals(user.getId()))
.forEach(userItem -> {
userItem.setUserName(user.getUserName());
userItem.setPassword(user.getPassword());
});
}
@Override
public void delete(Long id) {
User user = getUser(id);
if (user != null) {
userList.remove(user);
}
}
@Override
public User getByUserName(String userName) {
List<User> findUserList = userList.stream().filter(userItem -> userItem.getUserName().equals(userName))
.collect(Collectors.toList());
if (!CollectionUtils.isEmpty(findUserList)) {
return findUserList.get(0);
}
return null;
}
@Override
public List<User> getUserByIds(List<Long> ids) {
return userList.stream().filter(userItem -> ids.contains(userItem.getId()))
.collect(Collectors.toList());
}
@PostConstruct
public void initData() {
userList = new ArrayList<>();
userList.add(new User(1L, "zgpeace", "123456"));
userList.add(new User(2L, "John", "123456"));
userList.add(new User(3L, "Gary", "123456"));
}
}
2.7 添加UserController
用于提供调用接口
package com.zgpeace.cloud.controller;
import com.zgpeace.cloud.domain.CommonResult;
import com.zgpeace.cloud.domain.User;
import com.zgpeace.cloud.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/user")
public class UserController {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
@Autowired
private UserService userService;
@PostMapping("/create")
public CommonResult create(@RequestBody User user) {
userService.create(user);
return new CommonResult("操作成功", 200);
}
@GetMapping("/{id}")
public CommonResult<User> getUser(@PathVariable Long id) {
User user = userService.getUser(id);
LOGGER.info("根据id获取用户信息,用户名称为:{}", user.getUserName());
return new CommonResult<>(user);
}
@GetMapping("/getUserByIds")
public CommonResult<List<User>> getUserByIds(@RequestParam List<Long> ids) {
List<User> userList = userService.getUserByIds(ids);
LOGGER.info("根据ids获取用户信息,用户列表为:{}", userList);
return new CommonResult<>(userList);
}
@GetMapping("/getByUserName")
public CommonResult<User> getByUserName(@RequestParam String userName) {
User user = userService.getByUserName(userName);
return new CommonResult<>(user);
}
@PostMapping("/update")
public CommonResult update(@RequestBody User user) {
userService.update(user);
return new CommonResult("更新成功", 200);
}
@PostMapping("/delete/{id}")
public CommonResult delete(@PathVariable Long id) {
userService.delete(id);
return new CommonResult("删除成功", 200);
}
}
2. 8 Run Dashboard
运行完成后访问地址http://localhost:8001/可以看到Eureka注册中心的界面, 多了一个client,端口为8201
2.9 访问数据请求链接http://192.168.0.106:8201/user/1
% curl http://192.168.0.106:8201/user/1
{"data":{"id":1,"userName":"zgpeace","password":"123456"},"message":"操作成功","code":200}%
2.10 启动另一个UserServiceClient
于8202端口,可以通过修改IDEA中的SpringBoot的启动配置实现
3. 创建一个RibbonClient工程
创建一个RibbonClient模块来调用UserServiceClient模块演示负载均衡的服务调用。
3.1 在pom.xml
中添加相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
3.2 在application.yml
进行配置
server:
port: 8301
spring:
application:
name: ribbon-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8001/eureka/
service-url:
user-service: http://user-service
3.3 使用@LoadBalanced
注解赋予RestTemplate
负载均衡的能力
可以看出使用Ribbon的负载均衡功能非常简单,和直接使用RestTemplate没什么两样,只需给RestTemplate添加一个@LoadBalanced即可。
package com.zgpeace.cloud.config;
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;
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
3.4 拷贝UserServiceClient
中domain package
以及下面的类
com.zgpeace.cloud.domain.CommonResult
com.zgpeace.cloud.domain.User
3.5 添加UserRibbonController
类
注入RestTemplate,使用其调用UserServiceClient中提供的相关接口,这里对GET和POST调用进行了演示,其他方法调用均可参考。
package com.zgpeace.cloud.config;
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;
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
4. 负载均衡功能演示
4.1 Run Dashboard
启动服务如下:
- eureka-server于8001端口;
- UserServiceClient于8201端口;
- 另一个UserServiceClient于8202端口
- RibbonClient于8301端口
此时运行中的服务如下:
4.2 调用接口进行测试:http://172.20.10.3:8301/user/1. 注意:这里必须要用注册进来的ip地址,localhost访问不了
% curl http://172.20.10.3:8301/user/1
{"data":{"id":1,"userName":"zgpeace","password":"123456"},"message":"操作成功","code":200}%
4.3 可以发现运行在8201和8202的UserServiceClient控制台交替打印如下信息:
5. Ribbon的常用配置
5.1 全局配置
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
5.2 指定服务进行配置
与全局配置的区别就是
Ribbon
节点挂在服务名称下面,如下是对RibbonClient
调用UserServiceClient
时的单独配置。
user-service:
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
6. Ribbon的负载均衡策略
所谓的负载均衡策略,就是当A服务调用B服务时,此时B服务有多个实例,这时A服务以何种方式来选择调用的B实例,ribbon可以选择以下几种负载均衡策略。
com.netflix.loadbalancer.RandomRule
:从提供服务的实例中以随机的方式;com.netflix.loadbalancer.RoundRobinRule
:以线性轮询的方式,就是维护一个计数器,从提供服务的实例中按顺序选取,第一次选第一个,第二次选第二个,以此类推,到最后一个以后再从头来过;com.netflix.loadbalancer.RetryRule
:在RoundRobinRule的基础上添加重试机制,即在指定的重试时间内,反复使用线性轮询策略来选择可用实例;com.netflix.loadbalancer.WeightedResponseTimeRule
:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择;com.netflix.loadbalancer.BestAvailableRule
:选择并发较小的实例;com.netflix.loadbalancer.AvailabilityFilteringRule
:先过滤掉故障实例,再选择并发较小的实例;com.netflix.loadbalancer.ZoneAwareLoadBalancer
:采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。
代码下载
https://github.com/zgpeace/SpringCloudGreenwich
参考
https://juejin.im/post/5d7f9006f265da03951a260c