微服务组件 -- 服务调用

服务调用

SpringCloud 中服务间调用方式主要是使用 http restful方式进行服务间调用。

所谓的服务键调用,简单来说,就是我可以调用其他模块的接口。

比如:

在服务A中有一个接口。

1
2
3
4
5
6
7
8
@RestController
public AController{

@GetMapping("/msga")
public String GetAMeg(){
return "AController";
}
}

服务B想要直接调用这个接口。需要通过什么方式或什么组件可以调用这个功能。

一、负载均衡

服务间调用还需要考虑一个问题:负载均衡

负载均衡在系统架构中是非常重要的。因为负载均衡对系统的高可用,网络压力的缓解和处理能力扩容的重要手段之一。

1、客户端负载均衡

在客户端负载均衡中,客户端知道所有服务端的详细信息,当需要调用服务端上的接口时,客户端从服务端列表中,根据负载均衡策略,自己挑选一个服务端来调用,此时客户端知道自己调用的是哪一个服务端。

2、负载均衡策略

  • 轮询:就是将客户端的请求轮流分配给服务器,是最常用的负载均衡策略。

  • 随机:随机挑选一个服务端来调用。保证请求的分散性达到了均衡。

  • 加权:根据服务端的权重来分配客户端的请求。

二、基于 RestTemplate 服务调用

​ spring框架提供的RestTemplate类可用于在应用中调用rest服务,我们只需要传入url及返回值类型即可。

1、环境搭建

新建两个client实例,注册到consul中。
1
2
服务A Client-A  端口号 8801
服务B Client-B 端口号 8802

consul 注册中心

为服务A添加一个接口

我们希望在服务B中调用服务A的接口

1
2
3
4
5
6
7
@RestController
public class MsgController {
@GetMapping("/getmsg")
public String getMsg(){
return "这是服务A中的接口";
}
}

调用localhost:8801/getmsg

服务A的接口

2、RestTemplate调用

在服务B中编写Controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class ClientBController {

@GetMapping("/getmsg")
public String getMsg(){
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject("http://localhost:8801/getmsg",String.class);

return result;
}
}

服务B的接口

3、基于RestTemplate调用存在的问题

核心代码

1
restTemplate.getForObject("http://localhost:8801/getmsg",String.class);
  1. 没有经过注册中心。

    从这行代码可以看出,是直接使用的url,没有从注册中心中获取服务A地址。

  2. 没有实现负载均衡

    因此直接写上服务A的url,如果这个服务A宕机,那么程序会出现错误。

  3. 不利于维护

    直接将服务A地址写死,无法高效修改或服务剔除。

三、基于Ribbon的服务调用

1、什么是Ribbon

Ribbon是Netflixfa发布的一个负载均衡器,有助于控制HTTP和TCP客户端行为。

Ribbon提供客户端负载均衡的功能,Ribbon利用从注册中心读取的服务信息,在调用服务结点提供的服务时,会合理的进行负载。

2、开发环境配置

重新配置开发环境

创建两个服务并注册到Consul注册中心中,并希望用户服务去调用产品服务中的接口:

1
2
user服务      端口号   8881    用户服务
products服务 端口号 8882 产品服务

并且引入Ribbon依赖:

1
2
3
4
5
6
<!--引入ribbon依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>

两个服务

服务

在商品服务中实现接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class ProductsController {

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

@GetMapping("/product/findAll")
public Map<String,Object> findAll(){
Map<String,Object> map = new HashMap<>();
map.put("msg","服务调用成功,服务提供的端口号是:" + port);

return map;
}
}

服务调用成功

3、基于Ribbon服务调用

方法简述

1
2
3
1. 使用discovery client    进行调用
2. 使用loadBalanceClient 进行调用
3. 使用@loadBalanced 进行调用
3.1、使用 discovery client

核心代码:

1
discoveryClient.getInstances("服务提供者");
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
@RestController
@Slf4j
public class UserController {

@Autowired
private DiscoveryClient discoveryClient;

//选择一个服务器
private ServiceInstance getInstance(){
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("product");
//选择第一个服务器
return serviceInstances.get(0);

}

@GetMapping("/user/findAll")
public Map<String,Object> findAll(){
RestTemplate restTemplate = new RestTemplate();

HashMap<String,Object> map = new HashMap<>();
String url = "http://" + getInstance().getHost() + ":" + getInstance().getPort() + "/product/findAll";
map = restTemplate.getForObject(url, HashMap.class);

return map;
}
}

在用户服务中调用生产服务

3.2、Load Balance Client

通过DiscoveryClient可以从注册中心中将所有的服务提供者列举出来。

我们从中选择一个服务提供者,为服务的消费者提供服务。

DiscoveryClient中,我们并没有添加负载均衡。只是简单的从提供者中选择了一项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@Slf4j
public class UserController {

@Autowired
private LoadBalancerClient loadBalancerClient;

@GetMapping("/user/findAll")
public Map<String,Object> findAll(){
RestTemplate restTemplate = new RestTemplate();

HashMap<String,Object> map = new HashMap<>();

ServiceInstance product = loadBalancerClient.choose("product");
String url = "http://" + product.getHost() + ":" + product.getPort() + "/product/findAll";
log.info("==============" + url);
map = restTemplate.getForObject(url, HashMap.class);

return map;
}
}

调用成功

3.3、@LoadBalance
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class UserConfig {

//整合restTemplate + ribbon

//在工厂中创建一个restTemplate
//添加loadBalanced注解 代表ribbon负载均衡的restTemplate客户端对象
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@Slf4j
public class UserController {

//服务名
private String serverName = "product";

//2.调用服务位置注入RestTemplate
@Autowired
private RestTemplate restTemplate;

@GetMapping("/user/findAll")
public Map<String,Object> findAll(){
HashMap<String,Object> map = new HashMap<>();
//3.调用
map = restTemplate.getForObject("http://"+ serverName +"/product/findAll", HashMap.class);
return map;
}
}

四、基于OpenFeign的服务调用

1、OpenFeign简介

前面的 RestTemplate + Ribbon 去进行服务间的调用,核心代码是:

1
restTemplate.getForObject(url,Class);

可以看到,我们使用的是拼接字符串的方式构造URL的。

这样直接在代码中写出你要调用的服务和资源路径,这样系统的耦合度太高了。如果资源路径修改,就必须在代码上修改。

OpenFeign是Netflflix开发的声明式,模板化的HTTP客户端。

Feign可帮助我们更加便捷,优雅的调用HTTP API。

2、OpenFeign实例

1、引入依赖

1
2
3
4
5
<!--   引入OpenFegin依赖     -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2、入口类加入注解,开启支持OpenFeign

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@FeignClient
public class User8881Application {

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

}

3、创建客户端调用接口

1
2
3
4
5
6
//通过注解指定服务名称
@FeignClient("PRODUCT")
public interface ProductClient {
@GetMapping("/product/findAll")
String findAll();
}

4、使用feignClient客户端对象调用服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@Slf4j
public class UserController {

@Autowired
private ProductClient productClient;

@GetMapping("/user/findAllByFeign")
public Map<String, Object> findAll(){
log.info("通过使用OpenFeign组件调用服务");
Map<String,Object> map = productClient.findAll();

return map;
}

}

调用成功

3、使用OpenFeign的细节问题

1、传参

服务提供者添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class ProductsController {

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

@GetMapping("/product/findOne")
public Map<String,Object> findOne(String id){
HashMap<String, Object> map = new HashMap<>();
map.put("port","服务提供者的port:" + port);
map.put("id","参数id" + id);

return map;
}
}

接口

1
2
3
4
5
6
@FeignClient("PRODUCT")
public interface ProductClient {
@GetMapping("/product/findOne")
Map<String,Object> findOne(@RequestParam("id") String id);

}

添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@Slf4j
public class UserController {
@Autowired
private ProductClient productClient;

@GetMapping("/user/findOne")
public Map<String,Object> findOne(String id){
log.info("通过使用OpenFeign组件调用服务");
Map<String,Object> map = productClient.findOne("1");
return map;
}
}

调用成功

如果传递的是对象

1
2
3
4
5
6
7
8
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Student {
private String id;
private String name;
}

Product Post请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Slf4j
@RestController
public class PostController {


@RequestMapping(value = "/product/save",method = RequestMethod.POST)
public Map<String,Object> save(@RequestBody Student student){

//log.info("============" + request.getMethod());
log.info("=====================");
HashMap<String, Object> map = new HashMap<>();

map.put("stu",student);
map.put("status",200);

return map;
}
}

在调用方

1
2
3
4
5
@FeignClient("PRODUCT")
public interface ProductClient {
@PostMapping("/product/save")
Map<String,Object> save( @RequestBody Student student);
}
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@Slf4j
public class UserController {
@PostMapping("/user/save")
public Map<String,Object> save(Student student){

log.info("==========" + student);

Map<String,Object> map = productClient.save(student);
return map;
}
}

调用成功

2、超时设置

默认情况下,OpenFeign在进行服务调用时,要求服务提供方处理业务逻辑时间必须在1 S内返回,如果超过1 S没有返回则OpenFeign会直接报错,不会等待服务执行。

但是由于复杂的业务逻辑,可能会超过1 S,因此需要修改OpenFeign的默认服务调用超时事件。

1
2
3
4
5
6
7
8
9
10
@GetMapping("/product/findAll")
public Map<String,Object> findAll() throws InterruptedException {
//设置 暂停2s
Thread.sleep(2000);

Map<String,Object> map = new HashMap<>();
map.put("msg","服务调用成功,生产服务提供的端口号是:" + port);

return map;
}

超时

修改默认超时时间

1
2
feign.client.config.product.connectTimeout=5000  #配置指定服务连接超时
feign.client.config.product.readTimeout=5000 #配置指定服务等待超时

可能服务的调用者会同时调用多个服务,写单个服务超时时间麻烦,可以简写默认的超时时间:

1
2
feign.client.config.default.connectTimeout=5000         #配置所有服务连接超时
feign.client.config.default.readTimeout=5000 #配置所有服务等待超时
3、日志设置

默认OpenFeign在调用是并不是最详细日志输出,因此在调试程序时应该开启feign的详细日志展示。

feign对日志的处理非常灵活可为每个feign客户端指定日志记录策略,每个客户端都会创建一个logger。

我们可以为feign客户端配置各自的logger.level对象,告诉feign记录那些日志logger.lever有以下的几种值

1
2
3
4
NONE  不记录任何日志
BASIC 仅仅记录请求方法,url,响应状态代码及执行时间
HEADERS 记录Basic级别的基础上,记录请求和响应的header
FULL 记录请求和响应的headerbody和元数据

开始日志配置

1
2
3
4
feign.client.config.product.loggerLevel=full   #开启指定服务日志展示
#feign.client.config.default.loggerLevel=full #全局开启服务日志展示

logging.level.com.jiang.feignclients=debug #指定feign调用客户端对象所在包,必须是debug级别

微服务组件 -- 服务调用
https://johnjoyjzw.github.io/2021/07/12/微服务组件-服务调用/
Author
John Joy
Posted on
July 12, 2021
Licensed under