一、RPC和HTTP
应用服务间通信调用的方式主要有两种,一种是HTTP,另一种是RPC。
RPC形式的常见代表是Dubbo。
Dubbo的定位就是一款RPC服务调用框架,基于Dubbo开发的应用还是要依赖周边的平台和生态,比如配合以Zookeeper实现的服务注册中心一起使用。Dubbo不仅提供了服务注册和发现、负载均衡等面向分布式系统的基础能力,还提供了开发测试阶段的Mock机制。在SpringCloud流行之前,Dubbo应用的十分广泛。
HTTP形式的常见代表是SpringCloud。
Dubbo自身的定位就只是一款RPC服务调用框架,而SpringCloud的目标是微服务下的一站式解决方案。
SpringCloud中服务调用采用的是http的restful方式,http-restful方式的特点是轻量,易用,便于跨语言跨平台。
SpringCloud中有两种restful调用方式:RestTemplate、Feign。
在比对完RPC和HTTP之后,我将主要实践SpringCloud中的服务调用。
二、基于RestTemplate的服务调用
RestTemplate是一款Http客户端,功能和HttpClient类似,但RestTemplate用法更简单。
场景:现在系统中有Product服务和Order服务,我需要在Order服务中去调用Product服务的接口。
Product服务(被调用方)
在product服务中定义了一个controller,其中提供了一个获取product信息的接口:
/**
* @Auther: jesses
* @Description: product服务中的controller
*/
@RestController
public class ProductController {
@GetMapping("getMsg")
public String getMsg(){
return "this is product's message";
}
}
Order服务(调用方)
使用RestTemplate方式调用上面的product接口,有三种实现方式。
- 方式一、直接new RestTemplate()调用:
/**
* @Auther: jesses
* @Description: order服务中的controller
*/
@RestController
@Slf4j
public class ClientController {
@GetMapping("/getProductMsg")
public String getProductMsg() {
//第一种方式
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject("http://127.0.0.1:9080/getMsg", String.class);
log.info("response={}", response);
return response;
}
}
可以看到这种方式是写死Url,如果是在生产环境,别人的product服务部署到的IP地址,调用方却未必知道。
不知道服务地址,如何调用?这种方式肯定是存在缺陷的。
而且可能product被部署了多台实例,做了集群。我们肯定是想调用其中某一台就够了,这就涉及到需要实现负载均衡。
- 方式二、利用LoadBalancerClient获取服务实例
这次我启动了两台product实例。port分别是9080、9081。
可以看到,在product服务中,配置了服务名为product:
SpringCloud提供了LoadBalancerClient,通过服务名来获取product服务的实例对象,从而获得IP和PORT:
/**
* @Auther: jesses
* @Description: order服务中的controller
*/
@RestController
@Slf4j
public class ClientController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/getProductMsg")
public String getProductMsg() {
ServiceInstance serviceInstance = loadBalancerClient.choose("PRODUCT");
String url = String.format("http://%s:%s%s",
serviceInstance.getHost(),
serviceInstance.getPort(),
"/getMsg");
return new RestTemplate().getForObject(url, String.class);
}
}
- 方式三、注解配置RestTemplate
在order服务中定义一个配置类,用于配置RestTemplate,使用@LoadBalanced注解配置负载均衡器,将其注册进spring容器。
/**
* @Auther: jesses
* @Description: 配置restTemplate,使用注解配置LoadBalanced
*/
@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
直接注入配置了的 restTemplate,将调用的ip和port替换为服务名:
/**
* @Auther: zhaoshuai
* @Date: 2020/5/4
* @Description: order服务中的controller
*/
@RestController
@Slf4j
public class ClientController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/getProductMsg")
public String getProductMsg() {
return restTemplate.getForObject("http://PRODUCT/getMsg", String.class);
}
}
第三种方式只是使用注解简化了第二种,实际上实现机制是一样的。
这三种方式归根结底还是基于RestTemplate的服务调用。
三、基于feign组件的服务调用
3.1 基于Ribbon实现的负载均衡
Feign是一款客户端http调用组件,属于SpringCloudNetflix的组件之一,而Feign是依赖于Ribbon组件的,
所以先来了解Ribbon及其负载均衡策略。
Ribbon实现负载均衡的核心要素有三点:
- 服务发现:根据服务名,找到该服务的所有实例
- 负载均衡策略:根据负载均衡策略,从多个服务实例中选择一个有效的服务
- 服务监听:检测失效的服务,做到高效剔除。
概括Ribbon实现负载均衡的流程,大致是先通过ServerList获取所有可用服务列表,之后通过ServerListFilter过滤掉一部分,最后通过IRule选择一个目标实例。
接下来查看部分源码看Ribbon的具体实现:
3.1.1 Ribbon的服务发现:
之前在RestTemplate调用服务的第二种方式用到了loadBalancerClient.choose("PRODUCT") 这个API,现在查看这个api的实现。
LoadBalancerClient继承了ServiceInstanceChooser接口,
choose()方法的实现来自于LoadBalancerClient的实现类RibbonLoadBalancerClient:
在choose()实现中调用了getServer(),继续进入getServer()内查看:
到getServer()方法的底层可以发现调用了chooseServer()这个API,再次进入查看:
可以发现获取所有服务列表的api方法List<Server> getAllServers();就定义在接口ILoadBalancer中。
查看getAllServers()的实现,在此施加断点。
调用第二种restTemplate方式的controller接口,进入断点后发现的确返回了product服务的实例列表:
3.1.2 Ribbon的负载均衡策略
再查看ILoadBalancer接口中的另一个api,即chooseServer(),查看其在BaseLoadBalancer实现类中的方法实现:
可以看到,其中是通过一个rule对象调用choose()方法的,
在BaseLoadBalancer构造器中对rule对象进行了初始化,赋予其一个默认的轮询规则RoundRobinRule:
既然默认的负载均衡策略是轮询,如果想要配置其他规则,如何修改?
3.2 基于Feign实现的服务调用
第一步,在调用方服务引入依赖:
要使用feign,需要先引入其依赖spring-cloud-starter-openfeign。
需要注意的是,在较低版本的springcloud中,使用的是artifactId为spring-cloud-starter-feign的依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.0.0.M3</version>
</dependency>
第二步,在调用方服务的启动类上配置开启feign客户端,@EnableFeignClient:
第三步,定义feign接口:
例如,在被调用方product服务的controller中,我提供了两个接口,查询商品列表接口、扣库存接口。
/**
* @Auther: jesses
* @Description: product服务controller
*/
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
/**根据商品ids查询商品列表*/
@PostMapping("/listForOrder")
public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList){
return productService.findList(productIdList);
}
/**减库存*/
@PostMapping("/decreaseStock")
public void decreaseStock(@RequestBody List<CartDTO> cartDTOList){
productService.decreaseStock(cartDTOList);
}
}
现在我需要在order服务调用product服务的这两个接口。
在order服务中定义feign客户端及接口:
1.定义一个interface用作feign客户端定义
2.使用@FeignClient(name="${application name of Called App}")注解标注。注解的name属性为被调用方服务名。
3.接口的@RequestMapping中method和value值,必须和被调用方服务定义的一致。
4.feign接口与被调用方服务匹配的要素只在于@FeignClient中name属性值,以及@RequestMapping中的value和method,
方法名可以不同,不影响使用。
/**
* @Auther: jesses
* @Description: 调用product服务的feign客户端
*/
@FeignClient(name = "product")
public interface ProductClient {
@PostMapping("/product/listForOrder")
List<ProductInfo> listForOrder(@RequestBody List<String> productIdList);
@PostMapping("/product/decreaseStock")
void decreaseStock(@RequestBody List<CartDTO> cartDTOList);
}
之后在order服务的controller使用@Autowired注入ProductClient的Bean。直接调用api即可:
Feign本质上是一款http客户端,基于feign做服务调用,开发体验如通调用本地方法一样,感知不到是在调用远程接口。
Feign内部也使用了Ribbon实现了服务调用的负载均衡。