微服务-远程调用
前置
一、微服务与单体架构
1.1 单体架构
定义:一个项目所有功能模块都在一个工程中开发,项目的部署和开发比较简单
缺点:
- 团队协作成本高
- 系统的发布效率差:一个项目的任意一个模块的bug都会导致整个项目需要重新编译部署
- 系统可用性差:各模块之间会互相影响,一个热点的功能会耗尽大部分的服务器资源,导致其他功能模块服务低可用
1.2 微服务
- 定义:将一个项目中的不同模块拆分出来,独立部署为多个服务。
- 特点:
- 单一职责:一个微服务负责一部分业务功能,核心模块不依赖其他模块
- 团队自治:每个微服务有自己的一整套开发、测试、运维等人员
- 服务自治:每个服务独立打包、部署,访问自己独立的数据库。
1.3 SpringCloud
SpringCloud是一个标准规范,其并不提供具体的服务,而SpringCloudAlibaba是一个满足SpringCloud规范的微服务框架的具体实现,它提供了具体的解决方案。两者关系类似于接口和实现类
使用方法
在父工程里面引入SpringCloud和SpringCloudAlibaba的依赖管理,这样有关这两个依赖的版本就无需配置了
1 | <properties> |
二、服务拆分和调用
当一个项目是微服务架构时,难免会出现一个服务调用另一个服务功能的情况,那么此时就需要跨微服务的远程调用(RPC)
此时由于微服务不在同一个模块中,并且每个微服务独立访问自己的数据库,服务之间不能直接通过对象调用,那么就需要http请求来调用服务接口,Spring提供了一个RestTemplate的API可以方便的发送http请求。
2.1 RestTemplate
使用方法
- 步骤一:创建一个config类,将
RestTemplate
注册为一个bean1
2
3
4
5
6
7
8
public class RemoteCallConfig {
public RestTemplate restTemplate() {
return new RestTemplate();
}
} - 步骤二:使用
RestTemplate
提供的exchange方法向其他服务发送http请求1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {},
Map.of("ids", CollUtil.join(itemIds, ","))
);
// 解析响应
if(!response.getStatusCode().is2xxSuccessful()){
// 查询失败,直接结束
return;
}
List<ItemDTO> items = response.getBody();
if (CollUtils.isEmpty(items)) {
return;
}
三、服务注册和发现
上一小节使用restTemplate实现了对其他服务的远程调用,但是在代码中将发送http请求的IP和端口写“死”了,这样后面对服务进行多机部署时,只能访问其中一台服务器,没有进行负载均衡。
3.1 Nacos基本情况
为了解决上述问题,需要一个能够管理所有所有服务的中间件,而Nacos就是这样一个中间件,它可以管理所有对外提供服务接口的服务器地址,并将服务推送到需要的服务中。
基本情况
- 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
- 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
- 调用者自己对实例列表负载均衡,挑选一个实例
- 调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
3.2 Nacos部署
这里是用的是docker容器部署,Nacos部署需要一个使用到mysql数据库,同时有一个nacos/custom.env
文件,里面包含mysql的地址信息,下面是mysql的表信息内容以及配置文件内容 (注:Nacos要在mysql之后启动,否则会连接不上nacos)
1 | PREFER_HOST_MODE=hostname |
随后使用docker进行部署
1 | docker run -d \ |
3.3 服务注册
nacos部署完成后,可以通过8848
端口访问测试,若成功部署,接下来就是在java中使用nacos提供的服务。
首先是服务注册,若一个服务需要对外提供服务接口,那么就需要在Nacos中注册,分为以下两步:
- 步骤一:引入Nacos依赖
1
2
3
4
5<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> - 步骤二:配置Nacos
1
2
3
4
5
6spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
3.4 服务发现
服务的消费者需要订阅服务,此时有以下三步:
- 步骤一:引入Nacos依,与服务注册依赖一致
1
2
3
4
5<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> - 步骤二:配置Nacos地址,服务发现不依赖服务名称,但是一般来说也会配置服务名称
1
2
3
4spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848 - 步骤三:使用SpringCloud提供的
DiscoveryClient
发现服务,随后进行负载均衡,再调用restTemplate
进行发起http请求。这里与直接使用restTemplate不同的地方是,首先发现所有的服务实例,随后获取一个实例,最后通过实例获取服务的ip地址和端口号1
2
3
4
5
6
7
8
9
10
11
12
13
14// 2.1 发现服务的所有实例
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
// 2.2 使用负载均衡算法选择一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
// 2.3 向商品管理微服务发起http请求
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
instance.getUri() + "/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {},
Map.of("ids", CollUtil.join(itemIds, ","))
);
四、OpenFeign
上一小节使用了Nacos实现了服务治理,也就是其中管理所有提供服务接口的服务地址,但是使用起来比较复杂,而OpenFeign组件可以大大简化RPC的过程。
4.1 快速入门
步骤一:创建一个新模块
hm-api
,专门用来提供所有服务接口的。创建这个模块的目的是可能多个模块都需要调用同一个服务的接口,这样可以提供一个统一的服务接口,不用每个模块都创建服务接口步骤二:需要引入两个依赖,一个是OpenFeign,另一个是负载均衡依赖
1
2
3
4
5
6
7
8
9
10<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>步骤三:在使用服务接口的启动类上添加注解,启用OpenFeign功能,其中basePackages的值是统一服务接口的包名
1
步骤四:编写OpenFeign客户端,在
hm-api
模块中新建client包,创建如下接口(示例)
该接口指明了服务提供者的服务名称
对应的方法上有请求路径和参数以及返回值1
2
3
4
5
6
public interface ItemClient {
List<ItemDTO> queryItemByIds(; Collection<Long> ids)
}步骤五:服务消费者引入
hm-api
依赖1
2
3
4
5
6<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>步骤六:服务消费者创建
ItemClient
对象,并通过该对象调用在hm-api
中声明的方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {
private final ItemClient itemClient;
private void handleCartItems(List<CartVO> vos) {
List<ItemDTO> items = itemClient.queryItemsByIds(itemIds);
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
}
4.2 配置连接池
OpenFeign发起http请求时使用的http客户端基于HttpURLConnection
,该http客户端不支持连接池,效率较低,因此配置为支持连接池的OKHttp
步骤一:在服务消费者中引入OKHttp客户端的依赖
1
2
3
4
5<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>步骤二:配置连接池生效
1
2
3feign:
okhttp:
enabled: true # 开启OKHttp功能