Spring Cloud 全体系学习笔记(REST、Nacos、Feign、Gateway)

Java 微服务

一、REST

REST,指的是 Re presentational S tate Transfer,表现层资源状态转移,其中涉及下面三个概念

  • 表现层 ,资源的表现形式,比如文本可以通过 txt、HTML、JSON 等形式甚至二进制的方式表现

  • 资源 ,网络上的一切都可以抽象为资源,对应唯一资源标识符(URI)

  • 状态转移 ,访问 URI,使用 HTTP 进行资源操作,对资源的增删查改都涉及到资源的状态转移

RESTful API 是指借鉴 REST 风格的接口设计,主要涉及两个概念

  • 资源 ,可以是音频、图片、视频、文本等。除二进制数据外,其他更多通过 JSON 的方式传递,通常从数据库中取到

  • 统一接口 ,RESTful 的接口,对于资源的具体操作只能从 HTTP 请求方法中看出,请求中只含有资源的种类,简单来说:接口只有名词,动词(即资源操作)体现在请求方法

REST 方法对于请求方法的要求较高,但是有的浏览器对于除 GET、POST 之外的请求方法支持不好;请求方法不在请求中直接体现,需要通过抓包的方式查看。所以依据情况选择设计风格

RestTemplate 是 Spring 提供的同步 HTTP 客户端,一般通过它调用 RESTful 的接口

二、服务注册与发现

2.1 CAP 理论

一个分布式系统,存在下面的关系图

  • P ,Partition Tolerance,分区容错性。这里的分区指的是网络分区,指的是分布式系统中各个节点沟通不畅的情况(是节点间的网络发生问题,而不是节点本身挂掉)

  • C ,Consistency,一致性,各个节点之间持有的数据是相同的

  • A ,Availability,可用性,不管何时对集群请求,总是能得到结果(正确性不保证)

根据 CAP 理论,C、A、P 是不可全部实现的

在实际操作中,网络波动是不可避免地,所以会选择实现分区容错性,那么接下来有两种架构方向:AP、CP

AP 即保证可用性,在出现网络分区时,为了保证数据可用性,仍然返回节点当前持有的数据(这意味着数据可能是错误的,或者说不是最新的),后续通过异步同步数据,最后实现数据的统一性(即 AP 架构放弃了强一致性,最终还是会实现数据一致的)

CP 即保证一致性,在发生网络分区时,选择不返回数据,等到网络分区结束并且数据同步后,再进行请求处理、数据返回

2.2 常见的注册中心

  • Eureka,AP

  • Zookeeper,CP

  • Nacos,AP/CP,默认 AP

2.3 负载均衡

Spring Cloud LoadBalancer 是 Spring Cloud 内置的负载均衡器,默认通过轮询的方式实现负载均衡(也可以改为随机的方式)

使用

给 RestTemplate 的配置加上 @LoadBalanced 注解,使用 RestTemplate 发送请求时,通过 service-name 代替 ip:port 的方式配置请求即可自动完成负载均衡的服务调用

Java 复制代码
@Component
public class BeanConfig {
    @Bean
    @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡
    RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

请求构造

Java 复制代码
String url = "http://product-service/product/" + orderInfo.getProductId();

Spring Cloud LoadBalancer 是通过 LoadBalanceInterceptor 拦截实现的,向服务注册中心获取服务并将服务名替换为 ip:port 的形式

需要注意的是,负载均衡的引入不能重复,不可以既导入 Enreka,又导入 Nacos

2.4 Eureka

2.4.1 Eureka Server
  • 导入依赖

    XML 复制代码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  • 在微服务 SpringBoot 入口启动服务注册

    Java 复制代码
    @EnableEurekaServer //开启 Eureka 服务注册
    @SpringBootApplication
    public class EurekaServiceApplication {
    public static void main(String[] args){
        SpringApplication.*run*(EurekaServiceApplication.class, args);
    }
  • 配置 Eureka 服务注册地址

    YAML 复制代码
    eureka:
      instance:
        hostname: localhost
      client:
        fetch-registry: false 
    *    *register-with-eureka: false 
    *    *service-url:
    *      *defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

其中,eureka 几项配置为:

  • fetch-registry,是否从其他 Eureka 节点获取注册信息,默认是 true,如果是单点 Eureka 的话,设置为 false

  • register-with-eureka,是否将当前服务注册到 Eureka,默认是 true。对于一个 Eureka Server 本身来说,不需要进行注册,设置为 false

  • defaultZone,设置服务注册地址,后续注册服务、获取服务列表都需要使用这个 URL

2.4.2 Eureka Client
  • 导入依赖

    XML 复制代码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  • 配置服务注册地址,注册自身服务

    YAML 复制代码
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:10010/eureka
  • RestTemplate 开启负载均衡能力

    Java 复制代码
    @Configuration
    public class BeanConfig {
        @Bean
        @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡
        public RestTemplate getRestTemplate(){
            return new RestTemplate();
        }
    }
  • 从服务中心获取服务

    Java 复制代码
    List<ServiceInstance> servers = discoveryClient.getInstances("product-service"); //获取服务实例列表
    EurekaServiceInstance server = (EurekaServiceInstance)servers.get(0); //去列表中取第一个
    String url = server.getUri() + "/product/" + orderInfo.getProductId(); //根据实例信息拼接请求,后续可以通过 RestTemplate 发起调用

2.5 Nacos

2.5.1 Docker 部署
Dockerfile 复制代码
sudo docker run -d   --name nacos   -p 8848:8848   -p 9848:9848   -e MODE=standalone   nacos/nacos-server:v2.3.2

使用 docker 部署后,进入容器可以看到 Nacos 的运行目录,如果出现问题时往往需要在这里进行处理

Shell 复制代码
diinkikot@DESKTOP-EI61K7J:~/nacos/bin$ sudo docker exec -it nacos /bin/bash
[root@79168aedf03c nacos]# ls
LICENSE  NOTICE  bin  conf  data  derby.log  logs  start.out  target  work

或者也可以在 Linux 通过 zip 压缩包解压后运行,目录如下

Shell 复制代码
diinkikot@DESKTOP-EI61K7J:~/nacos$ ls
LICENSE  LICENSE:Zone.Identifier  NOTICE  NOTICE:Zone.Identifier  bin  conf  data  logs  target

其中,bin 中包含了 nacos 启动、停止脚本,conf 中包含配置文件(如配置端口,默认为 8848,对应需同时保证 9848、9848 未被占用,data 中为服务数据

2.5.2 快速上手
  • 导入 Nacos 依赖

    XML 复制代码
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-loadbalancer</artifactId>
    </dependency>
  • 配置 Nacos 地址

    YAML 复制代码
    # application.yaml
    spring:
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
  • 进行远程调用:与 Eureka 不同,Nacos 不用通过微服务的方式运行,而是和 MySQL、Redis 一样单独运行;并且负载均衡的调用和 Eureka 的相同,因为都是通过 Spring Cloud LoadBalancer 实现负载均衡器 - 请求的转接

    • RestTemplate 开启负载均衡能力

      Java 复制代码
      @Configuration
      public class BeanConfig {
          @Bean
          @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡
          public RestTemplate getRestTemplate(){
              return new RestTemplate();
          }
      }
    • 从服务中心获取服务

      Java 复制代码
      List<ServiceInstance> servers = discoveryClient.getInstances("product-service"); //获取服务实例列表
      EurekaServiceInstance server = (EurekaServiceInstance)servers.get(0); //去列表中取第一个
      String url = server.getUri() + "/product/" + orderInfo.getProductId(); //根据实例信息拼接请求,后续可以通过 RestTemplate 发起调用
2.5.3 Nacos 负载均衡

Spring Cloud LoadBalancer 有自己的负载均衡实现方式,所以需要额外的配置让 Nacos 的权重配置、同集群优先访问生效

YAML 复制代码
spring:
    cloud:
        loadbalancer:
            nacos:
                enabled: true
  • Nacos 网页管理页面中可以下线服务、配置权重。

  • Nacos 会把同一个机房中的机器上的服务认为在同一个集群上,这样这些节点之间的访问是要快于跨局域网的,即某种程度上可以是认为是同一机房优先访问

    配置集群名称

    YAML 复制代码
    spring:
        cloud:
            discovery:
                cluster-name: SH
2.5.4 Nacos 健康检查
2.5.4.1 健康检查机制
  • 客户端主动上报,即客户端主动发送信息表明尚未下线,默认间隔 5s 发送,30s 内无响应咋认为节点下线删除节点

  • 服务端反向探测,服务端发送信息主动确认客户端的情况,20s 没有反应会将节点标记为 unhealthy,而不会下线节点

2.5.4.2 临时实例和非临时实例

Nacos 对于临时实例使用客户端主动上报,而非临时实例使用的是服务端反向探测。节点默认是临时实例,如果需要设置为非临时实例,需要如下配置

YAML 复制代码
spring:
    cloud:
        discovery:
            ephemeral: false
  1. 报错:当前节点为临时实例不可转换为非临时实例,或者当前节点为非临时实例不可转换为临时实例

    原因是 Nacos 会记录每个实例的 ip、端口,在两者都没有更改时是不允许转换的。

    解决方式:

    1. 更改端口

    2. 以 docker 为例,进入容器后,删除 /data/protocol 中的内容,之后重启容器,即可以完成转换

  2. 实例状态正常,但是显示 unhealthy

    对于非临时节点来说,Nacos 的健康检查方式是主动发送信息确认。但是对于通过阿里云内部 MSE 托管的 Nacos 来说,为了安全规范禁止服务端主动向客户端发送请求,所以误认为非临时实例下线

    解决方式:将实例设置为非临时实例

2.5.5 Nacos 服务环境隔离

通过 namespace 进行,不同命名空间的服务不能互相访问(获取实例列表)

YAML 复制代码
spring:
    cloud:
        discovery:
            namespace: namespace-id # 在 nacos 中建立 namespace,并将命名空间 id 放在这里
2.5.6 Nacos 配置中心
2.5.6.1 Nacos 服务器建立配置文件
  • 目前支持的方式有 yaml、properties(其他选项仅限于有)

  • Data-Id,对应到项目名称,比如 product-service、order-service

2.5.6.2 客户端服务器获取配置
  • 客户端相应配置都要放在 bootstrap.* 中,文件类型需要和 Nacos 服务端配置文件的类型相同,默认需要指定的有项目名称和 Nacos 服务器地址。Nacos 的服务注册中心地址和配置中心地址是在两个不同的项中

    YAML 复制代码
    spring:
      application:
        name: product-service
      profiles:
        active: dev
      cloud:
        nacos:
          config:
            server-addr: localhost:8848
  • 导入 Nacos Config 依赖

    XML 复制代码
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!-- SpringCloud 2020.*之后版本需要引入bootstrap-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
  • 在启动微服务时,会首先通过 bootstrap.yaml 尝试从 Nacos 获取配置,之后和 application.yaml/.properties 的配置进行合并

2.5.6.3 配置中心命名空间

配置中心的命名空间和服务发现的命名空间是两个不同的命令空间,但默认都是 public,同时也都可以起到隔离的作用

YAML 复制代码
spring:
  cloud:
    nacos:
      config:
        namespace: 6e61929d-a9bd-4504-9031-6ca57bc6c0b0
2.5.7 Data Id

前面提到了 Nacos 服务端的配置文件的 Data Id。Nacos 读取配置文件的完整结构为

Plain 复制代码
${spring.application.name}[-${spring.profiles.active}].[${spring.cloud.nacos.config.file-extension}]

Nacos 按照优先级读取下面三个文件

  • KaTeX parse error: Undefined control sequence: \- at position 30: ...ication\.name\}\̲-̲{spring.profiles.active}.${spring.cloud.nacos.config.file_extension}

  • KaTeX parse error: Expected group as argument to '\.' at end of input: ...ation\.name\}\.{spring.cloud.nacos.config.file_extension}

  • ${spring.application.name}

当配置内容重复时,会优先读取优先级更高的 Data Id 对应的配置文件的内容

2.5.8 AP 与 CP

Nacos 既可以 AP 架构,也可以 CP 架构,对于临时节点采取 AP 的策略,对于非临时节点采取 CP 策略

三、远程调用

3.1 快速上手

RPC ,Remote Process Calling,是指像调用本机方法一样调用远程的方法,在 Spring 中常使用 OpenFeign

  • 导入 OpenFeign 依赖

    XML 复制代码
    <dependency>    
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  • 给启动类增加 @EnableFeignClients 注解,开启 OpenFeign 功能

    Java 复制代码
    @EnableFeignClients
    @SpringBootApplication
    public class OrderServiceApplication {
        public static void main(String[] args){
            SpringApplication.*run*(OrderServiceApplication.class, args);
        }
    }
  • 编写 OpenFeign 客户端,通过 SpringMVC 的方式指定请求路径、请求参数、服务名称(Spring 自行通过 LoadBalancer 进行负载均衡),接口需要使用 @FeignClient 修饰

    Java 复制代码
    @FeignClient(path="/product", value="product-service")
    public interface ProductApi {
        @RequestMapping("/get_num")
        public Integer getNum(Integer num);
    
        @RequestMapping("/test1")
        public String test1(@RequestParam("num1") Integer num1, @RequestParam("num2") Integer num2);
    
        @RequestMapping("/test2")
        public String test2(ProductInfo productInfo);
    
        @RequestMapping("/{id}")
        public ProductInfo selectProductById(@PathVariable("id") Integer id);
    
        @RequestMapping("/test3")
        public String test3(@RequestBody ProductInfo productInfo);
    }

其中,value 指定服务名称,用于服务发现、负载均衡;path 为所有请求的前缀路径,对应 @RequestMapping 作用于 Controller 类时的效果

  • 远程调用

    Java 复制代码
    ProductApi productApi;
    
    @RequestMapping("get_num")
    Integer getNum(Integer num){
        *log*.info(num + "");
        return productApi.getNum(num);
    }

3.2 参数传递

FeignClient 起到了一个代替我们手动拼接字符串发送请求的作用,所以说参数对应的关系依然是存在的,需要满足 Spring MVC 的参数传递规则,例如

  1. 单参数传递

    Java 复制代码
    @RequestMapping("/get_num")
    public Integer getNum(Integer num)
  2. 多参数,需要使用 @RequestParam 绑定参数(默认 required = true)

    Java 复制代码
    @RequestMapping("/test1")
    public String test1(@RequestParam("num1") Integer num1, @RequestParam("num2") Integer num2);
  3. 传递对象,如果目的是将对象作为请求字符串的一部分,需要加上 @SpringQueryMap 注解,自动将类序列化后加入请求参数;否则默认将对象加入请求正文

    Java 复制代码
    @RequestMapping("/test2")
    public String test2(@SpringQueryMap ProductInfo productInfo);
  4. 传递 JSON,放在请求正文中。在服务消费者接受请求,从正文中取数据,使用 @RequestBody;服务消费者调用 Feign 客户端,同样使用 @RequestBody,不过这里的意思是告诉 FeignClient 进行远程调用的时候,将数据拼接到请求正文中。一个是取数据,一个是放数据

    Java 复制代码
    //ServiceConsumerController or ServiceProvidorController
    @RequestMapping("/test3")
    public String test3(@RequestBody ProductInfo productInfo){
    
    //FeignClient
    @RequestMapping("/test3")
    public String test3(@RequestBody ProductInfo productInfo);

3.3 抽取式和继承式

按照上面的方式,我们要根据服务提供方的接口设计单独"描绘"出来一个 FeignClient 客户端,同时不同服务调用这个服务时,都需要重复一遍上述步骤,多出了很多代码。

所以我们选择把这些接口单独抽出来,放到一个 api 模块中,这个模块只放接口和 DTO,由服务提供者实现接口中的内容,这样就解决了一个问题:每个服务调用者都要"描绘"一遍服务提供者的 Controller

现在还有一个问题,每个服务调用者的 FeighClient 呢?有两种代码构建方式

继承式 是 OpenFeign 推荐的方式,将存放接口的模块单独打成 jar 包,放到本地仓库中,然后需要调用服务的模块引入,自己的 FeignClient(使用 @FeignClient 修饰的类)继承这个接口,这样就完成了 FeignClient 的编写

抽取式 使更多实际使用的方式,在 api 模块中定义接口时,将这个接口作为 FeignClient,使用 @FeignClient 注解修饰并由服务提供者实现,将这个模块打 jar 包后分发给服务调用者;服务调用者必须要再自己定义 FeignClient,只需要在自己的启动类处通过 @EnableFeignClients 的方式指定扫描路径或者客户端类即可

Java 复制代码
@EnableFeignClients(basePackages = {"com.wjl.api"}) //指定扫描路径
@EnableFeignClients(clients = {ProductApi.class}) //指定要加载的 feign client 客户端

四、网关

前面介绍了服务注册与发现、负载均衡、远程调用,还缺少一个网关,作为整个后端服务的入口。

网关可以起到如下作用

  • 对请求进行鉴权过滤

  • 一切请求经过网关,起到动态路由的作用

  • 流量限制,按照后端服务能接受的流量放行请求

网关我们介绍 Spring Cloud Gateway,和 Eureka 一样作为一个服务在后端运行

4.1 快速上手

  • 导入依赖

    XML 复制代码
    *<!--网关-->*
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    *<!--基于nacos实现服务发现依赖-->*
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    *<!--负载均衡-->*
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
  • 添加路由配置

    YAML 复制代码
    server:
      port: 10030
    spring:
      application:
        name: gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          routes:
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**
              *#filters:*
    *            #- AddRequestParameter=username, wjl*
    *        *- id: order-service
              uri: lb://order-service
              predicates:
                - Path=/order/** # 路径匹配规则
                - After=2025-01-01T00:00:00.000+08:00[Asia/Shanghai] # 时间限制规则
    • server.port,网关的ip、端口

    • spring.cloud.gateway.routes,路由配置,后续通过 yaml 数组的语法添加多条路由配置

      • id,路由id,保证唯一即可

      • uri,只能是普通 uri ;或者通过 lb://service-name 的方式,到注册中心获取实例列表,由 Spring LoadBalancer 负载均衡的调用(lb 代表负载均衡)

      • predicates,路由条件,只有符合要求要求的请求会被路由到相应服务

4.2 Gateway Filter Factories

Predicates 决定了哪些请求需要被处理,而 filter 负责对请求处理前后增添额外的逻辑,包含两种

  • GatewayFilter,作用于一个或一组路由

    YAML 复制代码
    spring:
      cloud:
        gateway:
          routes:
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**
              *filters:*
    *            - AddRequestParameter=username, wjl*

    GatewayFilter 提供了很多 Filter,AddRequestParameter 会在所有请求中加上参数,可以后端直接获取

    这种方式作用于部分接口,可以通过 default-filters 作用于所有路由接口

    YAML 复制代码
    spring:
      cloud:
        gateway:
          default-filters:
            - AddRequestParameter=username, wjl
          routes:
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**
  • GlobalFilter,作用于所有路由

添加依赖

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加配置

YAML 复制代码
spring:
  cloud:
    gateway:
      metrics:
        enabled: true
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
    shutdown:
      enabled: true
相关推荐
Mr数据杨3 分钟前
【Codex】用学生入学评估模块建立新生能力画像
android·java·javascript·django·codex·项目开发
Bruce_Liuxiaowei18 分钟前
从基础理论开始学习人工智能(三):盲目搜索——从状态空间图到生成-测试范式
人工智能·学习
2601_9620562319 分钟前
Spring Boot 入门 与 无法解析符号 springframework 的解决
java·spring boot·后端
有梦想的骇客40 分钟前
PyTorch学习笔记
pytorch·笔记·学习
Thomas214342 分钟前
Java scala 数组 链表
java·链表·scala
亚历克斯神1 小时前
低代码与 AI 的融合趋势——从可视化搭建到自然语言生成应用
java·spring·微服务
Capricorn19881 小时前
个人知识库接入大模型频现“幻觉引用”?排查 RAG 溯源失效问题,解析知芽构建可信第三大脑的底层架构
大数据·论文阅读·人工智能·笔记·架构·论文笔记
学长毕业设计1 小时前
基于SpringBoot的瑜伽馆网站的设计与实现(源码+文档+讲解视频)
java·spring boot·后端
一嘴一个橘子1 小时前
nginx 常用命令(在 nginx 目录下执行)
java
陈年老古董1 小时前
深度学习入门笔记:从神经元到深度神经网络
人工智能·笔记·深度学习·神经网络·学习