51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Cloud Alibaba 系列之 Ribbon(补充)

一、前言 {#一、前言}

Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡器,它本身不属于 Spring Cloud Alibaba 提供的组件,而是 Spring Cloud 将其封装成 starter 供微服务使用。另外,笔者在之前的 文章 中也做过 Ribbon 相关的知识介绍,故本篇章只作为对 Ribbon 内容的补充。

二、RestTemplete 介绍 {#二、RestTemplete-介绍}

2.1 RestTemplete 请求模板 {#2.1-RestTemplete-请求模板}

Spring Cloud 底层对 Ribbon 做了二次封装,可以让我们使用 RestTemplate 的服务请求,同时搭配 @LoadBalanced 注解使用,从而实现客户端负载均衡的服务调用。

RestTemplate 提供两种方法 getForObjectgetForEntity 去请求调用服务端的数据,接下来笔者将介绍 RestTemplate 基于 REST 的常用的 2 种请求方式。

2.1.1 GET 请求 {#2.1.1-GET-请求}

无参情况:

|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | ## 第一个参数:微服务接口地址,第二个参数:返回值类型 ResponseEntity<User> responseEntity = restTemplate.getForEntity("xxx", User.class); User user = responseEntity.getBody(); HttpStatus statusCode = responseEntity.getStatusCode(); int statusCodeValue = responseEntity.getStatusCodeValue(); HttpHeaders headers = responseEntity.getHeaders(); |

有参情况:

|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | String[] paramArr = {"1000", "张三"}; ## 第一个参数:服务接口地址,第二个参数:返回值类型,第三个参数:入参数组 ResponseEntity<User> responseEntity = restTemplate.getForEntity("xxx?id={0}&name={1}", User.class, paramArr); Map<String, Object> paramMap = new HashMap<>(); paramMap.put("id", 1000); paramMap.put("name", "张三"); ## 第一个参数:服务接口地址,第二个参数:返回值类型,第三个参数:入参 Map ResponseEntity<User> responseEntity = restTemplate.getForEntity("xxx?id={id}&name={name}", User.class, paramMap); |

2.1.2 POST 请求 {#2.1.2-POST-请求}

方式一:使用 Map 传参

|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | # 注意,使用的是 MultiValueMap 类型 MultiValueMap<String, Object> dataMap = new LinkedMultiValueMap<>(); dataMap.add("id", "1000"); dataMap.add("name", "张三"); # 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型 ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx", dataMap, User.class); |

注意:如果使用上边的方式传参,接口提供方使用 @RequestParam("id") Integer id 形式接收参数。

方式二:使用实体传参

|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | User user = new User(); user.setId("1000"); user.setName("张三"); # 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型 ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx", user, User.class); |

注意:如果使用上边的方式传参,接口提供方使用 @RequestBody User user 形式接收参数。

方式三:使用 JSON 传参

|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | String userJson = "{\"id\": 4, \"name\": \"张三\"}"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<String> entity = new HttpEntity<>(userJson, headers); # 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型 ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx", entity, User.class); |

注意:如果使用上边的方式传参,接口提供方使用 @RequestBody User user 形式接收参数。

方式四:已封装参数,URL 仍需添加额外参数

|-----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | String token = "abc123"; # 第一个参数:服务接口地址,第二个参数:入参,第三个参数:返回值类型,第四个参数:配对 url 后边的参数 ResponseEntity<User> responseEntity = restTemplate.postForEntity("xxx?token={token}", user, User.class, token); |

2.2 负载均衡 {#2.2-负载均衡}

要使用 Ribbon 的负载均衡,只需要在 RestTemplateBean 上添加 @LoadBalanced 即可,如下:

使用 Ribbon 需要添加依赖:

|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.2.RELEASE</version> </dependency> |

|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 | @Configuration public class RestConfiguration { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } } |

2.2.1 常规负载均衡策略 {#2.2.1-常规负载均衡策略}

Ribbon 的负载均衡策略是由 IRule 接口定义, 该接口有如下实现:

从图中可知,Ribbon 已经默认实现了多种负载均衡策略,我们可以根据自己的实际情况替换这些策略:

| 负载均衡实现类 | 策略 | |---------------------------|---------------------------------------------------------------------------------------------------------------------| | RandomRule | 随机 | | RoundRobinRule | 轮询 | | AvailabilityFilteringRule | 先过滤掉由于多次访问故障的服务,以及并发连接数超过阈值的服务,然后对剩下的服务按照轮询策略进行访问 | | WeightedResponseTimeRule | 根据平均响应时间计算所有服务的权重,响应时间越快服务权重就越大被选中的概率即 越高,如果服务刚启动时统计信息不足,则使用RoundRobinRule策略,待统计信息足够会切换到该WeightedResponseTimeRule策略 | | RetryRule | 先按照RoundRobinRule策略分发,如果分发到的服务不能访问,则在指定时间内进行重 试,然后分发其他可用的服务 | | BestAvailableRule | 先过滤掉由于多次访问故障的服务,然后选择一个并发量最小的服务 | | ZoneAvoidanceRule (默认) | 综合判断服务节点所在区域的性能和服务节点的可用性,来决定选择哪个服务 |

如果想替换默认的负载均衡策略,操作非常简单,只需重新创建 IRule 接口的实现即可,我们拿 RoundRobinRule 举例:

|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Configuration public class RestConfiguration { @Bean @LoadBalanced public RestTemplate getRestTemplate() { return new RestTemplate(); } @Bean public IRule testRule() { return new RoundRobinRule(); } } |

这样,启动项目后,Ribbon 就会使用该策略进行接口调用了。

2.2.2 细粒度策略配置 {#2.2.2-细粒度策略配置}

假设现有用户微服务和订单微服务,我们希望用户微服务依然使用默认的轮询策略,订单微服务使用随机策略,应该怎么处理呢?

很简单,共有两种方式进行处理(修改订单微服务):

方式一:文件配置(推荐,配置简单且优先级高)

|---------------|-------------------------------------------------------------------------------------------------| | 1 2 3 | order-service: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule |

其中,order-service 为订单微服务的应用名。

方式二:java 配置

|-----------------|----------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | @RibbonClient(name = "order-service", configuration = RibbonConfiguration.class) public class OrderServiceRibbonRule { } |

|-----------------------|--------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | @Configuration public class RibbonConfiguration { @Bean public IRule ribbonRule() { return new RandomRule(); } } |

注意:RibbonConfiguration 类必须放在启动类所在的包之外,否则 Spring 父子容器都扫描 RibbonConfiguration 类无法实现细粒度配置的效果。

2.2.3 自定义负载均衡策略 {#2.2.3-自定义负载均衡策略}

如果上述的策略不满足自身要求,我们还可以自定义负载均衡策略,需要操作 2 个步骤:

第一步,实现 AbstractLoadBalancerRule 接口,例如:

|------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | public class MyNacosRule extends AbstractLoadBalancerRule { @Override public void initWithNiwsConfig(IClientConfig clientConfig) { //基本上不需要实现 } @Override public Server choose(Object key) { //实现该方法 } } |

第二步,配置文件(application.properties 或 application.yml)配置负载均衡策略:

|---------------|--------------------------------------------------------------------------------| | 1 2 3 | xxx: ribbon: NFLoadBalancerRuleClassName: com.light.ribbon.MyNacosRule |

其中,xxx表示远程服务的名称。

三、OpenFeign 补充 {#三、OpenFeign-补充}

先前也介绍过 Feign 的使用,至 Spring Cloud F 及F版本以上 Spring Boot 2.0 以上基本上使用 OpenFeign,本小节作为补充内容讲解。

3.1 区别 {#3.1-区别}

  • Feign : Spring Cloud 组件中的一个轻量级 Restful 的 HTTP 服务客户端,Feign 内置了 Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。
  • OpenFeignSpring CloudFeign 的基础上支持了 SpringMVC 的注解,如 @RequestMapping 等等。OpenFeign 的 @FeignClient 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

3.2 实战演练 {#3.2-实战演练}

| 项目名称 | 端口 | 描述 | |---------------|------|-----------------------------------| | feign-test | - | pom 项目,父工厂 | | feign-common | - | jar 项目,通用 api 项目,包含 model,feign | | user-service | 9001 | 用户微服务,依赖 feign-common,服务注册到 nacos | | order-service | 9002 | 订单微服务,依赖 feign-common,服务注册到 nacos |

不熟悉 Nacos 的读者可以先打开 传送门 浏览相关文章。

测试流程:请求订单接口返回订单信息和订单关联的用户信息。其调用链:order-service -> user-service

现在开始搭建项目:

  1. feign-test 为父工程,pom 项目,只负责配置依赖:

|---------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent> <dependencyManagement> <dependencies> <!-- spring cloud 依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR3</version> <type>pom</type> <scope>import</scope> </dependency> <!-- spring cloud alibaba 依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.1.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> |

  1. feign-common 为通用工程,管理 model 和 feign 的 API。

依赖:

|---------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> |

OpenFeign 类:

|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | @FeignClient(value="user-service") public interface UserServiceFeign { @RequestMapping("/user/findById/{id}") public User findById(@PathVariable("id") Integer id); } |

注意:@FeignClient 注解中的 values 对应用户微服务的应用名, UserServiceFeign 中声明的接口与用户微服务公开的接口保持一致

  1. user-service 为用户微服务,提供用户相关接口, 依赖 feign-common 项目:

配置文件:

|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | server: port: 9001 spring: application: name: user-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: nacos |

接口:

|---------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @RestController @RequestMapping("/user") public class UserController { private static Map<Integer, User> userMap; static { userMap = new HashMap<>(); userMap.put(1, new User(1, "张三")); userMap.put(2, new User(2, "李四")); userMap.put(3, new User(3, "王五")); } @RequestMapping("/findById/{id}") public User findById(@PathVariable("id") Integer id) { // 为了测试方便,用此方式模拟用户查询 return userMap.get(id); } } |

在用户微服务启动类上添加 @EnableFeignClients(basePackages={"com.light.feign"}) 注解。

  1. order-service 为订单微服务,提供订单相关接口, 依赖 feign-common 项目:

配置文件:

|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 | server: port: 9002 spring: application: name: order-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: nacos |

接口:

|------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | @RestController @RequestMapping("/order") public class OrderController { @Autowired private UserServiceFeign userServiceFeign; // @Autowired // private RestTemplate restTemplate; private static Map<Integer, Order> orderMap; static { orderMap = new HashMap<>(); orderMap.put(1, new Order(1, 1, 10.0)); orderMap.put(2, new Order(2, 2, 20.0)); orderMap.put(3, new Order(3, 3, 30.0)); } @RequestMapping("/getOrderInfo/{orderId}") public Map<String, Object> getOrderInfo(@PathVariable Integer orderId) { Map<String, Object> result = new HashMap<>(); // 模拟数据库查询 Order order = this.orderMap.get(orderId); if (order != null) { Integer userId = order.getUserId(); // http://服务提供者的应用名称/接口名称/参数 // ResponseEntity<User> userEntity = this.restTemplate.getForEntity("http://user-service/user/findById/" + userId, User.class); // User user = userEntity.getBody(); // 使用 openfeign 请求用户接口 User user = this.userServiceFeign.findById(userId); // 订单信息 result.put("order", order); // 用户信息 result.put("user", user); } return result; } } |

在订单微服务启动类上添加 @EnableFeignClients(basePackages={"com.light.feign"}) 注解。

依次启动 Nacos 服务,用户微服务,订单微服务。打开浏览器访问: http://localhost:9002/order/getOrderInfo/1 ,结果如下图:

成功返回数据,OpenFeign 整合成功。

3.3 性能优化 {#3.3-性能优化}

OpenFeign 底层默认使用 URLConnection 进行连接请求,性能仅为 RestTemplate 的 50%。为了更高效的请求,我们需要修改底层请求组件,改用 HttpClient,性能可提高 15%。

只需如下 2 步操作:

  1. 添加依赖:

|-----------------|---------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 | <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency> |

  1. 修改配置文件:

|-------------------|-----------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 | feign: httpclient: enabled: true max-connections: 200 # 最大连接数 max-connections-per-route: 50 # 单个路径最大连接数 |

赞(0)
未经允许不得转载:工具盒子 » Spring Cloud Alibaba 系列之 Ribbon(补充)