目录
1 认识微服务
1.1 单体架构
1.2 微服务
1.3 SpringCloud
2 服务拆分原则
2.1 什么时候拆
2.2 怎么拆
2.3 服务调用
3. 服务注册与发现
3.1 注册中心原理
3.2 Nacos注册中心
3.3 服务注册
3.3.1 添加依赖
3.3.2 配置Nacos
3.3.3 启动服务实例
3.4 服务发现
3.4.1 发现并调用服务
4 OpenFeign
4.1 快速入门
4.1.1 引入依赖
4.1.2 启用OpenFeign
4.1.3 编写OpenFeign客户端
4.1.4 使用FeignClient
4.2 连接池
4.2.1 引入依赖
4.2.2 开启连接池
4.3 最佳实践
4.3.1 思路分析
4.3.2 抽取Feign客户端
4.3.3 扫描包
4.4 日志配置
4.4.1 定义日志级别
4.4.2 配置
4 总结
4.1 如何利用 OpenFeign 实现远程调用?
4.2 如何配置 OpenFeign 的连接池?
4.3 OpenFeign 使用的最佳实践方式是什么?
4.4 如何配置 OpenFeign 输出日志的级别?
1 认识微服务
1.1 单体架构
单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在一个工程中开发;项目部署时需要对所有模块一起编译、打包;项目的架构设计、开发模式都非常简单。
当项目规模较小时,这种模式上手快,部署、运维也都很方便,因此早期很多小型项目都采用这种模式。
缺点:
团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能会耗尽系统资源,导致其它服务低可用。
1.2 微服务
微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时要满足下面的一些特点:
单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
分布式就是服务拆分的过程,其实微服务架构正是分布式架构的一种最佳实践的方案。
1.3 SpringCloud
微服务拆分以后碰到的各种问题都有对应的解决方案和微服务组件,而SpringCloud框架可以说是目前Java领域最全面的微服务组件的集合了。
2 服务拆分原则
2.1 什么时候拆
对于大多数小型项目来说,一般是先采用单体架构,随着用户规模扩大、业务复杂后再逐渐拆分为微服务架构。这样初期成本会比较低,可以快速试错。但是,这么做的问题就在于后期做服务拆分时,可能会遇到很多代码耦合带来的问题,拆分比较困难(前易后难)。
而对于一些大型项目,在立项之初目的就很明确,为了长远考虑,在架构设计时就直接选择微服务架构。虽然前期投入较多,但后期就少了拆分服务的烦恼(前难后易)。
2.2 怎么拆
高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。
hmall可以分为以下几个微服务
-
用户服务
-
商品服务
-
订单服务
-
购物车服务
-
支付服务
拆分方式:
1. 纵向拆分:就是按照项目的功能模块来拆分
2. 抽取公共部分,提高复用性
2.3 服务调用
在拆分的时候发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service
服务,导致无法查询。
最终结果就是查询到的购物车数据不完整,因此要想解决这个问题就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)。
查询购物车流程:
Java发送http请求可以使用Spring提供的RestTemplate,使用的基本步骤如下:
-
注册RestTemplate到Spring容器
-
调用RestTemplate的API发送请求,常见方法有:
-
getForObject:发送Get请求并返回指定类型对象
-
PostForObject:发送Post请求并返回指定类型对象
-
put:发送PUT请求
-
delete:发送Delete请求
-
exchange:发送任意类型请求,返回ResponseEntity
-
3. 服务注册与发现
我们通过Http请求实现了跨微服务的远程调用,不过这种手动发送Http请求的方式存在一些问题。
此时,每个item-service
的实例其IP或端口不同,问题来了:
-
item-service这么多实例,cart-service如何知道每一个实例的地址?
-
http请求要写url地址,
cart-service
服务到底该调用哪个实例呢? -
如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? -
如果并发太高,
item-service
临时多部署了N台实例,cart-service
如何知道新实例的地址?
为了解决上述问题,引入注册中心概念。
3.1 注册中心原理
在微服务远程调用的过程中 包括俩角色:
1.服务提供者:提供接口供其他微服务访问
2.服务消费者:调用其他微服务提供的接口
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:
流程如下:
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
-
服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
-
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
3.2 Nacos注册中心
基于Docker来部署Nacos的注册中心
1. 首先要准备MySQL数据库表,用来存储Nacos的数据
2. 导入nacos文件夹
3. 进入root目录 执行
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
启动完成后,访问下面地址:http://192.168.79.132:8848/nacos/,注意将IP地址替换为你自己的虚拟机IP地址。
首次访问会跳转到登录页,账号密码都是nacos
3.3 服务注册
步骤如下:
-
引入依赖
-
配置Nacos地址
-
重启
3.3.1 添加依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.3.2 配置Nacos
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
3.3.3 启动服务实例
为了测试一个服务多个实例的情况,我们再配置一个item-service
的部署实例
然后配置启动项,注意重命名并且配置新的端口,避免冲突
重启这两个实例
访问nacos控制台,可以发现服务注册成功
3.4 服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
-
引入依赖
-
配置Nacos地址
-
发现并调用服务
3.4.1 发现并调用服务
接下来,服务调用者cart-service
就可以去订阅item-service
服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
-
随机
-
轮询
-
IP的hash
-
最近最少访问
-
...
我们可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具,DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
我们通过DiscoveryClient发现服务实例列表,然后通过负载均衡算法,选择一个实例去调用:
4 OpenFeign
4.1 快速入门
以cart-service中的查询我的购物车为例。因此下面的操作都是在cart-service中进行。
4.1.1 引入依赖
在cart-service
服务的pom.xml中引入OpenFeign
的依赖和loadBalancer
依赖:
<!--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 启用OpenFeign
接下来,我们在cart-service
的CartApplication
启动类上添加注解,启动OpenFeign功能:
4.1.3 编写OpenFeign客户端
在cart-service
中,定义一个新的接口,编写Feign客户端:
package com.hmall.cart.client;
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
-
@FeignClient("item-service")
:声明服务名称 -
@GetMapping
:声明请求方式 -
@GetMapping("/items")
:声明请求路径 -
@RequestParam("ids") Collection<Long> ids
:声明请求参数 -
List<ItemDTO>
:返回值类型
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items
发送一个GET
请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>
。
我们只需要直接调用这个方法,即可实现远程调用了。
4.1.4 使用FeignClient
我们在cart-service
的com.hmall.cart.service.impl.CartServiceImpl
中改造代码,直接调用ItemClient
的方法:
Feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作。
4.2 连接池
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
-
HttpURLConnection:默认实现,不支持连接池
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
4.2.1 引入依赖
在cart-service
的pom.xml
中引入依赖:
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
4.2.2 开启连接池
在cart-service
的application.yml
配置文件中开启Feign的连接池功能:
feign:
okhttp:
enabled: true # 开启OKHttp功能
4.3 最佳实践
如果拆分了交易微服务(trade-service
),它也需要远程调用item-service
中的根据id批量查询商品功能。这个需求与cart-service
中是一样的。
因此,我们就需要在trade-service
中再次定义ItemClient
接口,这就是重复编码了
4.3.1 思路分析
避免重复编码的办法就是抽取。不过这里有两种抽取思路:
-
思路1:抽取到微服务之外的公共module
-
思路2:每个微服务自己抽取一个module
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
4.3.2 抽取Feign客户端
在hmall
下定义一个新的module,命名为hm-api
其依赖如下:
<?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>
然后把ItemDTO和ItemClient都拷贝过来。
现在,任何微服务要调用item-service
中的接口,只需要引入hm-api
模块依赖即可,无需自己编写Feign客户端了。
4.3.3 扫描包
我们在cart-service
的pom.xml
中引入hm-api
模块:
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
因为ItemClient
现在定义到了com.hmall.api.client
包下,而cart-service的启动类定义在com.hmall.cart
包下,扫描不到ItemClient
解决办法很简单,在cart-service的启动类上添加声明即可,两种方式:
-
方式1:声明扫描包:
-
方式2:声明要用的FeignClient
4.4 日志配置
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
4.4.1 定义日志级别
在hm-api模块下新建一个配置类,定义Feign的日志级别:
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)
4 总结
4.1 如何利用 OpenFeign 实现远程调用?
-
引入依赖:
引入 OpenFeign 和 Spring Cloud LoadBalancer 的相关依赖。 -
启用 OpenFeign 功能:
在主程序类上添加@EnableFeignClients
注解,开启 OpenFeign 的功能。 -
定义 FeignClient 接口:
编写@FeignClient
注解的接口,指定远程服务名称和对应的路径,通过方法调用实现远程服务的访问。
4.2 如何配置 OpenFeign 的连接池?
-
引入 Http 客户端依赖:
根据需求选择适合的 Http 客户端,例如 OKHttp 或 HttpClient,并引入相关依赖。 -
配置连接池参数:
在application.yml
文件中配置 OpenFeign 的连接池:- 开启连接池功能。
- 配置最大连接数、超时时间等参数。
4.3 OpenFeign 使用的最佳实践方式是什么?
-
服务提供者抽取公共模块:
服务提供者将 FeignClient 接口及 DTO(数据传输对象)抽取到一个独立的模块中,供调用方直接依赖使用,保证代码的一致性。 -
接口复用:
调用方通过依赖服务提供者的公共模块,减少代码冗余,避免重复定义接口和 DTO。
4.4 如何配置 OpenFeign 输出日志的级别?
-
声明日志级别的 Bean:
定义一个类型为Logger.Level
的 Bean,例如:@Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; }
-
启用日志配置:
在@FeignClient
或@EnableFeignClients
注解中,通过defaultConfiguration
属性指定包含日志配置的类。例如:@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
-
日志级别说明:
根据实际需求选择不同的日志级别:- NONE: 不输出任何日志。
- BASIC: 记录请求方法、URL、响应状态码及执行时间。
- HEADERS: 记录 BASIC 级别的内容以及请求和响应的头信息。
- FULL: 记录请求和响应的所有内容,包括头信息和正文。