一文搞懂SpringCloud,详解干货,做好笔记

1.微服务环境搭建

1.1.系统架构演变

随着互联网的发展,网站应用的规模也在不断的扩大,进而导致系统架构也在不断的进行变化。从互联网早起到现在,系统架构大体经历了下面几个过程: 单体应用架构 —> 垂直应用架构 —> 分布式架构 —> SOA架构 —> 微服务架构,当然还有悄然兴起的Service Mesh(服务网格化)。

1.1.1.单体应用架构

互联网早期,一般的网站应用流量较小,只需一个应用,将所有功能代码都部署在一起就可以,这样可以减少开发、部署和维护的成本。比如说一个电商系统,里面会包含很多用户管理,商品管理,订单管理,物流管理等等很多模块,我们会把它们做成一个web项目,然后部署到一台tomcat服务器上。

在这里插入图片描述

优点:

  • 项目架构简单,小型项目的话, 开发成本低
  • 项目部署在一个节点上, 维护方便

缺点:

  • 全部功能集成在一个工程中,对于大型项目来讲不易开发和维护
  • 项目模块之间紧密耦合,单点容错率低
  • 无法针对不同模块进行针对性能优化和水平扩展

1.1.2.垂直应用架构

随着访问量的逐渐增大,单一应用只能依靠增加节点来应对,但是这时候会发现并不是所有的模块都会有比较大的访问量。还是以上面的电商为例子, 用户访问量的增加可能影响的只是用户和订单模块, 但是对消息模块的影响就比较小. 那么此时我们希望只多增加几个订单模块, 而不增加消息模块. 此时单体应用就做不到了, 垂直应用就应运而生了。
所谓的垂直应用架构,就是将原来的一个应用拆成互不相干的几个应用,以提升效率。比如我们可以将上面电商的单体应用拆分成:

  • 电商系统(用户管理 商品管理 订单管理)
  • 后台系统(用户管理 订单管理 客户管理)
  • CMS系统(广告管理 营销管理)

这样拆分完毕之后,一旦用户访问量变大,只需要增加电商系统的节点就可以了,而无需增加后台和CMS的节点。

在这里插入图片描述

优点:

  • 系统拆分实现了流量分担,解决了并发问题,而且可以针对不同模块进行优化和水平扩展
  • 一个系统的问题不会影响到其他系统,提高容错率

缺点:

  • 系统之间相互独立, 无法进行相互调用
  • 系统之间相互独立, 会有重复的开发任务

1.1.3.分布式架构

当垂直应用越来越多,重复的业务代码就会越来越多。这时候,我们就思考可不可以将重复的代码抽取出来,做成统一的业务层作为独立的服务,然后由前端控制层调用不同的业务层服务呢?
这就产生了新的分布式系统架构。它将把工程拆分成表现层服务层两个部分,服务层中包含业务逻辑。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。

在这里插入图片描述

优点:

  • 抽取公共的功能为服务层,提高代码复用性

缺点:

  • 系统间耦合度变高,调用关系错综复杂,难以维护

1.1.4.SOA架构

在分布式架构下,当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心对集群进行实时管理。此时,用于资源调度和治理中心(SOA Service Oriented Architecture,面向服务的架构)是关键。

在这里插入图片描述

优点:

  • 使用注册中心解决了服务间调用关系的自动调节

缺点:

  • 服务间会有依赖关系,一旦某个环节出错会影响较大( 服务雪崩 )
  • 服务关系复杂,运维、测试部署困难

1.1.5.微服务架构

微服务架构在某种程度上是面向服务的架构SOA继续发展的下一步,它更加强调服务的"彻底拆分"。

在这里插入图片描述

优点:

  • 服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展
  • 微服务之间采用Restful等轻量级http协议相互调用

缺点:

  • 分布式系统开发的技术成本高(容错、分布式事务等)

1.2.微服务架构介绍

微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。

一旦采用微服务系统架构,就势必会遇到这样几个问题:

  • 这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])
  • 这么多小服务,他们之间如何通讯?(restful rpc)
  • 这么多小服务,客户端怎么访问他们?(网关)
  • 这么多小服务,一旦出现问题了,应该如何自处理?(容错)
  • 这么多小服务,一旦出现问题了,应该如何排错? (链路追踪)

对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。

在这里插入图片描述

1.2.1.微服务架构的常见概念

  • 服务治理

服务治理就是进行服务的自动化管理,其核心是服务的自动注册与发现。
**服务注册:**服务实例将自身服务信息注册到注册中心。
**服务发现:**服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。
服务剔除:服务注册中心将出问题的服务自动剔除到可用列表之外,使其不会被调用到。

在这里插入图片描述

  • 服务调用

在微服务架构中,通常存在多个服务之间的远程调用的需求。目前主流的远程调用技术有基于HTTPRESTful接口以及基于TCPRPC协议。

  • REST(Representational State Transfer):这是一种HTTP调用的格式,更标准,更通用,无论哪种语言都支持http协议
  • RPC(Remote Promote Call):一种进程间通信方式。允许像调用本地服务一样调用远程服务。RPC框架的主要目标就是让远程服务调用更简单、透明。RPC框架负责屏蔽底层的传输方式、序列化方式和通信细节。开发人员在使用的时候只需要了解谁在什么位置提供了什么样的远程服务接口即可,并不需要关心底层通信细节和调用过程。

区别与联系

比较项RESTfulRPC
通讯协议HTTP一般使用TCP
性能略低较高
灵活度
应用微服务架构SOA架构
  • 服务网关

随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:

  • 客户端需要调用不同的url地址,增加难度
  • 在一定的场景下,存在跨域请求的问题
  • 每个微服务都需要进行单独的身份认证
  • 针对这些问题,API网关顺势而生。

API网关直面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。

在这里插入图片描述

  • 服务容错

在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错的话,极有可能会造成一连串的服务不可用,这就是雪崩效应。我们没法预防雪崩效应的发生,只能尽可能去做好容错。服务容错的三个核心思想是:

  • 不被外界环境影响
  • 不被上游请求压垮
  • 不被下游响应拖垮

在这里插入图片描述

  • 链路追踪

随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的多个服务链路进行日志记录,性能监控即链路追踪。

在这里插入图片描述

1.2.2.微服务架构的常见解决方案

  • ServiceComb

Apache ServiceComb,前身是华为云的微服务引擎 CSE (Cloud Service Engine) 云服务,是全球首个Apache微服务顶级项目。它提供了一站式的微服务开源解决方案,致力于帮助企业、用户和开发者将企业应用轻松微服务化上云,并实现对微服务应用的高效运维管理。

在这里插入图片描述

  • SpringCloud

Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

在这里插入图片描述

  • SpringCloud Alibaba

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

在这里插入图片描述

1.3.SpringCloud Alibaba介绍

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

参考地址:
https://spring.io/projects/spring-cloud-alibaba
https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

【主要功能】

  • 服务限流降级:默认支持 WebServlet、WebFlux, OpenFeign、RestTemplate、Spring Cloud Gateway, Zuul, Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修 改限流降级规则,还支持查看限流降级 Metrics 监控。
  • **服务注册与发现:**适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持。
  • **分布式配置管理:**支持分布式系统中的外部化配置,配置更改时自动刷新。
  • **消息驱动能力:**基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • **分布式事务:**使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • **阿里云对象存储:**阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • **分布式任务调度:**提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行。
  • **阿里云短信服务:**覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

【核心组件】

  • **Sentinel:**把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳
    定性。
  • **Nacos:**一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • **RocketMQ:**一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
  • **Dubbo:**Apache Dubbo™ 是一款高性能 Java RPC 框架。
  • **Seata:**阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  • **Alibaba Cloud ACM:**一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。
  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
  • Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

1.4.快速入门

我们本次是使用的电商项目中的商品、订单、用户为案例进行讲解。

1.4.1.前期准备

【技术选型】

maven:3.5.4
数据库:MySQL 5.7
持久层:SpingData Jpa/Mybatis-plus
其他: SpringCloud Alibaba 技术栈

【模块设计】

# maven项目主模块:
springcloud-shop父工程

# springcloud项目子模块
shop-common  - 公共模块 【实体类】
shop-user    - 用户微服务 【端口: 807x】
shop-product - 商品微服务 【端口: 808x】
shop-order   - 订单微服务 【端口: 809x】

在这里插入图片描述

【微服务调用】

在微服务架构中,最常见的场景就是微服务之间的相互调用。我们以电商系统中常见的用户下单为例来演示微服务的调用:客户向订单微服务发起一个下单的请求,在进行保存订单之前需要调用商品微服务查询商品的信息。

我们一般把服务的主动调用方称为服务消费者,把服务的被调用方称为服务提供者。在这种场景下,订单微服务就是一个服务消费者, 商品微服务就是一个服务提供者。

在这里插入图片描述

1.4.2.项目搭建

  • 创建父工程

创建一个maven工程,然后在pom.xml文件中添加下面内容

<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">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.zking</groupId>
    <artifactId>springcloud-alibaba</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <!--依赖版本的锁定-->
   <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.2.RELEASE</spring-boot.version>
        <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- SpringBoot 依赖配置 -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>

    </dependencyManagement>
</project>

版本对应:

https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

在这里插入图片描述

  • 创建基础模块

创建shop-common 模块,在pom.xml中添加依赖

<?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>springcloud-alibaba</artifactId>
        <groupId>com.zking</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>shop-common</artifactId>

    <!--依赖-->
    <dependencies>
        <!-- spring boot web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- spring boot test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- mybatis plus依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>

        <!-- mybatis-plus-generator依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.6</version>
        </dependency>
    </dependencies>

</project>

在基础模块中分别创建User、Product和Order实体类:

User:

@Data
public class User {
    private Integer uid;//主键
    private String username;//用户名
    private String password;//密码
    private String telephone;//手机号
}

Product:

@Data
public class Product {
    private Integer pid;//主键
    private String pname;//商品名称
    private Double pprice;//商品价格
    private Integer stock;//库存
}

Order:

@Data
public class Order {
    private Long oid;//订单id
    //用户
    private Integer uid;//用户id
    private String username;//用户名
    //商品
    private Integer pid;//商品id
    private String pname;//商品名称
    private Double pprice;//商品单价
    //数量
    private Integer number;//购买数量
}

然后依次以SpringBoot方式创建User用户模块、Product商品模块和Order订单模块。

  • 统一配置pom.xml,不同的模块,标签配置的服务名不同:
<parent>
    <artifactId>springcloud-alibaba</artifactId>
    <groupId>com.zking</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<!-- 不同模块不同的服务名!!! -->
<artifactId>shop-order</artifactId>
<dependencies>
    <!--springboot-web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--shop-common-->
    <dependency>
        <groupId>com.zking</groupId>
        <artifactId>shop-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 统一配置application.yml,不同的模块,服务名spring.application.name和端口server.port不同:
server:
  port: 8070
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/t286?useSSL=false&useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC&characterEncoding=UTF8
    type: com.zaxxer.hikari.HikariDataSource
    username: root
    password: 1234
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: shop-order

最后使用RestTemplate的方式实现订单服务调用商品服务。

2.Nacos注册中心和服务消费方式

2.1.服务治理介绍

通过上一章的操作,我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址(ip,端口)等硬编码到了代码中,这种做法存在许多问题:

  • 一旦服务提供者地址变化,就需要手工修改代码
  • 一旦是多个服务提供者,无法实现负载均衡功能
  • 一旦服务变得越来越多,人工维护调用关系困难

那么应该怎么解决呢, 这时候就需要通过注册中心动态的实现服务治理。

什么是服务治理?

服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现

  • **服务注册:**在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳的方式去监测清单中的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务
  • **服务发现:**服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实例的访问。

在这里插入图片描述

通过上面的调用图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

  1. 服务发现:
    • 服务注册:保存服务提供者和服务调用者的信息
    • 服务订阅(发现):服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息
  2. 服务配置:
    • 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
    • 配置下发:主动将配置推送给服务提供者和服务调用者
  3. 服务健康检测
    • 检测服务提供者的健康情况,如果发现异常,执行服务剔除

常见的注册中心

  • Zookeeper:是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
  • Eureka:是Springcloud Netflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经闭源。
  • Consul:是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。
  • Nacos:是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 Spring Cloud Alibaba 组件之一,负责服务注册发现和服务配置,可以这样认为nacos=eureka+config

2.2.Nacos

2.2.1.环境搭建

**第一步:**安装Nacos

下载Nacos,参考地址:https://github.com/alibaba/nacos/releases 如下:

在这里插入图片描述

下载完成之后,直接解压到指定位置即可。

**第二步:**启动Nacos

  • 直接通过命令方式启动,并设置启动模式为单机模式(默认集群模式)
#切换目录
cd nacos/bin
#命令启动
startup.cmd -m standalone
  • 打开启动文件startup.cmd,将默认集群模式修改为单机模式(推荐)

在这里插入图片描述

**第三步:**访问Nacos

双击startup.cmd启动Nacos,打开浏览器输入访问地址 :http://localhost:8848/nacos

默认账号密码是nacos/nacos

在这里插入图片描述

2.2.2.服务注册

修改商品微服务、订单微服务,配置Nacos服务注册。

**第一步:**在shop-common模块的pom.xml中添加nacos的依赖;

<!--nacos客户端-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

**此时启动项目,将会出现报错情况。**问题在于父模块中并没有锁定SpringCloud Alibaba的版本所致,请在父模块pom.xml中配置以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>${spring-cloud.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>${spring-cloud-alibaba.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

**第二步:**分别在商品微服务、订单微服务启动类上添加@EnableDiscoveryClient注解;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class ShopOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(ShopOrderApplication.class, args);
    }
}

**第三步:**分别修改商品微服务、订单微服务的application.yml文件;

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

**第四步:**启动服务,观察Nacos的控制面板中是否已经存在注册的商品和订单微服务;

在这里插入图片描述

以上四个步骤基本上已完成了服务注册相关配置。

2.3.负载均衡

2.3.1.什么是负载均衡

通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。根据负载均衡发生位置的不同,一般分为服务端负载均衡和客户端负载均衡

  • 服务端负载均衡指的是发生在服务提供者一方,比如常见的Nginx负载均衡;
  • 客户端负载均衡指的是发生在服务请求(消费者)的一方,也就是在发送请求之前已经选好了由哪个实例处理请求;

我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行。

2.3.2.Ribbon实现负载均衡

RibbonSpring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡。

**第一步:**在RestTemplate 的生成方法上添加@LoadBalanced注解

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
	return new RestTemplate();
}

**第二步:**修改订单微服务调用的方法

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/addOrder/{pid}")
    public Order addOrder(@PathVariable("pid") Integer pid){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
        String serverName="shop-product";
        //订单微服务远程调用商品微服务
        Product pro = restTemplate.getForObject(
                "http://"+serverName+"/product/detail/" + pid,
                Product.class);
        log.info(">>商品信息,查询结果:" + JSON.toJSONString(pro));
        Order order=new Order();
        order.setOid(1L);
        order.setNumber(1);
        order.setPid(pid);
        order.setPname(pro.getPname());
        order.setPprice(pro.getPprice());
        order.setUsername("zs");
        order.setUid(2);
        return order;
    }
}

**第三步:**通过idea开启双节点的商品微服务(shop-product),并设置其端口为:8082

在这里插入图片描述

**第四步:**启动双节点的商品微服务,并在Nacos中查看微服务的启动情况:

在这里插入图片描述

最后,通过访问订单微服务接口进行测试,其中可以停掉双节点商品微服务接口用于测试Ribbon负载均衡效果。

Ribbon支持的负载均衡策略

Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为com.netflix.loadbalancer.IRule ,具体的负载策略如下图所示:

策略策略名称实现原理
RoundRobinRule轮询策略(默认)轮询策略表示每次都顺序取下一个 provider,比如一共有 5 个provider,第 1 次取第 1 个,第 2次取第 2 个,第 3 次取第 3 个,以此类推。
WeightedResponseTimeRule权重轮询策略1.根据每个 provider 的响应时间分配一个权重,响应时间越长,权重越小,被选中的可能性越低。
2.原理:一开始为轮询策略,并开启一个计时器,每 30 秒收集一次每个 provider 的平均响应时间,当信息足够时,给每个 provider附上一个权重,并按权重随机选择provider,高权越重的 provider会被高概率选中。
RandomRule随机策略从 provider 列表中随机选择一个provider
BestAvailableRule最少并发数策略选择正在请求中的并发数最小的 provider,除非这个provider 在熔断中。
RetryRule在“选定的负载均衡策略”基础上进行重试机制1.“选定的负载均衡策略”这个策略是轮询策略RoundRobinRule
2.该重试策略先设定一个阈值时间段,如果在这个阈值时间段内当选择 provider不成功,则一直尝试采用“选定的负载均衡策略:轮询策略”最后选择一个可用的provider
AvailabilityFilteringRule可用性敏感策略过滤性能差的 provider,有 2种:
第一种:过滤掉在 eureka 中处于一直连接失败 provider
第二种:过滤掉高并发的 provider
ZoneAvoidanceRule区域敏感性策略1.以一个区域为单位考察可用性,对于不可用的区域整个丢弃,从剩下区域中选可用的provider
2.如果这个 ip 区域内有一个或多个实例不可达或响应变慢,都会降低该 ip 区域内其他 ip 被选中的权重。

我们可以通过修改配置来调整Ribbon的负载均衡策略,具体代码如下

# 调用的提供者的名称
shop-product: 
	ribbon:
		NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

2.3.3.DiscoveryClient实现负载均衡

**第一步:**再次修改shop-order的代码,实现DiscoveryClient负载均衡:

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private DiscoveryClient discoveryClient;

    @RequestMapping("/addOrder/{pid}")
    public Order addOrder(@PathVariable("pid") Integer pid){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
        String serverName="shop-product";
        List<ServiceInstance> instances = discoveryClient.getInstances(serverName);
        //通过随机数的方式来实现服务随机挑选
        int index=new Random().nextInt(instances.size());
        //根据得到服务下标获取服务
        ServiceInstance serviceInstance = instances.get(index);
        //根据得到服务下标拼接服务地址
        //例如:127.0.0.1:8080 或者 localhost:8080
        String url=serviceInstance.getHost()+":"+serviceInstance.getPort();
        log.info("订单服务地址:"+url);
        //订单微服务远程调用商品微服务
        Product pro = restTemplate.getForObject(
                "http://"+url+"/product/detail/" + pid,
                Product.class);
        log.info(">>商品信息,查询结果:" + JSON.toJSONString(pro));
        Order order=new Order();
        order.setOid(1L);
        order.setNumber(1);
        order.setPid(pid);
        order.setPname(pro.getPname());
        order.setPprice(pro.getPprice());
        order.setUsername("zs");
        order.setUid(2);
        return order;
    }
}

**第二步:**启动两个服务提供者和一个服务消费者,多访问几次消费者测试效果

在这里插入图片描述

2.4.Feign服务消费

2.4.1.什么是Feign

Feign是一个声明式的、基于注解的HTTP客户端框架,用于简化构建基于HTTP的服务调用。它是Netflix开源的一部分,旨在简化在Java应用程序中编写可读性高、可维护性强的HTTP调用代码。

Feign的主要特点和功能包括:

  • 声明式的API定义:通过使用Feign的注解,开发人员可以在接口上定义HTTP请求的细节,包括URL、请求方法、请求体、请求头等。这样可以使得HTTP调用的代码更加直观和易于理解。
  • 集成了RibbonEurekaFeignNetflix的负载均衡器Ribbon和服务发现组件Eureka集成,可以通过服务名进行服务的负载均衡和自动服务发现。
  • 支持多种编码器和解码器:Feign内置了对多种数据格式的编码器和解码器的支持,包括JSON、XML等,使得开发人员无需手动处理序列化和反序列化的逻辑。
  • 支持熔断器:Feign可以与Netflix的熔断器Hystrix集成,提供服务调用的熔断、降级和容错能力,增加系统的可靠性和弹性。
  • 可扩展性:Feign提供了一些扩展点,开发人员可以根据需求自定义和扩展Feign的功能,如编码器、解码器、拦截器等。

使用Feign可以大大简化基于HTTP的服务调用的代码编写工作,使得开发人员可以更专注于业务逻辑而不是网络调用的细节。它与Spring Cloud等微服务框架集成紧密,是构建和管理微服务架构中的常用组件之一。

2.4.2.Feign入门

**第一步:**在公共模块shop-common中添加Feign依赖

<!--fegin组件-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

**第二步:**在消费者服务(订单微服务)启动类上加入Feign注解

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients//开启Fegin
public class OrderApplication {}

**第三步:**创建一个Service,并使用Feign实现微服务调用

//指定消息的服务名称,与生产者服务中的applicaiton.yml中的spring.application.name一致
@FeignClient("shop-product")
public interface IProductService {

    /**
     * http://shop-product/product/detail/10
     * 第一次:http://192.168.158.1:8080/product/detail/10
     * 第二次:http://192.168.158.1:8081/product/detail/10
     * 第三次:http://192.168.158.1:8080/product/detail/10
     * 注意:
     * 如果生产者服务接口提供了模块名,则使用@RequestMapping时必须加入!!!
     * @param pid
     * @return
     */
    @RequestMapping("/product/detail/{pid}")
    Product detail(@PathVariable("pid") Integer pid);
}

**第四步:**修改OrderController代码,并启动验证

@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private IProductService productService;

    @RequestMapping("/addOrder/{pid}")
    public Order addOrder(@PathVariable("pid") Integer pid){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");

        //通过fegin调用商品微服务
        Product product = productService.detail(pid);
        log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
        Order order = new Order();
        Order order=new Order();
        order.setOid(1L);
        order.setNumber(1);
        order.setPid(pid);
        order.setPname(pro.getPname());
        order.setPprice(pro.getPprice());
        order.setUsername("zs");
        order.setUid(2);
        return order;
    }
}

最后,重启微服务,查看效果。

2.4.3.参数传递

**第一步:**在商品微服务中创建FeignDemoController,定义提供多种不同传参类型的方法:

@Slf4j
@RestController
@RequestMapping("/feign")
public class FeignDemoController {
    @RequestMapping("/findByParameter")
    public String findByParameter(String name,Double price){
        log.info("服务提供者日志:{}",name);
        return "hello:"+name;
    }
    @RequestMapping("/findByParameter2")
    public String findByParameter2(
            @RequestParam("name") String name,
            @RequestParam("price") Double price){
        log.info("服务提供者日志:{},{}",name,price);
        return "hello:"+name+price;
    }
    @RequestMapping("/findByPathVariable/{name}")
    public String findByPathVariable(@PathVariable("name") String name){
        log.info("服务提供者日志:{}",name);
        return "hello:"+name;
    }	
    @RequestMapping("/findByRequestBody")
    public Product findByRequestBody(@RequestBody Product product){
        log.info("服务提供者日志:{}",product.getPname());
        return product;
    }
}

**第二步:**在订单微服务中再次创建一个Service,定义Feign接口

此处省略万一字…

**第三步:**在订单微服务中创建Controller接口,用于Feign接口传参测试

此处省略万一字…

**第四步:**启动项目,测试接口

此时启动项目,可能会引发错误导致启动失败(bean重复定义问题)。通过配置application.yml,来解决问题:

spring:
  main:
    allow-bean-definition-overriding: true

Feign传参的注意事项:

  • FeignClient接口,不能使用@GettingMapping之类的组合注解;
  • FeignClient接口中,如果使用到@PathVariable必须指定其value
  • 只要参数是复杂对象,即使指定了是GET方法,Feign依然会以POST方法进行发送请求,同时生产者必须支持POST请求并给参数添加@RequestBody注解。建议使用公共vo+@RequestBody方式;
  • springcloudfeign访问其他服务并传参数出现错误的问题:status 405 reading LogisticsOrderService#getLogistics(Integer,String,Integer,Integer)当使用feign传参数的时候,需要加上@RequestParam注解,否则对方服务无法识别参数;

3.使用Nacos作为配置中心

3.1.配置中心介绍

3.1.1.前言

首先我们来看一下,微服务架构下关于配置文件的一些问题:

  • 配置文件相对分散。在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理。
  • 配置文件无法区分环境。微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环境。每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动维护,这比较困难。
  • 配置文件无法实时更新。我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一个正在运行的项目来说是非常不友好的。

基于上面这些问题,我们就需要配置中心的加入来解决这些问题。

3.1.2.实现思路

  • 首先把项目中各种配置全部都放到一个集中的地方进行统一管理,并提供一套标准的接口。
  • 当各个服务需要获取配置的时候,就来配置中心的接口拉取自己的配置。
  • 当配置中心中的各种参数有更新的时候,也能通知到各个服务实时的过来同步最新的信息,使之动态更新。

当加入了服务配置中心之后,我们的系统架构图会变成下面这样:

在这里插入图片描述

3.1.3.常见的配置中心

  • Apollo:是由携程开源的分布式配置中心。特点有很多,比如:配置更新之后可以实时生效,支持灰度发布功能,并且能对所有的配置进行版本管理、操作审计等功能,提供开放平台API。并且资料也写的很详细。
  • Disconf:是由百度开源的分布式配置中心。它是基于Zookeeper来实现配置变更后实时通知和生效的。
  • SpringCloud Config:这是Spring Cloud中带的配置中心组件。它和Spring是无缝集成,使用起来非常方便,并且它的配置存储支持Git。不过它没有可视化的操作界面,配置的生效也不是实时的,需要重启或去刷新。
  • Nacos:这是SpingCloud alibaba技术栈中的一个组件,前面我们已经使用它做过服务注册中心。其实它也集成了服务配置的功能,我们可以直接使用它作为服务配置中心。

3.2.Nacos Config

3.2.1.快速入门

中心思想: 使用Nacos作为配置中心,其实就是将Nacos当做一个服务端,将各个微服务看成是客户端,我们将各个微服务的配置文件统一存放在Nacos上,然后各个微服务从Nacos上拉取配置即可。

接下来我们以商品微服务为例,学习Nacos Config的使用。

**第一步:**启动Nacos服务;

**第二步:**在微服务公共模块shop-common中引入Nacos配置中心依赖;

<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

由于配置中心的依赖在多个微服务中都需要引入,因此建议将其加入到common公共模块中。

**第三步:**在商品微服务中添加Nacos Config的配置;

spring:
  application:
    name: shop-product
  cloud:
    nacos:
      # Nacos注册中心地址
      discovery:
        server-addr: nacos.com:8848
      # Nacos配置中心地址
      config:
        server-addr: nacos.com:8848 
        # 配置文件格式
        file-extension: yml 
  profiles:
    active: dev # 环境标识

配置文件优先级(由高到低):
bootstrap.properties->bootstrap.yml->application.properties->application.yml

**注意:**在bootstrap和application数据项相同时,bootstrap中的配置不会被覆盖;

配置完成之后,请启动微服务shop-product并查看控制台日志信息:

在这里插入图片描述

这个时候可以很明显发现启动项目之后,已经开始从Nacos配置中心拉去相应配置信息了,只是现在还没有到Nacos配置中心去配置而已。

**说明:**之所以需要配置 spring.application.name ,是因为它是构成 Nacos 配置管理 dataId字段的一部分。在 Nacos Spring Cloud 中,dataId 的完整格式如下:

${prefix}-${spring.profiles.active}.${file-extension} 
  • prefix:默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix来配置
  • spring.profiles.active:即为当前环境对应的 profile注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId 的拼接格式变
  • file-exetension:为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 propertiesyaml 类型。

**第四步:**在Nacos中添加配置文件;

点击配置列表,点击右边+号,新建配置。在新建配置过程中,要注意下面的细节:

  • Data ID不能随便写,要跟配置文件中的对应,对应关系如图所示;
  • 配置文件格式要跟配置文件的格式对应,且目前仅仅支持YAML和Properties
  • 配置内容按照上面选定的格式书写

在这里插入图片描述

添加完成之后,重启商品微服务,查看控制台日志输出情况。

**第五步:**入门案例

在商品微服务中创建Controller,动态从Nacos配置中心获取配置信息。

@RestController
public class NacosConfigController {
    
    //示例一:Nacos快速入门
    //1)方式一:硬编码方式
	@Autowired
	private ConfigurableApplicationContext applicationContext;
	
	@GetMapping("/nacos-config-test1")
	public String nacosConfingTest1() {
		return applicationContext.getEnvironment().getProperty("config.appName");
	}
	
	//2)方式二:注解方式(推荐)
	@Value("${config.appName}")
	private String appName;
	
	@GetMapping("/nacos-config-test2")
	public String nacosConfingTest2() {
		return appName;
	}
}

3.2.2.动态刷新

在入门案例中,我们实现了配置的远程存放,但是此时如果修改了配置,我们的程序是无法读取到的,因此,我们需要开启配置的动态刷新功能。

@RestController
@RefreshScope//只需要在需要动态读取配置的类上添加此注解就可以
public class NacosConfigController {
	...
}

加完配置后,请重启项目;在Nacos配置中心里面直接修改配置项,然后在浏览器中重新刷新接口查看结果。

3.2.3.共享配置

当配置越来越多的时候,我们就发现有很多配置是重复的,这时候就考虑可不可以将公共配置文件提取出来,然后实现共享呢?当然是可以的。接下来我们就来探讨如何实现这一功能。

  • 同一个微服务下的配置共享

如果想在同一个微服务的不同环境之间实现配置共享,其实很简单。只需要提取一个以spring.application.name 命名的配置文件,然后将其所有环境的公共配置放在里面即可。

**第一步:**新建一个名为shop-product.yml配置存放商品微服务的公共配置

**第二步:**添加测试方法

@RestController
@RefreshScope//动态刷新的注解
public class NacosConfigController {

	//示例二:同一个微服务下不同环境之间的配置共享
    @Value("${nacos.appName}")
    private String appName;

    @RequestMapping("/nacosConfigTest3")
    public String nacosConfigTest3(){
        return "同一个微服务下不同环境之间的配置共享:"+appName;
    }
}

重启项目,访问测试。

**第三步:**同一个微服务中不同环境下的共享配置

spring:
  profiles:
    active: test # 环境标识,例如:test/dev/prod

接下来,修改bootstrap.yml中的配置,将active设置成test,再次访问,观察结果。

不管如何切换微服务的环境都能成功获取Nacos配置中心的shop-product.yml文件配置内容,从而实现不同环境下的共享配置。

  • 不同微服务中的配置共享

不同微服务之间实现配置共享的原理类似于文件引入,就是定义一个公共配置,然后在当前配置中引
入即可实现效果。

**第一步:**在Nacos中定义一个DataIDall-service.yml的配置,用于所有微服务共享;

**第二步:**修改商品微服务的bootstrap.yml文件;

spring:
  cloud:
     ...
     config:
      	server-addr: nacos.com:8848
        file-extension: yml # 配置文件格式
        extension-configs[0]:
          data-id: all-service.yml
          group: DEFAULT_GROUP
          refresh: true
        shared-configs[0]:
          data-id: all-service.yml
          group: DEFAULT_GROUP
          refresh: true

extension-configs的优先级高于shared-configs

**第三步:**添加测试方法

@RestController
@RefreshScope//动态刷新的注解
public class NacosConfigController {
	
	//示例三:不同微服务下的配置共享
    @Value("${nacos.os}")
    private String os;

    @RequestMapping("/nacosConfigTest4")
    public String nacosConfigTest4(){
        return "【shop-product】不同微服务下的配置共享:"+os;
    }

}

最后,启动项目测试接口。

3.3.核心概念

  • 命名空间(Namespace):可用于进行不同环境的配置隔离。一般一个环境划分到一个命名空间
  • 配置分组(Group):用于将不同的服务可以归类到同一分组。一般将一个项目的配置分到一组
  • 配置集(Data ID):在系统中,一个配置文件通常就是一个配置集。一般微服务的配置就是一个配置集

在这里插入图片描述

**第一步:**创建命名空间(Namespace

在这里插入图片描述

如果没有出现命名空间ID(示填则自动生成),注意可能需要清空缓存。

**第二步:**修改商品微服务bootstrap.yml文件,配置命名空间和分组

spring:
  application:
    name: shop-product  # 微服务服务名称
  cloud:
    nacos:
      # 配置nacos注册中心地址
      discovery:
        server-addr: nacos.com:8848
      # 配置nacos配置中心地址
      config:
        server-addr: nacos.com:8848
        file-extension: yml # 配置文件格式
        extension-configs[0]:
          data-id: all-service.yml
          group: DEFAULT_GROUP
          refresh: true
        shared-configs[0]:
          data-id: all-service.yml
          group: DEFAULT_GROUP
          refresh: true
        group: SHOP_GROUP
        namespace:a4776bdc-6683-4d0f-a971-67553404c618
  profiles:
    active: dev # 环境标识,dev/test/prod

这里需要注意,由于切换了命名空间和分组可能会导致分布属性配置在Nacos中不存在,这时为了防止访问接口时出现报错情况,建议在bootstrap.yml配置文件中加入本地配置项。

nacos:
  config: location nacos config
  appName: location nacos appName
  os: location nacos os

其目的是远程有读取远程配置,远程没有读取本地配置。

最后,再次重启项目访问接口进行测试。

3.4.多环境切换

在这里插入图片描述

通过Idea配置Edit Configurations...,将商品微服务配置成双节点,并修改spring.cloud.nacos.config.namespace命名空间配置从而实现多环境的切换:

在这里插入图片描述

最后,再次重启项目访问接口进行测试。

4.服务网关

4.1.Gateway网关服务

4.1.1.前言

大家都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。

在这里插入图片描述

这样的架构,会存在着诸多的问题:

  • 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
  • 认证复杂,每个服务都需要独立认证。
  • 存在跨域请求,在一定场景下处理相对复杂。

针对上面的这些问题可以借助API网关来解决。

所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证鉴权、监控、路由转发等等。添加上API网关之后,系统的架构图变成了如下所示:

在这里插入图片描述

我们也可以观察下,我们现在的整体架构图:

在这里插入图片描述

4.1.2.常见的网关

  • Ngnix+lua:用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用
    lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本
  • Kong:基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。 问题:只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。
  • **Zuul Netflix:**开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺乏管控,无法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx
  • Spring Cloud GatewaySpring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。

注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关。

4.1.3.Gateway简介

Spring Cloud GatewaySpring公司基于Spring 5.0Spring Boot 2.0Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控和限流。

优点:

  • 性能强劲:是第一代网关Zuul的1.6倍
  • 功能强大:内置了很多实用的功能,例如转发、监控、限流
  • 设计优雅,容易扩展

缺点:

  • 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
  • 不能将其部署在TomcatJettyServlet容器里,只能打成jar包执行
  • 需要Spring Boot 2.0及以上的版本,才支持

4.1.4.快速入门

实现目标: 通过浏览器访问API网关,然后通过网关将请求转发到商品微服务

  • 【基础版】

**第一步:**创建一个api-gateway 的模块并导入相关依赖

<?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>springcloud-alibaba</artifactId>
        <groupId>com.zking</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>api-gateway</artifactId>

    <dependencies>
        <!--gateway 注意 此模式不能引入starter-web -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

    </dependencies>

</project>

注意:这里api-gateway模块将不引入公共模块中的依赖。

**第二步:**配置application.yml

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes: # 路由数组[路由 就是指定当请求满足什么条件的时候转到哪个微服务]
        - id: product_route # 当前路由的标识, 要求唯一
          uri: http://localhost:8080/ # 请求要转发到的地址
          order: 1 # 路由的优先级,数字越小级别越高
          predicates: # 断言(就是路由转发要满足的条件)
            - Path=/csdn/** # 当请求路径满足Path指定的规则时,才进行路由转发
          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1 # 转发之前去掉1层路径

第三步: 启动项目, 并通过网关去访问微服务

在这里插入图片描述

  • 【增强版】

现在在配置文件中写死了转发路径的地址, 前面我们已经分析过地址写死带来的问题, 接下来我们从注册中心获取此地址。

**第一步:**添加Nacos依赖

<!--nacos客户端-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

**第二步:**在启动类上添加@EnableDiscoveryClient注解

@SpringBootApplication
@EnableDiscoveryClient
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class);
    }
}

**第三步:**修改application.yml

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: nacos.com:8848
    gateway:
      routes:
        - id: product_route
          uri: lb://shop-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
          predicates:
            - Path=/product-serv/**
          filters:
            - StripPrefix=1

**第四步:**启动测试

在这里插入图片描述

  • 【简写版】

**第一步:**去掉关于路由的配置

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: nacos.com:8848
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务

如果是spring cloud alibaba 2021.1需要额外导入以下依赖:

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

**第二步:**启动项目,并通过网关去访问微服务

在这里插入图片描述

这时候,就发现只要按照网关地址/微服务/接口的格式去访问,就可以得到成功响应。

4.1.5.跨域请求

Spring Cloud Gateway还针对跨域访问做了设计,可以使用以下配置解决跨域访问问题:

spring:
  cloud:
    gateway:
      # 跨域
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedHeaders: "*"
            allowedOrigins: "*"
            allowedMethods: "*"

allowedHeaders: 头部充许字段
allowedOrigins:充许地址
allowedMethods:允许请求方法

4.2.Gateway核心架构

4.2.1.基本概念

路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个
信息:

  • id,路由标识符,区别于其他 Route
  • uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
  • order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
  • predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
  • filter,过滤器用于修改请求和响应信息。

4.2.2.执行流程

在这里插入图片描述

执行流程大体如下:

  1. Gateway ClientGateway Server发送请求
  2. 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文
  3. 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给
    RoutePredicateHandlerMapping
  4. RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用
  5. 如果过断言成功,由FilteringWebHandler创建过滤器链并调用
  6. 请求会一次经过PreFilter--微服务--PostFilter的方法,最终返回响应

4.3.断言

Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。断言就是说: 在 什么条件下 才能进行路由转发

4.3.1.内置路由断言工厂

SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下:

  • 基于Datetime类型的断言工厂
    此类型的断言根据时间做判断,主要有三个:
    AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期
    BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期
    BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内

    -After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]

  • 基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主
    机地址是否在地址段中

    -RemoteAddr=192.168.1.1/24

  • 基于Cookie的断言工厂
    CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求
    cookie是否具有给定名称且值与正则表达式匹配。

    -Cookie=chocolate, ch.

  • 基于Header的断言工厂
    HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否
    具有给定名称且值与正则表达式匹配。

    -Header=X-Request-Id, \d+

  • 基于Host的断言工厂
    HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。

    -Host=.testhost.org

  • 基于Method请求方法的断言工厂
    MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。

    -Method=GET

  • 基于Path请求路径的断言工厂

    PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。

    -Path=/foo/{segment}

  • 基于Query请求参数的断言工厂
    QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具
    有给定名称且值与正则表达式匹配。

    -Query=baz, ba.

  • 基于路由权重的断言工厂
    WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发

    routes:
    -id: weight_route1 uri: host1 predicates:
    -Path=/product/
    -Weight=group3, 1
    -id: weight_route2 uri: host2 predicates:
    -Path=/product/**
    -Weight= group3, 9

内置路由断言工厂的使用,接下来我们验证几个内置断言的使用:

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: nacos.com:8848
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
      routes:
        - id: product_route
          uri: lb://shop-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
          predicates:
            - Path=/product-serv/**
            - Query=name,zk.
            - Method=POST #限制请求方式为POST
          filters:
            - StripPrefix=1

4.3.2.自定义路由断言工厂

我们来设定一个场景:假设我们的应用仅仅让age(min,max)之间的人来访问。

**第一步:**修改application.yml,添加一个Age的断言配置

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: nacos.com:8848
    gateway:
      discovery:
        locator:
          enabled: true # 让gateway可以发现nacos中的微服务
      routes:
        - id: product_route
          uri: lb://shop-product # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
          predicates:
            - Path=/product-serv/**
            - Age=18,60
          filters:
            - StripPrefix=1

**第二步:**自定义一个断言工厂,实现断言方法

package com.zking.predicates;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;

//这是一个自定义的路由断言工厂类,要求有两个
//1 名字必须是 配置+RoutePredicateFactory
//2 必须继承AbstractRoutePredicateFactory<配置类>
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
    
    public AgeRoutePredicateFactory() {
        super(AgeRoutePredicateFactory.Config.class);
    }

    //用于从配置文件中获取参数值赋值到配置类中的属性上
    @Override
    public List<String> shortcutFieldOrder() {
         //这里的顺序要跟配置文件中的参数顺序一致
        return Arrays.asList("minAge", "maxAge");
    }

    //断言
    @Override
    public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config
                                                      config) {
        return new Predicate<ServerWebExchange>() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                //从serverWebExchange获取传入的参数
                String ageStr =
                        serverWebExchange.getRequest().getQueryParams().getFirst("age");
                if (StringUtils.isNotEmpty(ageStr)) {
                    int age = Integer.parseInt(ageStr);
                    return age > config.getMinAge() && age < config.getMaxAge();
                }
                return true;
            }
        };
    }
    //配置类,用于接收配置文件中的对应参数
    @Data
    @NoArgsConstructor
    public static class Config {
        private int minAge;//18
        private int maxAge;//60
    }
}

**第三步:**启动测试

测试发现当age在(20,60)可以访问,其它范围不能访问:

http://localhost:7000/product-serv/product/1?age=30
http://localhost:7000/product-serv/product/1?age=10

4.4.过滤器

三个知识点:

  • 作用:过滤器就是在请求的传递过程中,对请求和响应做一些手脚
  • 生命周期:Pre和Post
  • 分类:局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上)

Gateway中, Filter的生命周期只有两个:Pre和Post

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择
    请求的微服务、记录调试信息等。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP
    Header、收集统计信息和指标、将响应从微服务发送给客户端等。

在这里插入图片描述

GatewayFilter从作用范围可分为两种: GatewayFilterGlobalFilter

  • GatewayFilter:应用到单个路由或者一个分组的路由上。
  • GlobalFilter:应用到所有的路由上。

4.4.1.局部过滤器

局部过滤器是针对单个路由的过滤器。

1)内置局部过滤器

SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。具体如下:

  • AddRequestHeader GatewayFilter Factory:通过配置namevalue可以增加请求的header
spring:  
	cloud:    
		gateway:   
        	routes: 
            - id: add_request_header_route  
              uri: http://www.google.com  
              filters:
              	- AddRequestHeader=X-Request-Foo, Bar

对匹配的请求,会额外添加X-Request-Foo:Barheader

  • AddRequestParameter GatewayFilter Factory:通过配置namevalue可以增加请求的参数。
spring:  
	cloud:  
    	gateway:   
        	routes:      
        		- id: add_request_parameter_route     
                uri: http://www.google.com     
                filters:       
                	- AddRequestParameter=foo, bar

对匹配的请求,会额外添加foo=bar的请求参数。

  • AddResponseHeader GatewayFilter Factory:通过配置namevalue可以增加响应的header
spring:  
    cloud:   
        gateway:  
            routes:      
                - id: add_request_header_route    
                  uri: http://www.google.com     
                  filters:        
                	- AddResponseHeader=X-Response-Foo, Bar
  • Hystrix GatewayFilter Factory

HystrixNetflix实现的断路器模式工具包,The Hystrix GatewayFilter就是将断路器使用在gateway的路由上,目的是保护你的服务避免级联故障,以及在下游失败时可以降级返回。

**第一步:**项目里面引入spring-cloud-starter-netflix-hystrix依赖,并提供HystrixCommand的名字,即可生效Hystrix GatewayFilter

**第二步:**修改 application.yml

spring:  
	cloud:    
		gateway:      
			routes:     
                - id: hystrix_route      
                uri: http://www.google.com     
                filters:        
                - Hystrix=myCommandName

那么剩下的过滤器,就会包装在名为myCommandNameHystrixCommand中运行。

Hystrix过滤器也是通过配置可以参数fallbackUri,来支持路由熔断后的降级处理,降级后,请求会跳过fallbackUri配置的路径,目前只支持forward:URI协议。

spring:
  cloud:
    gateway:
      routes:
      - id: hystrix_route
        uri: lb://backing-service:8088
        predicates:
        	- Path=/consumingserviceendpoint
        filters:
        	- name: Hystrix
          	  args:
            	name: fallbackcmd
            	fallbackUri: forward:/incaseoffailureusethis

Hystrix降级后就会将请求转发到/incaseoffailureusethis

整个流程其实是用fallbackUri将请求跳转到gateway内部的controller或者handler,然而也可以通过以下的方式将请求转发到外部的服务:

spring:
  cloud:
    gateway:
      routes:
      - id: ingredients
        uri: lb://ingredients
        predicates:
            - Path=//ingredients/**
        filters:
            - name: Hystrix
              args:
                name: fetchIngredients
                fallbackUri: forward:/fallback
              - id: ingredients-fallback
                uri: http://localhost:9994
                predicates:
                - Path=/fallback

以上的例子,gateway降级后就会将请求转发到http://localhost:9994。

Hystrix Gateway filter在转发降级请求时,会将造成降级的异常设置在ServerWebExchangeUtils.HYSTRIX_EXECUTION_EXCEPTION_ATTR属性中,在处理降级时也可以用到。

比如下一节讲到的FallbackHeaders GatewayFilter Factory,就会通过上面的方式拿到异常信息,设置到降级转发请求的header上,来告知降级下游异常信息。

通过下面配置可以设置Hystrix的全局超时信息:

hystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 5000

  • FallbackHeaders GatewayFilter Factory:可以将Hystrix执行的异常信息添加到外部请求的fallbackUriheader上:
spring:
  cloud:
    gateway:
      routes:
      - id: ingredients
        uri: lb://ingredients
        predicates:
        	- Path=//ingredients/**
        filters:
            - name: Hystrix
              args:
                name: fetchIngredients
                fallbackUri: forward:/fallback
      - id: ingredients-fallback
        uri: http://localhost:9994
        predicates:
        	- Path=/fallback
        filters:
            - name: FallbackHeaders
              args:
                executionExceptionTypeHeaderName: Test-Header

在这个例子中,当请求lb://ingredients降级后,FallbackHeadersfilter会将HystrixCommand的异常信息,通过Test-Header带给http://localhost:9994服务。

你也可以使用默认的header,也可以像上面一下配置修改header的名字:

  • executionExceptionTypeHeaderName ("Execution-Exception-Type")

  • executionExceptionMessageHeaderName ("Execution-Exception-Message")

  • rootCauseExceptionTypeHeaderName ("Root-Cause-Exception-Type")

  • rootCauseExceptionMessageHeaderName ("Root-Cause-Exception-Message")

  • PrefixPath GatewayFilter Factory:通过设置prefix参数来路径前缀。

spring:
    cloud:
        gateway:
            routes:
                - id: prefixpath_route
                  uri: http://www.google.com
                  filters:
                	 - PrefixPath=/mypath

内置局部过滤器的使用

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: nacos.com:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: product_route
          uri: lb://service-product
          predicates:
            - Path=/product-serv/**
          filters:
            - StripPrefix=1
            - SetStatus=2000 # 修改返回状态

2)自定义局部过滤器

**第一步:**在配置文件中,添加一个Log的过滤器配置

server:
  port: 7000
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: product_route
          uri: lb://shop-product
          predicates:
            - Path=/product-serv/**
          filters:
            - StripPrefix=1
            - Log=true,false # 控制日志是否开启

**第二步:**自定义一个过滤器工厂,实现方法

package com.zking.filters;

import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;

//自定义局部过滤器
@Component
public class LogGatewayFilterFactory
        extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {

    //构造函数
    public LogGatewayFilterFactory() {
        super(Config.class);
    }

    //读取配置文件中的参数 赋值到 配置类中
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("consoleLog", "cacheLog");
    }

    //过滤器逻辑
    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if (config.isCacheLog()) {
                    System.out.println("cacheLog已经开启了....");
                }
                if (config.isConsoleLog()) {
                    System.out.println("consoleLog已经开启了....");
                }

                return chain.filter(exchange);
            }
        };
    }

    //配置类 接收配置参数
    @Data
    @NoArgsConstructor
    public static class Config {
        private boolean consoleLog;
        private boolean cacheLog;
    }
}

**第三步:**启动测试

4.4.2.全局过滤器

全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

1)内置全局过滤器

SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:

在这里插入图片描述

在这里插入图片描述

这里将 Spring Cloud Gateway内置的所有全局过滤器简单整理成了一张表格,用作速览:

全局过滤器作用
Forward Routing Filter用于本地forward,也就是将请求在Gateway服务内进行转发,而不是转发到下游服务
LoadBalancerClient Filter整合Ribbon实现负载均衡 lb://服务名
Netty Routing Filter使用Netty的 HttpClient 转发http、https请求
Netty Write Response Filter将代理响应写回网关的客户端侧
RouteToRequestUrl Filter将从request里获取的原始url转换成Gateway进行请求转发时所使用的url
Websocket Routing Filter使用Spring Web Socket将转发 Websocket 请求
Gateway Metrics Filter整合监控相关,提供监控指标

参考: https://www.jianshu.com/p/3ab97acf1e69

2)自定义全局过滤器

内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。开发中的鉴权逻辑:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务端对token进行解密,判断是否有效。

在这里插入图片描述

如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。

在这里插入图片描述

检验的标准就是请求中是否携带token凭证以及token的正确性。

下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含token,如何不包含请求参数token则不转发路由,否则执行正常的逻辑。

package com.zking.filters;

import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

//自定义全局过滤器需要实现GlobalFilter和Ordered接口
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    //完成判断逻辑
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (!StringUtils.equals(token, "admin")) {
            System.out.println("鉴权失败");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        //调用chain.filter继续向下游执行
        return chain.filter(exchange);
    }

    //顺序,数值越小,优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

5.限流和熔断

5.1.前言

5.1.1.高并发带来的问题

在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。

接下来,我们基于订单微服务来模拟一个高并发的场景:

**第一步:**定义测试接口

@RequestMapping("/addOrder/{pid}")
public Order addOrder(@PathVariable("pid") Integer pid){
    log.info(">>客户下单,这时候要调用商品微服务查询商品信息");

    //通过fegin调用商品微服务
    Product product = productService.detail(pid);
    log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));

    //模拟一次网络延时
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    //下单
    Order order = new Order();
    order.setUid(1);
    order.setUsername("测试用户");
    order.setPid(product.getPid());
    order.setPname(product.getPname());
    order.setPprice(product.getPprice());
    order.setNumber(1);
    log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
    return order;
}

**第二步:**修改application.yml,配置tomcat的并发数

server:
  port: 8090
  tomcat:
    max-connections: 10  # 最大连接数
    accept-count: 10     # 最大等待数
    threads:
      max: 2             # 最大线程数

**第三步:**接下来使用Jmeter压测工具,对请求进行压力测试

1)添加线程组:

在这里插入图片描述

2)配置线程并发数:

测试数据:

  • 1秒之内同时发送20个请求,连续发送100次(正常)
  • 1秒之内同时发送30个请求,连续发送100次(陆陆续续出现异常情况)

在这里插入图片描述

3)添加http取样:

在这里插入图片描述

4)配置取样,并启动测试:

在这里插入图片描述

此时会发现, 由于order方法囤积了大量请求,导致message方法的访问出现了问题,这就是服务雪崩的雏形。

5.1.2.服务雪崩效应

在分布式系统中,由于网络原因或自身的原因,服务一般无法保证100%可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应” 。

在这里插入图片描述

雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。

5.1.3.常见容错方案

要防止雪崩的扩散,我们就要做好服务的容错,容错说白了就是保护自己不被猪队友拖垮的一些措施, 下面介绍常见的服务容错思路和组件。

常见的容错思路有隔离、超时、限流、熔断、降级这几种,下面分别介绍一下。

  • 隔离

它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离。

在这里插入图片描述

  • 超时

在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。

在这里插入图片描述

  • 限流

限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。

在这里插入图片描述

  • 熔断

在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断

在这里插入图片描述

服务熔断一般有三种状态:

  1. 熔断关闭状态(Closed):服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制
  2. 熔断开启状态(Open):后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法
  3. 半熔断状态(Half-Open):尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断关闭状态。
  • 降级

降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案。

在这里插入图片描述

5.1.4.常见的容错组件

  • Hystrix:是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止
    级联失败,从而提升系统的可用性与容错性。
  • Resilience4J:一款非常轻量、简单,并且文档非常清晰、丰富的熔断工具,这也是Hystrix官方推荐的替代产品。不仅如此,Resilicence4j还原生支持Spring Boot 1.x/2.x,而且监控也支持和prometheus等多款主流产品进行整合。
  • Sentinel:是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。

下面是三个组件在各方面的对比:

SentinelHystrix
隔离策略信号量隔离线程池隔离/信号量隔离
熔断降级策略基于响应时间或失败比率基于失败比率
实时指标实现滑动窗口滑动窗口(基于 RxJava
规则配置支持多种数据源支持多种数据源
扩展性多个扩展点插件的形式
基于注解的支持即将支持支持
限流基于 QPS,支持基于调用关系的限流不支持
流量整形支持慢启动、匀速器模式不支持
系统负载保护支持不支持
控制台开箱即用,可配置规则、查看秒级监控、机器发现等不完善
常见框架的适配Servlet、Spring Cloud、Dubbo、gRPC 等Servlet、Spring Cloud Netflix

5.2.Sentinel快速入门

5.2.1.什么是Sentinel

Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。它以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景, 例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 提供了实时的监控功能。通过控制台可以看到接入应用的单台机器秒级数据, 甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块, 例如与 SpringCloudDubbogRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快
    速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 分为两个部分:

  • 核心库Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo /
    Spring Cloud 等框架也有较好的支持。
  • 控制台Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

5.2.2.快速入门

  • 微服务集成Sentinel

为微服务集成Sentinel非常简单,只需要加入Sentinel的依赖即可。

**第一步:**在订单微服务中的pom.xml引入依赖:

<!--sentinel-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

如果网关gateway集成sentinel,需还另添加以下依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

**第二步:**编写Controller接口进行测试

@RestController
@RequestMapping("/order")
public class OrderController{
	
	@RequestMapping("/message1")
    public String message1() {
        return "message1";
    }
}

**第三步:**启动项目。

  • 安装Sentinel控制台

Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。

第一步:下载jar包,解压到指定位置

在这里插入图片描述

**第二步:**启动控制台

# 直接使用jar命令启动项目(控制台本身是一个SpringBoot项目)
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.0.jar

#参考1
java -jar sentinel-dashboard-1.8.1.jar --server.port=8080
#参考2
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.1.jar

**第三步:**修改订单微服务的application.yml,加入有关Sentinel控制台的配置

spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos.com:8848
    sentinel:
      transport:
        port: 9999 #跟控制台交流的端口,随意指定一个未使用的端口即可
        dashboard: localhost:8080 # 指定控制台服务的地址

**第四步:**通过浏览器访问http://localhost:8080进入控制台 ( 默认用户名密码是 sentinel/sentinel )

在这里插入图片描述

补充:了解控制台的使用原理

Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上,即在微服务中指定控制台的地址,并且还要开启一个跟控制台传递数据的端口,控制台也可以通过此端口调用微服务中的监控程序获取微服务的各种信息。

在这里插入图片描述

  • 实现一个接口限流

**第一步:**直接在浏览器中访问/order/message1接口,这时在回到Sentinel控制台刷新即可发现接口信息

**第二步:**通过Sentinel控制台为message1接口添加一个流控规则

在这里插入图片描述

在这里插入图片描述

**第三步:**通过Sentinel控制台快速频繁访问,观察效果

在这里插入图片描述

5.3.Sentinel的概念和功能

5.3.1.基本概念

  • 资源:就是Sentinel要保护的东西

    资源是Sentinel的关键概念。它可以是 Java应用程序中的任何内容,可以是一个服务,也可以是一个方法,甚至可以是一段代码。上述案例中的message1方法就可以认为是一个资源。

  • 规则:就是用来定义如何进行保护资源的

    作用在资源之上, 定义以什么样的方式保护资源,主要包括流量控制规则、熔断降级规则以及系统保护规则。上述案例中就是为message1资源设置了一种流控规则, 限制了进入message1的流量。

5.3.2.重要功能

Sentinel的主要功能就是容错,主要体现为下面这三个:

在这里插入图片描述

  • 流量控制:在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
  • 熔断降级:当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
  • 系统负载保护Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。

总之一句话: 我们需要做的事情,就是在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能。

5.4.Sentinel规则

5.4.1.流控规则

流量控制:其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

第一步:点击簇点链路,我们就可以看到访问过的接口地址,然后点击对应的流控按钮,进入流控规则配置页面。新增流控规则界面如下:

在这里插入图片描述

相关属性介绍:

属性说明
资源名唯一名称,默认是请求路径,可自定义
针对来源指定对哪个微服务进行限流,默认指default,意思是不区分来源,全部限制
阈值类型/单机阈值1)QPS(每秒请求数量): 当调用该接口的QPS达到阈值的时候,进行限流
2)线程数:当调用该接口的线程数达到阈值的时候,进行限流
是否集群暂不需要集群
  • 【简单配置】

我们先做一个简单配置,设置阈值类型为QPS,单机阈值为3。即每秒请求量大于3的时候开始限流。

在这里插入图片描述

然后快速访问/order/message1接口,观察效果。此时发现,当QPS > 3的时候,服务就不能正常响应,而是返回Blocked by Sentinel (flow limiting)结果。

  • 【配置流控模式】

点击上面设置流控规则的编辑按钮,然后在编辑页面点击高级选项,会看到有流控模式一栏。

在这里插入图片描述

Sentinel共有三种流控模式,分别是:

  • 直接(默认):接口达到限流条件时,开启限流
  • 关联:当关联的资源达到限流条件时,开启限流 [适合做应用让步]
  • 链路:当从某个接口过来的资源达到限流条件时,开启限流

下面呢分别演示三种模式:

  • 直接流控模式:是最简单的模式,当指定的接口达到限流条件时开启限流。上面案例使用的就是直接流控模式。
  • 关联流控模式:当指定接口关联的接口达到限流条件时,开启对指定接口开启限流。流量让步

**第一步:**在订单微服务中,再次新增一个测试接口

@RestController
@RequestMapping("/order")
public class OrderController{
	
	...
	
	@RequestMapping("/message2")
    public String message2() {
        return "message2";
    }
}

**第二步:**配置限流规则,将流控模式设置为关联,关联资源设置为/order/message2

在这里插入图片描述

**第三步:**通过postman等软件向/order/message2连续发送请求,注意:QPS一定要大于3

**第四步:**打开浏览器访问/order/message1,会发现已经被限流

  • 链路流控模式:当从某个接口过来的资源达到限流条件时,开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。

第一步: 编写一个service,在里面添加一个方法message

@Service
public class OrderServiceImpl {
    @SentinelResource("message")
    public void message() {
    	System.out.println("message");
    }
}

**第二步:**在Controller中声明两个方法,分别调用service中的方法message

@RestController
@Slf4j
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private OrderServiceImpl orderService;
    
    @RequestMapping("/message1")
    public String message1() {
        orderService.message();
        return "message1";
	}
    @RequestMapping("/message2")
    public String message2() {
        orderService.message();
        return "message2";
    }
}

**第三步:**禁止收敛URL的入口 context

从1.6.3 版本开始,Sentinel Web filter默认收敛所有URL的入口context,因此链路限流不生效。1.7.0 版本开始(对应SCA2.1.1.RELEASE),官方在CommonFilter 引入了WEB_CONTEXT_UNIFY 参数,用于控制是否收敛context。将其配置为 false 即可根据不同的URL 进行链路限流。

可以通过配置spring.cloud.sentinel.web-context-unify=false即可关闭收敛,如下所示:

spring:
  cloud:
    sentinel:
      web-context-unify: false

第四步: 控制台配置限流规则

在这里插入图片描述

**第五步:**分别通过/order/message1/order/message2 访问, 发现2没问题, 1的被限流了

  • 【配置流控效果】

  • 快速失败(默认): 直接失败,抛出异常,不做任何额外的处理,是最简单的效果

  • Warm Up:它从开始阈值到最大QPS阈值会有一个缓冲阶段,一开始的阈值是最大QPS阈值的1/3,然后慢慢增长,直到最大阈值,适用于将突然增大的流量转换为缓步增长的场景。

  • **排队等待:**让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待; 它还会让设置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。

5.4.2.降级规则

降级规则就是设置当满足什么条件的时候,对服务进行降级。Sentinel提供了三个衡量条件:

  • 平均响应时间 :当资源的平均响应时间超过阈值(以 ms 为单位)之后,资源进入准降级状态。

在这里插入图片描述

Controller中添加一个message3的接口,并设置延迟访问。

@RequestMapping("/message3")
public String message3() {
    try {
        TimeUnit.MILLISECONDS.sleep(220);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "message3";
}

效果:在统计时长1秒内当请求数大于5以后,如果请求的响应时间大于200毫秒的超过20%,则熔断5秒。

**注意:**RT指的响应时间,Sentinel默认统计的RT上限是4900ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置。

  • **异常比例:**当资源的每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态,即在接下的时间窗口(以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0,1.0]。

在这里插入图片描述

Controller中添加一个message4的接口,并设置异常比例为0.33。

int i = 0;
@RequestMapping("/message4")
public String message4() {
    i++;
    //异常比例为0.333
    if (i % 3 == 0){
        throw new RuntimeException();
    }
    return "message4";
}

效果:在统计时长1秒内当请求数大于5以后,如果请求异常比例超过25%,则熔断5秒。

  • **异常数 :**当资源近 1 分钟的异常数目超过阈值之后会进行服务降级。注意由于统计时间窗口是分钟级别的,若时间窗口小于 60s,则结束熔断状态后仍可能再进入熔断状态。

在这里插入图片描述

5.4.3.热点规则

热点参数流控规则是一种更细粒度的流控规则,它允许将规则具体到参数上。

**第一步:**编写后台Controller接口

@RequestMapping("/order/message5")
@SentinelResource("message5")//注意这里必须使用这个注解标识,热点规则不生效
public String message5(String name, Integer age) {
    return name + age;
}

**第二步:**配置热点规则

在这里插入图片描述

**第三步:**分别用两个参数访问,会发现只对第一个参数限流了

在这里插入图片描述

热点规则增强使用:参数例外项允许对一个参数的具体值进行流控。编辑刚才定义的规则,增加参数例外项:

在这里插入图片描述

5.4.4.授权规则

很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:

访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:

  • 若配置白名单,则只有请求来源位于白名单内时才可通过;
  • 若配置黑名单,则请求来源位于黑名单时不通过,其余的请求通过。

在这里插入图片描述

上面的资源名和授权类型不难理解,但是流控应用怎么填写呢?

其实这个位置要填写的是来源标识,Sentinel提供了 RequestOriginParser 接口来处理来源。只要Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析访问来源。

**第一步:**自定义来源处理规则

@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
    @Override
    public String parseOrigin(HttpServletRequest request) {
        String serviceName = request.getParameter("serviceName");
        return serviceName;
    }
}

**第二步:**授权规则配置

这个配置的意思是只有serviceName=pc不能访问(黑名单)

在这里插入图片描述

**第三步:**访问接口http://localhost:8091/order/message1?serviceName=pc,查看结果

5.4.5.系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。

使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
  • CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。

5.4.6.自定义异常返回

在这里插入图片描述

BlockException异常接口包含Sentinel的五个异常:

  • FlowException 限流异常
  • DegradeException 降级异常
  • ParamFlowException 参数限流异常
  • AuthorityException 授权异常
  • SystemBlockException 系统负载异常
package com.zking.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//异常处理页面
@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        response.setContentType("application/json;charset=utf-8");
        ResponseData data = null;
        if (e instanceof FlowException) {
            data = new ResponseData(-1, "接口被限流了...");
        } else if (e instanceof DegradeException) {
            data = new ResponseData(-2, "接口被降级了...");
        }
        response.getWriter().write(JSON.toJSONString(data));
    }
}

@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor//无参构造
class ResponseData {
    private int code;
    private String message;
}

是来源标识,Sentinel提供了 RequestOriginParser 接口来处理来源。只要Sentinel保护的接口资源被访问,Sentinel就会调用 RequestOriginParser 的实现类去解析访问来源。

**第一步:**自定义来源处理规则

@Component
public class RequestOriginParserDefinition implements RequestOriginParser{
    @Override
    public String parseOrigin(HttpServletRequest request) {
        String serviceName = request.getParameter("serviceName");
        return serviceName;
    }
}

**第二步:**授权规则配置

这个配置的意思是只有serviceName=pc不能访问(黑名单)

[外链图片转存中…(img-PiFr9bsw-1710401659191)]

**第三步:**访问接口http://localhost:8091/order/message1?serviceName=pc,查看结果

5.4.5.系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 、CPU使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。

使用率和线程数五个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量 (进入应用的流量) 生效。

  • Load(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。设定参考值一般是 CPU cores * 2.5
  • RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
  • CPU使用率:当单台机器上所有入口流量的 CPU使用率达到阈值即触发系统保护。

5.4.6.自定义异常返回

[外链图片转存中…(img-JT3NJWdd-1710401659191)]

BlockException异常接口包含Sentinel的五个异常:

  • FlowException 限流异常
  • DegradeException 降级异常
  • ParamFlowException 参数限流异常
  • AuthorityException 授权异常
  • SystemBlockException 系统负载异常
package com.zking.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

//异常处理页面
@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        response.setContentType("application/json;charset=utf-8");
        ResponseData data = null;
        if (e instanceof FlowException) {
            data = new ResponseData(-1, "接口被限流了...");
        } else if (e instanceof DegradeException) {
            data = new ResponseData(-2, "接口被降级了...");
        }
        response.getWriter().write(JSON.toJSONString(data));
    }
}

@Data
@AllArgsConstructor//全参构造
@NoArgsConstructor//无参构造
class ResponseData {
    private int code;
    private String message;
}
  • 11
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值