目录
[1 认识微服务](#1 认识微服务)
[1.1 单体架构](#1.1 单体架构)
[1.2 微服务](#1.2 微服务)
[1.3 SpringCloud](#1.3 SpringCloud)
[2 服务拆分原则](#2 服务拆分原则)
[2.1 什么时候拆](#2.1 什么时候拆)
[2.2 怎么拆](#2.2 怎么拆)
[2.3 服务调用](#2.3 服务调用)
[3. 服务注册与发现](#3. 服务注册与发现)
[3.1 注册中心原理](#3.1 注册中心原理)
[3.2 Nacos注册中心](#3.2 Nacos注册中心)
[3.3 服务注册](#3.3 服务注册)
[3.3.1 添加依赖](#3.3.1 添加依赖)
[3.3.2 配置Nacos](#3.3.2 配置Nacos)
[3.3.3 启动服务实例](#3.3.3 启动服务实例)
[3.4 服务发现](#3.4 服务发现)
[3.4.1 发现并调用服务](#3.4.1 发现并调用服务)
[4 OpenFeign](#4 OpenFeign)
[4.1 快速入门](#4.1 快速入门)
[4.1.1 引入依赖](#4.1.1 引入依赖)
[4.1.2 启用OpenFeign](#4.1.2 启用OpenFeign)
[4.1.3 编写OpenFeign客户端](#4.1.3 编写OpenFeign客户端)
[4.1.4 使用FeignClient](#4.1.4 使用FeignClient)
[4.2 连接池](#4.2 连接池)
[4.2.1 引入依赖](#4.2.1 引入依赖)
[4.2.2 开启连接池](#4.2.2 开启连接池)
[4.3 最佳实践](#4.3 最佳实践)
[4.3.1 思路分析](#4.3.1 思路分析)
[4.3.2 抽取Feign客户端](#4.3.2 抽取Feign客户端)
[4.3.3 扫描包](#4.3.3 扫描包)
[4.4 日志配置](#4.4 日志配置)
[4.4.1 定义日志级别](#4.4.1 定义日志级别)
[4.4.2 配置](#4.4.2 配置)
[4 总结](#4 总结)
[4.1 如何利用 OpenFeign 实现远程调用?](#4.1 如何利用 OpenFeign 实现远程调用?)
[4.2 如何配置 OpenFeign 的连接池?](#4.2 如何配置 OpenFeign 的连接池?)
[4.3 OpenFeign 使用的最佳实践方式是什么?](#4.3 OpenFeign 使用的最佳实践方式是什么?)
[4.4 如何配置 OpenFeign 输出日志的级别?](#4.4 如何配置 OpenFeign 输出日志的级别?)
1 认识微服务
1.1 单体架构
单体架构(monolithic structure):顾名思义,整个项目中所有功能模块都在一个工程中开发;项目部署时需要对所有模块一起编译、打包;项目的架构设计、开发模式都非常简单。
当项目规模较小时,这种模式上手快,部署、运维也都很方便,因此早期很多小型项目都采用这种模式。
缺点:
团队协作成本高:试想一下,你们团队数十个人同时协作开发同一个项目,由于所有模块都在一个项目中,不同模块的代码之间物理边界越来越模糊。最终要把功能合并到一个分支,你绝对会陷入到解决冲突的泥潭之中。
系统发布效率低:任何模块变更都需要发布整个系统,而系统发布过程中需要多个模块之间制约较多,需要对比各种文件,任何一处出现问题都会导致发布失败,往往一次发布需要数十分钟甚至数小时。
系统可用性差:单体架构各个功能模块是作为一个服务部署,相互之间会互相影响,一些热点功能会耗尽系统资源,导致其它服务低可用。
1.2 微服务
微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。同时要满足下面的一些特点:
单一职责:一个微服务负责一部分业务功能,并且其核心数据不依赖于其它模块。
团队自治:每个微服务都有自己独立的开发、测试、发布、运维人员,团队人员规模不超过10人(2张披萨能喂饱)
服务自治:每个微服务都独立打包部署,访问自己独立的数据库。并且要做好服务隔离,避免对其它服务产生影响
分布式就是服务拆分的过程,其实微服务架构正是分布式架构的一种最佳实践的方案。
1.3 SpringCloud
微服务拆分以后碰到的各种问题都有对应的解决方案和微服务组件,而SpringCloud框架可以说是目前Java领域最全面的微服务组件的集合了。
2 服务拆分原则
2.1 什么时候拆
对于大多数小型项目来说,一般是先采用单体架构 ,随着用户规模扩大、业务复杂后再逐渐拆分为 微服务架构 。这样初期成本会比较低,可以快速试错。但是,这么做的问题就在于后期做服务拆分时,可能会遇到很多代码耦合带来的问题,拆分比较困难(前易后难)。
而对于一些大型项目,在立项之初目的就很明确,为了长远考虑,在架构设计时就直接选择微服务架构。虽然前期投入较多,但后期就少了拆分服务的烦恼(前难后易)。
2.2 怎么拆
高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。
hmall可以分为以下几个微服务
-
用户服务
-
商品服务
-
订单服务
-
购物车服务
-
支付服务
拆分方式:
-
纵向拆分:就是按照项目的功能模块来拆分
-
抽取公共部分,提高复用性
2.3 服务调用
在拆分的时候发现一个问题:就是购物车业务中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service
服务,导致无法查询。
最终结果就是查询到的购物车数据不完整,因此要想解决这个问题就必须改造其中的代码,把原本本地方法调用,改造成跨微服务的远程调用(RPC,即R emote P roduce 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的注册中心
-
首先要准备MySQL数据库表,用来存储Nacos的数据
-
导入nacos文件夹
-
进入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: 记录请求和响应的所有内容,包括头信息和正文。