一、前言 {#一、前言}
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 提供两种方法 getForObject 和 getForEntity 去请求调用服务端的数据,接下来笔者将介绍 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 的负载均衡,只需要在 RestTemplate 的 Bean 上添加 @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,用来做客户端负载均衡,去调用服务注册中心的服务。
- OpenFeign :Spring Cloud 在 Feign 的基础上支持了 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
。
现在开始搭建项目:
- 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>
|
- 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 中声明的接口与用户微服务公开的接口保持一致。
- 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"}) 注解。
- 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 2 3 4
| <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
|
- 修改配置文件:
|-------------------|-----------------------------------------------------------------------------------------------------------------|
| 1 2 3 4 5
| feign: httpclient: enabled: true max-connections: 200 # 最大连接数 max-connections-per-route: 50 # 单个路径最大连接数
|