SpringCloud学习笔记

SpringCloud学习笔记

本篇博文通过尚硅谷周阳老师《SpringCloud H版》课程所做,在此非常感谢!

文章目录

学习目标

须知:分布式系统架构包括:服务注册与发现、服务调用、服务熔断、负载均衡、服务降级、服务消息队列、配置中心管理、服务网关、服务监控、全链路追踪、自动化构建部署、服务定时任务调度操作
在这里插入图片描述
SpringCloud:分布式技术的一种体现,是分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶;

热部署Devtools(电脑不太好,我没开,要不太卡了)

步骤?

一、首先确保子工程中由依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

二、父类总工程引入插件依赖

<build>
    <finalName>stu_springcloud</finalName><!--指定父工程名可有可无-->
    <!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <fork>true</fork>
                <addResources>true</addResources>
            </configuration>
        </plugin>
    </plugins>
</build>

三、开启自动编译的权限
在这里插入图片描述

四、CTRL+SHIFT+ALT+/,出现如下图所示,选择红色圈定项
在这里插入图片描述
在这里插入图片描述

前置环境搭建

提前说明,下面所有新建的模块都是本工程的子模块

父工程的pom.xml内容,如下:

<!--子模块继承之后,提供作用:锁定版本+子模块不用写groupId和version-->
<dependencyManagement>
    <dependencies>
        <!--spring boot2.2.2-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.2.2.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--spring-cloud H版SR1(Hoxton.SR1)-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <!--spring-cloud-alibaba 2.1.0.RELEASE-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>

    </dependencies>
</dependencyManagement>

<build>
    <finalName>stu_springcloud</finalName><!--指定父工程名可有可无-->
    <!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <fork>true</fork>
                <addResources>true</addResources>
            </configuration>
        </plugin>
    </plugins>
</build>

创建cloud-api-commons模块,并将其达成jar包方便后续的其他模块使用,依赖如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!--糊涂工具包-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.1.0</version>
    </dependency>
</dependencies>

实体类Payment的创建:

package edu.hebeu.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {

    private static final long serialVersionUID = 4668732331848071816L;

    private Long id;
    private String serial;
}

响应实体类CommonResult<T>的创建:

package edu.hebeu.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {

    private Integer code;
    private String msg;
    private T data;

    public CommonResult(Integer code, String msg) {
        this(code, msg, null);
    }
}

将该模块打包后在下面的各个子模块可以导入使用

Eureka

什么是服务治理?

SpringCloud封装了Netflix公式开发的Eureka模块来实现服务治理。在传统的rpc远程调用框架中,管理每个服务和服务之间依赖关系比较复杂,管理也比较复杂,所以需要使用服务治理,管理服务与服务之间依赖关系,可以实现服务调用,负载均衡、容错率等,实现服务发现与注册;

什么是服务注册与发现?

Eureka采用了CS的设计架构,EurekaServer做为服务注册功能的服务器,它是服务注册中心。系统中的其他微服务使用Eureka的客户端连接到EurekaServer并维持心跳连接,这样系统的维护人员就可以通过EurekaServer来监控系统中各个微服务是否正常运行。

在服务注册与发现中,有一个注册中心,当服务启动时,会把当前自己服务器的信息(如:服务地址、通讯地址等)以别名的方式注册到注册中心上;另一方(消费者|服务提供者)以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用远程RPC,远程调用框架核心设计思想:对于注册中心,因为使用注册中心管理每个服务与服务之间的依赖关系(服务治理概念),在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关信息<接口地址>)

在这里插入图片描述
对上述图片的名词解读:

  • Eureka Server作用:
    • 服务注册:将服务信息注册进注册中心
    • 服务发现:从注册中心上获取服务信息
    • 实质:存KEY服务,取VALUE调用地址
  • 流程:
    1. 启动Eureka服务注册中心;
    2. 启动服务提供者Service Provider
    3. 服务提供者启动后会把自身信息(如:服务地址)也别名的方式注册到Eureka
    4. 服务消费者Service Consumer在需要调用接口时,使用服务别名取注册中心获取实际的RPC远程调用地址
    5. 服务消费者获得调用地址后,底层实际是利用HttpClient技术实现远程调用
    6. 服务消费者获得服务地址后会缓存在本地JVM内存中,默认每间隔30秒更新一次服务调用地址

*问题:微服务RPC远程服务调用最核心的是什么?*答案:高可用!,搭建Eureka注册中心集群,实现负载均衡+故障容错

了解一下,Dubbo系统架构
在这里插入图片描述
Eureka的两个组件:Eureka ServerEureka Client

Eureka Server提供服务注册服务:各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到;

Eureka Client通过注册中心进行访问:是一个Java客户端,用于简化EurekaServer的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向EurekaServer发送心跳(默认周期为30秒)。如果EurekaServer在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除掉(默认90秒)

Eureka集群的配置

C:\Windows\System32\drivers\etc\hosts文件添加如下内容:

########### SpringCloud H版学习使用 #############
# Eureka集群的配置
127.0.0.1	eureka7000.com

127.0.0.1	eureka7001.com

127.0.0.1	eureka7002.com

第一台Eureka(eureka7000)注册中心的配置

################### Eureka-Server(Eureka注册中心集群的一个节点)模块的配置 ###################
server:
  port: 7000

eureka:
  instance:
  #    hostname: localhost # Eureka服务端的主机名称,单机时(一个Eureka注册中心)使用该方式
    hostname: eureka7000.com # 本机host文件指定的实例名
  client:
    register-with-eureka: false # 修改为false,表示不会向服务中心注册自己
    fetch-registry: false # 修改为false,表示自己就是注册中心,用来维护服务实例,并不需要去检索服务
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/ # 将这个注册中心 注册到 另一个注册中心

第二台Eureka(eureka7001)注册中心的配置

################### Eureka-Server(Eureka注册中心集群的一个节点)模块的配置 ###################
server:
  port: 7001

eureka:
  instance:
    #    hostname: localhost # Eureka服务端的主机名称,单机时(一个Eureka注册中心)使用该方式
    hostname: eureka7001.com # 本机host文件指定的实例名
  client:
    register-with-eureka: false # 修改为false,表示不会向服务中心注册自己
    fetch-registry: false # 修改为false,表示自己就是注册中心,用来维护服务实例,并不需要去检索服务
    service-url:
      defaultZone: http://eureka7000.com:7000/eureka/ # 将这个注册中心 注册到 另一个注册中心

客户端(微服务提供者)的配置

############################### 消费者订单模块的配置 ############################
server:
  port: 80

spring:
  application:
    name: cloud-order-service

# Eureka客户端(微服务提供者)的配置,注册进Eureka会默认以该服务的spring.application.name值做为该服务在Eureka注册中心的名
eureka:
  client:
    register-with-eureka: true # 设置为true,表示将自己注册到EurekaServer(Eureka服务注册中心)
    fetch-registry: true # 设置为true,表示能从Eureka服务注册中心抓取已有的注册信息(单节点无所谓,集群必须设置为true才能配合ribbon使用负载群均衡)
    service-url:
#      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
      defaultZone: http://eureka7000.com:7000/eureka,http://eureka7001.com:7001/eureka # 设置Eureka的服务注册中心集群(集群版)

######################## 本服务模块的配置 ##############################
server:
  port: 9000

spring:
  application:
    name: cloud-payment-service
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver # MySQL驱动
    url: jdbc:mysql://localhost:3306/stu_springcloud?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: "root"
    password: "072731"
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: edu.hebeu.entity # 所有实体类所在的包(还自动设置了别名)
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名规则
# Eureka客户端(微服务提供者)的配置,注册进Eureka会默认以该服务的spring.application.name值做为该服务在Eureka注册中心的名
eureka:
  client:
    register-with-eureka: true # 设置为true,表示将自己注册到EurekaServer(Eureka服务注册中心)
    fetch-registry: true # 设置为true,表示能从Eureka服务注册中心抓取已有的注册信息(单节点无所谓,集群必须设置为true才能配合ribbon使用负载群均衡)
    service-url:
#      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
      defaultZone: http://eureka7000.com:7000/eureka,http://eureka7001.com:7001/eureka # 设置Eureka的服务注册中心(集群版)

微服务集群的配置

支付模块cloud-payment-service如果只有一个,当这个服务挂掉后,显然系统就崩溃了,但是如果配置多个这个微服务(微服务集群)就能很好的解决这个问题,开发多个cloud-pay-service微服务模块(这里我们开发两个),两个模块完全一样,这是端口改变接口(注意:这两个微服务的名称<spring.application.name值>要一样),此时微服务集群就搭建完成了;

当搭建完微服务集群,并将它们注册进服务注册中心后,我们如何进行调用?(因为此时服务注册中心就有多个同名的微服务了,如果我们在调用这个微服务时不加以处理就会出现异常),即cloud-order-service服务如何调用cloud-payment-service

cloud-order-service配置,其他均不变,将Controller层原先写死的微服务API变成注册进服务注册中心的微服务名即可,如下:

package edu.hebeu.controller;

import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class OrderController {

    /**
     * PAYMENT服务的微服务名称(以注册进Eureka的服务名为)
     */
//    private static final String PAYMENT_URL = "http://127.0.0.1:9000"; // 不建议这样写死(因为如果这个微服务有集群,这样调用就只能调用集群中的某个节点,无法发挥集群的优势)
    private static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE"; // 直接写微服务的名称,让负载均衡决定使用哪个微服务(注册进Eureka的哪个端口下的CLOUD-PAYMENT-SERVICE微服务)

    /**
     * 用来调用远程接口的组件对象,RestTemplate提供了多种便捷访问远程HTTP服务的方法,是一种简单便捷的访问RESTFUL服务模板类,
     * 是Spring提供的用于访问REST服务的客户端模板工具集
     */
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 创建订单的请求
     * @param payment
     * @return
     */
    @PostMapping("/payment")
    public CommonResult creat(Payment payment) {
        log.info("消费者发起了创建订单的请求" + payment);
        // 调用Payment服务的创建订单的接口(调用的请求是POST类型,将参数payment放在请求体)
        return restTemplate.postForObject(PAYMENT_URL + "/payment", payment, CommonResult.class);
    }

    @GetMapping("/payment/{id}")
    public CommonResult getById(@PathVariable("id") Long id) {
        log.info("消费者发起了查询订单的请求");
        // 调用Payment服务的查询订单的接口(调用的请求是GET类型)
        return restTemplate.getForObject(PAYMENT_URL + "/payment/" + id, CommonResult.class);
    }

}

RestTemplate组件如何通过负载均衡调用微服务模块?在该组件加入容器时添加@LoadBalanced注解即可,如下:

package edu.hebeu.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * @Auther: tyong--汤勇 13651
 * @Description: edu.hebeu.config
 * @Version: 1.0
 */
@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced // 赋予RestTemplate组件负载均衡的能力(如果有多个同名的微服务,RestTemplate组件会通过负载均衡进行合理的调用)
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

服务发现

什么是服务发现?

对于注册进Eureka里面的微服务,可以通过服务发现来获得该服务的信息;SpringCloud通过org.springframework.cloud.client.discovery.DiscoveryClient组件即可实现服务发现,实例如下:

package edu.hebeu.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Slf4j
@RestController
public class InfoController {

    /**
     * 测试 “服务发现” 的组件
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * 测试 “服务发现” 的接口
     * @return
     */
    @GetMapping("/discovery")
    public Object discovery() {
        List<String> services = discoveryClient.getServices();

        // 获取全部的微服务
        for (String element : services) {
            log.info("element: " + element);
        }

        // 获取特定的微服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");// 获取名为CLOUD-PAYMENT-SERVICE的微服务实例
        for (ServiceInstance serviceInstance : instances) {
            log.info(serviceInstance.getServiceId() + "\t" + serviceInstance.getHost() + "\t" + serviceInstance.getPort() + "\t" + serviceInstance.getUri());
        }

        return discoveryClient;
    }
}

Eureka的自我保护

概念

保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护,一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。

导致原因

一句话:某一时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存;属于CAP里面的A(高可用)P(分区容错性)分支;

为什么会产生Eureka的自我保护机制?

为了防止EurekaClient可以正常运行,但是与EurekaServer网络不通情况下,EurekaServer不会立即将EurekaClient服务剔除;

什么是自我保护模式?

默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的(此时本不应该注销该微服务)。Eureka通过自我保护模式来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发送了网络分区古灶),那么这个节点就会进入自我保护模式;

==自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例;==它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例;综上:自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的和不健康的),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加健壮、稳定;

在这里插入图片描述

如果在Eureka Server的首页看到如下提示,则说明Eureka进入了保护模式

EMERGENCY! EUREKA MAY BE INCORECTLY CLAIMINS INSTANCES WHEN THEY'RE NOT.
RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
如何关闭默认的自我保护模式
eureka:
  instance:
    hostname: localhost # Eureka服务端的主机名称,单机时(一个Eureka注册中心)使用该方式
  client:
    register-with-eureka: false # 修改为false,表示不会向服务中心注册自己
    fetch-registry: false # 修改为false,表示自己就是注册中心,用来维护服务实例,并不需要去检索服务
    service-url:
      defaultZone: http://localhost:7000/eureka/ # 设置这个注册中心的地址(单机版)
  server:
    enable-self-preservation: false ### 设置为false,表示关闭Eureka默认的自我保护机制 ###
    eviction-interval-timer-in-ms: 2000 # 设置没有心跳的时间间隔为2000ms内

Zookeeper

确保Zookeeper服务开启,然后导入必要的依赖,如下:

<!--SpringBoot整合的Zookeeper客户端-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    <!--排除自身携带的Zookeeper依赖(因为自身携带的Zookeeper版本是3.5.3-beta,会出现冲突的异常,所以要先排除掉自身的依赖,我们自己引入合适的Zookeeper依赖即可)-->
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!--引入Zookeeper3.4.9版本的依赖-->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.9</version>
</dependency>

yaml配置文件的配置,如下:

############################# Zookeeper的微服务 的配置 ############################
server:
  port: 9000

spring:
  application:
    name: cloud-payment-service
# 配置Zookeeper注册中心地址
  cloud:
    zookeeper:
      connect-string: zookeeper所在的主机:zookeeper所使用的端口(默认2181)

启动类的配置

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient // 该注解用于向Consul或者Zookeeper做为注册中心时注册服务
public class PaymentProvider9000Main {

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

此时该服务启动后就会注册进入Zookeeper

Consul

Consul是一套开源的分布式服务发现和配置管理系统,有HashiCorp公司使用Go语言开发,提供了微服务系统中的服务治理、配置中心、控制总线等功能,这些功能的每一个都可以根据需要单点使用,也可以一起使用以构建全方位的服务网络,总之Consul提供了一种完整的服务网格解决方案;

优点:基于RAFT协议,比较简洁;支持健康检查,,同时支持HTTP和DNS协议,支持跨数据中心的WAN集群,提供图形界面,跨平台(支持Linux、Mac、Windows)

能干什么?

  • 服务发现:提供HTTP和DNS两种发现方式;
  • 健康监测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制化;
  • KV存储:KEY、VALUE的存储方式;
  • 多数据中心:Consul支持多数据中心;
  • 可视化WEB界面;

中文文档地址

使用

同样,先确保Consul开启,然后引入依赖:

<!--SpringCloud整合的Consul-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

yaml文件配置

############################# Consul的微服务 的配置 #############################
server:
  port: 9000

spring:
  application:
    name: cloud-payment-service

# 配置Consul注册中心地址
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery: # 发现服务 的配置
        service-name: ${spring.application.name} # 指定被服务在Consul中的名称

启动类的配置

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient // 标注这是客户端(微服务提供者),以能够注册到Consul服务注册中心
public class PaymentProvider9000Main {

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

此时,该服务启动后,就能注册进入Consul

三种服务注册中心(Eureka、Zookeeper、Consul)的异同

组件名语言CAP服务健康检查对外暴漏接口SpringCloud集成
EurekaJavaAP(主要保证A)可配支持HTTP已集成
ConsulGoCP(主要保证C)支持HTTP/DNS已集成
ZookeeperJavaCP(主要保证C)支持客户端已集成

CAP的解读:

C:强一致性(Consistency)

A:高可用性(Availability)

P:分区容错性(Partition tolerance),这个是必须要保证的

如下的经典图示:
在这里插入图片描述
可见,CAP最多只能同时较好的满足两个,CAP理论的核心是:一个分布式系统不可能很好的满足一致性、可用性和分区容错性这三个需求,因此根据CAP理论将NoSQL数据库分成了满足CA原则、满足CP原则、满足AP原则三大类,其中:

  • CA——单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大;
  • CP——满足一致性,分区容忍性的系统,通常性能不是很高;
  • AP——满足可用性、分区容忍性的系统,通常可能对一致性要求低一些;

Ribbon

概述

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具;简单来说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用Ribbon客户端组件提供一系列完善的配置项和连接超时、重试等。通过配置文件中列出的Load Balancer(简称LB)后面所有的集群,Ribbon会自动的帮助你基于某种规则(如简单轮询、随机连接等)去连接这些机器,我们很容易使用Ribbon实现自定义的负载均衡算法;

什么是LB负载均衡(Load Balance)?

简单来说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡软件包括:NginxLVS硬件F5

Ribbon本地负载均衡 和 Nginx服务端负载均衡的区别?

  • Nginx是服务端负载均衡(集中式LB),客户端所有请求都会交给Nginx,然后由Nginx实现转发请求,即负载均衡是由服务端实现的;
  • Ribbon本地负载均衡(进程内LB),在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术;

集中式LB和进程内的LB 两种负载均衡的区别?

  • 集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
  • 进程内LB:将LB逻辑集中到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器,Ribbon就属于进程内的LB,它是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址;

架构说明

须知,Ribbon其实就是一个软件负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用(和Eureka结合只是其中的一个实例)
在这里插入图片描述
Ribbon在工作时分成两步:

  1. 先选择服务注册中心EurekaServer,它优先选择在同一区域内负载较少的server;
  2. 再根据用户指定的策略,从server取到的服务注册列表中选择一个地址,其中Ribbon提供了多种策略,如:轮询、随机、根据响应时间加权;

使用

须知:引入cloud集成的Eureka依赖后,Ribbon就自带了,如下图:
在这里插入图片描述
并且,在由多个服务提供者时,我们使用RestTemplate,并且RestTemplate使用@LoadBalanced注解标注,此时使用RestTemplate组件就能够默认的进行复杂均衡;简单来说,Ribbon的使用:负载均衡 + RestTemplate调用

Ribbon的负载规则

首先须知:Ribbon的核心接口组件IRule(IRule会根据特定算法从服务列表中选取一个要访问的服务),Ribbon的主体结构图如下:
在这里插入图片描述
对于上述的图中,就有我们需要的自带的负载规则算法:

  • RoundRobinRule:轮询(默认);
  • RandomRule:随机;
  • RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,再获取可用的服务;
  • WeightResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择;
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于熔断跳闸状态的服务,然后选择一个并发量最小的服务;
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例;
  • ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器;

对于以上这么多的负载规则算法,我们如何替换使用?

须知:官方文档给出了明确的警告,这个自定义配置类不能放在@ComponentScan所能扫描的当前包以及其子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的!!!

因为程序入口类被@SpringBootApplication注解标注,而且这个注解内含@ComponentScan这个注解,因此如果没有配置要扫描的包区域,就会默认为当前项目启动类所在的包及其子包为扫描的包区域

综上,Ribbon自定义配置类要放在程序入口类所在包的外面!

替换Ribbon规则的配置类(在程序启动类所在包之外!)

package ribbon_config;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Auther: tyong--汤勇 13651
 * @Description: edu.ribbon
 * @Version: 1.0
 */
@Configuration
public class MyRule {

    @Bean
    public IRule iRule() {
        return new RandomRule(); // 随机的负载规则
    }

}

指定完成之后,在程序主启动类上添加@RibbonClient注解指明要访问哪个微服务时所使用的负载规则,如下:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import ribbon_config.MyRule;

/**
 * @Auther: tyong--汤勇 13651
 * @Description: edu.hebeu
 * @Version: 1.0
 */
@SpringBootApplication
@EnableEurekaClient // 标注这是Eureka的客户端(微服务提供者),以能够注册到Eureka服务注册中心
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyRule.class) // 表示指定这个服务访问注册中心中名为CLOUD-PAYMENT-SERVICE的微服务时使用的负载规则是MyRule类所指定的
public class OrderConsumer80Main {
    public static void main(String[] args) {
        SpringApplication.run(OrderConsumer80Main.class, args);
    }
}

轮询规则负载均衡的原理

轮询负载规则算法:rest接口第几次请求 % 集群中该服务的总数量 = 实际调用的这个服务的位置的下标,每次服务重启动之后rest接口计数从1开始

如我们如果在服务注册中心有2个名为CLOUD-PAYMENT-SERVICE的微服务,我们可以通过List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");此时,instances变量就存在两个元素:instance[0] = CLOUD-PAYMENT-SERVICE服务1instance[1] = CLOUD-PAYMENT-SERVICE服务2

那么按照轮询算法的原理:

该服务接口的总请求数为1时, 1 % 2 = 1,则获取对应下标为1的CLOUD-PAYMENT-SERVICE服务,即CLOUD-PAYMENT-SERVICE服务2

该服务接口的总请求数为2时, 2 % 2 = 0,则获取对应下标为0的CLOUD-PAYMENT-SERVICE服务,即CLOUD-PAYMENT-SERVICE服务1

该服务接口的总请求数为3时, 3 % 2 = 1,则获取对应下标为1的CLOUD-PAYMENT-SERVICE服务,即CLOUD-PAYMENT-SERVICE服务2

该服务接口的总请求数为4时, 4 % 2 = 0,则获取对应下标为1的CLOUD-PAYMENT-SERVICE服务,即CLOUD-PAYMENT-SERVICE服务1

以此类推…

自定义轮询规则实现负载均衡

首先,先把之前RestTemplate组件的@LoadBalanced注解去掉(因为该注解会让RestTemplate组件默认使用Ribbon的轮询算法);然后再把上一步中的为指定微服务指定负载算法的注解@RibbonClient也关闭,此时我们开始放在源码创建并使用自己定义的轮询的负载均衡算法;

程序入口类

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient // 标注这是Eureka的客户端(微服务提供者),以能够注册到Eureka服务注册中心
public class OrderConsumer80Main {
    public static void main(String[] args) {
        SpringApplication.run(OrderConsumer80Main.class, args);
    }
}

配置类

package edu.hebeu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {

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

开始编写自定义的负载算法类:

自定义接口类

package edu.hebeu.component;

import org.springframework.cloud.client.ServiceInstance;

import java.util.List;

/**
 * 自定义的负载规则算法的接口
 */
public interface ILoadBalancer {

    /**
     * 通过 某种微服务的集合 选择出合适的,要使用的微服务实例,实现负载均衡
     * @param serviceInstances
     * @return
     */
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

自定义轮询算法实现负载的类

package edu.hebeu.component;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @Auther: tyong--汤勇 13651
 * @Description: edu.hebeu.component
 * @Version: 1.0
 *
 * 实现ILoadBalancer
 */
@Component
public class MyLoadBalancer implements ILoadBalancer{

    /**
     * Integer的原子类
     */
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    /**
     * 获取是第几次请求 这个微服务
     * @return
     */
    private int getRequestServiceCount() {
        int current; // 存放当前服务器访问的次数(期望值)
        int next; // 存放当前服务器访问之后的 访问次数(修改值)

        // TODO 这里是自旋锁
        do {
            current = this.atomicInteger.get(); // 获取当前值
            next = current >= Integer.MAX_VALUE ? 0 : current + 1; // 由当前值得到要修改的值,即当前值+1(该服务访问次数+1)
        } while (!this.atomicInteger.compareAndSet(current, next)); // TODO CAS算法
        System.out.printf("第%d次请求该服务\n", next);
        return next;
    }

    /**
     * 通过自定义的轮询算法得到此次请求该微服务时 应该使用哪个微服务实例,实现负载均衡
     * @param serviceInstances
     * @return
     */
    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getRequestServiceCount() % serviceInstances.size(); // 通过当前服务器的访问次数 % 这个服务的实例个数 计算得到要访问该服务的哪个实例
        return serviceInstances.get(index);
    }
}

此时,自定义的轮询算法实现负载的类就创建完毕了,下面是使用这个算法类的代码

package edu.hebeu.controller;

import edu.hebeu.component.ILoadBalancer;
import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@RestController
@Slf4j
public class OrderController {

    /**
     * PAYMENT服务的微服务名称(以注册进Eureka的服务名为)
     */
    private static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE"; // 直接写微服务的名称,让负载均衡决定使用哪个微服务(注册进Eureka的哪个端口下的CLOUD-PAYMENT-SERVICE微服务)

    /**
     * 用来调用远程接口的组件对象,RestTemplate提供了多种便捷访问远程HTTP服务的方法,是一种简单便捷的访问RESTFUL服务模板类,
     * 是Spring提供的用于访问REST服务的客户端模板工具集
     */
    @Autowired
    private RestTemplate restTemplate;

    /**
     * 服务发现的组件
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * 使用自定义的负载均衡组件
     */
    @Autowired
    private ILoadBalancer loadBalancer;

    /**
     * 测试自定义的负载均衡算法
     * @return
     */
    @GetMapping("/myLoadBalancer/{id}")
    public CommonResult testMyLoadBalancer(@PathVariable("id") Long id) {
        // 获得所有的 CLOUD-PAYMENT-SERVICE 微服务实例
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        if (instances == null || instances.size() <= 0) {
            return new CommonResult(404, "未找到该服务...");
        }
        ServiceInstance instance = loadBalancer.instances(instances); // 通过自定义的负载规则算法得到 该微服务本次要使用的哪个实例
        return restTemplate.getForObject(instance.getUri() + "/payment/" + id, CommonResult.class);
    }

}

当然,前提是注册中心得有CLOUD-PAYMENT-SERVICE名字的服务实例

OpenFegin

概述

什么是Fegin

Fegin是一个声明式的WebService客户端,使用Fegin能让编写WebService客户端更加简单,它的使用方法是定义一个服务接口然后在上面添加注解。Fegin也支持可拔插式的编码器和解码器。SpringCloudFegin进行了封装,使其支持了SpringMVC标注注解和HttpMessageConvertersFegin也可以与EurekaaRibbon组合使用以支持负载均衡;即Fegin是一个声明式的Web服务客户端,让编写Web服务端变得非常容易,只需创建一个接口并在接口上添加注解即可

Fegin能干什么?

Fegin旨在使编写Java Http客户端变得更容易。前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成一套模板化的调用方法,但是实际开发中,由于对服务器依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以Fegin在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。Fegin的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Fegin注解即可),即可完成对服务提供方的接口绑定,简化了使用SpringCloud Ribbon时,自动封装服务调用客户端的开发量;同时Fegin继承了Ribbon(默认就有Ribbon的负载均衡功能),利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。但是与Ribbon不同的是,通过Fegin只需要定义服务绑定接口且以声明式的方法,就可以优雅而简单的实现了服务调用;

FeginOpenFegin的区别?

FeginOpenFegin
FeginSpringCloud组件中的一个轻量级的RESTFUL的HTTP服务客户端,Fegin内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务,Fegin的使用方式是:使用Fegin的注解定义接口,调用这个接口,就可以调用服务注册中心的服务OpenFeginSpringCloudFegin的基础上支持了SpringMVC的注解,如@RequestMapping等,OpenFegin@FeginClient可用解析SpringMVC@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务
org.springframework.cloud spring-cloud-starter-fegin org.springframework.cloud spring-cloud-starter-openfegin

使用

首先,要引入OpenFeign依赖,如下

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

须知:OpenFeign是客户端使用的,来调用服务端的接口,因此Feign的使用场景是客户端!!!

然后,通过客户端的Service层进行调用,如下:

package edu.hebeu.service;

import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@Service
@FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 添加@FeignClient注解,表示这是Feign管理的Service,该接口下的方法会调用服务注册中心名为CLOUD-PAYMENT-SERVICE的微服务的指定接口
public interface IOrderFeignService {

    /**
     * 调用 @FeignClient对应的服务下 指定的POST类型的 /payment请求
     * @param payment
     * @return
     */
    @PostMapping("/payment")
    CommonResult<Integer> add(@RequestBody Payment payment);

    /**
     * 调用 @FeignClient对应的服务下 指定的GET类型的 /payment/{id}请求
     * @param id
     * @return
     */
    @GetMapping(value = "/payment/{id}")
    CommonResult<Payment> findById(@PathVariable("id") Long id);

    /**
     * 调用 @FeignClient对应的服务下 指定的POST类型的 /feignTimeout请求
     * @return
     */
    @PostMapping(value = "/feignTimeout")
    String feignTimeout();

}

此时,就完成了客户端通过OpenFeign对服务端的调用

OpenFeign超时控制

是什么?

**OpenFeign默认等待1秒(等待服务提供方1秒),超时就报错;**如何避免这种情况(延长超时等待)?可以通过yaml文件配置,如下:

# 设置OpenFeign客户端连接时间(因为OpenFeign底层是使用的Ribbon)
ribbon:
  ReadTimeout: 5000 # 建立连接所用的时间为5000ms。适用于网络状态正常的情况下,两端连接所用的时间
  ConnectTimeout: 5000 # 设置建立连接后从服务端读取到可用资源所用的时间为5000ms

OpenFeign日志打印功能

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

OpenFeign的日志级别

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及数据;

如何配置?

第一步,给IOC注入日志组件(并设置该组件的类型),如下:

package edu.hebeu.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

    /**
     * Feign日志相关的组件
     * @return
     */
    @Bean
    public Logger.Level feignLogLevel() {
        return Logger.Level.FULL; // 设置该日志组件的级别为FULL
    }
}

第二步,yaml配置,如下:

# 配置Feign的日志相关
logging:
  level:
    # Feign日志以什么级别监控哪个接口
    edu.hebeu.service.IPaymentFeignService: debug # 这里表示以debug级别监控IPaymentFeignService接口

如果此时发送能够调用以上指定监控的接口的请求,控制台打印信息如下:
在这里插入图片描述

Hystrix

概念

分布式系统面临的问题?

复杂的分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免的面临失败

服务雪崩的概念

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的扇出。如果扇出的链路上某个微服务的调用响应的时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的雪崩效应

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的资源都在几秒内饱和,比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加、备份队列、线程和其他系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统;所以通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩

Hystrix是什么?

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整个服务失败,避免级联故障,以提高分布式系统的弹性;

断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩;

Hystrix能干什么?

  • 服务降级
  • 服务熔断
  • 接近实时的监控
  • 隔离
  • 限流

服务降级(Fallback)的概念和发生的情况?

服务降级:服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,Fallback;

发生降级的情况:

  • 程序运行异常
  • 超时
  • 服务器熔断触发服务降级
  • 线程池/信号量打满也会导致服务降级

服务熔断(Break)的概念?

相当于保险丝,即当达到最大服务访问后,直接拒绝访问,然后调用服务降级的方法并返回友好的提示;熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息,当检测到该节点微服务调用相应正常后,恢复调用链路;在SpringCloud框架里,熔断机制是通过Hystrix实现的,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制(熔断机制的注解是@HystrixCommand);

熔断的类型?

  • 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均处理时间),当打开时长达到所设时钟则进入半熔断状态;
  • 熔断关闭:熔断关闭不会对服务进行熔断;
  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则,则认为当前服务恢复正常,关闭熔断;

熔断器在什么情况下开始起作用?

首先要知道断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值;

  • 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒;
  • 请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断,默认为20,意味着10秒内,如果该Hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开;
  • 错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发送了30次调用,如果其中有15次出现了异常(即超过了50%的百分比),在默认设定50%阀值情况下,这时候就会将断路器打开;

断路器开启或者关闭的条件?

  • 当满足一定阀值的时候(默认10秒内超过20个请求次数);
  • 当失败了达到一定的时候(默认10秒内超过50%的请求失败);
  • 当达到以上阀值,断路器就会开启
  • 当开启时,所有的请求都不会进行转发;
  • 一段时间之后(默认为5秒),这个时候断路器是半开状态,会让其中一个请求进行转发,如果成功,断路器会关闭;如果失败,断路器继续开启,一直重复4和5步

断路器打开之后?

再有请求调用的时候,将不会调用主逻辑,而是直接调用降级Fallback,通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果;

断路器打开之后原来的主逻辑要如何恢复呢?

Hystrix为我们实现了自动恢复功能,当断路器打开之后。对主逻辑进行熔断之后,Hystrix会启动一个休眠时间窗,降级逻辑是临时的成为主逻辑,当休眠时间窗期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将继续闭合,主逻辑恢复;如果此次请求依然有问题,断路器进入打开状态,休眠时间窗重新计时;

服务限流(FlowLimit)的概念?

秒杀高并发等操作,严禁一窝蜂的拥挤过滤,而是进行排队,1秒钟N个,有序的进行

解决的问题场景

访问超时导致服务器变慢(转圈)?

此时超时,就不应该再等待

服务器出错(宕机或程序出错)?

出错要有兜底

场景:

  • 对方服务超时了,调用者不能一直卡死等待,必须有服务降级;
  • 对方服务宕机了,调用者不能一直卡死等待,必须有服务降级;
  • 对付服务没有问题,但是调用者自己出现故障或有自我要求(如自己的等待时间小于服务提供者),自己处理降级;

服务降级

服务端的降级操作

首先,引入依赖

<!--Hystrix依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

然后,在启动类上标注注解开启Hystrix服务降级的注解,如下:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker // 激活Hystrix服务降级的注解
public class PaymentProvider9000Main {

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

之后,通过@HystrixCommand注解配置Service层需要降级的方法降级的方法,如下:

package edu.hebeu.service;

public interface IPaymentService {

    /**
     * 模拟查询Payment的信息成功的方法
     * @param id
     * @return
     */
    String paymentInfoOK(Integer id);

    /**
     * 模拟查询Payment的信息超时
     * @param id
     * @return
     */
    String paymentInfoTimeout(Integer id);

}

package edu.hebeu.service.impl;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import edu.hebeu.service.IPaymentService;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentServiceImpl implements IPaymentService {

    @Override
    public String paymentInfoOK(Integer id) {
        return "获取信息成功:" + Thread.currentThread().getName() + "-----paymentInfoOK, id = " + id;
    }

    /**
     *  -@HystrixCommand注解的作用,表示如果被标注的方法出现异常、错误信息后,就调用该注解的fallbackMethod属性值对应的方法做为兜底方法
     * @param id
     * @return
     */
    @HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") // 标注这个方法在5000ms内执行完毕就是正常的,超过5000ms就抛出异常信息
    }) // 指定如果本方法出现问题,就调用paymentInfoTimeoutHandler()方法进行处理
    @Override
    public String paymentInfoTimeout(Integer id) {
//        int aa = 1 / 0; // 模拟一个异常

        int seconds = 3;
        try {
            TimeUnit.SECONDS.sleep(seconds); // 模拟业务处理了seconds秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "获取信息成功:" + Thread.currentThread().getName() + "-----paymentInfoTimeout, id = " + id + ", 处理时间:" + seconds;
    }

    /**
     * 由@HystrixCommand注解的fallbackMethod属性值调用这个方法(即当paymentInfoTimeout方法出现异常信息时,就会调用本方法)
     * @param id
     * @return
     */
    private String paymentInfoTimeoutHandler(Integer id) {
        return "paymentInfoTimeout方法的兜底方法:paymentInfoTimeoutHandler方法, id = " + id;
    }
}

客户端的降级操作

需要注意:Hystrix主要还是用来在客户端(服务调用方)进行降级处理的

首先我们这里的客户端使用了Feign去调用服务。所以我们要有一下两个依赖:

<!--Hystrix依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--Feign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

然后,因为OpenFeign使用的是接口调用服务,所以我们不能使用上面在服务端的方式来进行降级,因此,这里我们需要在配置文件开启Feign支持Hystrix,如下:

# 表示开启Feign支持Hystrix的配置
feign:
  hystrix:
    enabled: true

之后在启动类上标注开启Hystrix的注解,如下:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients // 激活Feign
@EnableHystrix // 激活Hystrix
public class OrderConsumer80Main {

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

完成上述的环境搭建后,对于客户端的降级,我们有两种措施,一是在Controller层进行降级保护,原理和在服务端进行降级保护的原理相同;二是在Service层进行降级,但是Service层是通过Feign调用服务的,是接口类型,所以我们需要另一种方式进行降级;

对客户端的Controller层进行降级的操作
package edu.hebeu.controller;

import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import edu.hebeu.service.IOrderFeignService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Slf4j
@DefaultProperties(defaultFallback = "orderGlobalFallbackMethod", commandProperties = {
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") // 标注本接口下的所有未特殊指明降级方法 的方法在1500ms内执行完毕就是正常的,超过1500ms就抛出异常信息
}) // 指定本接口下所有未特定指明降级方法 的方法 的降级方法
public class OrderController {

    @Resource
    private IOrderFeignService orderFeignService;

    /**
     * 调用对应微服务的/payment/{id}/ok接口地址 的接口
     * @param id
     * @return
     */
    @GetMapping("/orderOK/{id}")
    public String orderGetOK(@PathVariable("id") Integer id) {
        log.info("hystrix-feign.....");
        return orderFeignService.getOK(id);
    }

    /**
     * 调用对应微服务的/payment/{id}/timeout接口地址 的接口
     *
     * 该方法被@HystrixCommand注解标注且明确指明了降级方法,表示如果被标注的方法出现异常、错误信息后,就调用该注解
     * 的fallbackMethod属性值对应的方法做为兜底方法
     * @param id
     * @return
     */
    @GetMapping("orderTimeout/{id}")
    @HystrixCommand(fallbackMethod = "orderGetTimeoutHandler", commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000") // 标注这个方法在2000ms内执行完毕就是正常的,超过2000ms就抛出异常信息
    }) // 指定如果本方法出现问题,就调用orderGetTimeoutHandler()方法进行处理
    public String orderGetTimeout(@PathVariable("id") Integer id) {
//        int a = 1 / 0; // 模拟一个异常
        log.info("hystrix-feign.....");
        return orderFeignService.getTimeout(id);
    }

    /**
     * 由orderGetTimeout方法出现异常信息触发本方法,需要注意:该方法的参数要和orderGetTimeout方法的参数保持一致
     * @param id
     * @return
     */
    private String orderGetTimeoutHandler(@PathVariable("id") Integer id) {
//        return "服务消费端,orderGetTimeoutHandler:超时2秒,系统忙(或本服务出现问题)!请稍后重试!查询id:" + id;
        return "服务消费端,orderGetTimeoutHandler:超时2秒,系统忙(或本服务出现问题)!请稍后重试!";
    }

    /**
     * 该方法未使用@HystrixCommand标注,所以该方法出现问题后 不会有任何方法来为它做降级处理
     * @return
     */
    @GetMapping("/orderTestDefaultFallback1")
    public String orderTestDefaultFallback1() {
        int a = 1 / 0;
        return "1服务正常...";
    }

    /**
     * 该方法使用了@HystrixCommand注解,但是@HystrixCommand没有特殊指明使用哪个方法来为本方法做降级,所以当该方法出现问题后,会使
     * 用@DefaultProperties注解defaultFallback属性值对应的全局降级方法 来进行降级处理
     * @return
     */
    @HystrixCommand // 未特定指明降级的方法,所以该方法出现问题后会使用@DefaultProperties注解defaultFallback属性值对应的全局降级方法
    @GetMapping("/orderTestDefaultFallback2")
    public String orderTestDefaultFallback2() {
        int a = 1 / 0;
        return "2服务正常...";
    }

    /**
     * 全局Fallback方法(对没有用@HystrixCommand特定指明fallback值 的方法出现异常时使用)
     * @return
     */
    private String orderGlobalFallbackMethod() {
        return "服务消费端,OrderController.orderGlobalFallbackMethod:出问题啦,请稍后稍后重试~~~";
    }

}

对客户端的Serviice层进行降级的操作
package edu.hebeu.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Service
/*添加@FeignClient注解,表示这是Feign管理的Service,该接口下的方法会调用服务注册中心名为CLOUD-PAYMENT-HYSTRIX-SERVICE的微服务
的指定接口,并且通过fallback指定了降级处理类(该类必须要实现本接口)
 */
@FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE", fallback = OrderFeignFallbackService.class)
public interface IOrderFeignService {

    /**
     * 表示调用 @FeignClient对应的服务下 指定的GET类型的 /payment/{id}/ok请求
     * @param id
     * @return
     */
    @GetMapping("/payment/{id}/ok")
    String getOK(@PathVariable("id") Integer id);

    /**
     * 表示调用 @FeignClient对应的服务下 指定的GET类型的 /payment/{id}/timeout请求
     * @param id
     * @return
     */
    @GetMapping("/payment/{id}/timeout")
    String getTimeout(@PathVariable("id") Integer id);

}

package edu.hebeu.service;

import org.springframework.stereotype.Component;

/**
 * 这个类实现了IOrderFeignService接口,并且被IOrderFeignService接口的@FeignClient注解的fallback属性所指定,那么此时该类就
 * 是IOrderFeignService接口的Fallback降级处理类,实现的方法就做为IOrderFeignService接口对应方法出现异常时(如调用的微服务端宕机)
 * 调用的 降级方法!!!
 */
@Component
public class OrderFeignFallbackService implements IOrderFeignService {
    @Override
    public String getOK(Integer id) {
        return "edu.hebeu.service.OrderFallbackService.getOK:出现一点小问题~~~~~~";
    }

    @Override
    public String getTimeout(Integer id) {
        return "edu.hebeu.service.OrderFallbackService.getTimeout:出现一点小问题~~~~~~";
    }
}

服务熔断

对服务提供方使用熔断,和使用降级的原理十分相似,我们在对服务提供方降级的代码基础上改写,如下:

package edu.hebeu.service;

public interface IPaymentService {

    /**
     * 模拟服务熔断使用
     * @param id
     * @return
     */
    String paymentCircuitBreaker(Integer id);

}

package edu.hebeu.service.impl;

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import edu.hebeu.service.IPaymentService;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentServiceImpl implements IPaymentService {

    @Override
    @HystrixCommand(fallbackMethod = "orderCircuitBreakerFallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启熔断器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 10秒的时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), // 窗口期内,失败率达到60%后跳闸(即10次请求中有6次失败就跳闸)
    })
    public String paymentCircuitBreaker(Integer id) {
        if (id < 0) {
            throw new RuntimeException("id不能为负数!!!");
        }

        return Thread.currentThread().getName() + "\t服务调用成功,流水号:" + IdUtil.simpleUUID();
    }
    private String orderCircuitBreakerFallback(Integer id) {
        return "id不能为负数,请稍后重试!\tid:" + id;
    }

}

通过Controller层指定的接口调用这个方法来测试,首先先发送能触发@HystrixCommand注解的fallbackMethod属性对应方法的请求,如下:

package edu.hebeu.controller;

import edu.hebeu.service.IPaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class PaymentController {

    @Autowired
    private IPaymentService paymentService;

    /**
     * 测试服务熔断的接口
     * @param id
     * @return
     */
    @GetMapping("/paymentCircuitBreaker/{id}")
    public String orderTestCircuitBreaker(@PathVariable("id") Integer id) {
        String result = paymentService.paymentCircuitBreaker(id);
        log.info("result = " + result);
        return result;
    }

}

保证发送超过@HystrixCommand注解的commandProperties属性的请求,这里我们先发送1次id为正数的请求,再发送9次id为负数的请求,然后再发送一个id为正数的请求,会发现此时id虽然为正数,但是还是去了fallbackMethod属性指定的方法,如果我们继续发送id为正数的请求,会发现在某一时刻跳转到了正常的逻辑(这就是熔断);

Hystrix工作流程

在这里插入图片描述

HystrixDashboard

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(HystrixDashboard),Hystrix会持续的记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求,多少成功、多少失败等。Netflix通过hystrix-metrics-stream项目实现了对以上指标的监控。SpringCloud也提供了HystrixDashboard的整合,对监控内容转化成可视化界面;

搭建Hystrix监控端

首先要引入依赖

<!--HystrixDashboard依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后,修改配置文件端口号:

server:
  port: 7999

开启HystrixDashboard,如下:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard // 激活HystrixDashboard
public class HystrixDashboard7999Main {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDashboard7999Main.class, args);
    }
}

此时我们可以通过:这个模块的地址/hystrix即可访问Hystrix的监控首页,如这里就是通过http://localhost:7999/hystrix访问到Hystrix监控主页,此时在输入要监控的服务地址/hystrix.stream即可通过HystrixDashboard监控该服务,如下:
在这里插入图片描述
点击Monitor Stream即可监控http://localhost:9000/服务
在这里插入图片描述
在这里插入图片描述
其中:

  • 实心圆
    • 颜色:表示健康状况:由颜色依次递减:绿色 -> 黄色 -> 橙色 -> 红色
    • 大小:表示流量的变化,流量越大实心圆越大;流量越小实心圆越小;
  • 实线:观察流量的上升和下降趋势
被监控端的注意事项

Hystrix监控的服务要注意一下几点:

一、必须含义以下依赖:

<!--Hystrix依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<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>

二、主启动类的设置,如下

package edu.hebeu;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker // 开启熔断器的注解
public class PaymentProvider9000Main {

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

    /**
     * 此配置是为了能够让Hystrix能够监控该服务而配置的,与服务容错本身无关,SpringCloud升级之后的坑!(ServletRegistrationBean因
     * 为SpringBoot是默认路径不是"/hystrix.stream",只要在自己的项目里部署上如下的Servlet就可以了)
     * @return
     */
    @Bean
    public ServletRegistrationBean<HystrixMetricsStreamServlet> servletRegistrationBean() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean<HystrixMetricsStreamServlet> registrationBean = new ServletRegistrationBean<>(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }

}

Gateway

概述

Gateway是在Spring生态系统上构建的AP网关,基于Spring5SpringBopot2Project Reactor等技术。Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能;如:熔断、限流、重试等;

SpringCloud Gateway做为SpringCloud生态系统中的网关,目标是代替Zuul,在SpringCloud2.0以上的版本中,没有对新版本Zuul2.0以上最新高性能版本进行集成,任然还是使用Zuul 1.xReactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而Webflux框架底层则使用了高性能的Reactor模式通信框架Netty

SpringCloud Gateway的目标是提供统一的路由方式且基于Filter链的方式提供网关基本的功能,如:安全、监控/指标、限流等;

SpringCloud Gateway使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架;

Filter(路由过滤器)可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用,Spring Cloud Gateway内置了多种过滤器,它们都由GatewayFilter的工厂类产生;

能干什么?

  • 反向代理
  • 鉴权
  • 流量监控
  • 熔断
  • 日志监控

微服务架构中网关在哪里?
在这里插入图片描述
SpringCloud Gateway的特性?

Gateway是基于异步的非阻塞模型上进行开发的,性能上不用担心

  • 基于Spring Framework5.0Project ReactorSpring Boot2.0进行构建;
  • 动态路由:能够匹配任何请求属性;
  • 可以对路由指定Predicate(断言)和Filter(过滤器);
  • 集成Histrix的断路器功能;
  • 集成SpringCloud服务发现功能;
  • 易于编写的Predicate(断言)和Filter(过滤器);
  • 请求限流功能;
  • 支持路径重写

SpringCloud GatewayZuul的区别?

  • Zuul1.x是基于阻塞I/O的,Zuul1.x基于Servlet2.5使用阻塞架构,它不支持任何长连接(如WebSocket),Zuul的设计模式和Nginx比较像,每次I/O操作都是从工作线程中选择一个执行、请求线程被阻塞到工作线程完成;但是差别是Nginx是用C++实现的,而Zuul使用Java实现(因为JVM本身会有第一次加载较慢的情况,使得Zuul的性能相对较差);
  • Zuul2.x理念先进,想基于Nginx非阻塞和支持长连接,性能相较于Zuul 1.x有较大提升
  • SpringCloud Gateway建立在Spring Framework5Project ReactorSpring Boot 2之上,使用非阻塞API
  • SpringCloud Gateway还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验;

Zuul1.x模型?

采用的是Tomcat容器,使用的是传统的IO处理模型;ServletServlet Container进行生命周期管理(Servlet生命周期):
在这里插入图片描述

  • Container启动时构造Servlet对象并调用servlet init()方法进行初始化;
  • Container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程),然后调用servlet service()方法;
  • Container关闭时调用servlet destory()方法销毁Servlet

该模型的缺点:Servlet是一个简单的网络IO模型,当请求进入Servlet Container时,Servlet Container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,但是一旦高并发的情况出现时,线程数量就会上涨,而线程资源代价是昂贵的(因为上下文切换,内存消耗非常大),严重的影响请求的处理时间;在一些简单业务场景下,我们不希望为每个请求分配一个线程,只需1个或几个线程就能应对极大并发的请求,这种业务场景下Servlet模型没有优势

因此,Zuul 1.x是基于Servlet之上的一个阻塞式处理模型,即Spring实现了处理所有请求的一个Servlet(DispatcherServlet)并由该Servlet阻塞式处理,所以Zuul 1.x无法摆脱Servlet模型的弊端!!!

Gateway模型?

传统的WEB框架,如struts2SpringMVC等都是基于Servlet APIServlet容器基础之上运行的,但是在Servlet3.1之后有了异步非阻塞的支持,而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的,相对于传统的WEB框架来说,它可以运行在如NettyUndertow及支持Servlet3.1的容器上,非阻塞式+函数式编程(Spring5必须让使用Java8);

Spring WebFluxSpring5.0引入的新的响应式框架,区别于SpringMVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于Reactir来实现响应式流规范;

三大概念?

核心概念一、Route(路由):构建网关的基本模块,由ID、目标URL,一系列的断言和过滤器组成,如果断言为true则匹配该路由

核心概念二、Predicate(断言):参考Java8java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(如请求头或请求参数),如果请求与断言相匹配则进行路由

核心概念三、Filter(过滤):指的是Spring框架中的GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改

总体而言:WEB请求通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制;Predicate就是我们的匹配条件,而Filter就可以理解为一个无所不能的拦截器,有了这两个元素,再加上目标URI,就可以实现一个具体的Route路由了

Gateway的工作流程?
在这里插入图片描述
即客户端向Spring Cloud Gateway发出请求,然后在Gatway Handler Mapping中找到与请求匹配的路由,将其发送到Gateway Web HandlerHandler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回,过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑;核心逻辑:路由转发 + 执行过滤链

其中,Filterpre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;在post类型的过滤器中可以做响应的内容、响应头的修改、日志的输出、流量监控等有着非常重要的作用;

生命周期:pre -> post

种类:GatewayFilter(31种)和GlobalFilter(10多种)

使用

首先搭建环境,网关也要注册进服务注册中心,同时需要注意网关不能配置web依赖和actuor依赖。但是我们必须要导入如下依赖:

<!--Gateway的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

然后我们将其做为一个微服务注册进服务注册中心,yaml文件配置如下:

server:
  port: 9999

spring:
  application:
    name: cloud-gateway

eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
      register-with-eureka: true
      fetch-registry: true

在启动类上添加注解,如下:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class Gateway9999Main {

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

此时网关的环境就搭建完成了,我们配置路由有两种方式,一是通过yaml配置文件,二是通过硬编码;

yaml配置文件配置网关路由

yaml配置文件注册配置完路由后的样例如下:

############################## 网关Gateway的配置 ###############################

server:
  port: 9999

spring:
  application:
    name: cloud-gateway

  cloud:
    # Gateway网关的配置
    gateway:
      routes:
        # 路由1
        - id: payment_routh1 # 设置路由的ID,无要求规则,但是必须保证唯一,建议配合服务名定义
          uri: http://localhost:9000 # 配置后提供服务的路由地址
          predicates:
            - Path=/payment/** # 断言,路径相匹配的进行路由

        # 路由2
        - id: payment_routh2 # 设置路由的ID,无要求规则,但是必须保证唯一,建议配合服务名定义
          uri: http://localhost:9000 # 配置后提供服务的路由地址
          predicates:
            - Path=/feignTimeout/** # 断言,路径相匹配的进行路由

        # 路由3
        - id: payment_routh2 # 设置路由的ID,无要求规则,但是必须保证唯一,建议配合服务名定义
          uri: http://localhost:9001 # 配置后提供服务的路由地址
          predicates:
            - Path=/payment/** # 断言,路径相匹配的进行路由

eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
#      defaultZone: http://eureka7000.com:7000/eureka,http://eureka7001.com:7001/eureka # 设置Eureka的服务注册中心(集群版)
      register-with-eureka: true
      fetch-registry: true

配置完成后,就可以通过网关访问配置的路由地址了,如上,此时我们可以通过访问网关的指定地址来通过配置的路由转发到注册中心的指定服务的接口地址,如本例中,可以通过访问网关的http://localhost:9999/feignTimeout通过路由访问到http://localhost:9001/feignTimeout、通过http://localhost:9999/payment/17访问到http://localhost:9000/payment/17
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

硬编码配置网关路由

IOC注入特定的组件,如下:

package edu.hebeu.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GatewayConfig {

    /**
     * 自定义的网关路由转发配置
     * @param routeLocatorBuilder
     * @return
     */
    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder builder = routeLocatorBuilder.routes(); // 相当于配置文件的routes

        // 表示注册id为route_baiduNews的路由 当访问 本服务的地址(网关的地址)/guonei 或者 本服务的地址(网关的地址)/mil时,会转发到http://news.baidu.com/guonei 或者 http://news.baidu.com/mil地址
        builder.route("route_baiduNews",
                r -> r.path("/guonei", "/mil").uri("http://news.baidu.com")).build();

        // 表示注册id为route_finance的路由 当访问 本服务的地址(网关的地址)/finance时,会转发到https://news.baidu.com/finance地址
        builder.route("route_finance", r -> r.path("/finance").uri("https://news.baidu.com/finance")).build();

        return builder.build();
    }

    /**
     * 自定义的网关路由转发配置
     * @param routeLocatorBuilder
     * @return
     */
    @Bean
    public RouteLocator routeLocator2(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder builder = routeLocatorBuilder.routes(); // 相当于配置文件的routes

        // 表示注册id为route_internet的路由 当访问 本服务的地址(网关的地址)/internet时,会转发到http://news.baidu.com/internet地址
        builder.route("route_internet",
                r -> r.path("/internet").uri("http://news.baidu.com")).build();

        // 表示注册id为route_game的路由 当访问 本服务的地址(网关的地址)/game时,会转发到http://news.baidu.com/game地址
        builder.route("route_game",
                r -> r.path("/game").uri("http://news.baidu.com/game")).build();

        return builder.build();
    }

}

此时即可实现转发,如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过微服务名实现动态路由

默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建路由进行转发,从而实现动态路由的功能,可以通过yaml文件配置,如下:

############################## 网关Gateway的配置 ###############################
server:
  port: 9999

spring:
  application:
    name: cloud-gateway

  cloud:
    # Gateway网关的配置
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由(实现动态路由)
      routes:
        # 路由1
        - id: payment_routh1 # 设置路由的ID,无要求规则,但是必须保证唯一,建议配合服务名定义
          uri: lb://CLOUD-PAYMENT-SERVICE # 提供服务的 服务在注册中心中的名字,lb:表示LoadBalancer,即负载均衡
          predicates:
            - Path=/payment/** # 断言,路径相匹配的进行路由

        # 路由2
        - id: payment_routh2 # 设置路由的ID,无要求规则,但是必须保证唯一,建议配合服务名定义
          uri: lb://CLOUD-PAYMENT-SERVICE # 提供服务的 服务在注册中心中的名字,lb:表示LoadBalancer,即负载均衡
          predicates:
            - Path=/feignTimeout/** # 断言,路径相匹配的进行路由

eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
      register-with-eureka: true
      fetch-registry: true

启动:
在这里插入图片描述

此时如果访问网关地址的映射路由地址会发现已经实现了轮询方式的负载均衡

Predicate的使用

Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理

常见的配置项如下:

spring:
  application:
    name: cloud-gateway

  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由(实现动态路由)
      routes:
      
        - id: payment_routh1 # 设置路由的ID,无要求规则,但是必须保证唯一,建议配合服务名定义
          uri: lb://CLOUD-PAYMENT-SERVICE # 提供服务的 服务在注册中心中的名字,lb:表示LoadBalancer,即负载均衡
          predicates:
            - Path=/payment/** # 表示路径是以/payment/开头的请求才会执行该路由
            - After=2021-06-09T19:17:33.177+08:00[Asia/Shanghai] # 表示这个路由在 指定的时间之后 才能使用
            - Before=2021-06-09T19:17:33.177+08:00[Asia/Shanghai] # 表示这个路由在 指定的时间之前 才能使用
            - Between=2021-06-09T19:20:33.177+08:00[Asia/Shanghai],2022-07-09T19:17:33.177+08:00[Asia/Shanghai] # 表示这个路由在 指定的时间之内 才能使用
            - Cookie=account,tyong # 两个参数,一个是CookieName,一个是正则表达式,路由规则会通过获取对应的CookieName值和正则表达式匹配,匹配成功就会执行路由,反之则不执行;这里的意思是如果有account名字的COOKIE,并且COOKIE的值为tyong才会执行该路由
            - Header= X-Request-Id,\d+ # 两个参数,一个是属性名称,一个是正则表达式,这个属性值和正则表达式匹配则执行;这里意思是请求头要含有X-Request-Id属性,并且值为正整数才会执行该路由
            - Host= **.com # 接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.做为分隔符,它通过参数中的主机地址做为匹配规则;这里表示主机名是.com结尾的才会执行该路由
            - Method=POST # 表示请求是POST才会执行该路由
            - Query=username,\d+ # 表示要有参数名为username,并且值还是正整数,才会执行该路由

所有指定的Predicate会形成与的关系,共同作用请求,来决定是否转发的路由

Filter的使用

常见的配置项:

spring:
  application:
    name: cloud-gateway

  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由(实现动态路由)
      routes:
        - id: payment_routh2 # 设置路由的ID,无要求规则,但是必须保证唯一,建议配合服务名定义
          uri: lb://CLOUD-PAYMENT-SERVICE # 提供服务的 服务在注册中心中的名字,lb:表示LoadBalancer,即负载均衡
          filters:
            - AddRequestParameter=X-Request-Id,1002 # 过滤工厂会在匹配的请求头位置加上一对名为X-Request-Id,值为1002的请求头
          predicates:
            - Path=/feignTimeout/** # 断言,路径相匹配的进行路由

过滤器类有40多种,这里就不一一指定了,我们着重来看如何自定义Filter

自定义Filter

实现GlobalFilterOrdered接口,并注入到IOC即可,如下:

package edu.hebeu.component;

import lombok.extern.slf4j.Slf4j;
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;

import java.util.Date;

@Component
@Slf4j
public class MyGatewayFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info(new Date() + ": 已经进入自定义的edu.hebeu.component.MyGatewayFilter过滤器");
        String myName = exchange.getRequest().getQueryParams().getFirst("myName");// 表示获取此时经过该过滤器的请求 的名为myName的参数值
        if (myName == null) {
            log.info("myName参数为空,非法用户!!!");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); // 响应状态码406(不被接受的请求)
            return exchange.getResponse().setComplete(); // 让该请求退出(排除出正在经过此过滤器的请求)
        }
        return chain.filter(exchange); // 将处理后的请求放到链中,以让下一个过滤器处理该请求
    }

    /**
     * 该方法表示本过滤器 加载的顺序(返回值越小,加载优先级越高)
     * @return
     */
    @Override
    public int getOrder() {
        return 0; // 返回0,表示该过滤器最先处理请求
    }
}

测试:
在这里插入图片描述
在这里插入图片描述

Config

概述

分布式系统面临的问题——配置问题

微服务意味着将要单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务,由于每个服务都需要必要的配置信息才能运行,所以一套集中的、动态的配置管理设施是必不可少的;SpringCloud提供了ConfigServer来解决这个问题(我们每一个微服务自己带着一个application.yaml,上百个配置文件的管理)

是什么?
在这里插入图片描述
Spring Cloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置Spring Cloud Config分为服务端和客户端两部分,其中:服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口客户端则是通过指定的配置中心来管理应用资源,以及业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容

能干什么?

  • 集中管理配置文件;
  • 不同环境不同配置,动态化的配置更新,分环境部署(如:dev、test、prod、beta、release);
  • 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,配置会向配置中心统一拉取配置自己的信息;
  • 当配置文件发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置;
  • 将配置信息以RESTFUL接口的形式暴漏(访问刷新即可);

Spring Cloud Config默认使用Git来存储配置文件(也有其他方式,如:SVN或本地文件),推荐使用http/https形式的Git访问

服务端的配置

首先要知道配置中心也应该做为一个服务注册进注册中心,因此必须引入如下依赖

<!--Config Server(配置中心)依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!--引入Eureka-Client依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

然后,创建git仓库(用来存放配置文件,文件命名最好以{name}-${profile}进行命名),如下:
在这里插入图片描述
yaml配置文件的配置:

########################### 配置中心服务的配置 ################################
server:
  port: 9899

spring:
  application:
    name: cloud-config-center
  cloud:
    config:
      server:
        git:
          username: xxx
          password: xxxxx
          uri: https://gitee.com/tangyong-top/spring-cloud-config.git # 存放配置中心配置文件的Git仓库地址
          search-paths: # 指明搜索目录(对应的就是上述的git仓库名)
            - spring-cloud-config
      label: master # 读取的分支

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心

之后设置启动类,如下:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableConfigServer // 激活配置中心
public class ConfigCenter9899Main {

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

此时配置中心的环境就搭建完成了,那如何进行访问呢?我们可以参考如下的规则:

指定规则:name:要读取配置文件的前缀(前面创建配置文件时的文件名是以{name}-{profile}命名的);profile:对应的是那种情况下的配置文件(前面创建配置文件时的文件名是以{name}-{profile}命名的);branch:存放需要读取配置文件的分支(要读取的分支);

  • 访问方式一:配置中心地址/{branch}/{application}-{profile};如:
    • http://localhost:9899/master/config-dev.yaml
    • http://localhost:9899/master/config-test.yaml
    • http://localhost:9899/master/config-prod.yaml
  • 方式二:配置中心地址/{name}-{profile}.yaml;如:
    • http://localhost:9899/config-dev.yaml
    • http://localhost:9899/config-test.yaml
    • http://localhost:9899/config-prod.yaml
  • 方式三:配置中心/{name}/{profile}/{branch},如:
    • http://localhost:9899/config/dev/master
    • http://localhost:9899/config/test/master
    • http://localhost:9899/config/prod/master

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

客户端的配置

application.yamlbootstrap.yaml是异同?

  • application.yaml是用户级的资源配置项;bootstrap.yaml是系统级的,优先级更高;SpringCloud会创建一个Bootstrap Context,做为Spring应用的``ApplicationContext的父上下文,初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置,这两个上下文共享一个从外部获取的Environment`;
  • Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖,Bootstrap ContextApplication Context有着不同的约定,所有新增了一个bootstrap.yaml文件,保证Bootstrap ContextApplication Context配置的分离;
  • bootstrap.yamlapplication.yaml优先级高,先加载

第一步,导入依赖:

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

然后,使用bootstrap.yaml配置文件:

############################# Config客户端的bootstrap.yaml配置文件(系统的) ####################################
server:
  port: 9898

spring:
  application:
    name: cloud-config-client
  cloud:
    config: # SpringCloudConfig客户端的配置
      label: master # 读取的分支名
      name: config # 读取的配置文件名
      profile: test # 读取的配置文件的后缀名
      uri: http://localhost:9899 # 配置中心服务的地址

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心

主启动类:

@SpringBootApplication
@EnableEurekaClient
public class ConfigClient9898Main {

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

Conroller层的创建:

package edu.hebeu.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConfigClientController {

    @Value(("${config.info}")) // 读取到的远程仓库的配置文件中的属性
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return configInfo;
    }

}

此时客户端的配置就完成了,启动注册中心、配置中心、和这个配置客户端,即可通过该客户端提供的/configInfo接口实现对远程仓库的配置文件的访问,如下:
在这里插入图片描述
在这里插入图片描述

实现动态刷新

对于上述搭建起来的配置中心客户端,我们无法实现动态刷新(远程仓库的配置文件修改后,配置中心可以访问到修改后的属性,但是配置中心的客户端却不能访问到修改后的属性);

实现动态刷新的第一步,在上一例子的基础上进行修改,引入actuator图形化监控依赖,如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

bootstrap.yaml文件新加配置,暴漏监控端点,如下:

# 暴漏监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"

在Controller层上添加@RefreshScope注解,保证能够动态的获取最新的配置文件,如下:

package edu.hebeu.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope // 配置该Controller刷新的功能,保证能够动态的获取最新的配置文件
public class ConfigClientController {

    @Value(("${config.info}")) // 读取到的远程仓库的配置文件中的属性
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return configInfo;
    }
}

此时就能实现动态的刷新了,如下:

原先的配置中心
在这里插入图片描述
原先的配置中心客户端
在这里插入图片描述
修改远程仓库的配置文件
在这里插入图片描述
修改后的配置中心
在这里插入图片描述
发送POST请求通知配置中心客户端进行刷新(注意:必须是POST请求,application/json形式的数据,请求地址配置中心客户端地址/actuator/refresh,如本例的:http://localhost:9898/actuator/refresh)
在这里插入图片描述
响应结果:
在这里插入图片描述此时刷新配置中心客户端,可以发现读取到的配置信息已经修改了,如下:
在这里插入图片描述

Bus

概述

是什么?

Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能,Bus目前只支持两种消息代理:RabbitMQKafkaBus可以配合Spring Cloud Config使用可以实现配置的动态刷新;
在这里插入图片描述
能干什么?

Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、时间推送等,也可以当作微服务间的通信通道;
在这里插入图片描述
总线是什么?

在微服务架构中,通常会使用轻量级的消息代理来构建一个公用的消息主题,并让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线,在总线上的各个实例都可以方便的广播一些需要让其他连接在该主题上的实例都知道的消息;

其基本原理是ConfigClient实例都监听MQ中同一个Topic(默认是SpringCloudBus),当一个服务刷新数据的时候,它会把这个信息放到Topic中,这样其他监听同一个Topic的服务就能得到通知,然后去更新自身的配置;

使用

在概述中我们看到了有两种方式实现Bus全局广播,一:通过POST请求/bus/refresh通知一个配置中心客户端,让这个客户端去发送消息通知配置中心,然后配置中心在通知其他的客户端;二:通过POST请求/bus/refresh通知配置中心,然后让配置中心通知其他的客户端;

因此显然第二种方式更适合实现,因为:

  • 第一种方式打破了微服务的职责单一性,由于微服务本身是业务模块,它本不应该承担配置刷新的职责;
  • 第一种方式破坏了微服务各节点的对等性;
  • 第一种方式有一定的局限性,如:微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那会增加更多的修改;

由此,我们通过第二种方式实现广播效果

首先,我们在Config-服务端的配置->的基础上改写配置中心,引入RabbitMQ整合BUS的依赖,同时保证有actuator依赖,如下:

<!--RabbitMQ依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--actuator依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

yaml文件添加RabbitMQ的依赖,bus刷新端点的暴漏,如下:

spring:
  # RabbitMQ的相关配置
  rabbitmq:
    host: 主机名
    port: 端口
    username: rabbitMQ的用户名
    password: rabbitMQ用户的密码
    
# RabbitMQ的相关配置,暴漏bus刷新配置的端点
management:
  endpoints: # 暴漏bus的刷新配置端点
    web:
      exposure:
        include: 'bus-refresh'

到此配置中心(服务端)就配置完成了,接下来配置服务中心的客户端

首先,我们参照Config->客户端的配置->实现动态刷新搭建一个新的配置中心客户端,定义端口为9897,此时我们就有两个服务中心配置端了,然后再引入RabbitMQ依赖,同时保证有actuator依赖,如下:

<!--RabbitMQ与BUS整和的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<!--actuator依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加yaml配置

spring:
  # RabbitMQ的相关配置
  rabbitmq:
    host: 主机名
    port: 端口
    username: rabbitMQ的用户名
    password: rabbitMQ用户的密码

两个配置中心客户端都按照这种方式配置,此时配置中心客户端就配置完成了
在这里插入图片描述
我们改写远程仓库的配置文件之后,只需要通过POST请求配置中心地址/配置中心配置文件设置的暴漏端点,即可实现通知配置中心(通知配置中心后,配置中心就会通过总线通知其他的客户端刷新配置文件,完成一次通知全部生效的广播形式刷新配置文件);广播地址:配置中心的地址/actuator/配置中心配置文件设置的暴漏端点,这里就是http://localhost:9899/actuator/bus-refresh请求(需要注意:该请求必须是POST类型的,数据格式为application/json),发送该请求之后,就会发现该配置中心的所有客户端的配置(通过REST接口访问)就全部修改了;

以上发送广播会让全部的在总线上的客户端刷新,那如何实现定点的刷新?

可以通过发送配置中心的地址/actuator/配置中心配置文件设置的暴漏端点/要刷新的客户端应用名(spring.application.name的值):客户端的端口即可,如本例中修改远程仓库配置文件,我们只通知9898的客户端但是不通知9897客户端,可以通过:http://localhost:9899/actuator/bus-refresh/cloud-config-center:9898POST请求即可实现

Stream

概述

消息驱动是什么?

是屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型;

什么是SpringCloud Stream

是一个构建消息驱动微服务的框架,用于构建与共享消息传递系统连接的高度可伸缩的事件驱动微服务框架,该框架提供了一个灵活的编程模型,它建立在已经建立和熟悉的Spring的最佳实践上,包括支持持久化的发布/订阅、消费组以及消息分区这三个核心概念;通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动,Spring Cloud Stream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念;目前仅支持RabbitMQKafka

我们先来看看标准的MQ结构图
在这里插入图片描述
可以看到:

  • 生产者/消费者之间靠消息媒介Message传递消息内容;
  • 消息必须走特定的通道MessageChannel
  • 消费通道MessageChannel的子接口SubscribableChannelMessageHandler消息处理器所订阅;

为什么用Cloud Stream

当我们一个系统中用到了RabbitMQKafka这两个消息中间件时,由于RabbitMQExchange交换机的概念,KafkaTopicPartitions分区概念,这两个消息中间件的架构是不同的,这些中间件的差异导致我们在实际项目开发中如果使用了两个MQ时就会出现问题,此时Spring Cloud Stream给我们提供了一种解耦的方式,如下图示:
在这里插入图片描述
Binder绑定器的概念?

通过定义绑定器做为中间层,完美的实现了应用程序和消息中间件细节之间的隔离,通过向应用程序暴漏统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件来实现;即,通过定义绑定器Binder做为中间层,实现了应用程序和消息中间件之间的隔离;Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件,使得微服务开发的高度解耦,服务可以关注自己的业务流程;

BinderINPUT对应于消费组,OUTPUT对应于生产者
在这里插入图片描述
Stream中的消息通信方式遵循了发布-订阅模式,TOPIC主题进行广播,在RabbitMQ中就是ExchangeKafka中就是Topic

Stream重要的组件和常用注解:

  • Binder:用来很方便的连接中间件,屏蔽差异;
  • Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置;
  • SourceSink:简单的理解为参照对象是SpringCloud Stream自身,从Stream发布消息就是输出,接收消息就是输入;
  • @Input:注解标识输入通道,通过该输入通道接收到的消息进入应用程序;
  • @Output:注解标识输出通道,发布的消息将通过该通道离开应用程序;
  • @StreamListener:监听队列,用于消费者的队列的消息接收;
  • @EnableBinding:指信道ChannelExchange绑定在一起;

Stream编码的流程和套路?

先来看一张图:
在这里插入图片描述
通过上图我们可以发现,无论是使用那种MQ,也无论是生产者还是消费者,我们在Stream中都不需要关注,生产者只需要做到将管道设置为Source并把消息数据给管道Channel即可;消费者只需要做到将管道设置为Sink,然后从管道Channel中取出消费消息即可

消息驱动——生产者的搭建

该服务要注册进服务注册中心,且该服务使用RabbitMQ,导入如下依赖:

<!--Stream整合RabbitMQ的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

yaml配置文件如下:

############################ Stream 生产端的配置 ###############################
server:
  port: 9000

spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: # 开始配置要绑定的RabbitMQ服务信息
        defaultRabbit: # 表示定义的名称,用于与binding整合
          type: rabbit # 消息组件的类型
          environment: # rabbitmq的环境配置
            spring:
              rabbitmq:
                host: RabbitMQ的主机地址
                port: 5672
                username: 用户名
                password: 密码
      bindings: # 服务的整合处理
        output: # 这个名字是一个通道的名字
          destination: myExchange # 表示要使用的交换机的名称定义
          content-type: application/json # 设置消息类型,这里表示使用json;需要注意如果是文本则设置为"text/plain"
          binder: defaultRabbit # 表示要绑定的消息服务的具体设置


eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(这里为2秒,默认为30秒)
    lease-expiration-duration-in-seconds: 5 # 最后一次收到心跳的时间上限(这里为5秒,默认90秒)
    instance-id: send-9000.com # 在信息列表中显示的主机名
    prefer-ip-address: true # 访问的路径变成IP地址

启动类:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class StreamProvider9000Main {

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

业务层的创建,业务层此时操作的是管道,而且因为该模块是生产者,所以是要向管道中发送数据的,因此业务层的代码如下:

package edu.hebeu.service;

public interface IMessageProviderService {

    /**
     * 发送消息的方法
     * @return
     */
    String send();
}

package edu.hebeu.service.impl;

import edu.hebeu.service.IMessageProviderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;

import java.util.UUID;

@EnableBinding(Source.class) // 定义消息的推送管道(指定为Source,即出消息的通道),该业务类此时不需要加入到IOC,而是与Stream通道进行交互
@Slf4j
public class MessageProviderServiceImpl implements IMessageProviderService {

    @Autowired
    private MessageChannel output; // 消息发送管道

    @Override
    public String send() {
        String data = UUID.randomUUID().toString();
        output.send(MessageBuilder.withPayload(data).build()); // 将data构建成消息并加入到通道
        log.info("edu.hebeu.service.impl.MessageProviderServiceImpl.send, data = " + data);
        return "消息已发送,data = " + data;
    }
}

此时我们只需要提供一个接口让这个方法触发即可,如下:

package edu.hebeu.controller;

import edu.hebeu.service.IMessageProviderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MessageProviderController {

    @Autowired
    private IMessageProviderService messageProviderService;

    @GetMapping("/send-message")
    public String sendMessage() {
        return messageProviderService.send();
    }

}

至此,Stream生产端配置完毕

消息驱动——消费者的搭建

对于消费者端,也应该要注册进服务注册中心,因此我们需要如下配置:

<!--Stream整合RabbitMQ的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

yaml的配置(和生产者端基本一样,但是要注意一些细节,在如下配置中体现(如:使用input、交换机要与生产者端一致等)

############################ Stream 消费端的配置 #############################
server:
  port: 8002

spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: # 开始配置要绑定的RabbitMQ服务信息
        defaultRabbit: # 表示定义的名称,用于与binding整合
          type: rabbit # 消息组件的类型
          environment: # rabbitmq的环境配置
            spring:
              rabbitmq:
                host: RabbitMQ的主机地址
                port: 5672
                username: 用户名
                password: 密码
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称(注意生产者是output,消费者是input)
          destination: myExchange # 表示要使用的交换机的名称(注意要和生产者配置的交换机保持一致)
          content-type: application/json # 设置消息类型,这里表示json
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置


eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(这里为2秒,默认为30秒)
    lease-expiration-duration-in-seconds: 5 # 最后一次收到心跳的时间上限(这里为5秒,默认90秒)
    instance-id: receive-8002.com # 在信息列表中显示的主机名
    prefer-ip-address: true # 访问的路径变成IP地址

启动类的配置

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
public class StreamConsumer8002Main {

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

该模块做为消费者,应该是由生产端发送消息,该模块才能接收,因此,这里我们通过一个方法来接收生产端发来的消息,如下:

package edu.hebeu.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;

@EnableBinding(Sink.class) // 绑定通道,并将通道指定为Sink,即接收消息的通道
@Slf4j
public class MessageConsumerListener {

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

    /**
     * 该方法用来接收发来的消息
     * @param message
     */
    @StreamListener(Sink.INPUT)
    public void input(Message<String> message) {
        log.info("edu.hebeu.listener.MessageConsumerListener.input消费者,收到消息:" + message.getPayload() + "\t" + serverPort);
    }

}

此时消费者端就搭建完成了!

通过测试,我们可以发现已经搭建成功
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

重复消费的解决

-我们如果按照上面的消费者端再创建消费者并将它加入到集群(此时就是一个生产者+两个消费者),会发现,如果我们此时生产一个消息,这两个消费者就会同时将这个消息消费掉,这就是重复消费的问题;

Stream中,处于同一个group中的消费者是竞争的关系,此时就能够保证消息只能被其中的一个消费者消费;如果是不同组的消费者,它们之间是没有竞争关系的(此时就会出现重复消费的问题);因此,重复消费的解决方案就是分组(将需要解决的消费者分到同一个组中即可);

观察RabbitMQ的网页配置页面,可以发现因为我们没有指定分组,RabbitMQ会为每个消费者自定义一个组,导致这两个消费者不在同一个组,进而导致了它们之间没有了竞争关系,从而导致重复消费的问题,因此我们可以在这两个消费者的配置文件中指定它们所在的组(指定为一样的),就能实现它们在同一个组,从而使它们之间产生竞争关系,解决重复消费的问题,通过将两个配置文件的spring.cloud.stream.bindings.group属性为同样的即可,如下配置:

############################ Stream 消费端的配置 #############################
server:
  port: 8002

spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: # 开始配置要绑定的RabbitMQ服务信息
        defaultRabbit: # 表示定义的名称,用于与binding整合
          type: rabbit # 消息组件的类型
          environment: # rabbitmq的环境配置
            spring:
              rabbitmq:
                host: tyong-top.design
                port: 5672
                username: guest
                password: guest
      bindings: # 服务的整合处理
        input: # 这个名字是一个通道的名称(注意生产者是output,消费者是input)
          destination: myExchange # 表示要使用的交换机的名称(注意要和生产者配置的交换机保持一致)
          content-type: application/json # 设置消息类型,这里表示json
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置
          group: groupA # 指定这个分组到groupA组

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7000/eureka # 设置Eureka的服务注册中心(单机版)
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(这里为2秒,默认为30秒)
    lease-expiration-duration-in-seconds: 5 # 最后一次收到心跳的时间上限(这里为5秒,默认90秒)
    instance-id: receive-8002.com # 在信息列表中显示的主机名
    prefer-ip-address: true # 访问的路径变成IP地址

当两个文件都配置完成后,此时就解决了重复消费的问题(默认轮询);并且该组的消息是有持久化的(如果消费端宕机,生产端继续生产消息,若某一时刻消费端恢复,则会将生产端之前生产的又未消费的消息 消费掉)

Sleuth

概述

引入问题

在微服务架构中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前段请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败;

是什么?

Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并且兼容支持了Zipkin;需要注意:Spring CloudF版开始就已经不需要自己构建Zipkin Server了,只需要调用jar包即可;Zipkin的jar包下载地址:https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/
在这里插入图片描述
调用的完整链路
在这里插入图片描述
名词解释

  • Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识;
  • Span:表示调用链路来源,通俗的理解Span就是一次请求信息

使用

下载启动zipkin-server-2.14.1-exec.jarjar包,此时可以通过CMD窗口或者localhost:9411/zipkin地址访问WEB端管理页面

在要监控的微服务中引入zipkin依赖,如下:

<!--包含了Sleuth+zipkin-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

此时给要监控的服务的yaml文件添加zipkinsleuth配置,如下:

spring:
    # zipkin的配置
  zipkin:
    base-url: http://localhost:9411 # 指定启动zipkin jar包后的网络请求地址
  # sleuth的配置
  sleuth:
    sampler:
      probability: 1 # 采集率(0到1之间),1表示全部采集
  ############# 由此可以看出sleuth用来进行采集数据,zipkin用来将采集到的数据展示 #################

此时就可以通过http://localhost:9411地址得到每次请求微服务调用的链路
在这里插入图片描述

SpringCloudAlibaba

能干什么?

  • 服务限流降级,默认支持ServletFeignRestTemplateDubboRocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics监控;
  • 服务注册和发现:适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持;
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新;
  • 消息驱动能力:基于Spring Cloud Stream为微服务应用构建消息驱动能力;
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务,支持在任何应用、任何时间、任何地点存储和访问任意类型的数据;
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网络任务,网络任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行;

产品

  • Sentinel:阿里巴巴开源产品,把流量作为切入点,从流量控制、熔断降级、系统负载保护等的哦个维度保护服务的稳定性;
  • Nacos:阿里巴巴开源产品,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台;
  • RocketMQ:基于Java的高性能、高吞吐量的分布式消息和流计算平台;
  • Dubbo:一款高性能的Java RPC框架;
  • Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案;
  • Alibaba Cloud OSS:阿里云对象存储服务(Object Storage Service,简称OOS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务,可以在任何应用、任何时间、任何地点存储和访问任意类型的数据;
  • Alibaba Cloud SchedulerX:阿里中间件团队开发的一款分布式任务调度产品,支持周期性的任务与固定时间点触发任务;

Nacos

概述

是什么?

一个易于构建云原生应用的动态服务发现、配置管理和服务管理平台;Nacos相当于注册中心+配置中心的组合(即:Nacos = Eureka + Config + Bus);可以替代Eureka做服务注册中心,代替Config做 服务配置中心;CP + AP模型,支持控制台管理;

安装启动

下载后的压缩包解压,然后启动bin/startup.cmd命令即可此时单机版的Nacos就启动成功了,可以通过:http://localhost:8848/nacos地址访问;默认的账号密码都是nacos

Nacos服务发现实例模型,可以在APCP之间来回切换,如下图示:
在这里插入图片描述
何时选择使用何种模型?

  • 需要注意:C是所有节点在同一时间看到的数据是一致的,而A的定义是所有的请求都会收到响应;
  • 一般来说,如果不需要存储服务级别的信息且服务实例是通过Nacos-Client注册,并能够保持心跳上报,那么就可以选择AP模式。当前珠泪的服务如:Spring CloudDubbo,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下支支持注册临时实例;
  • 如果需要在服务级别编辑或者存储配置信息,那么CP是必须的,K8S服务和DNS服务则适用于CP模式,CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误;
  • 可以通过命令curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switch?entry=serverMode&value=CP'

做服务注册中心

首先保证Nacos启动,然后创建微服务并注册进Nacos

单机版(一个Nacos)

参考Eureka配置,将Eureka客户端的依赖替换成Nacos依赖即可,如下:

微服务提供者集群的搭建
<!--SpringCloud Alibaba Nacos依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

配置yaml

#################################### 注册进Nacos 的微服务 ###################################

server:
  port: 9000

spring:
  application:
    name: nacos-payment-service # 该属性值默认会作为该服务在注册中心的名称
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos服务端的地址

management:
  endpoints: # 监控端点
    web:
      exposure:
        include: '*' # 全部暴漏

启动类

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentProvider9000Main {

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

此时该微服务启动后就会注册进入Nacos,我们此时就在这个微服务上编写逻辑即可,如下:

package edu.hebeu.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PaymentController {

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

    @GetMapping("/payment/{id}")
    public String getPayment(@PathVariable("id") Integer id) {
        return "Nacos-Client-Payment" + serverPort + ", id = " + id;
    }
}

按照上述的逻辑再搭建一个同上面的微服务,并注册进入Nacos,结果如下:
在这里插入图片描述

微服务提供者的搭建

这个模块同样也是微服务,因此和上述的配置一样,引入Nacos依赖即可,如下:

<!--SpringCloud Alibaba Nacos依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

yaml的配置如下

############################### 消费服务 的微服务模块配置 ########################
server:
  port: 80

spring:
  application:
    name: nacos-order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:848 # Nacos服务注册中心的地址

# 配置这个模块(消费者)要去访问哪个微服务(名称是注册成功进Nacos的微服务)
service-url:
  nacos-user-service: http://nacos-payment-service

启动类

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class OrderConsumer80Main {

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

配置类,注入RestTemplate组件,如下:

package edu.hebeu.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced // 赋予RestTemplate组件负载均衡的能力(如果有多个同名的微服务,RestTemplate组件会通过负载均衡进行合理的调用)
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

业务类的创建,用来调用进行消费的微服务接口

package edu.hebeu.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@Slf4j
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 这个模块(消费者)要去访问哪个微服务(名称是注册成功进Nacos的微服务)
     */
    private static final String SERVICE_URL = "http://nacos-payment-service";

    /**
     * 微服务消费者暴漏的接口
     * @param id
     * @return
     */
    @GetMapping("/order/{id}")
    public String order(@PathVariable("id") Integer id) {
        // TODO 调用微服务提供者的接口
        return restTemplate.getForObject(SERVICE_URL + "/payment/" +id, String.class);
    }

}

此时,消费者的微服务就搭建完毕了,需要注意由于Nacos整合了Ribbon,所以Nacos自带负载均衡功能!

做配置中心

Nacos管理中心网页的配置管理->配置列表中会发现一个dataId的字段,这个字段如何指定?

dataId的公式:${prefix}-${spring.profile.active}.${file-extension}

  • prefix:默认为spring.application.name的值,也可以通过配置项spring.cloud.config.prefix进行配置;
  • spring.profile.active:即当前环境对应的profile,当spring.profile.active为空时,对应的连接方-将不存在,即dataId的拼接格式变成${prefix}.${file-extendsion}
  • file-extension:即配置内容的数据格式,可以通过配置项spring.cloud.nacos.file-extension来配置,目前支持propertiesyaml
  • 则最终公式:${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension};即在Nacos配置中心的文件命名最终为此

首先在前面的配置中心客户端的基础上,替换原先的配置为Nacos的配置依赖,如下:

<!--Nacos Config的依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

添加application.yaml配置文件

########################### 配置中心的客户端模块配置 ############################
spring:
  profiles:
    active: dev # 激活为开发时的配置(配合bootstrap.yaml配置文件,表示从配置中心读取一个名为:xxx-dev.yaml的配置文件)

为了能够读取配置中心的配置文件,这里我们还需要创建一个bootstrap.yaml,如下:

########################### 配置中心的客户端模块配置(因为项目初始化时,要保证先从配置中心进行配置拉取配置,保证项目的正常启动,所以需要该配置文件) ############################
# 需要注意SpringBoot中配置文件的加载是存在优先级顺序的,bootstrap 优先于 application

server:
  port: 9898

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos作为服务注册中心的地址
      config:
        server-addr: localhost:8848 # Nacos作为配置中心的地址
        file-extension: yaml # 指定为yaml格式的配置(会从配置中心读取后缀为yaml的配置文件)


# dataId的配置公式:${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 因此,该模块在Nacos配置中心中配置文件的dataId就是:nacos-config-client-dev.yaml

此时通过上面的applicationbootstrap.yaml配置文件在Nacos配置中心创建出其对应的dataId配置文件,如下:
在这里插入图片描述
启动类

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ConfigClient9898Main {

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

在按照前面所描述的配置中心对外提供能够访问到配置的接口,如下:

package edu.hebeu.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RefreshScope // 配置该Controller刷新的功能,保证能够动态的获取最新的配置文件
public class ConfigClientController {

    @Value(("${config.info}")) // 读取到的远程仓库的配置文件中的属性
    private String configInfo;

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

    @Value("${config.uri}")
    private String configUri;

    @Value("${config.use}")
    private String configUse;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return configInfo;
    }

    @GetMapping("/configPort")
    public String getConfigPort() {
        return configPort;
    }

    @GetMapping("/configUri")
    public String getConfigUri() {
        return configUri;
    }

    @GetMapping("/configUse")
    public String getConfigUse() {
        return configUse;
    }
}

此时就可以通过指定的接口访问指定的配置信息,如下所示:
在这里插入图片描述
同时需要注意:Nacos自带动态刷新的功能(即我们不需要像Config一样修改完配置中心的文件还需要再通过指定请求来让配置客户端刷新得到最新的配置),Nacos只要修改了配置中心的配置文件,只要配置客户端被@RefreshScope注解标注,就能自动的实现!!!!

Nacos的图形化管理界面详解

命名空间Namespace分组GroupDataID三种之间的关系和设计思想?
在这里插入图片描述

  • 类似于Java中的包名、类名一样,最外层的namespace是可以用于区分部署环境的,GroupDataID逻辑上区分两个目标对象;
  • Namespace注意用来实现隔离,如开发、测试、生产环境,我们可以创建三个Nampespace用来进行隔离;Group默认是DEFAULT_GROUPGroup可以把不同的微服务划分到同一分组里面;Service就是微服务,一个微服务可以有多个Cluster(集群)Nacos默认集群是DEFAULTCluster是对指定微服务的一个虚拟划分;Instance就是微服务的实例;

须知:默认情况下:Namespace=publicGroup=DEFAULT_GROUPCluster默认是DEFAULT

DataID

通过上面的示例,我们可以发现配置中心的配置文件命名与本地客户端的配置文件有关系;上述的配置中心的配置文件是开发时测试,当我们如果需要使用的配置中心的配置文件是测试时,可以在配置中心新建nacos-config-client-test.yaml文件,然后修改application.yaml配置文件的spring.profile.active值为test即可

Group

同样也可以使用Group实现上面的需求,我们可以在配置中心新建三个同名的配置文件nacos-config-client-info.yaml,但是者这三个同名的配置文件分别放在不同的组下面(在当前命名空间中创建配置文件时进行指定,没有该分组就会自动创建至当前命名空间),此时我们将上面的配置文件名按照${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}公式构建配置项,然后设置spring.cloud.nacos.config.group配置项值为要使用的分组,此时该客户端就能去指定的分组下加载配置文件了,如下的bootstrap.xml配置文件和application.yaml配置文件如下所示:

########################### 配置中心的客户端模块配置(因为项目初始化时,要保证先从配置中心进行配置拉取配置,保证项目的正常启动,所以需要该bootstrap配置文件) ############################
# 需要注意SpringBoot中配置文件的加载是存在优先级顺序的,bootstrap 优先于 application

server:
  port: 9898

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos作为服务注册中心的地址
      config:
        server-addr: localhost:8848 # Nacos作为配置中心的地址
        file-extension: yaml # 指定为yaml格式的配置(会从配置中心读取后缀为yaml的配置文件)
        group: PROD_GROUP # 指定读取配置中心PROD-GROUP分组的nacos-config-client-info.yaml配置文件

########################### 配置中心的客户端模块application配置 ############################
spring:
  profiles:
    active: info # 激活为开发时的配置(配合bootstrap.yaml配置文件,表示从配置中心读取一个名为:info.yaml的配置文件)

Namespace

同样我们也可以将配置文件放在指定命名空间(命名空间需要新建),然后在客户端处通过指定命名空间,让其去指定的命名空间下按照配置文件的配置和公式加载配置中心的指定配置文件

需要注意的是,客户端指定命名空间是通过命名空间的id指定的,如下新建的命名空间myTest
在这里插入图片描述
id为:cf480543-7c15-4107-9018-9a28eaeb38c4,那么客户端指定时就通过如下的方式指定:

########################### 配置中心的客户端模块配置(因为项目初始化时,要保证先从配置中心进行配置拉取配置,保证项目的正常启动,所以需要该配置文件) ############################
# 需要注意SpringBoot中配置文件的加载是存在优先级顺序的,bootstrap 优先于 application

server:
  port: 9898

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos作为服务注册中心的地址
      config:
        server-addr: localhost:8848 # Nacos作为配置中心的地址
        file-extension: yaml # 指定为yaml格式的配置(会从配置中心读取后缀为yaml的配置文件)
        namespace: cf480543-7c15-4107-9018-9a28eaeb38c4 # 指定去哪个命名空间加载配置文件
########################### 配置中心的客户端模块配置 ############################
spring:
  profiles:
    active: dev # 激活为开发时的配置(配合bootstrap.yaml配置文件,表示从配置中心读取一个名为:dev.yaml的配置文件)

特别注意:

  • 上面的三个配置DataIDGroupNamespace都可以同时配合使用,同时生效;通过这三个配置 + 公式${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}配合配置文件来决定要加载配置中心的哪个配置文件;

集群和持久化配置

须知

默认Nacos使用嵌入式数据库实现数据的存储,所以如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的,为了解决该问题Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储;
在这里插入图片描述
Nacos支持三种部署模式?

  • 单机模式:用于测试和单机试用;
  • 集群模式:用于生产环境,保证高可用;
  • 多集群模式:用于多数据中心场景;

Nacos支持MySQL数据源配置的操作步骤:

  1. 安装MySQL数据库,要求版本5.6.5及以上;

  2. 初始化MySQL数据库,数据库初始化文件:nacos安装目录/conf/nacos-mysql.sql

  3. 修改nacos安装目录/conf/application.properties文件,增加支持MySQL数据源配置,添加MySQL数据源的URL、用户名、密码等配置,如下:

    • spring.datasource.platform=mysql
      db.num=1
      db.url.0=jdbc:mysql://localhost:3306/stu_nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
      db.user=root
      db.password=072731
      
  4. 此时启动Nacos,添加配置文件,会发现配置文件信息就添加到了MySQL数据库;

Nacos集群搭建

开始集群搭建,这里以1台Nginx主机、3台Nacos注册中心主机、1台MySQL主机为例,如下:

按照nacos安装目录/conf/nacos-mysql.sqlSQL脚本创建数据库(作为数据库的主机)

然后在nacos安装目录/conf/application.properties配置文件中添加配置信息,配置上面MySQL数据库的信息(注意:每个Nacos节点都要这样配置),如下:

spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://上面的MySQL的地址:3306/上面创建的数据库?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=mySQL用户名
db.password=MySQL密码

拷贝nacos安装目录/conf/cluster.conf.example文件为cluster.conf,再进行修改(注意:每个Nacos节点都要这样拷贝),如下所示:

192.168.199.133:8848
192.168.199.134:8848
192.168.199.135:8848

Nginx的配置文件配置

    upstream myCluster {
        server 192.168.199.133:8848;
        server 192.168.199.134:8848;
        server 192.168.199.135:8848;
    }

    server {
        listen       80;
        server_name  localhost;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            # root   html;
            # index  index.html index.htm;
            proxy_pass http://myCluster;
        }

此时Nacos集群就搭建完毕了,可以通过http://nginx所在的地址:nginx的端口/nacos地址访问Nginx集群,如本例通过:http://192.168.199.132:80/nacos 即可访问Nacos集群;

SpringCloud中的微服务可以通过192.168.199.132:80地址(Nginx地址)将自己注册进Nacos集群(需要注册进Nacos集群的微服务都需要将之前的地址修改为192.168.199.132:80),如下的几个示例的yaml文件配置

一般的微服务配置

#################################### 注册进Nacos 的微服务 ###################################

server:
  port: 9000

spring:
  application:
    name: nacos-payment-service # 该属性值默认会作为该服务在注册中心的名称
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.199.132:80 # Nacos集群的地址(Nginx的地址)
#        server-addr: localhost:8848 # Nacos服务端的地址

management:
  endpoints: # 监控端点
    web:
      exposure:
        include: '*' # 全部暴漏

做为配置中心客户端的微服务的bootstrap.yaml文件配置

########################### 配置中心的客户端模块配置(因为项目初始化时,要保证先从配置中心进行配置拉取配置,保证项目的正常启动,所以需要该配置文件) ############################
# 需要注意SpringBoot中配置文件的加载是存在优先级顺序的,bootstrap 优先于 application

server:
  port: 9898

spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.199.132:80 # Nacos集群的地址(Nginx的地址)
      #        server-addr: localhost:8848 # Nacos作为服务注册中心的地址
      config:
        server-addr: 192.168.199.132:80 # Nacos集群的地址(Nginx的地址)
        #        server-addr: localhost:8848 # Nacos作为配置中心的地址
        file-extension: yaml # 指定为yaml格式的配置(会从配置中心读取后缀为yaml的配置文件)
#        namespace: cf480543-7c15-4107-9018-9a28eaeb38c4 # 指定去哪个命名空间加载配置文件
#        group: PROD_GROUP # 指定去哪个组加载配置文件


# dataId的配置公式:${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 因此,该模块在Nacos配置中心的dataId就是:nacos-config-client-dev.yaml

配置中心客户端aplication.yaml文件配置

########################### 配置中心的客户端模块配置 ############################
spring:
  profiles:
    active: dev # 激活为开发时的配置(配合bootstrap.yaml配置文件,表示从配置中心读取一个名为:dev.yaml的配置文件)

结果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Sentinel

概述

能干什么?
在这里插入图片描述
GitHub下载.jar文件,运行下载的jar包即可,可以在localhost:8080地址访问Sentinel图形化控制界面;

Sentinel的构成?

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

使用

基本使用

首先启动NacosSentinel服务

然后新建模板,引入必要的依赖,如下:

<!--Nacos依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Sentinel依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--后续Sentinel持久化使用的依赖-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

yaml文件配置,如下所示:

server:
  port: 9002

spring:
  application:
    name: sentinel-payment-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 # Nacos服务注册中心的地址
    sentinel:
      transport:
        dashboard: localhost:8080 # 配置Sentinel Dashboard 地址
        port: 8719 # 默认8719端口,假如占用会自动从8719开始依次+1扫描,直至找到未被占用的端口

management:
  endpoints:
    web:
      exposure:
        include: '*'

启动类的配置

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class PaymentProvider9002Main {

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

添加Controller层接口

package edu.hebeu.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class PaymentController {

    @GetMapping("/paymentA")
    public String testA() {
        log.info(Thread.currentThread().getName() + ":处理A");
        return "paymentA----------------";
    }

    @GetMapping("/paymentB")
    public String testB() {
        log.info(Thread.currentThread().getName() + ":处理B");
        return "paymentB----------------";
    }
}

==此时启动该项目,就能将该项目注册进Nacos,且能被Sentinel监控保护(但是需要注意:Sentinel是懒加载的,因此该服务动后在Sentinel是看不到该服务的,需要访问该服务的接口才能在Sentinel中查看);==如下:
在这里插入图片描述

流控规则

概念介绍
在这里插入图片描述

  • 资源名:唯一名称,默认请求路径;
  • 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认为default(不区分来源);
  • 阈值类型/单机阈值:
    • QPS:每秒钟的请求数量,当调用该API的QPS达到阈值的时候,进行限流;
    • 线程数:当调用该API的线程数达到阈值的时候,进行限流;
  • 是否集群:是否为集群;
  • 流控模式:
    • 直接:API达到限流条件时,直接限流;
    • 关联:当关联的资源达到阈值时,就限流自己;
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API基本的针对来源】;
  • 流控效果:
    • 快速失败:直接失败,抛异常;
    • Warm up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值;
    • 排队等待:匀速排队,让请求 以匀速的速度通过,阈值类型必须设置为QPS,否则无效;
直接流控模式

QPS直接快速失败

Sentinel网页端的首页->对应的微服务->蔟点链路处选择需要配置的资源,点击流控按钮,如下图设置:
在这里插入图片描述
在这里插入图片描述
以上表示/paymentA资源当每秒峰值超过1次,就直接快速失败,如下测试:
在这里插入图片描述
线程数直接快速失败
在这里插入图片描述
以上表示该资源(/paymentA)处理的线程数阈值超过1后,就直接限流;如:当一个/paymentA请求处理时间为2秒,假设这个时间段内又有一个/paymentA请求,此时该资源处理的线程数等于1,就会直接限流;

关联流控模式

QPS关联快速失败
在这里插入图片描述
以上表示:当与/paymentA关联的资源/paymentB超过QPS阈值1之后,就限流/paymentA,如下:

首先使用接口测试工具模拟发送1000次/paymentB请求,开始发送后去浏览器访问/paymentA请求,会发现/paymentA请求已经被限流了;
在这里插入图片描述
在这里插入图片描述

链路流控模式

在这里插入图片描述
即服务链路的入口资源为/paymentB,且该资源的阈值超过10就直接快速失败;

快速失败流控效果

前面已经展示过,这里就不再展示;

预热(Warm Up)流控效果

须知公式:阈值 除以 coldFactor(默认值为3),经过预热时长后才会达到阈值;即默认coldFactor为3,即请求QPS从threshold / 3 开始,经预热时长逐渐升至设定的QPS阈值;

Warm Up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮,通过“冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮;
在这里插入图片描述
以上案例即:系统初始化的阀值为10 / 3 约等于3,即阀值刚开始为3,然后过了5秒后阀值才慢慢升高恢复到10

我们此时去浏览器一直请求该资源,会发现前5秒内如果请求阈值超过3就会出现限流,但是5秒过后阈值就会达到10;

排队流控效果

须知:匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效;

均匀排队?

均匀排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法,如下的时间轴图示:
在这里插入图片描述
该方式主要用于处理间隔性突发的流量,例如消息队列,如果在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求;

如下配置:
在这里插入图片描述
上面的含义表示:资源/paymentA每秒2次请求(阈值为2),超过的话就直接排队等待,等待超时时间为30000毫秒,如下:
在这里插入图片描述

降级规则(熔断规则)

概述

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保证高可用的重要措施之一,由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发送堆积,Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其他的资源而导致级联错误,当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是DegradeException);须知:Sentinel的断路器是没有半开状态的;

半开状态?

半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用(Hystrix的断路器就是半开状态的);

概念介绍
在这里插入图片描述

  • RT(平均响应时间,秒级)
    • 平均响应时间:超出阈值 且 在时间窗口内通过的请求>=5,这两个条件同时满足后触发降级;
    • 窗口期过后关闭断路器;
    • RT最大4900(更大的需要通过-Fcsp.sentinel.statistic.max.rt=XXX才能生效);
  • 异常比例(秒级)
    • QPS>=5 且 异常比例(秒级统计)超过阈值时触发降级;时间窗口结束后关闭降级;
  • 异常数(分钟级)
    • 异常数(分钟统计)超过阈值时触发降级;时间窗口结束后关闭降级;
RT(平均响应时间)降级规则

概述

平均响应时间:当1秒内有持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以毫秒为单位),那么在接下来的时间窗口(DegradeRule中的timeWindow,以秒为单位)之内,对这个方法的调用都会自动地熔断(抛出DegradeException);注意Sentinel默认统计的RT上限为4900毫秒,超过此阈值的都会算作4900毫秒,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt-xxx来配置;
在这里插入图片描述
首先添加/paymentC接口,如下

    @GetMapping("/paymentC")
    public String testC() {
        try {
            Thread.sleep(1000); // 模拟处理的1秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("测试RT业务处理完毕");
        return "测试RT...";
    }

如下设置:
在这里插入图片描述
即如果某一秒/paymentC进来6个线程(大于5个了),我们希望每个线程在200毫秒内处理完(平均响应时间RT),如果超过200毫秒后还没处理完,在该1秒时间窗口期内,断路器打开进行降级处理(即/paymentC不可用);

我们通过接口测试工具发送大量的请求至/paymentC,然后去浏览器访问/paymentC接口,会发现降级已经生效(断路器开启)如下:
在这里插入图片描述

异常比例降级规则

概述

异常比例:当资源的每秒请求量>=5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule中的count)之后,资源进入降级状态,即在接下来的时间窗口(DegradeRule中的timeWindow,以秒为单位)之内,对这个方法的调用都会自动地返回;异常比率的阈值范围是[0.0, 1.0],代表0% - 100%
在这里插入图片描述
首先添加接口/paymentD,如下:

@GetMapping("/paymentD")
    public String testD() {
        log.info("测试异常比例降级规则开始...");
        int a = 1 / 0; // 模拟异常
        return "测试异常比例降级规则...";
    }

如下设置:
在这里插入图片描述
即如果某一秒/paymentD进来6个线程(大于5个了),我们希望在该1秒内/paymentD请求的异常比例达到30%时,在该1秒时间窗口期内断路器打开进行降级处理(即/paymentD不可用);

我们通过接口测试工具发送大量的请求至/paymentD,然后去浏览器访问/paymentD接口,会发现降级已经生效(断路器开启)如下:
在这里插入图片描述

异常数降级规则

概述

异常数:当资源近1分钟的异常数目超过阈值之后会进行熔断,注意由于统计时间窗口是分钟级别的,若timeWindow小于60秒,则结束熔断状态后仍可能再进入熔断状态;需要注意:时间窗口一定要大于等于60秒;
在这里插入图片描述
首先添加接口/paymentE,如下:

    @GetMapping("/paymentE")
    public String testE() {
        log.info("测试异常数降级规则开始...");
        int a = 1 / 0; // 模拟异常
        return "测试异常数降级规则...";
    }

如下设置:
在这里插入图片描述
即如果某62秒时间段内/paymentE的异常数达到10时,在该段62秒时间窗口期内断路器打开进行降级处理(即/paymentE不可用);

然后去浏览器访问/paymentE接口10次,在第11次发送该请求时会发现降级已经生效(断路器开启)如下:
在这里插入图片描述

热点规则

概念介绍
在这里插入图片描述
何为热点?

热点即经常访问的数据,很多时候我们希望统计某个热点数据中访问频次最高的TOP K数据,并对其访问进行限制,如:

  • 商品ID为参数:统计一段时间内最常购买的商品ID并进行限制;
  • 用户ID为参数:针对一段时间内频繁访问的用户ID进行限制;

热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流;热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效;

Sentinel利用LRU策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控,热点参数限流支持集群模式;

添加接口和指定的兜底方法,如下:

    @GetMapping("/hotKey")
    @SentinelResource(value = "toHotKeyHandler", blockHandler = "testHotKey_Handler") // value属性值暴漏给热点规则资源参数使用。blockHandler属性值指定流控之后执行的方法
    public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
                             @RequestParam(value = "p2", required = false) String p2) {
        log.info("hotKey方法执行.....");
        return "hotKey";
    }
    public String testHotKey_Handler(String p1, String p2, BlockException blockException) {
        log.info("hotKey_Handler方法执行...");
        return "hotKey_Handler"; // Sentinel系统默认的提示:Blocked by Sentinel(flow limiting)
    }

配置如下:
在这里插入图片描述
即当被@SentinelResource注解标注的接口方法的有1号索引参数(第二个参数p2)的请求每秒超过2次时(超过阈值2),则在该1秒内的请求都会执行表示对于上述的@SentinelResource注解的value值对应的资源(toHotKeyHandler)对应的流控处理方法,这里就是testHotKey_Handler()方法;如下的请求结果:
在这里插入图片描述

参数例外项

对于上面的配置,当我们的请求的第二个参数超过1秒2次后马上会被限流,当我们期望P1参数值是某个特殊值时,它的限流值和平时不一样(如P1参数值等于5时,它的阈值可以达到200),可以通过如下配置:
在这里插入图片描述
即表示@SentinelResource注解标注的接口方法的有0号索引参数(第1个参数p1)的请求每秒超过2次时(超过阈值2),则在该1秒内的请求都会执行表示对于上述的@SentinelResource注解的value值对应的资源(toHotKeyHandler)对应的流控处理方法,这里就是testHotKey_Handler()方法;但是当该请求的P1参数类型为String,值为test时,限流的阈值为200,效果如下:

当请求带有P1参数时,该请求的阈值为2,当请求带有参数P1时,并且该参数值为test,该请求的阈值为200,如下:
在这里插入图片描述
在这里插入图片描述
总结:

  • @SentinelResource处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;但是对于Java允许报出的运行时异常,@SentinelResource不管;

系统规则(系统自适应限流)

概念介绍

Sentinel系统自适应限流从整体维度对应用入口流量进行控制,结合应用的Load、CPU使用率、总体平均RT、入口QPS和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性;

系统保护规则是应用整体维度的,而不是资源维度,并且仅对入口流量生效,入口流量指的是进入应用的流量(EntryType.IN),比如WEB服务或Dubbo服务端接收的请求,都属于入口类量;

系统规则支持以下模式:

  • Load自适应(仅对Linux/Unix-like机器生效):系统的load1做为启发指标,进行自适应系统保护;当系统load1超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR阶段);系统容量由系统的maxQPS * minRT估算得出,设定参考值一般是CPU cores * 2.5
  • CPU usage(1.5.0+版本):当系统CPU使用率超过阈值即触发系统保护(取值范围0.0-1.0),比较灵敏;
  • 平均RT:当单台机器上所有入口流量的平均RT达到阈值即触发系统保护,单位是毫秒;
  • 并发线程数:当单台机器上所有入口流量的并发线程达到阈值即触发系统保护;
  • 入口QPS:当单台机器上所有入口流量的QPS达到阈值即触发系统保护;

如下实例,表示该系统的所有资源每秒请求大于2(超过阈值2),就会控流,如下配置:
在这里插入图片描述
结果展示:
在这里插入图片描述

SentinelResource配置

@SentinelResource注解的概述

注解方式不支持private方法

@SentinelResource用于定义资源,并提供可用的异常处理和fallback配置项,@SentinelResource注解包含以下属性:

  • value:资源名称,必需项(不能为空);
  • entryType:entry类型,可选项(默认为EntryType.Out);

Sentinel主要的三个核心API

  • SphU定义资源;
  • Tracer定义统计
  • ContextUtil定义了上下文
按照资源名称(@SentinelResource注解的value属性值)限流

添加测试接口和配置规则:

    /**
     * 按照资源名称限流
     * @return
     */
	@SentinelResource(value = "toGetResourceHandler", blockHandler = "getResourceHandler")
    @GetMapping("/getResource")
    public CommonResult getResource() {
        return new CommonResult(200, "获取资源成功", new Payment(2355L, "id20033"));
    }
    public CommonResult getResourceHandler(BlockException blockException) {
        return new CommonResult(444, blockException.getClass().getCanonicalName() + "服务不可用!");
    }

如下实例配置:
在这里插入图片描述
即当被@SentinelResource注解标注的接口方法的请求每秒超过2次时(超过阈值2),则在该1秒内的请求都会执行表示对于上述的@SentinelResource注解的value值对应的资源(toGetResourceHandler)对应的流控处理方法,这里就是getResourceHandler()方法;如下的请求结果:
在这里插入图片描述

按照URL限流

添加测试接口和配置规则:

    /**
     * 按照URL限流
     * @return
     */
    @SentinelResource(value = "toByUrlHandler")
    @GetMapping(value = "/toByUrl")
    public CommonResult byUrl() {
        return new CommonResult(200, "获取资源成功", new Payment(143L, "1212df7"));
    }

如下实例配置:
在这里插入图片描述
表示URL/byUrl请求阈值超过2(每秒大于2个请求)时,就会对该URL限流(并返回Sentinel自带的限流页面信息),如下:
在这里插入图片描述
上述的问题总结:

  • 系统默认的,没有体现我们自己的业务要求;
  • 依照现有条件,我们自定义的处理又方法又和业务代码耦合在一块,不直观;
  • 每个业务方法都添加一个兜底的方法,那代码膨胀加剧就是必然的;
  • 全局统一的处理方法没有体现;
按照客户自定义限流

自定义异常处理类,如下:

package edu.hebeu.component;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;

public class MyHandler {

    public static CommonResult handlerException1(BlockException blockException) {
        return new CommonResult(444, "自定义的处理方法!——Global---1");
    }

    public static CommonResult handlerException2(BlockException blockException) {
        return new CommonResult(444, "自定义的处理方法!——Global---2");
    }
}

准备测试接口并使用自定义处理类的方法,如下:

    /**
     * 按照自定义限流的测试接口
     * @return
     */
    @GetMapping("/byCustomer")
    @SentinelResource(value = "toByCustomerHandler",
            blockHandlerClass = MyHandler.class,
            blockHandler = "handlerException1") // 表示被标注的方法限流后的方法会调用MyHandler类的handlerException1()方法
    public CommonResult byCustomer() {
        return new CommonResult(200, "获取资源成功", new Payment(844013L, "sdhjds22f"));
    }

配置实例:
在这里插入图片描述
即当被@SentinelResource注解标注的接口方法的请求每秒超过2次时(超过阈值2),则在该1秒内的请求都会执行表示对于上述的@SentinelResource注解的value值对应的资源(toByCustomerHandler)对应的流控处理方法,这里就是MyHandler类的handlerException1()方法,结果展示如下:
在这里插入图片描述

Sentinel服务熔断

先启动NacosSentinel,再准备如下环境:

首先搭建微服务提供者模块cloud-alibaba-provider-sentinel-payment9000,先导入如下依赖:

<dependencies>
    <!--SpringCloud Alibaba Nacos依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>edu.hebeu</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

yaml配置文件内容如下:

server:
  port: 9000

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

management:
  endpoints:
    web:
      exposure:
        include: '*'

启动类:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

业务类:

package edu.hebeu.controller;

import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
public class PaymentController {

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

    // 模拟的数据
    public static Map<Long, Payment> DATA = new HashMap<>();
    static {
        DATA.put(1L, new Payment(1L, "订单1的信息......."));
        DATA.put(2L, new Payment(2L, "订单2的信息......."));
        DATA.put(3L, new Payment(3L, "订单3的信息......."));
        DATA.put(4L, new Payment(4L, "订单4的信息......."));
        DATA.put(5L, new Payment(5L, "订单5的信息......."));
        DATA.put(6L, new Payment(6L, "订单6的信息......."));
    }

    @GetMapping("/payment/{id}")
    public CommonResult<Payment> paymentCommonResult(@PathVariable("id") Long id) {
        Payment payment = DATA.get(id);
        CommonResult<Payment> result = new CommonResult<>(200, "获取成功,服务提供者:" + serverPort, payment);
        return result;
    }

}

然后按照这个流程再创建一个服务提供模块cloud-alibaba-provider-sentinel-payment9001,此时就有两个微服务提供模块了;

完成上述操作后,创建微服务调用者模块cloud-alibaba-consumer-sentinel-order80,首先导入如下依赖:

<dependencies>
    <!--Nacos依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--Sentinel依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!--后续Sentinel持久化使用的依赖-->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    <!--Openfeign依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <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>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>edu.hebeu</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

yaml配置文件如下:

server:
  port: 80

spring:
  application:
    name: nacos-order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080 # 配置Sentinel Dashboard 地址
        port: 8719 # 默认8719端口,假如占用会自动从8719开始依次+1扫描,直至找到未被占用的端口

启动类:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

配置类(注入RestTemplate组件,完成远程微服务的调用)如下:

package edu.hebeu.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class ApplicationContextConfig {

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

业务类(通过RestTemplate组件进行微服务的调用)如下:

package edu.hebeu.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class OrderController {

    private static final String SERVICE_URL = "http://nacos-payment-provider";

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/order/{id}")
    public CommonResult<Payment> order(@PathVariable("id") Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/payment/" + id, CommonResult.class);
        if (id > 100) {
            throw new IllegalArgumentException("参数异常,订单号越界!");
        } else if (result.getData() == null) {
            throw new NullPointerException("空指针异常,未找到订单!");
        }
        return result;
    }
}

此时,前置的环境准备就完成了,并且已经有了负载均衡的效果,下面开始测试

无配置的效果

对上面的微服务调用模块的业务类的接口方法使用@SentinelResource注解,但是不指定其他,且无配置,改写为如下所示:

    @GetMapping("/order/{id}")
    @SentinelResource(value = "toXXX") // 无任何配置
    public CommonResult<Payment> order(@PathVariable("id") Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/payment/" + id, CommonResult.class);
        if (id > 100) {
            throw new IllegalArgumentException("参数异常,订单号越界!");
        } else if (result.getData() == null) {
            throw new NullPointerException("空指针异常,未找到订单!");
        }
        return result;
    }

效果展示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

只配置fallback的效果

对上面的微服务调用模块的业务类的接口方法使用@SentinelResource注解,并指定fallback配置,改写为如下所示:

    @GetMapping("/order/{id}")
    @SentinelResource(value = "toFallback", fallback = "orderFallback") // 只配置fallback
    public CommonResult<Payment> order(@PathVariable("id") Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/payment/" + id, CommonResult.class);
        if (id > 100) {
            throw new IllegalArgumentException("参数异常,订单号越界!");
        } else if (result.getData() == null) {
            throw new NullPointerException("空指针异常,未找到订单!");
        }
        return result;
    }

    /**
     * order()方法的fallback对应的处理方法
     * @param id
     * @param throwable
     * @return
     */
    public CommonResult orderFallback(Long id, Throwable throwable) {
        Payment payment = new Payment(id, "未找到订单" + id);
        return new CommonResult<>(444, "orderFallback()方法..." + throwable.getMessage(), payment);
    }

效果展示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

只配置blockHandler的效果

对上面的微服务调用模块的业务类的接口方法使用@SentinelResource注解,并指定blockHandler配置,改写为如下所示:

	@GetMapping("/order/{id}")
    @SentinelResource(value = "toBlockHandler", blockHandler = "orderBlockHandler") // 只配置blockHandler
    public CommonResult<Payment> order(@PathVariable("id") Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/payment/" + id, CommonResult.class);
        if (id > 100) {
            throw new IllegalArgumentException("参数异常,订单号越界!");
        } else if (result.getData() == null) {
            throw new NullPointerException("空指针异常,未找到订单!");
        }
        return result;
    }

    /**
     * order()方法的blockHandler对应的处理方法
     * @param id
     * @param blockException
     * @return
     */
    public CommonResult orderBlockHandler(Long id, BlockException blockException) {
        Payment payment = new Payment(id, "未找到订单" + id);
        return new CommonResult<>(443, "orderBlockHandler()方法..." + blockException.getMessage(), payment);
    }

Sentinel页面的配置
在这里插入图片描述
效果展示:
在这里插入图片描述
当我们首次出现异常,会进入默认的异常页面,但是在2秒内异常数达到3时,就会进入降级方法,如下:
在这里插入图片描述
在这里插入图片描述

同样的,如下:
在这里插入图片描述
在这里插入图片描述

fallback和blockHandler都配置的效果

都进行配置,改写为如下:

	@GetMapping("/order/{id}")
    @SentinelResource(value = "to", fallback = "orderFallback", blockHandler = "orderBlockHandler") // fallback和blockHandler都配置
    public CommonResult<Payment> order(@PathVariable("id") Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/payment/" + id, CommonResult.class);
        if (id > 100) {
            throw new IllegalArgumentException("参数异常,订单号越界!");
        } else if (result.getData() == null) {
            throw new NullPointerException("空指针异常,未找到订单!");
        }
        return result;
    }

    /**
     * order()方法的fallback对应的处理方法
     * @param id
     * @param throwable
     * @return
     */
    public CommonResult orderFallback(Long id, Throwable throwable) {
        Payment payment = new Payment(id, "未找到订单" + id);
        return new CommonResult<>(444, "orderFallback()方法..." + throwable.getMessage(), payment);
    }

    /**
     * order()方法的blockHandler对应的处理方法
     * @param id
     * @param blockException
     * @return
     */
    public CommonResult orderBlockHandler(Long id, BlockException blockException) {
        Payment payment = new Payment(id, "未找到订单" + id);
        return new CommonResult<>(443, "orderBlockHandler()方法..." + blockException.getMessage(), payment);
    }

Sentinel的配置
在这里插入图片描述
效果演示:
在这里插入图片描述
当触发Java异常时,就会执行fallback对应的处理方法:
在这里插入图片描述
在这里插入图片描述
同时,不论是否触发异常,只要请求阈值超过2(每秒超过2次请求),就会执行blockHandler对应的处理方法:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

忽略异常的配置

即指定哪些异常不再被fallback对应的方法处理,如下:

	@GetMapping("/order/{id}")
    @SentinelResource(value = "to", fallback = "orderFallback", blockHandler = "orderBlockHandler",
        exceptionsToIgnore = {IllegalArgumentException.class}) // fallback和blockHandler都配置,忽略IllegalArgumentException异常(即该异常出现就不会执行fallback对应的方法,而是直接执行blockHandler对应的方法)
    public CommonResult<Payment> order(@PathVariable("id") Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/payment/" + id, CommonResult.class);
        if (id > 100) {
            throw new IllegalArgumentException("参数异常,订单号越界!");
        } else if (result.getData() == null) {
            throw new NullPointerException("空指针异常,未找到订单!");
        }
        return result;
    }

    /**
     * order()方法的fallback对应的处理方法
     * @param id
     * @param throwable
     * @return
     */
    public CommonResult orderFallback(Long id, Throwable throwable) {
        Payment payment = new Payment(id, "未找到订单" + id);
        return new CommonResult<>(444, "orderFallback()方法..." + throwable.getMessage(), payment);
    }

    /**
     * order()方法的blockHandler对应的处理方法
     * @param id
     * @param blockException
     * @return
     */
    public CommonResult orderBlockHandler(Long id, BlockException blockException) {
        Payment payment = new Payment(id, "未找到订单" + id);
        return new CommonResult<>(443, "orderBlockHandler()方法..." + blockException.getMessage(), payment);
    }
整合Openfeign

首先在原先的基础上添加Openfeign依赖,如下:

<!--Openfeign依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

添加yaml配置文件,如下:

# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true

改写主启动类:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 激活启用Feign
public class OrderConsumer80Main {
    public static void main(String[] args) {
        SpringApplication.run(OrderConsumer80Main.class, args);
    }
}

编写Service业务层,如下:

package edu.hebeu.service;

import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "nacos-payment-provider", fallback = OrderFallbackService.class) // 指定调用注册中心名为nacos-payment-provider的微服务,该接口的降级处理类为OrderFallbackService
public interface OrderService {

    /**
     * 调用微服务提供者的 GET类型的 /payment/{id} 接口
     * @param id
     * @return
     */
    @GetMapping(value = "/payment/{id}")
    CommonResult<Payment> getPayment(@PathVariable("id") Long id);
}

Service层的异常处理类如下:

package edu.hebeu.service;

import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import org.springframework.stereotype.Service;

/**
 * OrderService接口的异常处理类,OrderService接口的每个方法的异常方法一一对应该类的方法
 */
@Service
public class OrderFallbackService implements OrderService{

    @Override
    public CommonResult<Payment> getPayment(Long id) {
        return new CommonResult<>(444, "服务降级, edu.hebeu.service.OrderFallbackService.getPayment()方法...",
                    new Payment(id, "未找到订单" + id));
    }
}

Controller层如下:

package edu.hebeu.controller;

import edu.hebeu.entity.CommonResult;
import edu.hebeu.entity.Payment;
import edu.hebeu.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 通过调用Openfeign的方式实现远程服务的调用
 */
@RestController
public class ByOpenfeignToOrderController {

    @Resource
    private OrderService orderService;

    @GetMapping("/order-openfeign/{id}")
    public CommonResult<Payment> orderResult(@PathVariable("id") Long id) {
        return orderService.getPayment(id);
    }
}

测试:
在这里插入图片描述
在这里插入图片描述
此时,将两个微服务提供者都关闭,再次让微服务调用者发送请求
在这里插入图片描述

熔断框架的比较

SentinelHystrixresilience4j
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数基于异常比率基于异常比率、响应时间
实时统计滑动窗口(LeapArray)滑动窗口(基于RXJava)Ring Bit Buffer
动态规划配置支持多种数据源支持多种数据源有限支持
扩展性多个扩展点插件的形式接口的形式
基于注解的支持支持支持支持
限流基于QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的Rate Limiter模式
系统自适应保护支持不支持不支持
控制台提供开源即用的控制台,可用配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其他监控系统

Sentinel规则持久化

是什么?

一旦我们重启应用,Sentinel规则将消失,生产环境需要将配置规则进行持久化;

如何实现?

将限流配置规则持久化进Nacos保存,只要刷新某个该Sentinel的某个REST地址,该Sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对该Sentinel的规则也就持续有效;

步骤

在上述Sentinel守护的微服务(微服务消费者)的依赖基础上添加如下依赖:

<!--后续Sentinel持久化使用的依赖-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

对上述Sentinel守护的微服务(微服务消费者)的yaml配置改写,指明配置规则保存到的Nacos地址,并且一定要暴漏WEB服务端点,如下:

server:
  port: 80

spring:
  application:
    name: nacos-order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080 # 配置Sentinel Dashboard 地址
        port: 8719 # 默认8719端口,假如占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
      datasource:  # 持久化的配置
        dsl:
          nacos:
            server-addr: localhost:8848 # Nacos地址
            dataId: nacos-order-service
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow


# 暴漏WEB监控端点
management:
  endpoints:
    web:
      exposure:
        include: '*'

# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true


Nacos中新建配置,命名(Data Id)为sentinel-order-service(yaml配置文件配置的spring.cloud.sentinel.datasource.dsl.nacos.dataId值),如下:
在这里插入图片描述
配置内容:

[
  {
    "resource": "/rateLimit/byUrl",
    "limitApp": "default",
    "grade": 1,
    "count": 1,
    "strategy": 0,
    "controlBehavior": 0,
    "clusterMode": false
  }
]

其中:

  • resource:资源名称
  • limitApp:来源应用
  • grade:阈值类型,0表示线程数,1表示QPS
  • count:单机阈值;
  • strategy:流控模式,0表示直接,1表示关联,2表示链路;
  • controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
  • clusterMode:是否集群;

发布该配置文件后,去Sentinel会看到该资源配置规则已经从Nacos中解析读取到了,如下:
在这里插入图片描述
那么此时被Sentinel守护的微服务nacos-order-service同样也会被遵从该规则,如下:
在这里插入图片描述
在这里插入图片描述

Seata

概述

分布式事务问题

  • 须知:单机、单库是没有这个问题;
  • 一次业务操作需要跨多个数据源或需要多个系统进行远程调用,就会产生分布式事务问题;

Seata术语?

  • TC (Transaction Coordinator) - 事务协调者(Seata服务器):维护全局和分支事务的状态,驱动全局事务提交或回滚
  • TM (Transaction Manager) - 事务管理器(事务的发起方,被标注@GlobalTransactional的方法):定义全局事务的范围:开始全局事务、提交或回滚全局事务
  • RM (Resource Manager) - 资源管理器(事务的参与方,每个微服务对应的数据库集群):管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

是什么?

是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务;官网:http://seata.io/zh-cn/,由1(Transaction ID XID:全局唯一的事务ID)+3(3大组件:TC、TM、RM) 组成;

Seata的几种模式?

  • AT模式(默认使用):提供无侵入自动补偿的事务模式,目前已支持MySQLOracle的AT模式;
  • TCC模式:支持TCC模式并可与AT混用,灵活度更高;
  • SAGA模式:为长事务提供有效的解决方案;
  • XA模式(2020年-开发中):支持已实现XA接口的数据库的XA模式;

AT模式?

  • 前提
    • 基于支持本地ACID事务的关系型数据库;
    • Java应用,通过JDBC访问数据库;
  • 整体机制(两阶段提交协议的演变)
    • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源;
    • 二阶段
      • 提交异步化,非常快速的完成;
      • 回滚通过一阶段的回滚日志进行反向补偿;

一阶段加载?

在一阶段,Seata会拦截"业务SQL",流程如下:

  • 解析SQL定义,找到"业务SQL"要更新的业务数据,在业务数据被更新前,将其保存称"before image";
  • 执行"业务SQL"更新业务数据,在业务数据更新之后;
  • 其保存成"after image",最后生成行锁;
  • 以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性;

在这里插入图片描述
二阶段提交?

二阶段如果是顺利提交的话,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需要将一阶段保存的快照数据和行锁删掉,完成数据清理即可;
在这里插入图片描述
二阶段回滚?

二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的"业务SQL",还原业务数据(反向补偿);回滚方式是用"before image"还原业务数据;但是还原前要首先校验脏写,对比"数据库当前业务数据"和"after image",如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理;
在这里插入图片描述
整体流程图?
在这里插入图片描述

Seata的处理过程?
在这里插入图片描述
处理过程:

  • TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
  • XID在微服务调用链路的上下文中传播;
  • RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
  • TM向TC发起针对XID的全局提交或回滚决议;
  • TC调度XID下管辖的全部分支事务完成提交或回滚请求;

Seata的分布式交易解决方案的案例
在这里插入图片描述
我们只需要使用一个@GlobalTransactional注解在业务方法上即可;

安装

  • 去官网下载得到zip文件(这里下载的是0.9版本),然后解压;

  • 修改配置文件seata安装目录/conf/file.conf的配置修改(主要修改为:自定义事务组名称 + 事务日志存储模型为db + 数据库连接信息)

    • service模块修改为:

    • service {
        #vgroup->rgroup
        vgroup_mapping.my_test_tx_group = "my_tx_group" # 值随便写,自定义
      
      
    • store模块修改为:

    • store {
        ## store mode: file、db
        mode = "db" # 将原先的文件存储模型修改为数据库存储模型
      
      
    • 数据库的配置(指定使用地址,用户名、密码等):

    • db {
        ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
        datasource = "dbcp"
        ## mysql/oracle/h2/oceanbase etc.
        db-type = "mysql"
        driver-class-name = "com.mysql.jdbc.Driver"
        url = "jdbc:mysql://127.0.0.1:3306/stu_seata"
        user = "数据库用户名"
        password = "密码"
      
  • 创建上面指定的数据库(这里就是stu_seata),按照seata安装目录/conf/db_store.sql文件创建表

  • 修改seata安装目录/conf/registry.conf配置文件

    • registry模块修改为:

    • registry {
        # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
        type = "nacos" # 指明注册中心为nacos
      
        nacos {
          serverAddr = "localhost:8848" # 指定nacos的地址
          namespace = ""
          cluster = "default"
        }
      
  • 此时,先启动Nacos,再启动seata,至此seata的下载、安装、配置、启动就完成了;

环境准备

首先要保证NacosSeata启动

业务分析

我们会创建三个微服务,一个订单服务、一个库存服务、一个账户服务;当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成;该操作要跨越三个数据库,有两次远程调用,很明显会有分布式事务问题;

数据库准备

创建三个数据库:seata_orderseata_storageseata_account,然后在订单库seata_order下面建t_order表、库存库seata_storage下面建t_storage表、账户库seata_account下面建t_account

订单库seata_order下面建t_order表的SQL:

CREATE TABLE t_order (
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
	`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
	`count` INT(11) DEFAULT NULL COMMENT '数量',
	`money` DECIMAL(11,0) DEFAULT NULL COMMENT '订单状态: 0、创建中; 1、已完结' 
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

库存库seata_storage下面建t_storage表的SQL:

CREATE TABLE t_storage (
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
	`total` INT(11) DEFAULT NULL COMMENT '总库存',
	`used` INT(11) DEFAULT NULL COMMENT '已用库存',
	`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('2', '568', '200', '0', '160');

账户库seata_account下面建t_account表的SQL:

CREATE TABLE t_account (
	`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
	`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
	`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
	`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
	`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用余额'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1365', '1000', '0', '999');
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('2', '5489', '10000', '0', '10000');

按照上述的3个库分别创建对应的回滚日志表,在每个库下都执行seata安装目录/conf/db_undo_log.sql文件,创回滚日志建表,SQL文件的内容如下:

-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
代码准备
公用模板的创建

导入依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!--糊涂工具包-->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.1.0</version>
    </dependency>
</dependencies>

公共响应实体类CommonResult的创建:

package edu.hebeu.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {

    private Integer code;

    private String message;

    private T data;

    public CommonResult (Integer code, String message) {
        this(code, message, null);
    }
}

账户类Account的创建:

package edu.hebeu.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Account {

    /**
     * 用户ID
     */
    private Long id;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 总库存(容量)
     */
    private BigDecimal total;

    /**
     * 已用库存
     */
    private BigDecimal used;

    /**
     * 剩余库存
     */
    private BigDecimal residue;

}

库存类Storage的创建:

package edu.hebeu.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Storage {

    private Long id;

    /**
     * 产品ID
     */
    private Long productId;

    /**
     * 总库存(容量)
     */
    private Integer total;

    /**
     * 已用库存
     */
    private Integer used;

    /**
     * 剩余库存
     */
    private Integer residue;
}

此时将该模块打包至Maven,后续的几个模块都必须要引入该模块

前置条件的说明

在后续的几个模块中,我们都要在其resource目录下创建两个文件:file.confregistry.conf(这两个文件的内容参考之前配置的Seata配置文件,但是要略做修改),两个文件的内容如下:

file.conf配置文件

transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  #thread factory for netty
  thread-factory {
    boss-thread-prefix = "NettyBoss"
    worker-thread-prefix = "NettyServerNIOWorker"
    server-executor-thread-prefix = "NettyServerBizHandler"
    share-boss-worker = false
    client-selector-thread-prefix = "NettyClientSelector"
    client-selector-thread-size = 1
    client-worker-thread-prefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    boss-thread-size = 1
    #auto default pin or 8
    worker-thread-size = 8
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #vgroup->rgroup
  vgroup_mapping.my_tx_group = "default" # 修改自定义事务组名称
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

client {
  async.commit.buffer.limit = 10000
  lock {
    retry.internal = 10
    retry.times = 30
  }
  report.retry.count = 5
  tm.commit.retry.count = 1
  tm.rollback.retry.count = 1
}

## transaction log store
store {
  ## store mode: file、db
  mode = "db"

  ## file store
  file {
    dir = "sessionStore"

    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    max-branch-session-size = 16384
    # globe session size , if exceeded throws exceptions
    max-global-session-size = 512
    # file buffer size , if exceeded allocate new buffer
    file-write-buffer-cache-size = 16384
    # when recover batch read size
    session.reload.read_size = 100
    # async, sync
    flush-disk-mode = async
  }

  ## database store
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/h2/oceanbase etc.
    db-type = "mysql"
    driver-class-name = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/stu_seata"
    user = "root"
    password = "072731"
    min-conn = 1
    max-conn = 3
    global.table = "global_table"
    branch.table = "branch_table"
    lock-table = "lock_table"
    query-limit = 100
  }
}
lock {
  ## the lock store mode: local、remote
  mode = "remote"

  local {
    ## store locks in user's database
  }

  remote {
    ## store locks in the seata's server
  }
}
recovery {
  #schedule committing retry period in milliseconds
  committing-retry-period = 1000
  #schedule asyn committing retry period in milliseconds
  asyn-committing-retry-period = 1000
  #schedule rollbacking retry period in milliseconds
  rollbacking-retry-period = 1000
  #schedule timeout retry period in milliseconds
  timeout-retry-period = 1000
}

transaction {
  undo.data.validation = true
  undo.log.serialization = "jackson"
  undo.log.save.days = 7
  #schedule delete expired undo_log in milliseconds
  undo.log.delete.period = 86400000
  undo.log.table = "undo_log"
}

## metrics settings
metrics {
  enabled = false
  registry-type = "compact"
  # multi exporters use comma divided
  exporter-list = "prometheus"
  exporter-prometheus-port = 9898
}

support {
  ## spring
  spring {
    # auto proxy the DataSource bean
    datasource.autoproxy = false
  }
}

registry.conf文件如下:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos" # 修改为Nacos

  nacos {
    serverAddr = "localhost:8848" # 设置Nacos地址
    namespace = ""
    cluster = "default"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

下面的几个服务模块使用的依赖都是如下所示:

<dependencies>
    <!--Nacos依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--Seata依赖-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>seata-all</artifactId>
                <groupId>io.seata</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>0.9.0</version>
    </dependency>
    <!--feign依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--web依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--actuator依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!--cloud-alibaba使用的自定义API-->
    <dependency>
        <groupId>edu.hebeu</groupId>
        <artifactId>cloud-alibaba-seata-common-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

</dependencies>

下面几个服务模块使用的两个配置类如下:

package edu.hebeu.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan({"edu.hebeu.dao"})
public class MyBatisConfig {
}

package edu.hebeu.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * 该配置类配置使用Seata对数据源进行代理:特别注意一个很大的坑,使用Seata对数据源代理时,Mybatis不能使用别名(使用别名会导致不识别,从而导致程序启动失败)!!!
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    /**
     * 添加德鲁伊连接池
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());

        return sqlSessionFactoryBean.getObject();
    }

}

再次提醒,上面的这些东西对于下面的所有服务模块是公用的,因此每个模块都要有这些环境!!!

账户服务模块

首先导入上面前置条件的说明的环境和配置

yaml配置文件如下:

server:
  port: 9002

spring:
  application:
    name: seata-account-service
  cloud:
    alibaba:
      seata:
        tx-service-group: my_tx_group # Seata配置文件file.conf文件service模式的配置
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account
    username: 'root'
    password: '072731'

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: edu.hebeu.domain # 所有实体类所在的包(还自动设置了别名)
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名规则

启动类:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源自动创建(装配)
public class SeataAccount9002Main {

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

Dao层的创建:

package edu.hebeu.dao;

import edu.hebeu.domain.Account;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface AccountDao {

    /**
     * 添加账户
     * @param account
     */
    void insert(Account account);

    /**
     * 修改账户
     * @param account
     */
    void update(Account account);

    /**
     * 通过用户ID精确获取账户信息
     * @param userId
     */
    Account selectByUserId(Long userId);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-/mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="edu.hebeu.dao.AccountDao">

    <insert id="insert" parameterType="edu.hebeu.domain.Account">
        <selectKey keyProperty="id" keyColumn="id" order="AFTER" resultType="java.lang.Long">
            SELECT LAST_INSERT_ID();
        </selectKey>
        INSERT INTO `t_account`(`user_id`, `total`, `used`, `residue`) VALUES(#{userId}, #{total},
        #{used}, #{residue});
    </insert>

    <update id="update" parameterType="edu.hebeu.domain.Account">
        UPDATE `t_account` SET `user_id`=#{userId}, `total`=#{total}, `used`=#{used}, `residue`=#{residue}
        WHERE `user_id`=#{userId};
    </update>

    <select id="selectByUserId" parameterType="long" resultType="edu.hebeu.domain.Account">
        SELECT `id` `id`, `user_id` `userId`, `total` `total`, `used` `used`, `residue` `residue`
        FROM `t_account` WHERE `user_id`=#{userId};
    </select>
</mapper>

Service层的创建:

package edu.hebeu.service;

import edu.hebeu.domain.Account;

public interface AccountService {

    /**
     * 添加账户
     * @param account
     */
    void insert(Account account);

    /**
     * 修改账户
     * @param account
     */
    void update(Account account);

    /**
     * 通过用户ID精确获取账户详情
     * @param userId
     */
    Account selectSingle(Long userId);
}

package edu.hebeu.service.impl;

import edu.hebeu.dao.AccountDao;
import edu.hebeu.domain.Account;
import edu.hebeu.service.AccountService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class AccountServiceImpl implements AccountService {

    @Resource
    private AccountDao accountDao;

    @Override
    public void insert(Account account) {
        accountDao.insert(account);
    }

    @Override
    public void update(Account account) {
        accountDao.update(account);
    }

    @Override
    public Account selectSingle(Long userId) {
        return accountDao.selectByUserId(userId);
    }
}

Controller层的创建:

package edu.hebeu.controller;

import edu.hebeu.domain.Account;
import edu.hebeu.domain.CommonResult;
import edu.hebeu.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
public class AccountController {

    @Autowired
    private AccountService accountService;

    /**
     * 添加Account的接口方法
     * @param account
     * @return
     */
    @PutMapping("/account")
    public CommonResult save(@RequestBody Account account) {
        accountService.update(account);
        return new CommonResult(200, "添加Account成功");
    }

    /**
     * 修改Account的接口方法
     * @param account
     * @return
     */
    @PostMapping("/account")
    public CommonResult alter(@RequestBody Account account) {
        accountService.insert(account);
        return new CommonResult(200, "修改Account成功");
    }

    /**
     * 通过用户ID精确获取Account
     * @param userId
     * @return
     */
    @GetMapping("/account/{userId}")
    public CommonResult<Account> getByUserId(@PathVariable("userId") Long userId) {
        return new CommonResult<>(200, "获取Account信息成功", accountService.selectSingle(userId));
    }

}

库存服务模块

首先导入上面前置条件的说明的环境和配置

yaml配置文件如下:

server:
  port: 9001

spring:
  application:
    name: seata-storage-service
  cloud:
    alibaba:
      seata:
        tx-service-group: my_tx_group # Seata配置文件file.conf文件service模式的配置
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_storage
    username: 'root'
    password: '072731'

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: edu.hebeu.domain # 所有实体类所在的包(还自动设置了别名)
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名规则

启动类:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableDiscoveryClient
@EnableFeignClients
public class SeataStorage9001Main {

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

Dao层:

package edu.hebeu.dao;

import edu.hebeu.domain.Storage;
import org.apache.ibatis.annotations.Mapper;

/**
 * @Auther: tyong--汤勇 13651
 * @Description: edu.hebeu.dao
 * @Version: 1.0
 */
@Mapper
public interface StorageDao {

    /**
     * 插入一条库存记录
     * @param storage
     */
    void insert(Storage storage);

    /**
     * 更新库存的方法
     * @param storage
     */
    void update(Storage storage);

    /**
     * 精确查询Storage
     * @param productId 产品ID
     * @return
     */
    Storage selectById(Long productId);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-/mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="edu.hebeu.dao.StorageDao">

    <insert id="insert" parameterType="edu.hebeu.domain.Storage">
        <selectKey keyProperty="id" keyColumn="id" order="AFTER" resultType="java.lang.Long">
            SELECT LAST_INSERT_ID();
        </selectKey>
        INSERT INTO `t_storage`(`product_id`, `total`, `used`, `residue`) VALUES(#{productId},
        #{total}, #{used}, #{residue});
    </insert>

    <update id="update" parameterType="edu.hebeu.domain.Storage">
        UPDATE `t_storage` SET `product_id`=#{productId}, `total`=#{total}, `used`=#{used}, `residue`=#{residue}
        WHERE `product_id`=#{productId};
    </update>

    <select id="selectById" parameterType="long" resultType="edu.hebeu.domain.Storage">
        SELECT `id` `id`, `product_id` `productId`, `total` `total`, `used` `used`, `residue` `residue`
        FROM `t_storage` WHERE `product_id`=#{productId}
    </select>

</mapper>

Service层:

package edu.hebeu.service;

import edu.hebeu.domain.Storage;

public interface StorageService {

    /**
     * 插入添加Storage记录
     * @param storage
     */
    void insert(Storage storage);

    /**
     * 修改更新Storage库存
     * @param storage
     */
    void alter(Storage storage);

    /**
     * 通过商品ID精确查询产品
     * @param productId
     */
    Storage selectSingle(Long productId);
}

package edu.hebeu.service.impl;

import edu.hebeu.dao.StorageDao;
import edu.hebeu.domain.Storage;
import edu.hebeu.service.StorageService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class StorageServiceImpl implements StorageService {

    @Resource
    private StorageDao storageDao;

    @Override
    public void insert(Storage storage) {
        storageDao.insert(storage);
    }

    @Override
    public void alter(Storage storage) {
        storageDao.update(storage);
    }

    @Override
    public Storage selectSingle(Long productId) {
        return storageDao.selectById(productId);
    }
}

Controller层:

package edu.hebeu.controller;

import edu.hebeu.domain.CommonResult;
import edu.hebeu.domain.Storage;
import edu.hebeu.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
public class StorageController {

    @Autowired
    private StorageService storageService;

    /**
     * 添加Storage的接口方法
     * @param storage
     * @return
     */
    @PostMapping("/storage")
    public CommonResult save(@RequestBody Storage storage) {
        storageService.insert(storage);
        return new CommonResult(200, "添加商品库存记录成功");
    }

    /**
     * 修改Storage的接口方法
     * @param storage
     * @return
     */
    @PutMapping("/storage")
    public CommonResult alter(@RequestBody Storage storage) {
        storageService.alter(storage);
        return new CommonResult(200, "扣减库存成功");
    }

    /**
     * 通过商品ID获取商品库存的接口方法
     * @param productId
     * @return
     */
    @GetMapping("/storage/{productId}")
    public CommonResult<Storage> getSingle(@PathVariable("productId") Long productId) {
        return new CommonResult<>(200, "获取商品库存信息成功", storageService.selectSingle(productId));
    }

}

订单服务模块

首先导入上面前置条件的说明的环境和配置

yaml配置文件如下:

server:
  port: 9000

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        tx-service-group: my_tx_group # Seata配置文件file.conf文件service模式的配置
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: 'root'
    password: '072731'

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info
mybatis:
  mapperLocations: classpath:mapper/*.xml
  type-aliases-package: edu.hebeu.domain # 所有实体类所在的包(还自动设置了别名)
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名规则

启动类:

package edu.hebeu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源自动创建(装配)
public class SeataOrder9000Main {

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

Dao层:

package edu.hebeu.dao;

import edu.hebeu.domain.Order;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderDao {

    /**
     * 新建订单
     * @param order
     */
    void insert(Order order);

    /**
     * 修改订单
     * @param order
     */
    void update(Order order);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-/mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="edu.hebeu.dao.OrderDao">

    <insert id="insert" parameterType="edu.hebeu.domain.Order">
        <selectKey keyProperty="id" keyColumn="id" order="AFTER" resultType="java.lang.Long">
            SELECT LAST_INSERT_ID();
        </selectKey>
        INSERT INTO `t_order`(`user_id`, `product_id`, `count`, `money`, `status`) VALUES(#{userId}, #{productId},
        #{count}, #{money}, #{status});
    </insert>

    <update id="update" parameterType="edu.hebeu.domain.Order">
        UPDATE `t_order` SET `user_id`=#{userId}, `product_id`=#{productId}, `count`=#{count}, `money`=#{money},
        `status`=#{status} WHERE `id`=#{id};
    </update>
</mapper>

Service层:

package edu.hebeu.service;

import edu.hebeu.domain.Account;
import edu.hebeu.domain.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(value = "seata-account-service")
public interface AccountService {

    /**
     * 调用获取账户信息的GET接口:/account/{userId}
     * @param userId
     * @return
     */
    @GetMapping("/account/{userId}")
    CommonResult<Account> toGet(@PathVariable("userId") Long userId);

    /**
     * 调用更新账户信息的PUT接口:/account
     * @param account
     * @return
     */
    @PutMapping("/account")
    CommonResult toUpdate(Account account);
}

package edu.hebeu.service;

import edu.hebeu.domain.CommonResult;
import edu.hebeu.domain.Storage;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(value = "seata-storage-service")
public interface StorageService {

    /**
     * 调用获取商品库存的GET接口:/storage/{productId}
     * @param productId
     * @return
     */
    @GetMapping(value = "/storage/{productId}")
    CommonResult<Storage> toGet(@PathVariable("productId") Long productId);

    /**
     * 调用更新商品库存的PUT接口:/storage
     * @return
     */
    @PutMapping(value = "/storage")
    CommonResult toUpdate(Storage storage);

}

package edu.hebeu.service;

import edu.hebeu.domain.Order;

public interface OrderService {

    /**
     * 创建订单Order
     * @param order
     */
    void create(Order order);
}

package edu.hebeu.service.impl;

import edu.hebeu.dao.OrderDao;
import edu.hebeu.domain.Account;
import edu.hebeu.domain.CommonResult;
import edu.hebeu.domain.Order;
import edu.hebeu.domain.Storage;
import edu.hebeu.service.AccountService;
import edu.hebeu.service.OrderService;
import edu.hebeu.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.BigInteger;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderDao orderDao;

    @Resource
    private StorageService storageService;

    @Resource
    private AccountService accountService;

    /**
     * 创建订单的业务方法
     * @param order
     */
    @Override
    public void create(Order order) {
        // TODO 1.新建订单
        log.info("---开始新建订单:" + order.toString());
        orderDao.insert(order);

        // TODO 2.扣减商品库存
        Storage storage = storageService.toGet(order.getProductId()).getData();
        storage.setResidue(storage.getResidue() - order.getCount()); // 修改剩余库存
        storage.setUsed(storage.getUsed() + order.getCount()); // 修改已用库存
        log.info("---订单微服务开始调用库存接口做扣减:" + storage.toString());
        storageService.toUpdate(storage);

        // TODO 3.扣减账户余额
        Account account = accountService.toGet(order.getUserId()).getData();
        account.setResidue(account.getResidue()
                .subtract(order.getMoney()
                    .multiply(new BigDecimal(order.getCount())))); // 修改剩余金额
        account.setUsed(account.getUsed()
                .add(order.getMoney()
                    .multiply(new BigDecimal(order.getCount())))); // 修改已用金额
        log.info("---订单微服务开始调用账户接口做扣减:" + account.toString());
        accountService.toUpdate(account);

        // TODO 4.修改更新订单状态
        order.setStatus(1);
        log.info("---订单微服务开始调用更新订单状态:" + order.toString());
        orderDao.update(order);

        log.info("---订单创建结束");
    }
}

Controller层:

package edu.hebeu.controller;

import edu.hebeu.domain.CommonResult;
import edu.hebeu.domain.Order;
import edu.hebeu.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 创建订单的接口方法
     * @param order
     * @return
     */
    @PostMapping("/order")
    public CommonResult createOrder(@RequestBody Order order) {
        orderService.create(order);
        return new CommonResult(200, "订单创建成功");
    }
}

至此,环境准备结束

使用

先来看一下初始的数据库
在这里插入图片描述

首先我们先来测试一下上面的环境,发送一个创建订单的请求,如下:
在这里插入图片描述
在这里插入图片描述

当我们对库存模块的修改库存的接口方法alter()方法设置延时操作(让调用端出现异常),如下:

/**
 * 修改Storage的接口方法
 * @param storage
 * @return
 */
@PutMapping("/storage")
public CommonResult alter(@RequestBody Storage storage) {
    try {
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    storageService.alter(storage);
    return new CommonResult(200, "扣减库存成功");
}

重启服务,发送创建订单的请求,此时会出现如下的情况:
在这里插入图片描述
在这里插入图片描述
会发现:

  • 当库存和账户金额扣款后,订单并没有设置为1(已支付)
  • 由于Feign有重试机制,因此账户余额还有可能被多次扣减

使用@GlobalTransactional注解标注创建订单的方法,如下:

/**
  * 创建订单的业务方法
  * @param order
  */
@Override
@GlobalTransactional(name = "myTransactional", rollbackFor = Exception.class) // 表示该事务在出现Exception类型的异常后就会进行回滚
public void create(Order order) {
    // TODO 1.新建订单
    log.info("---开始新建订单:" + order.toString());
    orderDao.insert(order);

    // TODO 2.扣减商品库存
    Storage storage = storageService.toGet(order.getProductId()).getData();
    storage.setResidue(storage.getResidue() - order.getCount()); // 修改剩余库存
    storage.setUsed(storage.getUsed() + order.getCount()); // 修改已用库存
    log.info("---订单微服务开始调用库存接口做扣减:" + storage.toString());
    storageService.toUpdate(storage);

    // TODO 3.扣减账户余额
    Account account = accountService.toGet(order.getUserId()).getData();
    account.setResidue(account.getResidue()
                       .subtract(order.getMoney()
                                 .multiply(new BigDecimal(order.getCount())))); // 修改剩余金额
    account.setUsed(account.getUsed()
                    .add(order.getMoney()
                         .multiply(new BigDecimal(order.getCount())))); // 修改已用金额
    log.info("---订单微服务开始调用账户接口做扣减:" + account.toString());
    accountService.toUpdate(account);

    // TODO 4.修改更新订单状态
    order.setStatus(1);
    log.info("---订单微服务开始调用更新订单状态:" + order.toString());
    orderDao.update(order);

    log.info("---订单创建结束");
}

此时重启服务,发送请求测试,如下:
在这里插入图片描述
在这里插入图片描述
发现因为异常的原因导致了数据库回滚,没有错误数据入库

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值