SpringCloud框架学习(第二部分:Consul、LoadBalancer和openFeign)

目录

六、Consul服务注册和发现

1.基本介绍

2.下载运行

3.服务注册与发现

(1)支付服务provider8001注册进consul

(2)修改订单服务cloud-consumer-order80

4.CAP

(1)CAP理论

(2)三个注册中心异同点

[Ⅰ. AP 架构(Eureka)](#Ⅰ. AP 架构(Eureka))

[Ⅱ. CP 架构 (Zookeeper/Consul)](#Ⅱ. CP 架构 (Zookeeper/Consul))

5.分布式配置中心(服务配置)

(1)功能介绍

(2)方法实现步骤:

[① 给对应的模块添加服务配置的依赖](#① 给对应的模块添加服务配置的依赖)

[② 在resources下,新建文件:bootstrap.yml](#② 在resources下,新建文件:bootstrap.yml)

[③ 在 consul 的 ui 中创建 config 文件夹](#③ 在 consul 的 ui 中创建 config 文件夹)

[④ 在 config 下创建二级文件夹,表示该微服务的不同开发环境](#④ 在 config 下创建二级文件夹,表示该微服务的不同开发环境)

[⑤ 在每个文件夹下,创建一个文件(key),名为 data。](#⑤ 在每个文件夹下,创建一个文件(key),名为 data。)

[Ⅰ. 配置不同的开发环境的配置信息](#Ⅰ. 配置不同的开发环境的配置信息)

[Ⅱ. 获取公共的配置信息](#Ⅱ. 获取公共的配置信息)

6.动态刷新

7.持久化配置

[七、LoadBlancer 负载均衡](#七、LoadBlancer 负载均衡)

1.基本介绍

2.基本使用

3.案例使用

4.基本原理

5.负载均衡算法介绍

(1)轮询算法(默认)

(2)随机算法

(3)自定义负载均衡算法(了解)

[(4) 算法切换](#(4) 算法切换)

八、OpenFeign服务接口调用

1.基本介绍

2.基本使用

3.高级特性

(1)超时控制

[Ⅰ. 全局配置](#Ⅰ. 全局配置)

[Ⅱ. 指定配置](#Ⅱ. 指定配置)

(2)重试机制

[(3)默认 HttpClient 修改与替换](#(3)默认 HttpClient 修改与替换)

(4)请求/响应压缩

(5)日志打印


六、Consul服务注册和发现

1.基本介绍

Question1:Consul是什么?

Consul是一款开源的分布式服务发现与配置管理系统,由HashiCorp公司使用Go语言开发。

官方:http://consul.io/

Question2:为什么不使用Eureka了?

① Eureka 停更了,不在开发新版本了

② Eureka 对初学者不友好,有自我保护机制

③ 我们希望注册中心能够从项目中分离出来,单独运行(解耦),而 Eureka 做不到这一点

Question3:Consul能干什么?

① 服务发现:提供HTTP和DNS两种发现方式

② 健康检测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制化监控

③ KV存储:Key、Value的存储方式

④ 多数据中心:Consul支持多数据中心

⑤ 可视化WEB界面

2.下载运行

下载地址: Install | Consul | HashiCorp Developerhttps://developer.hashicorp.com/consul/install?product_intent=consul

**注意:**下载对应的版本(adm64版本的就是x86_64版本的,386就是x86_32版本的)

安装目录下输入cmd,弹出命令行窗口:

输入命令:consul --version,检查是否安装成功

输入命令:consul agent -dev,以开发模式启动

访问 8500 端口,访问 Consul 首页:localhost:8500

3.服务注册与发现

需求说明:将前面单体服务中的支付模块、订单模块注册到 Consul 中

**(1)**支付服务provider8001注册进consul

步骤:

① 该模块的 pom 文件中引入依赖

XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

② 该模块的 yaml 配置文件编写配置

javascript 复制代码
spring:
  application:
    # 当前服务名
    name: cloud-payment-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #配置当前服务注册到里面使用的名字
        service-name: ${spring.application.name}

③ 启动类加上 @EnableDiscoveryClient 注解,开启服务发现功能

java 复制代码
@SpringBootApplication
@MapperScan("com.mihoyo.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient
public class Main8001 {
    public static void main(String[] args) {
        SpringApplication.run(Main8001.class, args);
    }
}

④ 启动 8001 服务,查看 consul 控制台:

(2)修改订单服务cloud-consumer-order80

步骤:

① 该模块的 pom 文件中引入依赖

XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

② 该模块的 yaml 配置文件编写配置

javascript 复制代码
spring:
  application:
    name: cloud-consumer-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}

③ 启动类加上 @EnableDiscoveryClient 注解,开启服务发现功能

java 复制代码
@SpringBootApplication
@EnableDiscoveryClient
public class Main80
{
    public static void main(String[] args)
    {
        SpringApplication.run(Main80.class,args);
    }
}

④ 修改 controller

java 复制代码
//服务注册中心上的微服务名称
public static final String PaymentSrv_URL = "http://cloud-payment-service";

⑤ 启动 80 服务,查看 consul 控制台:

⑥ 测试访问地址:http://localhost/consumer/pay/get/1

很显然,全局异常处理机制捕获到了异常:

原因:

当你把服务注册到Consul时,Consul充当了服务发现的角色,它会记录服务实例的信息(例如IP地址和端口),然后其他微服务可以通过查询Consul来获取这些服务实例的信息。

RestTemplate 并不具备从服务发现平台(如Consul)动态获取服务实例信息的能力。它会仅仅处理你直接提供的URL(如http://localhost:8080)。

⑦ 给 RestTemplate 组件加上 @LoadBalanced 注解

java 复制代码
@Configuration
public class RestTemplateConfig
{
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate()
    {
        return new RestTemplate();
    }
}

分析:

Consul 作为服务注册与发现平台,确实具有服务发现的能力。但 RestTemplate 本身并不知道如何与 Consul 进行交互,它只会处理你提供的字符串。它不会知道这个地址是不是微服务的名称,也不会知道如何去Consul中查找对应的服务实例。

所以即使你已将 ip 和端口号注册进 consul,但 RestTemplate 还是认为你设置的 url = "http://cloud-payment-service" 是通过硬编码的方式。因此,RestTemplate 是找不到该 url 的。

但加上 @LoadBalanced 后,SpringCloud 会让 RestTemplate 具有服务发现和负载均衡的功能。这意味着,当你传递一个服务名称(如 http://order-service)给 RestTemplate 时,它会自动知道这是一个微服务的名称,而不是一个硬编码的IP地址。

@LoadBalanced 会让 SpringCloud 通过服务发现机制(如Consul)查找这个服务的可用实例,然后根据负载均衡策略(如轮询等)选择一个实例进行访问。

运行结果:

4.CAP

(1)CAP理论

CAP理论的核心是: 一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。

因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三 大类:

CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。

CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。

AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

(2)三个注册中心异同点

组件名 语言 CAP 服务健康检查 对外暴露接口 SpringCloud 集成
Eureka Java AP 可配支持 HTTP 已集成
Consul Go CP 支持 HTTP/DNS 已集成
Zookeeper Java CP 支持 客户端 已集成
Ⅰ. AP 架构(Eureka)

当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。

当数据出现不一致时,虽然A, B上的注册信息不完全相同,但每个Eureka节点依然能够正常对外提供服务,这会出现查询服务信息时如果请求A查不到,但请求B就能查到。如此保证了可用性但牺牲了一致性

结论:违背了一致性 C 的要求,只满足可用性和分区容错,即AP。

Ⅱ. CP 架构 (Zookeeper/Consul)

当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性。Consul 遵循CAP原理中的 CP 原则,保证了强一致性和分区容错性,且使用的是 Raft 算法,比 zookeeper 使用的Paxos 算法更加简单。虽然保证了强一致性,但是可用性就相应下降了。

例如服务注册的时间会稍长一些,因为 Consul 的 raft 协议要求必须过半数的节点都写入成功才认为注册成功 ;在leader挂掉了之后,重新选举出leader之前会导致Consul 服务不可用。

结论:违背了可用性 A 的要求,只满足一致性和分区容错,即CP 。

5.分布式配置中心(服务配置)

(1)功能介绍

问题说明:

系统拆分之后,会产生大量的微服务。每个微服务都有其对应的配置文件 yml。如果其中的某个配置项发生了修改,一个一个微服务修改会很麻烦。因此一套集中式的、动态的配置管理设施是必不可少的。从而实现一次修改,处处生效。

**例如:**给班里同学通知下节课不上了

麻烦的方法:一个个发送消息

简单的方法:直接在班级群 @所有人

结论:

所以,consul 优于 Eureka,不仅式因为它符合 CP 原则,而且因为 consul 同时支持服务注册和分布式配置中心,而 Eureka 不支持分配式配置中心。

(2)方法实现步骤:

**需求:**通用全局配置信息,直接注册进 Consul 服务器,从 Consul 获取。

(**注意:**既然从 Consul 获取自然要遵守 Consul 的配置规则要求。)

① 给对应的模块添加服务配置的依赖
XML 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
② 在resources下,新建文件:bootstrap.yml

说明:

① bootstrap.yml和applicaiton.yml一样都是配置文件。applicaiton.yml是用户级,bootstrap.yml 是系统级的,优先级更加高

② Spring Cloud会创建一个"Bootstrap Context",作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的 Environment。

③ Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。

④ bootstrap.yml 比application.yml先加载的
注意:

application.yml 和 bootstrap.yml 可以共存:

bootstrap.yml 主要用于配置应用启动时所需的外部依赖和环境(配置服务注册和发现的客户端、配置中心的地址等)

application.yml 用于业务逻辑相关的配置(数据库连接、消息队列、线程池设置等)

bootstrap.yml:

javascript 复制代码
spring:
  application:
    name: cloud-payment-service
    ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #配置当前服务注册到里面使用的名字
        service-name: ${spring.application.name}
      config:
        enabled: true     #是否开启配置中心
        format: yaml      #配置文件格式,这里用的yaml
        profile-separator: "-"  #例如: service-provider和dev中间的符号 用-就是service-provider-dev
        data-key: data    #默认的值就是data  是config的key  写上方便阅读
        prefix: config    #默认的值就是config   是配置的前缀  写上方便阅读

application.yml:

javascript 复制代码
server:
  port: 8001

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: 123456

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.mihoyo.cloud.entities
  configuration:
    map-underscore-to-camel-case: true
③ 在 consul 的 ui 中创建 config 文件夹

打开consul 的 ui 界面,找到 key-value,点击右上角的 create,创建文件夹(名为:config)

注意:

① 起始文件夹名,默认为 config,如果需要修改,需要在 bootstrap.yml 中修改对应属性 (prefix)

② 如果输入结尾是 '/',则当前为文件夹,下方的文本框会消失。

④ 在 config 下创建二级文件夹,表示该微服务的不同开发环境
创建路径 作用
config/testApp,dev/ 全局公共配置, 对应使用 config 前缀的所有应用
config/testApp/ 全局dev公共配置, 对应使用 config 前缀的所有, 且启用 dev 环境的应用
config/application,dev/ 对应使用 config 前缀的, 名称为 testApp 的应用
config/application/ 对应使用 config 前缀的, 名称为 testApp, 且启用 dev 环境的应用

注意:

这里的,表示服务和环境之间的分割符,我们已经在 bootstrap.yml 中的 profile-separator 属性将其修改为 -

⑤ 在每个文件夹下,创建一个文件(key),名为 data。

**注意:**默认的文件名就是 data,如果需要修改,需要在 bootstrap.yml 中修改对应属性 (data-key)

说明,在 data 中:

① 可以从 bootstrap.yml 抽取一些公共的配置(如:数据库连接,redis 的配置,消息队列的配置),放在一个共享文件夹下的 data 中(config/application/data)

② 也可以将一些应用级别的内容(如:application.yml 中的,端口号,数据库配置等),即不同开发环境配置不一样的配置信息,放在对应开发环境文件夹下的 data 中。

Ⅰ. 配置不同的开发环境的配置信息

例如:不同开发环境使用的端口号不一致

此时,在bootstrap.yml 中

javascript 复制代码
# ==========applicationName + druid-mysql8 driver===================
spring:
  application:
    # 当前服务名
    name: cloud-payment-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #配置当前服务注册到里面使用的名字
        service-name: ${spring.application.name}
      config:
        enabled: true     #是否开启配置中心
        format: yaml      #配置文件格式,这里用的yaml
        profile-separator: "-"  #例如: service-provider和dev中间的符号 用-就是service-provider-dev
        data-key: data    #默认的值就是data  是config的key  写上方便阅读
        prefix: config    #默认的值就是config   是配置的前缀  写上方便阅读
  profiles:
        active: dev    #激活开发环境(默认是default默认环境)

注意:

prefix 前缀必须是 config,不用多一级应用目录(config/testApp ×)

通过激活不同的环境,就可以获得不同的配置!

Ⅱ. 获取公共的配置信息

创建共享文件夹:config/application/,和 config/application-dev/

注意: 文件名必须是 application 和 application-*

同样,分别在其下创建对应的文件(key),名为 data:

application.yml 中可改写为:

javascript 复制代码
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 通过占位符引用 Consul 中的值
    url: ${spring.datasource.url}
    username: ${spring.datasource.username}
    password: ${spring.datasource.password}

# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.mihoyo.cloud.entities
  configuration:
    map-underscore-to-camel-case: true

此时,和之前一样:

bootstrap.yml配置 效果
prefix:config,active:default 同时激活通用配置和私有配置的开发环境
prefix:config,active:dev 同时激活通用配置和私有配置的默认环境

注意:

① 通用配置和私有配置是相互关联的,共用一个环境!!!

(我是这么认为的,因为实验后,发现没法将两者分开使用不同环境。虽然 gpt 说可以,但是给的方案都不可行,如果有错可以评论或者私信我!!!)

② 默认的前缀和后缀修改的话,prefix 和 data-key,一定要随之改变。

③ 通用配置命名以 application 开头,私有配置命名以 微服务名称 开头,不能乱起!!!

(只能手动修改前缀和后缀,配置文件夹的命名是有严格约束的,无法修改的!)

④ 如果设置的 dev 开发环境,但开发环境中找不到该信息,就会去默认环境中找。

⑤ 如果本地 application.yml 和 bootstrap.yml 设置了同样的配置的话,会被 consul 的 kv 覆盖。 (优先级:consul > bootstrap.yml > application.yml)

⑥ consul 中的配置信息,也可以在 controller 中直接获得:

java 复制代码
@RestController
@Slf4j
@RequestMapping("pay")
@Tag(name = "支付微服务模块",description = "支付CRUD")
public class PayController {
    @Resource
    private PayService payService;

    @GetMapping(value = "/get/info")
    private String getInfoByConsul(@Value("${server.port}") String port)
    {
        return "port: "+port;
    }
    
    ...
}

运行结果:

6.动态刷新

需求:希望 Consul 的配置变动之后,刷新页面(不重启项目),项目读取的内容也能立马改变。

修改 data 中的 name 的值,再刷新页面,会发现没有发生改变。

注意:

这里不要用端口号来实验,就算实验后续也只会改变读取的值,不会改变 url 中的端口。因为 springboot 项目启动时,是会自动锁定端口号的,只有重启才能修改端口号。

解决步骤:

① 在主启动类加上 @RefreshScope 注解

java 复制代码
@SpringBootApplication
@MapperScan("com.mihoyo.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient
@RefreshScope   //动态刷新
public class Main8001 {
    public static void main(String[] args) {
        SpringApplication.run(Main8001.class, args);
    }
}

② 然后在 bootstrap.yml 中设置刷新的间隔

默认的刷新的间隔为 55s,我们可以手动降低刷新时间,实操时不推荐修改!

javascript 复制代码
## ==========applicationName + druid-mysql8 driver===================
spring:
  application:
    # 当前服务名
    name: cloud-payment-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #配置当前服务注册到里面使用的名字
        service-name: ${spring.application.name}
      config:
        enabled: true     #是否开启配置中心
        format: yaml      #配置文件格式,这里用的yaml
        profile-separator: "-"  #例如: service-provider和dev中间的符号 用-就是service-provider-dev
        data-key: data    #默认的值就是data  是config的key  写上方便阅读
        prefix: config   #默认的值就是config   是配置的前缀  写上方便阅读
        watch:
          wait-time: 1  #设置刷新时间为 1s,默认 55s
  profiles:
    active: dev
javascript 复制代码
@RestController
@Slf4j
@RequestMapping("pay")
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {

    @GetMapping(value = "/get/info")
    private String getInfoByConsul(@Value("${user.name}") String name) {
        return "name: " + name;
    }
}

注意:

这里 name 如果作为形参,似乎直接就可以动态刷新,不用配置也没事。

但如果 name 作为成员变量,即使设置动态刷新,也还是没有用。

查了一下,前者可以,是因为:改为方法参数是一种绕过 @RefreshScope 限制的办法。

后者不可以,是因为:@Value 注解的值并不会自动更新,因为 Spring Boot 默认只在启动时初始化这些值。

视频里是前者,我也不知道为啥不能自动刷新,所以,这里暂时存个疑,大家有想法可以私信或评论我!

7.持久化配置

我们发现:把Consul关了,下次启动的时候,之前配置的 kv 数据就会全丢失了!

解决步骤:

① consul 安装目录下新建一个文件夹,用于存储后续 consul 的配置数据

② 新建文件 consul_start.bat (先新建 .txt,再将后缀改为 .bat)

③ 填写 consul_start.bat 的内容(注意修改成自己的文件位置)

php 复制代码
@echo.服务启动......  
@echo off  
@sc create Consul binpath= "D:\consul_1.20.1_windows_amd64\consul.exe agent -server -ui -bind=127.0.0.1 -client=0.0.0.0 -bootstrap-expect  1  -data-dir D:\consul_1.20.1_windows_amd64\mydata   "
@net start Consul
@sc config Consul start= AUTO  
@echo.Consul start is OK......success
@pause

④ 右键以管理员身份运行 consul_start.bat

⑤ 启动结果

此时,数据就会永久保存了,即使我们电脑关机,重启之后 consul 也会自动打开。

但现在的数据是空,我们需要重写在 consul 中配置我们之前的 kv。

七、LoadBlancer 负载均衡

1.基本介绍

Question1:LoadBlancer 是什么?

LoadBlancer 的前身是 Ribbon,是一套负责负载均衡的客户端工具。

官方:Spring Cloud LoadBalancer :: Spring Cloud Commons

Question2:为什么不使用 Ribbon 了?

Ribbon 已经进入维护模式,了解即可

Question3:负载均衡(LoadBlancer)是什么?

通过算法,将用户的请求平摊的分配到多个服务上,从而达到系统的 HA(高可用),常见的负载均衡有软件 Nginx,LVS,硬件 F5等

Question4:LoadBlancer 能干什么?

主要作用: 就是提供客户端软件的负载均衡,然后由 OpenFeign 去调用具体的微服务。

组件介绍:

Spring Cloud LoadBalancer 是由 SpringCloud 官方提供的一个开源的、简单易用的客户端负载均衡器,它包含在 SpringCloud-commons 中用它来替换了以前的 Ribbon 组件。

相比较于 Ribbon,SpringCloud LoadBalancer 不仅能够支持 RestTemplate,还支持WebClient(WeClient是 Spring Web Flux 中提供的功能,可以实现响应式异步请求)

Question5:loadbalancer 本地负载均衡客户端 VS Nginx 服务端负载均衡区别?

loadbalancer 本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到 JVM 本地,从而在**本地(客户端)**实现 RPC 远程服务调用技术。

Nginx是 服务器负载均衡,客户端所有请求都会交给 nginx,然后由 nginx 实现转发请求,即负载均衡是由服务端 实现的。

简单理解:前者是客户端自己选择实例,后者是 nginx 帮我们选择实例。

2.基本使用

场景:订单模块通过负载均衡访问支付模块的 8001 / 8002 / 8003 服务(实验用 2 个就够了)

使用步骤:

  • 先从注册中心拉取可调用的服务列表,了解他有多少个服务
  • 按照指定的负载均衡策略,从服务列表中选择一个地址,进行调用

3.案例使用

准备:

① 在 consul 中添加

② 在 8001 的 controller 中添加

java 复制代码
@RestController
@Slf4j
@RequestMapping("pay")
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {
    @Resource
    private PayService payService;

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

    @GetMapping(value = "/get/info")
    private String getInfoByConsul(@Value("${env}") String env) {
        return "env: " + env + ",port:" + port;
    }

    ...
}

步骤:

① 按照 8001 拷贝后新建 8002 微服务(注意修改一下 yml 中的 端口号)

② 启动Consul,将 8001 / 8002 启动后注册进微服务

③ 在**客户端(订单模块)**的 pom 文件中,导入依赖

XML 复制代码
<!--loadbalancer-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

注意: 是客户端进行负载均衡,也就是消费者订单模块(cloud-consumer-order80),不要加错

④ 在订单模块(客户端)的 RestTemplate 上加上 @LoadBlancer 注解,开启负载均衡(之前已经加过)

⑤ 在 80 的 controller 中添加

java 复制代码
@RestController
public class OrderController {
    public static final String PaymentSrv_URL = "http://cloud-payment-service";//服务注册中心上的微服务名称

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping(value = "/consumer/pay/get/info")
    private String getInfoByConsul()
    {
        return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/info", String.class);
    }
    ...
}

⑤ 启动 80 服务

⑥ url 输入 ,测试

我们发现,客户端会通过负载均衡,轮询两个不同的服务实例!

4.基本原理

① 会在项目中创建一个DiscoveryClient对象

② 通过 DiscoveryClient 对象,就能够获取注册中心中所有注册的服务

③ 然后将获取的服务与调用地址中传入的微服务名称进行对比

④ 如果一致,就会将微服务集群的相关信息返回

⑤ 然后通过负载均衡算法,选择出其中一个服务进行调用


我们可以通过代码,对这个过程进行模拟:

java 复制代码
@RestController
public class OrderController {

    @Resource
    private DiscoveryClient discoveryClient;
    
    @GetMapping("/consumer/discovery")
    public String discovery()
    {
        List<String> services = discoveryClient.getServices();
        for (String element : services) {
            System.out.println(element);
        }

        System.out.println("===================================");

        List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
        for (ServiceInstance element : instances) {
            System.out.println(element.getServiceId()+"\t"+element.getHost()+"\t"+element.getPort()+"\t"+element.getUri());
        }

        return instances.get(0).getServiceId()+":"+instances.get(0).getPort();
    }
}

运行结果:

5.负载均衡算法介绍

LoadBlancer 默认包含两种负载均衡算法,轮询算法和随机算法,同时还可以自定义负载均衡算法。默认使用轮询算法。

(1)轮询算法(默认)

实际调用服务器位置下标 = rest 接口第几次请求数 % 服务器集群总数量

(每次服务重启动后rest接口计数从1开始)

如:

List [0] instances = 127.0.0.1:8002

List [1] instances = 127.0.0.1:8001

8001+ 8002 组合成为集群,它们共计2台机器,集群总数为2, 按照轮询算法原理:

当总请求数为1时: 1 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001

当总请求数位2时: 2 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002

当总请求数位3时: 3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001

当总请求数位4时: 4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002

如此类推......

(2)随机算法

随机给一个数,然后请求下标对应的微服务。

(3)自定义负载均衡算法(了解)

RoundRobinLoadBalancer 和 RandomLoadBalancer 都实现了 ReactorServiceInstanceLoadBalancer 接口

而该接口继承自 ReactiveLoadBalancer 接口,该接口中定义了 choose 方法

它规定了算法的细节,如果我们要自定义一个负载均衡算法类,重写 choose 方法很关键!

这里不再过多赘述,因为实际开发中很少会自定义负载均衡算法,一般轮询就可以了。

(4) 算法切换

默认的轮询足够开发中使用,这里只是简单说明一下:

java 复制代码
@Configuration
//value的值是指对哪个微服务生效
@LoadBalancerClient(value = "cloud-payment-service",configuration = RestTemplateConfig.class)
public class RestTemplateConfig
{
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);

        //这里切换成了随机算法
        return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

此时,url 重新输入 localhost/consumer/pay/get/info,就不会是轮询访问,而是随机访问了。

八、OpenFeign服务接口调用

1.基本介绍

Question1:OpenFeign 是什么?

OpenFeign 编写了一套声明式的 Web 服务客户端,从而使WEB服务的调用变得很简单。同时也提供了负载均衡的 http 客户端,可以有 SPringle Cloud LoadBalancer 组件同样的效果。

Question2:OpenFeign 能干什么?

前面在使用 SpringCloud LoadBalancer+ RestTemplate 时,利用 RestTemplate 对 http 请求的封装处理形成了一套模版化的调用方法。
但是在实际开发中, 由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。 所以,OpenFeign 在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。
在OpenFeign的实现下,我们只需创建一个接口并使用注解的方式来配置它(在一个微服务接口上面标注一个 @FeignClient注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提供者给出调用接口清单,消费者直接通过OpenFeign调用即可。

OpenFeign 同时还集成 SpringCloud LoadBalancer

可以在使用OpenFeign时提供Http客户端的负载均衡,也可以集成阿里巴巴 Sentinel 来提供熔断、降级等功能。而与 SpringCloud LoadBalancer 不同的是,通过 OpenFeign 只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

2.基本使用

服务消费者80 → 调用含有 @FeignClient 注解的 Api 服务接口 → 服务提供者(8001/8002)

步骤:

① 原本的消费者 80 是通过 RestTemplate 实现的,为了不造成混淆,新建一个 Feign 版本的 80

② 在 pom 文件中导入依赖(比之前的 80 多了一个 openfeign 的依赖)

XML 复制代码
    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--SpringCloud consul discovery-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包 -->
        <dependency>
            <groupId>com.mihoyo.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--hutool-all-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!--fastjson2-->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
        </dependency>
        <!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

③ 编写 application.yml 文件

javascript 复制代码
server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  ####Spring Cloud Consul for Service Discovery
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}

④ 修改主启动类,加上 @EnableFeignClients 注解,启动 OpenFeign 功能

javascript 复制代码
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul为注册中心时注册服务
@EnableFeignClients//启用feign客户端,定义服务+绑定接口,以声明式的方法优雅而简单的实现服务调用
public class MainOpenFeign80
{
    public static void main(String[] args)
    {
        SpringApplication.run(MainOpenFeign80.class,args);
    }
}

⑤ 修改 cloud-api-commons 通用模块,参考微服务8001的Controller层,创建 OpenFeign 的接口,加上 @FeignClient 注解

注意:

Ⅰ. 为什么要在通用模块中加上 OpenFeign 的接口?

  • 支付 API 可能会被多个服务调用,应该放在通用 API 模块
  • 支付模块中可能并不是所有 API 都向外暴露,可以将需要暴露的 API 加入通用 API 模块中

Ⅱ. 由于通用 API 模块要定义 feign 相关的接口,所以也要导入 openfeign 的依赖。

java 复制代码
//支付微服务的feign接口
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi
{
    //新增一条支付相关流水记录
    @PostMapping("/pay/add")
    public ResultData addPay(@RequestBody PayDTO payDTO);

    //按照主键记录查询支付流水信息
    @GetMapping("/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id);

    // openfeign天然支持负载均衡演示
    @GetMapping(value = "/pay/get/info")
    public String getConsulInfo();
}

⑥ 在新版本的 80 中,修改 Controller 层,调用 OpenFeign 的接口

java 复制代码
@RestController
public class OrderController {

    @Resource
    private PayFeignApi payFeignApi;

    @GetMapping("/feign/pay/add")
    public ResultData addOrder(@RequestBody PayDTO payDTO) {
        return payFeignApi.addPay(payDTO);
    }

    @GetMapping("/feign/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id) {
        return payFeignApi.getPayInfo(id);
    }

    @GetMapping(value = "/feign/pay/get/info")
    private String getInfoByConsul(){
        return payFeignApi.getConsulInfo();
    }
}

⑦ 启动 8001 / 8002 / feign80 微服务,进行测试

可以发现,openfeign 天然支持负载均衡!

3.高级特性

(1)超时控制

在 Spring Cloud 微服务架构中,大部分公司都是利用 OpenFeign 进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问题的。

但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现 Read Timeout 这个异常,因此定制化配置超时时间就有必要了。

**说明:**OpenFeign 客户端的默认等待时间60S,超过这个时间就会报错

验证步骤:

① 在 8001 中故意写暂停 62 秒程序

java 复制代码
    @GetMapping("get/{id}")
    @Operation(summary = "按照ID查流水", description = "查询支付流水方法")
    public ResultData<Pay> getById(@PathVariable("id") Integer id) {
        if (id == -4) throw new RuntimeException("id不能为负数");

        try {
            TimeUnit.SECONDS.sleep(62);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        Pay pay = payService.getById(id);
        return ResultData.success(pay);
    }

② 在 feign80 添加捕获超时异常

java 复制代码
    @GetMapping("/feign/pay/get/{id}")
    public ResultData getPayInfo(@PathVariable("id") Integer id) {
        ResultData resultData = null;
        try {
            System.out.println("调用开始-----:" + DateUtil.now());
            resultData = payFeignApi.getPayInfo(id);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("调用结束-----:" + DateUtil.now());
            resultData = ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
        }
        return resultData;
    }

可以发现, OpenFeign 默认等待60秒钟,超过后报错!


为了避免这样的情况,有时候我们需要设置 Feign 客户端的超时控制,默认 60 秒太长或者业务时间太短都不好。

官网解释说,要在 yml 中配置两个关键参数:

|----------------|----------|
| connectTimeout | 连接超时时间 |
| readTimeout | 请求处理超时时间 |

Ⅰ. 全局配置

application.yml:

javascript 复制代码
server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          default:
            #连接超时时间(单位:ms)
            connectTimeout: 3000
            #读取超时时间(单位:ms)
            readTimeout: 3000

运行结果:

此时,openfeign 的等待时间变为了 3s。

Ⅱ. 指定配置

application.yml:

注意: 如果同时配置了全局和指定,那么听从更细粒度化的(指定配置)。

(2)重试机制

通过上述实验,我们知道,一旦请求超时,就直接结束请求。

所以,openfeign 的重试机制默认是关闭的:

Question:如何开启重试机制,使得超时之后不会直接结束请求,而是会重新尝试连接?

javascript 复制代码
//1.创建一个配置类
@Configuration
public class RetryerConfig {
    //2.配置Retryer
    @Bean
    public Retryer retryer() {
        //3,设置重试机制
        //return Retryer.NEVER_RETRY;这个是默认的
        
        //第一个参数是多长时间后开启重试机制:这里设置100ms
        //第二个参数是重试的间隔:这里设置1s一次
        //第三个参数是最大请求次数:3次【这个次数是一共的,也就是最大请求几次,而不是第一次请求失败后再请求几次】
        return new Retryer.Default(100, 1, 3);
    }
}

运行结果:

注意:

① 重试机制,一般在网络错误,服务不可用(5xx),读取超时...等情况下触发

② 控制台没有看到3次重试过程,只看到结果,这是正常的,正确的,是feign的日志打印问题

③ 8001 和 8002 都要设置超时异常,不然重试时通过负载均衡进行轮询,就会请求成功了

(3)默认 HttpClient 修改与替换

OpenFeign 允许指定连接方式,但是默认方式使用 jdk 自带的 HttpURLConnection 发送 HTTP 请求。

由于 HttpURLConnection 不支持连接池,因此性能和效率比较低。

Apache HttpClient 5 和 OkHttp都支持连接池,因此为了提升OpenFeign的性能,我们将 OpenFeign 默认的 HttpURLConnection 替换为 Apache HttpClient 5**(官网推荐!)**。

步骤:

① 在 feign80 的 pom 文件中引入 HttpClient5 和 Feign-hc5 依赖

XML 复制代码
<!-- httpclient5-->
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
    <version>13.1</version>
</dependency>

② 在 application.yml 中开启 hc5

javascript 复制代码
server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          cloud-payment-service:
            connectTimeout: 3000
            readTimeout: 3000
      # Apache HttpClient5 配置开启
      httpclient:
        hc5:
          enabled: true

运行结果:

(4)请求/响应压缩

对请求和响应进行GZIP压缩:

Spring Cloud OpenFeign 支持对请求和响应 进行GZIP压缩,以减少通信过程中的性能损耗。

通过下面的两个参数设置,就能开启请求与相应的压缩功能:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.response.enabled=true

细粒度化设置:

对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限,

只有超过这个大小的请求才会进行压缩:

spring.cloud.openfeign.compression.request.enabled=true

spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型

spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小

application.yml:

javascript 复制代码
server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          cloud-payment-service:
            connectTimeout: 3000
            readTimeout: 3000
      httpclient:
        hc5:
          enabled: true # Apache HttpClient5 配置开启
      compression:
        request:
          enabled: true #开启请求压缩
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true #开启响应压缩

压缩效果将在下面日志打印中进行体现!

(5)日志打印

Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节,说白了就是对 Feign 接口的调用情况进行监控和输出。

Feign的日志级别:

|---------|-----------------------------|
| NONE | 不记录任何日志信息,这是默认值 |
| BASIC | 仅记录请求的方法,URL以及响应状态码和执行时间 |
| HEADERS | 在BASIC的基础上,额外记录了请求和响应的头信息 |
| FULL | 记录所有请求和响应的明细,包括头信息、请求体、元数据。 |

步骤:

① 新建一个配置类,手动定义 Feign 的日志级别

java 复制代码
@Configuration
public class DefaultFeignConfig {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.FULL;//日志级别
    }
}

② 在 application.yml 文件中确定需要开启日志的 feign 客户端,将其打印级别设为 debug

公式(三段):logging.level + 含有 @FeignClient 注解的全接口名 + debug

javascript 复制代码
server:
  port: 80

spring:
  application:
    name: cloud-consumer-openfeign-order
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        prefer-ip-address: true #优先使用服务ip进行注册
        service-name: ${spring.application.name}
    openfeign:
      client:
        config:
          cloud-payment-service:
            connectTimeout: 3000
            readTimeout: 3000
      httpclient:
        hc5:
          enabled: true # Apache HttpClient5 配置开启
      compression:
        request:
          enabled: true
          min-request-size: 2048 #最小触发压缩的大小
          mime-types: text/xml,application/xml,application/json #触发压缩数据类型
        response:
          enabled: true

# feign日志以什么级别监控哪个接口
logging:
  level:
    com:
      mihoyo:
        cloud:
          apis:
            PayFeignApi: debug 

运行结果:

如果不添加请求 / 响应压缩,运行结果:

相关推荐
Ling_suu8 分钟前
Spring——单元测试
java·spring·单元测试
V搜xhliang024618 分钟前
基于深度学习的地物类型的提取
开发语言·人工智能·python·深度学习·神经网络·学习·conda
代码小鑫40 分钟前
A032-基于Spring Boot的健康医院门诊在线挂号系统
java·开发语言·spring boot·后端·spring·毕业设计
VertexGeek1 小时前
Rust学习(四):作用域、所有权和生命周期:
java·学习·rust
喔喔咿哈哈1 小时前
【手撕 Spring】 -- Bean 的创建以及获取
java·后端·spring·面试·开源·github
码农小丘1 小时前
了解springboot国际化用途以及使用
java·spring boot·spring
tian-ming1 小时前
JavaWeb后端开发知识储备1
java·spring boot·nginx·spring·maven
夏微凉.1 小时前
【JavaEE进阶】Spring AOP 原理
java·spring boot·后端·spring·java-ee·maven
杨过姑父1 小时前
org.springframework.context.support.ApplicationListenerDetector 详细介绍
java·前端·spring
理想不理想v2 小时前
使用JS实现文件流转换excel?
java·前端·javascript·css·vue.js·spring·面试