目录
前言
一、纯 RestTemplate 方案存在的缺陷
二、注册中心模式介绍
三、注册中心技术:Nacos
3.1 Docker部署Nacos
3.2 服务注册
3.3 服务发现
四、代码优化:OpenFeign工具
4.1 OpenFeign快速入门
4.2 连接池的必要性
4.3 抽取服务、最佳实践
4.4 日志配置
五、服务注册与调用巩固
前言
前面通过微服务基础入门,我们大致了解的微服务的拆分。并且发现了跨微服务的请求调用问题。当时我们使用RestTemplate进行请求发送。也发现了一个比较大的问题——请求的url需要开发者人为提供,这种硬编码的方式无论是在什么项目里都应该被避免。更何况如果一个微服务分布在好几台服务器上,我们又该如何做负载均衡呢?因此本篇主要针对跨微服务的优化问题提出解决方案的学习。
一、纯 RestTemplate 方案存在的缺陷
-
item-service这么多实例,cart-service如何知道每一个实例的地址?
-
http请求要写url地址,
cart-service
服务到底该调用哪个实例呢? -
如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? -
如果并发太高,
item-service
临时多部署了N台实例,cart-service
如何知道新实例的地址?
因此,对于新方案,必须要有以下几个优势:
1. 只需关注有无实例有该功能调用?无需关注调用实例的地址
2. 拥有负载均衡策略,可以在多实例间自动进行策略切换
3. 自动监控实例健康状态,及时切断异常实例的连接
4. 允许实例动态变化,何时注册何时即可投入使用。
二、注册中心模式介绍
所谓注册中心模式可以理解为 “中介模式”,拿房屋中介来举例子吧:
房东【服务提供者】只需要把自己的房屋信息告诉(注册)中介,不需要自己去找租客。
租客【服务消费者】只需要到房屋中介处寻找(调用)自己需要的房屋,不需要满大街找房东。
而中介负责整合房屋资源(注册服务列表),同时在租客寻找的时候提供对应的房源(提供调用)
注意到我上面举例的用词了吧,在微服务中也是一样的。在微服务远程调用的过程中,包括两个角色:
服务提供者:提供接口供其它微服务访问,比如
item-service
服务消费者:调用其它微服务提供的接口,比如
cart-service
注册中心模式的整体流程如下:
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
如何实现宕机实例、异常实例的检测?
- 引入心跳检测机制【类似Redis的哨兵机制】
- 所有注册到中心的实例,每隔一段时间必须向中心发送信号,证实自己是健康状态
-
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
如何实现宕机通知、新添实例通知?
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表。
如何实现负载均衡策略?
- 成功获取到实例列表后,调用者可以根据一定的策略(随机、轮询等)挑选任意一个实例发送请求
三、注册中心技术:Nacos
目前开源的注册中心框架有很多,国内比较常见的有:
-
Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
-
Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
-
Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服务语言
其中Nacos上手快、配置简单、并且有详细的中文文档提供学习。因此本次实验采取Nacos进行。
Nacos 快速开始https://nacos.io/zh-cn/docs/quick-start.html
3.1 Docker部署Nacos
【部署步骤】
- 导入nacos数据库文件
- 修改nacos配置文件,并上传到服务器
- 执行容器创建命令
- 确保启动顺序:必须先启动数据库,再启动nacos
-
测试访问http://192.168.186.140:8848/nacos/ 账号密码均为 nacos
首先Nacos需要管理服务列表,必然是依赖数据库的。本次实验采取先前Docker布置好的MySQL。
第一步: 导入nacos数据库文件
第二步:修改nacos配置文件,并上传到服务器
第三步: 执行容器创建命令
8848 是用于客户端与服务通信的主要端口。
9848 是 gRPC 端口,用于与 Nacos 的 gRPC 通信(如果需要)。
9849 是 Raft 端口,用于 Nacos 集群中节点之间的通信(如果运行集群模式)。
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
第四步:确保启动顺序:必须先启动数据库,再启动nacos
第五步:测试访问http://192.168.186.140:8848/nacos/ 账号密码均为 nacos
3.2 服务注册
- 导入坐标依赖
- 配置nacos的访问地址和服务名称
- 使用nacos
3.2.1 导入坐标依赖
在需要注册服务的模块中,导入nacos的坐标
3.2.2 配置nacos的访问地址和服务名称
3.2.3 使用nacos
将服务注册到nacos,我们拿item-service为例,启动多个实例,模拟多服务器部署:
启动后查看nacos网站--服务列表
测试服务宕机后,nacos服务列表是否会更新:
3.3 服务发现
- 导入坐标依赖
- 配置nacos的访问地址和服务名称
- 发现并调用服务
3.3.1 导入坐标依赖
我们在cart-service
中的pom.xml
中添加nacos的依赖:
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
可以发现,这里Nacos的依赖于服务注册时一致,这个依赖中同时包含了服务注册和发现的功能。因为任何一个微服务都可以调用别人,也可以被别人调用,即可以是调用者,也可以是提供者。
因此,等一会儿cart-service
启动,同样会注册到Nacos
3.3.2 配置nacos的访问地址和服务名称
在cart-service
的application.yml
中添加nacos地址配置:
spring:
application:
name: cart-service
cloud:
nacos:
server-addr: 192.168.186.140:8848
3.3.3 发现并调用服务
到此为止,我们还需要准备负载均衡策略。以最简单的随机策略为例。
为了能够发现服务,获取实例列表,这里还需要使用SpringCloud提供的服务发现工具:DiscoveryClient
该工具被SpringCloud自动注入装配,我们只需要注入就可以使用,我们利用它修改我们原先的代码逻辑:
RestTemplase实现代码:
使用DiscoveryClient工具 + Nacos后代码
3.3.4 发现服务测试
3.3.5 完整代码
// 注入服务发现工具
@Resource
private DiscoveryClient discoveryClient;
/**
* DiscoveryClient
* @param vos
*/
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
// 2.1 发现item-service服务的请求实例
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if(CollUtils.isEmpty(instances)) {
throw new BizIllegalException("商品服务不可用");
}
//2.2 负载均衡选择一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
//2.3 构建请求、发送请求
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
instance.getUri() + "/items?ids={ids}", // 请求路径
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {},
Map.of("ids", CollUtil.join(itemIds, ","))
);
//2.4 解析响应对象
if(!response.getStatusCode().is2xxSuccessful()) {
// 查询失败
return;
}
// 2.5 获取查询商品对象
List<ItemDTO> items = response.getBody();
// 3.构建商品id与商品对象的映射
Map<Long,ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
//4. 写入VO对象返回前端
for(CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if(item == null) {
continue;
}
v.setNewPrice(item.getPrice()); // 最新价格
v.setStock(item.getStock()); // 库存
v.setStatus(item.getStatus()); // 状态
}
}
四、代码优化:OpenFeign工具
到这里我们已经解决了跨微服务请求的难题了,是不是挺简单的。确实,SpringCloud给我们提供了太多好用的工具了,使用DiscoveryClient + Nacos + RestTemplate解决了这个问题。
但是回看我们写的完整代码。是不是感觉有些复杂啊,想要构建一个简单的请求。我们先是去寻找服务,接着手写负载均衡选择实例、然后才是利用RestTemplate构建请求......
如何能够优化项目代码,减少开发者的工作量呢?这一节我们使用另一个工具——OpenFegin来解决这个问题。
4.1 OpenFeign快速入门
以cart-service中的查询我的购物车为例。因此下面的操作都是在cart-service中进行。
【使用步骤】
- 引入OpenFeign依赖 和
loadBalancer负载均衡依赖
- 启动类下添加 @EnableFeignClients 依赖 启动 OpenFeign服务
- 编写client接口,用于实现请求发送(这一步就跟你编写业务controller很像很像)
- 实现类中注入client接口
- 使用client接口中的方法发送请求
4.1.1 引入依赖
<!--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>
4.1.2 添加启动注解
在cart-service启动类下添加@EnableFeignClients 依赖 启动 OpenFeign服务
4.1.3 编写client接口
无需编写实现类,SpringCloud帮我们动态生成
4.1.4 实现类注入client接口
4.1.5 使用client定义的接口方法
你看看,现在的代码是不是简单暴了。完全不需要再手动找服务、手动完成负载均衡、手动编写restTemplate发送请求了。
/**
* OpenFeign实现
* @param vos
*/
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.查询商品
List<ItemDTO> items = itemClient.queryItemByIds(itemIds); // openfeign调用
// 3.构建商品id与商品对象的映射
Map<Long,ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
//4. 写入VO对象返回前端
for(CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if(item == null) {
continue;
}
v.setNewPrice(item.getPrice()); // 最新价格
v.setStock(item.getStock()); // 库存
v.setStatus(item.getStatus()); // 状态
}
}
4.1.6 测试代码
4.2 连接池的必要性
4.2.1 HTTP连接 与 HTTP消息
目前我们已经将代码优化得非常好了。理论上日常开发做到这块就可以了。但如如何还要想优化的话,接下来我们需要考虑的就是开销方面的问题了。我们回忆一下,现在我们每次需要发送跨端请求时,都需要先建立服务器之间的连接,然后才会去发送http消息。但是:
两台服务器建立HTTP连接的过程复杂且耗时,特别是其中的3次握手和4次分手过程产生的开销,对于传输大量较小的HTTP消息来说,这种开销显得尤为显著。
于是乎为了减少建立HTTP连接的大开销,我们需要建立HTTP连接池。
4.2.2 HTTP客户端技术选型
主要用到的HTTP客户端技术包括以下三种,其中第一种是OpenFeign默认的底层实现:
-
HttpURLConnection:默认实现,不支持连接池
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
由于HttpURLConnection不支持连接池,所有我们得更改其他的HTTP客户端技术,本次实验选取OKHttp。
【HTTP客户端技术补充说明】
HttpURLConnection
概述:
HttpURLConnection是Java标准库中的一部分,用于发送HTTP请求和接收HTTP响应。它提供了一组简单的方法来发送HTTP请求和处理响应,使开发人员能够轻松地与服务器进行通信。
特点:
- 简单易用:HttpURLConnection提供了直观的API,使得HTTP请求和响应的处理变得简单。
- 线程安全:HttpURLConnection是线程安全的,可以在多线程环境下使用,而无需额外的同步措施。
- 支持多种HTTP方法:如GET、POST、PUT、DELETE等,可以根据需要选择合适的方法进行请求。
- 支持HTTPS:可以与HTTPS服务器建立安全连接,通过SSL/TLS协议进行数据传输,确保数据的安全性。
- 跨平台:作为Java标准库的一部分,可以在各种Java平台上使用,具有良好的跨平台性。
限制:
HttpURLConnection的默认实现不支持连接池,这意味着每次发送HTTP请求时都需要建立新的连接,这可能会导致性能下降,特别是在发送大量HTTP请求的情况下。
Apache HttpClient
概述:
Apache HttpClient是Apache软件基金会的一个项目,是Java标准库之外的一个广泛使用的HTTP客户端库。它提供了丰富的功能和配置选项,可以满足各种复杂的HTTP请求场景。
特点:
- 稳定可靠:Apache HttpClient是一个成熟稳定的HTTP客户端库,拥有长期的开发历史和广泛的用户基础。
- 支持连接池:通过连接池技术,可以有效地复用已经建立的连接,减少连接建立和关闭的开销,提高性能。
- 支持HTTP/2:最新版本的Apache HttpClient支持HTTP/2协议,可以提供更高的性能和效率。
- 丰富的配置选项:提供了多种配置选项,以满足不同的HTTP请求需求。
应用:
Apache HttpClient适用于需要处理复杂HTTP请求和响应的场景,如需要设置自定义请求头、处理重定向、管理Cookies等。
OKHttp
概述:
OKHttp是一个开源的Java HTTP客户端库,由Square公司开发。它被广泛用于Android开发和Java后端开发。OKHttp提供了一个简洁的API,用于发送HTTP请求和处理HTTP响应。
特点:
- 高性能:OKHttp的底层实现基于Java的Socket和线程池,使用了连接池和请求重用机制,可以高效地处理大量的并发请求,并减少网络延迟。
- 支持连接池:与Apache HttpClient类似,OKHttp也支持连接池技术,可以复用已经建立的连接。
- 支持同步和异步请求:OKHttp支持发送同步和异步的HTTP请求,可以根据需要选择合适的请求方式。
- 拦截器机制:提供了拦截器机制,可以在发送请求和接收响应的过程中进行干预和操作,如添加公共头部、记录日志等。
- 支持HTTP/2和SPDY:这些协议可以提高网络性能和效率,OKHttp会自动选择支持的协议进行通信。
应用:
OKHttp适用于需要高性能和灵活配置的HTTP客户端场景,如需要处理大量并发请求、需要自定义请求和响应处理等。
4.2.3 连接池的使用
- 引入依赖
- 开启线程池配置
- 验证底层变化
第一步:引入依赖
在cart-service
的pom.xml
中引入依赖:
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
第二步: 开启线程池配置
在cart-service
的yml文件中引入
第三步:验证底层
4.3 抽取服务、最佳实践
呼!现在应该是最佳方案了吧.......是吧,其实还是有优化的(优化是无止境的哈哈)。你观察一下。其实微服务与微服务之间往往是双向调用的,假设有1000个微服务两两互相调用。按照我们这种写法,需要给每个微服务提供 999 个 client接口。那1000个微服务就要提供 999000个接口。喔喔喔,是不是特别吓人。而且这些接口文件大都是重复的呀。
所以我们能不能把接口抽取成一个顶层模块,其他微服务模块只需要“继承”该模块,就能获得其中的方法。哇是不是很可行,那咱们马上行动。
4.3.1 抽取思路分析
-
思路1:抽取到微服务之外的公共modul
-
第一种的就是将所有的需要Fegin的接口都放入这个hm-api中,不同模块都可以调用这个hm-api中的接口
-
优点:抽取更加简单,工程结构也比较清晰,不需要额外拷贝别的微服务的DTO。
-
缺点:耦合度比较高,每个模块都可能都需要调用hm-api。
-
-
思路2:每个微服务自己抽取一个module
-
第二种是将需要调用的接口放在自己的module下,耦合度没有那么高,但是实现相对麻烦,工程结构相对更复杂、而且要额外拷贝别的微服务的DTO。
-
4.3.2 .抽取Feign客户端实践
-
在
hmall
下定义一个新的module,命名为hm-api -
导入依赖、导入实体对象、导入client接口
在hmall
下定义一个新的module,命名为hm-api
导入依赖、导入实体对象、导入client接口
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hm-api</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- load balancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- swagger 注解依赖 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.6</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
现在,任何微服务要调用item-service
中的接口,只需要引入hm-api
模块依赖即可,无需自己编写Feign客户端了。
4.3.3 配置扫描包
为什么要配置扫描包?
cart-service的启动类定义在com.hmall.cart包下,扫描不到ItemClient,我们必须配置扫描包,不然会报错。
【使用步骤】
- 在cart-service的pom.xml中引入hm-api模块
- 配置扫描包路径
在cart-service的pom.xml中引入hm-api模块(模块调用模块)
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
配置扫描包路径方法一:声明扫描包
在cart-service的启动类上添加扫描 hm-api的声明
注意哈!由于删除了原本模块中的client、dto。导包部分要重新导
配置扫描包路径方法一:声明要用的FeignClient
在cart-service的启动类上添加声明要用的FeignClient
4.4 日志配置
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
4.4.1 定义日志级别
package com.hmall.api.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
}
4.4.2 日志配置生效
【局部生效】在某个FeignClient
中配置,只对当前FeignClient
生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
【全局生效】在@EnableFeignClients
中配置,针对所有FeignClient
生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
【日志格式】
22:26:25:336 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> GET http://item-service/items?ids=100000006163 HTTP/1.1
22:26:25:336 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> END HTTP (0-byte body)
22:26:25:518 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- HTTP/1.1 200 (182ms)
22:26:25:518 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] connection: keep-alive
22:26:25:519 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] content-type: application/json
22:26:25:519 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] date: Fri, 01 Nov 2024 14:26:25 GMT
22:26:25:519 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] keep-alive: timeout=60
22:26:25:519 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] transfer-encoding: chunked
22:26:25:519 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds]
22:26:25:520 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] [{"id":"100000006163","name":"巴布豆(BOBDOG)柔薄悦动婴儿拉拉裤XXL码80片(15kg以上)","price":67100,"stock":10000,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t23998/350/2363990466/222391/a6e9581d/5b7cba5bN0c18fb4f.jpg!q70.jpg.webp","category":"拉拉裤","brand":"巴布豆","spec":"{}","sold":11,"commentCount":33343434,"isAD":false,"status":2}]
22:26:25:520 DEBUG 4716 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- END HTTP (371-byte body)
22:26:26:007 INFO 4716 --- [ent-executor-12] com.alibaba.nacos.common.remote.client : [64fa38f0-9dff-4673-8014-52dbf5095a2f] Receive server push request, request = NotifySubscriberRequest, requestId = 22
22:26:26:008 INFO 4716 --- [ent-executor-12] com.alibaba.nacos.common.remote.client : [64fa38f0-9dff-4673-8014-52dbf5095a2f] Ack server push request, request = NotifySubscriberRequest, requestId = 22
五、服务注册与调用巩固
1. 注册中心模式的角色有哪些?流程是什么?
2. 注册中心模式是如何实现宕机实例、异常实例的检测的?
3. 概述一下Nacos的使用流程?
4. 微服务中的服务发现是如何实现的?
5. 如何优化微服务远程调用的代码逻辑?(取代RestTemplate的工具)
6. 谈谈OpenFeign工具的使用流程?
7. OpenFeign底层的HTTP客户端技术是什么?有什么特点?
8. 除了HttpURLConnection,还要哪些HTTP客户端技术,有何特点?
9. 如何提高client接口代码的复用性。你有什么实现思路?
10. 跨模块调用client接口方法的配置步骤?