SpringCloud+SpringCloud Alibaba详解

文章目录

该文章已同步收录到我的博客网站,欢迎浏览我的博客网站,xhang’s blog

1.传统单体架构和微服务架构的对比

1.1传统单体架构

1.1.1传统单体架构概述

单体架构在小微企业比较常见,典型代表就是一个应用、一个数据库、一个web容器就可以跑起来

image-20221027085343937

从图中可以分析,单体架构基本上就是如上所说的:一个应用,一个数据库,一个web容器,里面集成了所有的功能。

1.1.2单体架构的特点

  • 所有功能集中在一个项目工程中
  • 所有的功能打成一个war/jar包部署到服务器

1.1.3单体架构的优点

项目架构简单,前期开发成本低,周期短,小型项目的首选。

1.1.4单体架构的缺点

  • 扩展性和可靠性差,因为所有功能集成在一个服务或者一个war包中,修改某个功能时,需要所有服务重新打包。
  • 前期开发比较快,后期随着功能的增长,交互的周期会越变越长的。
  • 所有应用都在一个数据库上操作,数据库出现性能瓶颈。特别是数据分析跑起来的时候,数据库性能急剧下降。
  • 开发、测试、部署、维护愈发困难。即使只改动一个小功能,也需要整个应用一起发布。
  • 技术栈受限。

1.2微服务架构

1.2.1微服务架构概述

微服务是一种架构模式,就是把一个系统中的各个功能点都拆开为一个个的小应用然后单独部署,每个服务运行于独立的进程,具备独立的业务能力。

image-20221027090102038

每一个应用功能区都使用微服务完成,是相互独立的,之间通过轻量级的通信协议(Http)进行服务通信,这样的话,各个应用可以按实际业务需求,选择自己的技术栈和开发语言。

1.2.2微服务架构的特点

  • 将系统服务层完全独立出来,并将服务层抽取为一个一个的微服务。
  • 服务与服务之间互相协作,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中
  • 应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具(如Maven)对其进行构建。

1.2.3微服务架构的优点

  • 单一职责原则
  • 每个服务足够内聚,足够小,代码容易理解,这样能聚焦一个指定的业务功能或业务需求;
  • 开发简单,开发效率高,一个服务可能就是专一的只干一件事;
  • 微服务能够被小团队单独开发,这个团队只需2-5个开发人员组成;
  • 微服务是松耦合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的;
  • 微服务能使用不同的语言开发;
  • 易于和第三方集成,微服务允许容易且灵活的方式集成自动部署,通过持续集成工具,如jenkins,Hudson,bamboo;
  • 微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成果,无需通过合作才能体现价值;
  • 微服务允许利用和融合最新技术;
  • 微服务只是业务逻辑的代码,不会和HTML,CSS,或其他的界面混合;
  • 每个微服务都有自己的存储能力,可以有自己的数据库,也可以有统一的数据库;

1.2.4微服务架构的缺点

  • 开发人员要处理分布式系统的复杂性;
  • 多服务运维难度,随着服务的增加,运维的压力也在增大;
  • 系统部署依赖问题;
  • 服务间通信成本问题;
  • 数据一致性问题;
  • 系统集成测试问题;
  • 性能和监控问题;

2.SpringCloud简介

Spring Cloud官网

SpringCloud是分布式微服务架构的一站式解决方案,是多种微服务架构技术的集合体,俗称微服务全家桶。

SpringCloud利用SpringBoot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务注册发现、配置中心、消息总线、负载均衡,熔断器、数据监控等。

image-20221210135406842

微服务是可以独立部署、水平扩展、独立访问(或者有独立的数据库)的服务单元, Spring Cloud就是这些微服务的大管家,采用了微服务这种架构之后,项目的数量会非常多, Spring Cloud做为大管家就需要提供各种方案来维护整个生态。

image-20221027092520840

3.SpringCloud和SpringBoot的关系

  • SpringBoot专注于开发,方便的开发单个个体微服务

  • SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务,整合并管理起来,为各个微服务之间提供:配置管理、服务发现、断路器、路由、为代理、事件总栈、全局锁、决策竞选、分布式会话等等集成服务

  • SpringBoot可以离开SpringCloud独立使用,开发项目,但SpringCloud离不开SpringBoot,属于依赖关系

  • SpringBoot专注于快速、方便的开发单个个体微服务,SpringCloud关注全局的服务治理框架

4.SpringCloud和SpringBoot版本对应关系

  1. 版本对应关系一般对照表
image-20221027094533888
  1. 官网详细版本对应JSON数据格式说明

https://start.spring.io/actuator/info

......
    "spring-cloud": {
      "Hoxton.SR12": "Spring Boot >=2.2.0.RELEASE and <2.4.0.M1",
      "2020.0.6": "Spring Boot >=2.4.0.M1 and <2.6.0-M1",
      "2021.0.0-M1": "Spring Boot >=2.6.0-M1 and <2.6.0-M3",
      "2021.0.0-M3": "Spring Boot >=2.6.0-M3 and <2.6.0-RC1",
      "2021.0.0-RC1": "Spring Boot >=2.6.0-RC1 and <2.6.1",
      "2021.0.4": "Spring Boot >=2.6.1 and <3.0.0-M1",
      "2022.0.0-M1": "Spring Boot >=3.0.0-M1 and <3.0.0-M2",
      "2022.0.0-M2": "Spring Boot >=3.0.0-M2 and <3.0.0-M3",
      "2022.0.0-M3": "Spring Boot >=3.0.0-M3 and <3.0.0-M4",
      "2022.0.0-M4": "Spring Boot >=3.0.0-M4 and <3.0.0-M5",
      "2022.0.0-M5": "Spring Boot >=3.0.0-M5 and <3.1.0-M1"
    },
......
  1. SpringCloud版本推荐的SpringBoot版本
image-20221027100301401

image-20221027100325999

5.SpringCloud父工程环境搭建

5.1项目创建和基础设置

  1. 创建maven父项目,选择Maven框架
image-20221027103651406
  1. 填写项目名和包名
image-20221027104049548
  1. 选择Maven路径、setting文件地址、本地仓库地址

image-20221027104209354

  1. 删除src文件夹(父工程不需要)

    image-20221027105855656

  2. 设置字符编码

​ File->Setting->Editor->File Encodings

​ 修改字符编码为UTF-8

  1. 注解生效激活

image-20221027104819803

image-20221027104845855

  1. java编译版本

​ java编译版本选择8

image-20221027105154589

  1. File Type过滤
image-20221027105436898

5.2父工程pom文件设置

  1. 使用Maven的分模块管理,设置内容为pom
<groupId>com.xha.springcloud</groupId>
<artifactId>spring-cloud</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
  1. jar包版本说明,引入对应依赖

​ 完整的父工程pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xha.springcloud</groupId>
    <artifactId>spring-cloud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <!--    jar包版本说明-->
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <lombok.version>1.18.24</lombok.version>
        <mysql.vserion>8.0.19</mysql.vserion>
        <druid.version>1.2.13</druid.version>
        <mybatis.spring.boot.version>2.2.2</mybatis.spring.boot.version>
    </properties>

    <!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version  -->
    <dependencyManagement>
        <dependencies>
            <!--springboot 2.6.11-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.11</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--springcloud 2021.0.4-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

<!--            druid数据库连接池-->
            <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>

<!--            mysql数据库驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.vserion}</version>
            </dependency>

<!--            druid数据库连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>

<!--            SpringBoot对mybatis的整合-->
            <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>

<!--            lombok插件-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
                <optional>true</optional>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>mscloud</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.3.5.RELEASE</version>
                <configuration>
                    <fork>true</fork>
                    <addResources>true</addResources>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

5.3Maven问题说明

5.3.1dependencyManagement说明

Maven多模块的时候,管理依赖关系是非常重要的,各种依赖包冲突,查询问题起来非常复杂,采用

就是对依赖jar包进行版本管理的管理器,只做声明,不做引入。

在父工程中使用dependencyManagement,那么在子模块中引入相同的依赖只需要和即可,版本和父工程相同

如果子模块想要使用不同的版本,在子模块中添加相应的版本号即可。

5.3.2dependencyManagement和dependencies的区别

  • Dependencies相对于dependencyManagement,所有声明在dependencies里的依赖都会自动引入,并默认被所有的子项目继承。
  • dependencyManagement里只是声明依赖,并不自动实现引入。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom;另外如果子项目中指定了版本号,那么会使用子项目中指定的版本。

5.3.3Maven中如何跳过单元测试

image-20221027121004306

6.子模块支付模块搭建

6.1子模块的构建步骤

  1. 创建module
  2. 更改pom
  3. 配置yaml
  4. 写主启动类
  5. 写业务类

6.2子模块构建

  1. 新建Maven项目
image-20221027140311005
  1. 为子模块命名

image-20221027140535154

image-20221027140626476

  1. 生成子模块后在父工程的pom文件中自动生成子模块索引
image-20221027141213226
  1. 在子模块中添加依赖,子模块引入的和父工程中相同的依赖则无需指定版本号,解决各模块间依赖冲突问题。

image-20221027144551336

<!--        springboot的web依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

<!--        检测系统的健康情况、当前的Beans、系统的缓存-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

<!--        springboot对mybatis的整合-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

<!--        springboot对druid的整合-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

<!--        mysql依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

<!--        lombok插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
  1. 在子模块中新建ymal配置文件
#子模块端口号
server:
  port: 8001

#子模块名称
spring:
  application:
    name: cloud-provider-payment
#    mysql配置信息
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springcloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
    username: root
    password: xu.123456

#    mybatis映射文件地址和pojo路径
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.xha.springcloud.pojo
  1. 创建主启动类
@SpringBootApplication
public class PaymentMain {
    public static void main(String[] args) {
        SpringApplication.run(PaymentMain.class, args);
    }
}

6.3后端业务逻辑实现

6.3.1数据库

  1. 创建数据库,采用idea连接上数据库
create database springcloud;
use springcloud;
  1. 创建表
create table payment(
    id bigint(20) not null auto_increment comment 'ID',
    serial varchar(200) default '' comment '流水号',
    primary key (id)
)   ENGINE = InnoDB
    DEFAULT CHARSET = utf8mb4
    AUTO_INCREMENT = 1
    COMMENT = '订单表';

6.3.2根据数据库表使用mybatisx逆向工程

image-20221103174255050

  1. 实体Payment
/**
 * 订单表
 * @TableName payment
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
    /**
     * ID
     */
    private Long id;

    /**
     * 流水号
     */
    private String serial;
}
  1. 创建JSON封装体CommonResult
/**
 * JSON封装体CommonResult
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    /**
     * 状态码
     */
    private Integer code;

    /**
     * 提示信息
     */
    private String message;

    /**
     * 返回的数据
     */
    private T data;

    /**
     * 不含data的有参构造
     */
    public CommonResult(Integer code,String message){
        this(code,message,null);
    }
}

6.3.3dao层

  1. 在mapper层接口编写方法
@Mapper
public interface PaymentMapper extends BaseMapper<Payment> {
    /**
     * 创建流水订单
     * @param payment
     * @return
     */
    public int createPayment(Payment payment);

    /**
     * 根据id查询流水账单
     * @param id
     * @return
     */
    public Payment getPaymentById(Integer id);
    
}
  1. 编写对应的mapper映射文件
<mapper namespace="com.xha.springcloud.mapper.PaymentMapper">

    <insert id="createPayment" useGeneratedKeys="true" keyProperty="id">
        insert into payment (serial) values (#{serial});
    </insert>

    <resultMap id="BaseResultMap" type="com.xha.springcloud.entities.Payment">
        <id column="id" property="id"></id>
        <result column="serial" property="serial"></result>
    </resultMap>
    <select id="getPaymentById" resultMap="BaseResultMap">
        select * from payment where id = #{id}
    </select>
</mapper>

6.3.4Service层

Service调用Mapper(Dao)层

@Service
public class PaymentServiceImpl extends ServiceImpl<PaymentMapper, Payment>
        implements PaymentService {

    @Resource
    private PaymentMapper paymentMapper;

    @Override
    public int createPayment(Payment payment) {
        return paymentMapper.createPayment(payment);
    }

    @Override
    public Payment getPaymentById(Long id) {
        return paymentMapper.getPaymentById(id);
    }
}

6.3.5Controller层

@Slf4j
@RestController
@RequestMapping("payment")
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @PostMapping("/createPayment")
    public CommonResult createPayment(@RequestBody Payment payment){
        int result = paymentService.createPayment(payment);
        log.info("添加的结果:" + result);
        if (result == 0){
            return new CommonResult(400,"插入数据库失败!");
        }
        return new CommonResult(200,"插入数据库成功。");
    }

    @GetMapping("/getPaymentById/{id}")
    public CommonResult getPaymentById(@PathVariable("id") Long id){
        Payment payment = paymentService.getPaymentById(id);
        log.info("插入结果:" + payment);
        if (payment == null){
            return new CommonResult(400,"查询失败!");
        }
        return new CommonResult(200,"查询成功。");
    }
}

6.3.6测试

  1. 测试插入数据

image-20221103203651577

插入成功

image-20221103203714302

image-20221104104231208

  1. 测试根据id查询数据

image-20221103203930574

7.RestTemplate

RestTemplate提供了多种便捷访问远程Http服务的方法, 是一种简单便捷的访问Restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集,实现模块之间相互调用。

官网地址:

RestTemplate (Spring Framework 5.2.2.RELEASE API)

使用:

​ 使用restTemplate访问Restful接口非常的简单粗暴无脑

  • url:REST请求地址
  • requestMap:请求参数
  • ResponseBean.class:HTTP响应转换被转换成的对象类型

7.消费者订单模块

7.1子模块构建步骤

  1. 创建module
  2. 更改pom
  3. 配置yaml
  4. 写主启动类
  5. 写业务类

7.2子模块构建

  1. 首先复制cloud-provider-payment8001的enerties

7.2.1创建配置类,注入RestTemplate

@Configuration
public class ApplicationConfig {

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

7.2.2controller

在controller中注入RestTemplate,通过相应方法在参数中写入请求的另一个模块的url路径、参数和响应类型

@Slf4j
@RestController
@RequestMapping("/consumer")
public class ConsumerController {

    public static final String PAYMENT_URL = "http://localhost:8001";

    @Resource
    private RestTemplate restTemplate;

    /**
     * 创建订单
     * @param payment
     * @return
     */
    @PostMapping("/createPayment")
    public CommonResult<Payment> createPayment(Payment payment){
        return restTemplate.postForObject(PAYMENT_URL + "/payment/createPayment",payment,CommonResult.class);
    }

    /**
     * 查询订单
     * @param id
     * @return
     */
    @GetMapping("/getPaymentById/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
        return restTemplate.getForObject(PAYMENT_URL + "/payment/getPaymentById/" + id,CommonResult.class);
    }
}

7.2.3测试

  1. 测试插入数据

localhost:8002/consumer/createPayment?serial=201530328

image-20221104104427100

查看数据库

image-20221104104512059

  1. 测试查询数据

注意:如果启动报错信息是关于数据库的,因为SpringBoot主启动类启动默认会加载数据库驱动连接数据库,因为在cloud-consumer-8002模块中配置文件中未配置。解决方案是在启动类上排除掉数据库。

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

同时启动两个模块:

image-20221103220249875

测试:通过cloud-consumer-8002模块的8002端口调用cloud-provider-payment8001模块接口

​ 查询成功:

image-20221103221836560

8.工程重构

8.1存在的问题

多个模块之间会存在相同的pojo或者类,采用重构的方式创建common模块实现通用资源

8.2新建common模块

  1. 采用Spring initializr新建Boot模块(注意修改pom文件中父工程为spring-cloud

  2. 引入pom

  3. 将entitis复制到common模块中

  4. common模块Maven跳过test打包,将jar包上传到本地库

image-20221104133547718

  1. 其他模块删除entitis文件夹

  2. 到没有entities模块的的Controller中引入commons模块依赖

image-20221104132151263

查看依赖:

image-20221104132346586

9.注册中心与注册发现

9.1Eureka服务注册与发现

9.1.1Eureka基础知识

  • 什么是服务治理

在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

  • 什么是服务注册

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

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

image-20221210141739744
  • Eureka两组件

    • Eureka Server 提供服务注册服务

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

    • Eureka Client 通过注册中心进行访问

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

9.1.2单机Eureka环境搭建

9.1.2.1配置Eureka服务注册中心(EurekaServer)
  1. Idea新建子模块,作为eureka服务模块

    image-20221210143427652

  2. 添加EurekaServer的Maven坐标

        <!--eureka-server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
  1. 写yaml配置文件
server:
  port: 7001

eureka:
  instance:
    hostname: localhost #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
    #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

  1. 配置主启动类

​ 再配置类上添加Eureka的@EnableEurekaServer注解,表示当前模块为Eureka的服务注册中心

image-20221210150017125

  1. 测试,输入ip+eureka服务运行端口,查看Eureka的服务注册中心
image-20221210150703095
9.1.2.2将服务模块注册进EurekaServer服务注册中心
  1. 在服务模块添加EurekaClient坐标
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
  1. 在启动类上添加@EnableEurekaClient注解
image-20221210151544243
  1. 修改服务模块的application.yaml文件
eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka
  1. 重启EurekaClient服务模块,查看EurekaServer的服务注册情况
image-20221210153129428
  1. 其中在配置文件中配置的模块名就是注册进Eureka注册中心的名称

image-20221210153443477

9.1.3Eureka集群搭建

9.1.3.1Eureka集群原理说明
image-20221210175501947

EurekaServer 通过replicate来同步数据,==Eureka节点相互之间没有区分主节点和从节点,所有的节点都是平等的,每个节点都有其他节点的信息。==在这种架构之下,节点通过彼此注册来提高可用性,每个节点需要添加一个或者多个有效的serviceURL来指向其他的节点。

若某一台服务器发生宕机,EurekaClient的请求将会自动切换到新的Eureka Server 节点,当宕机后的服务器重新恢复之后,Eureka 会将其再次纳入到服务器的集群管理之中,当节点开始接受客户端的请求时,所有的操作都会进行节点之间的复制,将请求复制到其他的EurekaServer 当前所知的所有节点中

9.1.3.2Eureka集群搭建
  1. 新建Eureka子模块

  2. 修改本机映射配置

​ 进入本机的以下目录:C:\Windows\System32\drivers\etc,修改hosts文件

image-20221210182338443

  1. 修改两个Eureka模块的配置文件

​ Eureka节点之间相互绑定

​ Eureka-7001的配置文件

image-20221210183655380

​ Eureka-7002的配置文件

image-20221210183825871
  1. 查看eureka注册中心的信息
image-20221210183938786 image-20221210184016281
  1. 将微服务模块添加到Eureka集群当中
image-20221210184947045
  1. 查看,微服务模块成功的添加到了Eureka注册中心当中

image-20221210185417966

微服务模块之间正常调用

image-20221210185515735

9.1.3.3支付微服务集群配置
  1. 新建8002模块和8001模块相同,修改启动类、服务端口,并启动

可以看到一个Eureka实例有两个微服务模块

image-20221210191836454

  1. 修改消费者模块的订单模块访问地址(地址不能写死,而是要用实例名称)

image-20221210192152206

  1. 使用@loadBalance注解,使restTemplate实现微服务名的调用对应服务

@loadBalance注解修饰的restTemplate才能实现服务名的调用,一个服务名下可以注册多个微服务,从而实现负载均衡。

image-20221210192533982

  1. 测试。微服务集群模块之间实现轮询调用

GIF 2022-12-10 20-28-39

9.1.3.4修改实例名称、添加ip

image-20221210212745757

image-20221210212807439

9.1.4Eureka服务发现Discovery

9.1.4.1服务发现Discovery的功能

对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息

9.1.4.2配置步骤
  1. 注入DiscoveryClient对象
@Resource
private DiscoveryClient discoveryClient;
  1. 启动类添加@EnableDiscoveryClient注解

  2. 打印Eureka当中的微服务信息

@GetMapping(value = "/payment/discovery")
public Object discovery() {
    List<String> services = discoveryClient.getServices();
    for (String service : services) {
        log.info("service:" + service);
    }

    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PROVIDER-SERVER");
    for (ServiceInstance instance : instances) {
        log.info("instance:" + instance.getServiceId()
                + "\t" + instance.getHost()
                + "\t" + instance.getPort()
                + "\t" + instance.getUri());
    }
    return discoveryClient;
}

image-20221211114358433

9.1.5Eureka自我保护理论

9.1.5.1Eureka自我保护概念

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

即某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存

在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。

image-20221210221300364

9.1.5.2禁止Eureka自我保护
  1. 可以在Eureka服务模块之中添加配置禁止Eureka的自我保护,即当服务模块不可用的时候立即删除
eureka:
  instance:
    hostname: eureka7001.com #eureka服务端的实例名称
  client:
    #false表示不向注册中心注册自己。
    register-with-eureka: false
    #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
      #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://eureka7002.com:7002/eureka/
  #      关闭eureka的自我保护机制,不可用的服务及时剔除
  server:
    enable-self-preservation: false
    #    心跳时间调整为两秒钟,即两秒钟检测一次是否存在不可用的服务
    eviction-interval-timer-in-ms: 2000
  1. 在服务模块配置文件中添加检测心跳时间
eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
    #实例ID
    instance-id: cloud-provider-server-8001
    #显示ip地址
    prefer-ip-address: true
#    Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
    lease-expiration-duration-in-seconds: 2
#      Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认为30s)
    lease-renewal-interval-in-seconds: 1

自我保护关闭

image-20221211122936267

模拟8002服务模块宕机,则立即被Eureka注册中心删除

image-20221211123117347

9.2zookeeper服务注册与发现

9.2.1zookeeper简介

zookeeper是经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能,高可用,且具有严格顺序访问控制能力的分布式协调存储服务

总的来说,zookeeper就是一个分布式协调工具,可以实现注册中心功能。

image-20221211124727608

9.2.2zookeeper的安装

  1. docker拉取zookeeper镜像
docker pull zookeeper
  1. 使用镜像创建容器实例
docker run -d -p 2181:2181 -v /root/docker/zookeeper:/data --name zookeeper zookeeper
  1. 开放端口
firewall-cmd --zone=public --add-port=2181/tcp --permanent
  1. 进入容器中,进入根目录下的/apache-zookeeper-版本号-bin目录下(默认就是此目录下)
  2. 进入到bin目录下,执行脚本文件,启动zookeeper服务
./zkServer.sh start
  1. 启动zookeeper客户端
./zkCli.sh

9.2.3支付模块注册进zookeeper

  1. 新建模块

  2. 添加以下依赖,其中有zookeeper的依赖

<dependencies>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.xha.springcloud</groupId>
        <artifactId>cloud-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <!-- SpringBoot整合zookeeper客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </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>
  1. application.yaml配置文件
server:
  port: 8003
spring:
  application:
    name: cloud-provider-payment-zookeeper-8003
  cloud:
    zookeeper:
#      zookeeper的服务地址和服务运行端口
      connect-string: 192.168.26.135:2181
  1. 主启动类添加@EnableDiscoveryClient注解
@SpringBootApplication
@EnableDiscoveryClient
public class Zookeeper8003Main {
    public static void main(String[] args) {
        SpringApplication.run(Zookeeper8003Main.class,args);
    }
}
  1. 添加控制层
@RestController("/payment")
public class PaymentController {

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

    @GetMapping("/zookeeper")
    public String zookeeperInfo(){
        return "springCloud with zookeeper: "+serverPort+"\t"+ UUID.randomUUID();
    }
}
  1. 启动模块

查看模块已经注册进了zookeeper当中

image-20221211150021263

访问接口:

image-20221211160922595

  1. 查看微服务模块的注册信息

image-20221211152553696

转为对应的JSON字符串,查看注册的微服务模块信息

{
    "name":"cloud-provider-payment-zookeeper-8003",
    "id":"97268e69-5cd7-47b4-8051-784b5ff7ad95",
    "address":"DESKTOP-EOJJPUL",
    "port":8003,
    "sslPort":null,
    "payload":{
        "@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
        "id":"cloud-provider-payment-zookeeper-8003",
        "name":"cloud-provider-payment-zookeeper-8003",
        "metadata":{
            "instance_status":"UP"
        }
    },
    "registrationTimeUTC":1670743242777,
    "serviceType":"DYNAMIC",
    "uriSpec":{
        "parts":[
            {
                "value":"scheme",
                "variable":true
            },
            {
                "value":"://",
                "variable":false
            },
            {
                "value":"address",
                "variable":true
            },
            {
                "value":":",
                "variable":false
            },
            {
                "value":"port",
                "variable":true
            }
        ]
    }
}

9.2.4消费模块注册进zookeeper

  1. 搭建消费者模块,运行在80端口

  2. 控制层

在控制层中调用运行在8003端口的生产者模块

@RestController
@RequestMapping("/consumer")
public class ConsumerZookeeperController {

    public static final String URL = "http://cloud-provider-payment-zookeeper-8003";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/zookeeper")
    public String getZookeeper() {
        return restTemplate.getForObject(URL + "/payment/zookeeper", String.class);
    }
}
  1. 查看zookeeper中的微服务注册情况

image-20221211162921035

  1. 测试消费者接口

image-20221211163144402

9.3Consul服务注册与发现

9.3.1Consul简介

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

它具有很多优点。包括: 基于 raft 协议,比较简洁; 支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows

9.3.2docker安装Consul

  1. 拉取consul镜像
docker pull consul
  1. 开启8500端口
firewall-cmd --zone=public --add-port=8500/tcp --permanent
systemctl restart firewalld.service
  1. 启动容器实例
docker run -d -p 8500:8500 -v /root/docker/consul:/data --name consul consul:latest 
  1. 访问8500端口,查看consul的UI界面

image-20221212114314027

9.3.3支付模块注册进consul服务注册

  1. 新建子模块,添加pom
    <dependencies>
        <!--SpringCloud consul-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <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>
        <!--日常通用jar包配置-->
        <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>
  1. 配置文件内容
server:
  port: 8004

spring:
  application:
    name: cloud-provider-payment-consul-8004

# 配置consul
  cloud:
    consul:
      host: 192.168.26.144
      port: 8500
      discovery:
        service-name: ${spring.application.name}
  1. 创建启动类
@SpringBootApplication
@EnableDiscoveryClient
public class Consul8004Main {
    public static void main(String[] args) {
        SpringApplication.run(Consul8004Main.class,args);
    }
}
  1. 查看Consul的UI界面,支付模块已经成功注册进consul
image-20221212121428289
  1. 测试接口

image-20221212121605127

9.3.34消费模块注册进consul服务注册

  1. 新建子模块,添加pom
    <dependencies>

         <!--SpringCloud consul-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <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>
        <!--日常通用jar包配置-->
        <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>
  1. 配置文件内容
server:
  port: 80

spring:
  application:
    name: cloud-consumer-consul-80

  # 配置consul
  cloud:
    consul:
      host: 192.168.26.144
      port: 8500
      discovery:
        service-name: ${spring.application.name}
  1. 创建启动类
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerConsul80Main {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerConsul80Main.class,args);
    }
}
  1. 创建配置类
@Configuration
public class ApplicationConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  1. controller层
@RestController
@RequestMapping("/consumer")
public class ConsumerConsulController {

    public static final String URL = "http://cloud-provider-payment-consul-8004";

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consul")
    public String getConsulInfo(){
        return restTemplate.getForObject(URL + "/payment/consul",String.class);
    }

}
  1. 启动模块,查看模块的注册情况

image-20221212133529235

  1. 测试接口

image-20221212133605829

9.4Eureka、zookeeper和consul三种注册中心的异同点

组件名语言CAP健康服务检查对外暴露接口Spring Cloud集成情况
EurekajavaAP支持HTTP已集成
ZookeeperjavaCP支持客户端已集成
ConsulgoCP支持HTTP/DNS已集成

CAP理论:

image-20221212134543244

最多只能同时较好的满足两个。

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

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

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

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

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

10.负载均衡服务调用

10.1Ribbon

10.1.1Ribbon简介

Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具

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

10.1.2Ribbon的依赖

Ribbon是用于客户端负载均衡的工具。Ribbon只负责负载均衡,那么发送请求,还需要Spring提供的RestTemplate。

  1. Ribbon所以依赖的坐标

但是我们的消费者模块并没有引入Ribbon的坐标,为什么还实现了两个支付模块的负载均衡呢?

GIF 2022-12-12 18-03-32

​ 原因是因为当引入**低版本**的Eureka坐标当中已经引入了spring-cloud-starter-netflix-ribbon坐标。

image-20221212191231105

image-20221212191254266

**高版本**的Eureka已经是使用的是spring-cloud-starter-loadbalancer

image-20221212220402101

10.1.3Ribbon架构说明

image-20221212185922433

Ribbon在工作时分成两步

  • 第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server。

  • 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。

  • 其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。

10.1.4Ribbon负载均衡(LB)功能详解

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

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

  1. Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别

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

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

  2. 集中式LB

​ ==即在服务的消费方和提供方之间使用独立的LB设施==可以是硬件,如F5, 也可以是软件,如nginx), 由该 设施负责把访问请求通过某种策略转发至服务的提供方;

  1. 进程内LB

将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选 择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

10.1.5Ribbon的负载规则

10.1.5.1IRule接口

Ribbon的负载规则有如下七种,而默认的负载规则为轮询。

image-20221212193857987

image-20221212194154419

10.1.5.2Ribbon的负载规则替换
  1. 官方文档明确给出了警告:要实现负载规则替换的自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。

​ 而@ComponentScan就在@SpringBootApplication注解当中

image-20221212194931468

image-20221212195004277

所以就不能创建在com.xha.springcloud包下,而是创建在com.xha包下:

image-20221212195126963

  1. 新创建包,并创建配置类,替换规则为RandomRule,注入Bean
@Configuration
public class MySelfRule {

    @Bean
    public IRule iRule(){
//        替换为随机规则
        return new RandomRule();
    }
}
  1. 主启动类添加@RibbonClient注解,指定访问的服务和Ribbon的负载规则
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PROVIDER-SERVER",configuration = MySelfRule.class)
public class ConsumerMain {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerMain.class,args);
    }
}

10.1.6Ribbon的负载均衡算法

负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始。

10.2Feign

10.2.1Feign简介

Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。

它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

10.2.2Feign的功能

  1. Feign旨在使编写Java Http客户端变得更容易

前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。

  1. Feign集成了Ribbon

利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用

10.3OpenFeign

10.3.1OpenFeign简介

OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

10.3.2Feign和OpenFeign的区别

FeignOpenFeign
是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端,是SpringCloud中的第一代负载均衡客户端在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。是SpringCloud中的第二代负载均衡客户端
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

10.3.3OpenFeign服务调用

  1. 新建消费者模块

  2. pom文件

    <dependencies>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.xha.springcloud</groupId>
            <artifactId>cloud-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!--web-->
        <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.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>
  1. 配置文件
server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
  1. 主启动类
@SpringBootApplication
@EnableFeignClients
public class OpenfeignConsumer80Main {
    public static void main(String[] args) {
        SpringApplication.run(OpenfeignConsumer80Main.class,args);
    }
}
  1. 业务类

​ 实现方式:业务逻辑接口+@feignClient配置调用服务

​ 当前调用的就是CLOUD-PROVIDER-SERVER服务集群

@Component
@FeignClient(value = "CLOUD-PROVIDER-SERVER")
public interface ProviderFeignService {

    @GetMapping("/payment/getPaymentById/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);

}
  1. 控制层

​ 控制层调用添加@FeignClient的接口中的方法。@FeignClient的参数value值就是服务集群名称。该方法设置的有映射规则。

@RestController
@RequestMapping("/consumer")
public class OpenfeignController {

    @Resource
    private ProviderFeignService providerFeignService;

    @GetMapping("/getPaymentById/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
        return providerFeignService.getPaymentById(id);
    }
}
  1. 启动Eureka集群和CLOUD-PROVIDER-SERVER服务集群对OpenFeign实现负载均衡进行测试

GIF 2022-12-13 18-53-47

10.3.4OpenFeign超时控制

高版本的OpenFeign依赖的默认等待时间为60秒钟

如果有一个业务的逻辑流程过于复杂超过了60秒钟,客户端就会报错。

image-20221213201034749

为了避免Openfeign的超时控制机制,就需要设置Fegin客户端的超时控制。

再配置文件当中进行配置:

feign:
  client:
    config:
      default:
        # 指的是建立连接所用的时间,适用于网络状态正常的情况下,两端连接所用的时间
        ConnectTimeOut: 100000
        # 指的是建立连接后从服务器读取可用资源所用的时间
        ReadTimeOut: 100000

10.3.5OpenFigen日志打印

  1. OpenFeign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节

  2. 日志級別

    • NONE:默认的,不显示任何日志

    • BASIC:仅记录请求方法、URL、响应状态码及执行时间

    • HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息

    • FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据

  3. 日志实现步骤:1.创建配置类

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


@Configuration
public class OpenFeignConfig {
    @Bean
    Logger.Level feignLoggerLevel()
    {
        return Logger.Level.FULL;
    }
}
  1. 日志实现步骤:2.在配置文件中开启日志的Feign客户端
logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.xha.springcloud.service.ProviderFeignService: debug

image-20221213202846463

11.服务熔断、服务降级

11.1问题引入(分布式系统面临的问题)

复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败,如果有一个失败,那么整个分布式系统将面临严重得问题。

下图中的请求需要调用A,P,H,I四个服务,如果一切顺利则没有什么问题,但是如果I服务超时会出现什么情况呢?

image-20221214101534993

服务雪崩

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

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

11.2Hystrix

11.2.1Hystrix简介

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

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

11.2.2Hystrix的重要概念

11.2.2.1服务降级
  1. 服务降级的概念

当我们的服务器压力剧增为了保证核心功能的可用性 ,而选择性的降低一些功能的可用性。

  1. 服务降级的触发条件
    1. 程序运行异常
    2. 超时
    3. 服务熔断触发服务降级
    4. 线程池/信号量打满
11.2.2.2服务熔断
  1. 服务熔断工作机制

降级一般而言指的是我们自身的系统出现了故障而降级。而熔断一般是指依赖的外部接口出现故障的情况断绝和外部接口的关系。

熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。

==当检测到该节点微服务调用响应正常后,恢复调用链路。==即先进行服务降级,如果访问失败率过高就进行服务熔断,当响应正常后再恢复链路。

服务降级—>服务熔断—>恢复链路

在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand

  1. 服务熔断原理图

下图为服务熔断工作图:

image-20221215212851339

熔断关闭:熔断关闭不会对服务进行熔断。

熔断打开请求不再进行调用当前服务,内部设置时钟一般为MTTR (平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态。

熔断半开部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。

服务熔断大神文章:https://martinfowler.com/bliki/CircuitBreaker.html

11.2.2.3服务限流

系统规定了多少承受能力,只允许这些请求能过来,其他的请求将拒绝。

11.2.3Hystrix工作流程

官网流程说明地址:https://github.com/Netflix/Hystrix/wiki/How-it-Works

image-20221216132555405

  1. Construct a HystrixCommand or HystrixObservableCommand Object
  2. Execute the Command
  3. Is the Response Cached?
  4. Is the Circuit Open?
  5. Is the Thread Pool/Queue/Semaphore Full
  6. HystrixObservableCommand.construct() or HystrixCommand.run()
  7. Calculate Circuit Health
  8. Get the Fallback
  9. Return the Successful Response
步骤说明
1创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象。
2命令执行。其中 HystrixComand 实现了下面前两种执行方式;而 HystrixObservableCommand 实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常。queue():异步执行, 直接返回 一个Future对象, 其中包含了服务执行结束时要返回的单一结果对象。observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 Hot Obserable(不论 “事件源” 是否有 “订阅者”,都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个 “订阅者” 都有可能是从 “事件源” 的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable(): 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有 “订阅者” 的时候并不会发布事件,而是进行等待,直到有 “订阅者” 之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)。
3若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以 Observable 对象的形式 返回。
4检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步)。
5检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步)。
6Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。HystrixCommand.run() :返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct(): 返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知。
7Hystrix会将 “成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器, 而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 “熔断/短路”。
8当命令执行失败的时候, Hystrix 会进入 fallback 尝试回退处理, 我们通常也称该操作为 “服务降级”。而能够引起服务降级处理的情况有下面几种:第4步: 当前命令处于"熔断/短路"状态,断路器是打开的时候。第5步: 当前命令的线程池、 请求队列或 者信号量被占满的时候。第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候。
9当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回。

11.2.4Hystrix问题引出

11.2.4.1服务模块
  1. 新建服务模块

  2. pom文件

    <dependencies>
        <!--hystrix-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--web-->
        <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.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>
  1. 配置文件

​ 使用的注册中心是Eureka

server:
  port: 8005

spring:
  application:
    name: cloud-provider-hystrix-payment
eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
#    实例ID
    instance-id: cloud-provider-hystrix-payment-8005
#    显示ip
    prefer-ip-address: true

  1. 主启动类
@SpringBootApplication
@EnableEurekaClient
public class hystrix8005Main {
    public static void main(String[] args) {
        SpringApplication.run(hystrix8005Main.class,args);
    }
}
  1. 业务类

service层:

public interface HystrixService {
    String paymentNormal(Integer id);

    String paymentTimeout(Integer id);

}
@Service
public class HystrixServiceImpl implements HystrixService {
    @Override
    public String paymentNormal(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + "payment_normal,id:" + id;
    }

    @Override
    public String paymentTimeout(Integer id) {
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:" + Thread.currentThread().getName() + "payment_timeout,id:" + id;
    }
}

controller层:

@RestController
@RequestMapping("/payment")
public class HystrixController {

    @Resource
    private HystrixService hystrixService;

    @GetMapping("/hystrix/normal/{id}")
    public String paymentNormal(@PathVariable Integer id){
        return hystrixService.paymentNormal(id);
    }

    @GetMapping("/hystrix/timeout/{id}")
    public String paymentTimeout(@PathVariable Integer id){
        return hystrixService.paymentTimeout(id);
    }
}
  1. 启动Eureka集群,普通测试

image-20221214122834180

GIF 2022-12-14 12-29-11

  1. 高并发测试
    1. 打开jmeter,创建线程组,创建HTTP请求,发送HTTP请求

image-20221214123428824

image-20221214124338961

image-20221214124431698
  1. 在测试过程中可以发现即使是正常的请求,也将会有延迟,这是因为Tomcat的默认工作线程数被业务处理时间长的线程占用耗尽。短时间内没有多余的线程来处理
12.2.4.2消费者模块
  1. 新建消费者模块

  2. pom文件

<dependencies>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>com.xha.springcloud</groupId>
        <artifactId>cloud-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <!--web-->
    <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.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>
  1. 配置文件
server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
  1. 启动类
@SpringBootApplication
@EnableFeignClients
public class Hystrix80Main {
    public static void main(String[] args) {
        SpringApplication.run(Hystrix80Main.class,args);
    }
}
  1. Service层
@Component
@FeignClient("CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {

    @GetMapping("/payment/hystrix/normal/{id}")
    public String paymentNormal(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentTimeout(@PathVariable("id") Integer id);
}
  1. Controller层
@RestController
@RequestMapping("/consumer")
public class HystrixConsumerController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/hystrix/normal/{id}")
    public String paymentNormal(@PathVariable Integer id){
        return paymentHystrixService.paymentNormal(id);
    }

    @GetMapping("/hystrix/timeout/{id}")
    public String paymentTimeout(@PathVariable Integer id){
        return paymentHystrixService.paymentTimeout(id);
    }
}
  1. 测试,cloud-consumer-hystrix-80调用cloud-provider-payment-hystrix-8005服务

一切正常

image-20221214162935799

但是在高并发的情况下,即使是业务处理时间很短的接口访问响应速度也会很慢。

12.2.4.4问题分析

对于以上情况分析出问题如下:

  • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001)down机了,调用者(80)不能一直卡等待,必须有服务降级
  • 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级

11.2.5服务降级实现

  1. 当我们的服务器压力剧增为了保证核心功能的可用性 ,而选择性的降低一些功能的可用性。

  2. 采用@HystrixCommand注解来实现服务降级,一旦调用服务方法失败并抛出了错误信息后,
    会自动调用@HystrixCommand标注好的fallbackMethold调用属性中的指定方法。

11.2.5.1服务端服务降级
  1. 实现思路:设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback。

  2. 修改服务业务类,设置因超时而服务降级

@Service
public class HystrixServiceImpl implements HystrixService {
    @Override
    public String paymentNormal(Integer id) {
        return "线程池:" + Thread.currentThread().getName() + "payment_normal,id:" + id;
    }

    @Override
    @HystrixCommand(fallbackMethod = "paymentTimeoutHandler",commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
    })
    public String paymentTimeout(Integer id) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池:" + Thread.currentThread().getName() + "payment_timeout,id:" + id;
    }

    public String paymentTimeoutHandler(Integer id){
        return "服务端服务降级 id" + id;
    }
}
  1. 主启动类添加@EnableHystrix注解
@SpringBootApplication
@EnableEurekaClient
@EnableHystrix
public class hystrix8005Main {
    public static void main(String[] args) {
        SpringApplication.run(hystrix8005Main.class,args);
    }
}

测试因超时而服务降级

也能够处理异常情况而进行服务降级

image-20221214215812901

11.2.5.2客户端服务降级
  1. 在配置文件中添加配置,在OpenFegin中开启Hystrix服务熔断
# 开启服务熔断
feign:
  circuitbreaker:
    enabled: true
  1. 启动类添加@EnableHystrix注解,开启Hystrix的保护机制
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class Hystrix80Main {
    public static void main(String[] args) {
        SpringApplication.run(Hystrix80Main.class,args);
    }
}
  1. 控制层对应方法添加@HystrixCommand注解,
    @GetMapping("/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeoutHandler",commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000")
    })
    public String paymentTimeout(@PathVariable Integer id){
        return paymentHystrixService.paymentTimeout(id);
    }

    public String paymentTimeoutHandler(Integer id){
        return "客户端服务降级 id:" + id;
    }
  1. 修改服务端线程等待时间,设置线程等待时间低于@HystrixCommand设置的时间
  2. 测试客户端服务降级

image-20221214220018344

11.2.5.3目前的问题

每一个业务方法都对应一个降级方法,代码量大,代码复用率不高。

解决方案:全局服务降级和单独服务降级

  1. 在控制层或Service层添加@DefaultProperties注解指定全局服务降级方法
  • 添加@HystrixCommand注解的方法表示使用全局服务降级方法
  • 添加@HystrixCommand(fallbackMethod = "****",commandProperties = {***})表示使用单独服务降级方法
  • 没有@HystrixCommand注解表示不设置服务降级

注意:全局服务降级方法不能有参数

@RestController
@RequestMapping("/consumer")
@DefaultProperties(defaultFallback = "globalPaymentTimeoutHandler")
public class HystrixConsumerController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/hystrix/normal/{id}")
    public String paymentNormal(@PathVariable Integer id){
        return paymentHystrixService.paymentNormal(id);
    }

    @GetMapping("/hystrix/timeout/{id}")
//    @HystrixCommand(fallbackMethod = "paymentTimeoutHandler",commandProperties = {
//            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1000")
//    })
    @HystrixCommand
    public String paymentTimeout(@PathVariable Integer id){
        return paymentHystrixService.paymentTimeout(id);
    }

    public String paymentTimeoutHandler(Integer id){
        return "客户端服务降级 id:" + id;
    }

    /**
     * 全局服务降级方法
     *
     * @return {@link String}
     */
    public String globalPaymentTimeoutHandler(){
        return "客户端全局服务降级";
    }
}

image-20221214223945718

  1. **但是上面还是会出现问题,即服务降级方法和业务逻辑代码写在一起,耦合度太高。**修改如下:

创建实现类,实现OpenFeign处理负载均衡的接口,实现其中的方法,为方法做服务降级

在接口中的@FeignClient注解添加fallback属性,并指定属性值为该接口实现类

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentHystrixServiceImpl.class)
public interface PaymentHystrixService {

    @GetMapping("/payment/hystrix/normal/{id}")
    public String paymentNormal(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentTimeout(@PathVariable("id") Integer id);
}

实现类:

@Component
public class PaymentHystrixServiceImpl implements PaymentHystrixService {
    @Override
    public String paymentNormal(Integer id) {
        return "客户端服务降级,方法名:" + ",id:" + id;
    }

    @Override
    public String paymentTimeout(Integer id) {
        return "客户端服务降级,方法名:" + ",id:" + id;
    }
}

测试,当服务端宕机的时候,客户端做服务降级,而不会一直向服务端发送请求

image-20221215210707039

11.2.6服务熔断实现

降级一般而言指的是我们自身的系统出现了故障而降级。而熔断一般是指依赖的外部接口出现故障的情况断绝和外部接口的关系。

熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。

当检测到该节点微服务调用响应正常后,恢复调用链路。

在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand

11.2.6.1服务端服务熔断

在服务端开启服务熔断

  1. 下面方法配置的熔断器

其中熔断器的参数配置是在HystrixCommandProperties类中:

image-20221216115415045
  1. 涉及到断路器的三个重要参数:快照时间窗、请求总数阀值、错误百分比阀值

1:快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。

2:请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。

3:错误百分比阀值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开。

  1. 测试:下面的配置即当在10秒中的时间窗口期,在10次访问中如果失败率达到60%就进行服务熔断

如果没有达到失败率,且服务端异常就进行服务降级

    @HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
            @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //是否开启熔断器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), //请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") //失败率达到多少后跳闸
//            在10秒中的时间窗口期,在10次访问中如果失败率达到60%就进行服务熔断
    })
    public String paymentCircuitBreaker(Integer id) {
        if (id < 0) {
            throw new RuntimeException("当前id为:" + id + ",id不能为负数");
        }
        String uuid = IdUtil.simpleUUID();
        return Thread.currentThread().getName() + "\t" + "调用成功,流水号为:" + uuid;
    }

    public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
        return "id不能为负数";
    }

GIF 2022-12-16 12-17-34

11.2.7Hystrix服务监控Dashboard

11.2.7.1Dashboard简介

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

11.2.7.2Dashboard环境搭建
  1. 新建模块
  2. pom文件配置
<dependencies>
        <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>
        <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>
  1. yaml文件配置
server:
  port: 9001
  
hystrix:
  dashboard:
    proxy-stream-allow-list: "*"
  1. 主启动类

​ 主启动类添加@EnableHystrixDashboard注解

@SpringBootApplication
@EnableHystrixDashboard
public class DashBoard9001Main {
    public static void main(String[] args) {
        SpringApplication.run(DashBoard9001Main.class,args);
    }
}
  1. 所有要进行Hystrix服务监控的服务都要加springboot的监控功能,添加actuator坐标
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 启动DashBroad监控模块,访问路径

ip+运行端口+/hystrix

image-20221216134728298

  1. 添加被监控的服务模块

​ 注意:新版本Hytrix需要在主启动类MainAppHystrix8001中指定监控路径

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

    /**
     *此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
     *ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
     *只要在自己的项目里配置上下面的servlet就可以了
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}
11.2.7.3DashBroad监控
  1. 在Hystrix的监控页面添加要监控的模块路径

http://localhost:8005/hystrix.stream

image-20221216145537592

因为在服务端开启了服务熔断,所以以客户端进行测试:

​ 数据说明:

image-20221216151210024

正常访问服务端,服务熔断处于关闭状态

image-20221216145945168

当服务端处于异常状态时,服务熔断开启

image-20221216150350053

当再次正常请求后,服务熔断关闭

image-20221216150609043

12.服务网关(Gateway)

12.1Gateway简介

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,基于 Spring 5.0+Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

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

Spring Cloud Gateway的目标是提供统一的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如

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

官网地址:https://spring.io/projects/spring-cloud-gateway

image-20221216153552812

微服务架构中网关(gateway)的位置:

image-20221216170117513

image-20221216154125223

12.2Gateway的特点

  1. 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
  2. 动态路由:能够匹配任何请求属性;
  3. 可以对路由指定 Predicate(断言)和 Filter(过滤器);
  4. 集成Hystrix的断路器功能;
  5. 集成 Spring Cloud 服务发现功能;
  6. 易于编写的 Predicate(断言)和 Filter(过滤器);
  7. 请求限流功能;
  8. 支持路径重写。

12.3Spring Cloud Gateway 与 Zuul的区别

在SpringCloud Finchley 正式版之前,Spring Cloud 推荐的网关是 Netflix 提供的Zuul

  1. Zuul 1.x,是一个基于阻塞 I/ O 的 API Gateway
  2. Zuul 1.x 基于Servlet 2. 5使用阻塞架构它不支持任何长连接(如 WebSocket) Zuul 的设计模式和Nginx较像,每次 I/ O 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx 用C++ 实现,Zuul 用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得Zuul 的性能相对较差。
  3. Zuul 2.x理念更先进,基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。 Zuul 2.x的性能较 Zuul 1.x 有较大提升。在性能方面,根据官方提供的基准测试, Spring Cloud Gateway 的 RPS(每秒请求数)是Zuul 的 1. 6 倍。
  4. Spring Cloud Gateway 建立 在 Spring Framework 5、 Project Reactor 和 Spring Boot 2 之上, 使用非阻塞 API。
  5. Spring Cloud Gateway 还 支持 WebSocket, 并且与Spring紧密集成拥有更好的开发体验

12.4Gateway的3大核心概念

  1. Route(路由)

路由是构建网关的基本模块,它由ID,目标URI,一系列的断言过滤器组成,如果断言为true则匹配该路由

  1. Predicate(断言)

​ 参考的是Java8的java.util.function.Predicate

​ 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由

  1. Filter(过滤)

​ 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

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

image-20221216164004522

12.5Gateway的工作流程

核心逻辑为:

路由转发+执行过滤器链

image-20221216165404293

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler

Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。

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

12.6网关(gateway)模块创建

​ 此次网关模块为9527,作为路由模块的为8001。

  1. 新建网关模块
  2. pom文件
<dependencies>
    <!--gateway-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--eureka-client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.xha.springcloud</groupId>
        <artifactId>cloud-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </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>
  1. 在yaml文件中进行网关路由配置

根据路由和断言规则(即当uri下含有断言的内容,就返回true,匹配该路由)匹配路由。从而就不再使用为服务端口而统一使用网关端口9527

image-20221216215457732

server:
  port: 9527

spring:
  application:
    name: cloud-gateway-9527
  cloud:
    gateway:
      routes:
        - id: payment_route1             #路由的ID,没有固定规则但要求要唯一,建议配合服务名
          uri: http://localhost:8001     #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/getPaymentById/**  #断言,路径相匹配的进行路由

        - id: payment_route 2
          uri: http://localhost:8001
          predicates:
            - Path=/payment/discovery/**


eureka:
  instance:
    hostname: cloud-gateway-9527
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  1. 启动类
@SpringBootApplication
@EnableEurekaClient
public class Gateway9527Main {
    public static void main(String[] args) {
        SpringApplication.run(Gateway9527Main.class,args);
    }
}
  1. 将服务模块8001和网关模块9527注册进Eureka

image-20221216213716974

  1. 测试:通过服务模块访问和网关模块访问

image-20221216214450807

12.7Gateway网关路由配置

  1. 在配置文件中配置(即上一节的配置)
server:
  port: 9527

spring:
  application:
    name: cloud-gateway-9527
  cloud:
    gateway:
      routes:
        - id: 8001_payment_route1             #路由的ID,没有固定规则但要求要唯一,建议配合服务名
          uri: http://localhost:8001     #匹配后提供服务的路由地址
          predicates:
                - Path=/payment/getPaymentById/**  #断言,路径相匹配的进行路由

        - id: 8002_payment_route2
          uri: http://localhost:8001
          predicates:
            - Path=/payment/discovery/**


eureka:
  instance:
    hostname: cloud-gateway-9527
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  1. 创建配置类,代码中注入RouteLocator的Bean
@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator createRouteLocator1(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        return routes
                .route("8001_payment_routh1",
                        route -> route
                                .path("/payment/getPaymentById/**")
                                .uri("http://localhost:8001"))
                .build();

    }

    @Bean
    public RouteLocator createRouteLocator2(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        return routes
                .route("8001_payment_routh2",
                        route -> route
                                .path("/payment/discovery/**")
                                .uri("http://localhost:8001"))
                .build();

    }
}

12.8Gateway动态路由配置

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

修改配置文件:

server:
  port: 9527

spring:
  application:
    name: cloud-gateway-9527
  cloud:
    gateway:
      #      开启从注册中心动态创建路由的功能,利用微服务名进行路由
      discovery:
        locator:
          enabled: true
      routes:
        - id: payment_route1
          #          uri的协议为lb,表示启动Gateway的负载均衡功能,uri服务模块路径要为大写
          uri: lb://CLOUD-PROVIDER-SERVER
          predicates:
            - Path=/payment/getPaymentById/**

        - id: payment_route2
          uri: lb://CLOUD-PROVIDER-SERVER
          predicates:
            - Path=/payment/discovery/**


#  cloud:
#    gateway:
#      routes:
#        - id: 8001_payment_route1             #路由的ID,没有固定规则但要求要唯一,建议配合服务名
#          uri: http://localhost:8001     #匹配后提供服务的路由地址
#          predicates:
#            - Path=/payment/getPaymentById/**  #断言,路径相匹配的进行路由
#
#        - id: 8001_payment_route2
#          uri: http://localhost:8001
#          predicates:
#            - Path=/payment/discovery/**


eureka:
  instance:
    prefer-ip-address: true
    instance-id: cloud-gateway-9527
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka


通过网关访问实现为服务模块之间负载均衡,轮询调用。

GIF 2022-12-17 12-18-04

12.9Gateway常用的Predicate(断言)

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

  1. 查看网关模块控制台信息

image-20221217114906281

其中含有Path,就是之前配置文件使用的

image-20221217115130561

  1. 常见的Predicate列举

  2. After Route Predicate

After Route接受一个参数,即datetime(这里是时区时间)。匹配在当前时区日期时间之后发生的请求。

​ 得到当前的时区时间格式:

ZonedDateTime now = ZonedDateTime.now();

​ 配置形式:

image-20221217120057308

要满足断言规则,即请求路径要和uri匹配,时间要在After之后,如果时间不在After之后就会报错

  1. Before Route Predicate

​ 和After Route Predicate类似

  1. Between Route Predicate

​ 和After Route Predicate类似

  1. Cookie Route Predicate

​ Cookie Route Predicate需要两个参数, 一个是Cookie name ,一个是正则表达式。路由规则会通过获取对应 的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行

image-20221217122630072

​ 这里采用curl进行测试:

curl命令是一个利用URL规则在命令行下工作的文件传输工具。它支持文件的上传和下载,所以是综 合传输工具,但按传统,习惯称curl为下载工具。作为一款强力工具,curl支持包括HTTP、HTTPS、ftp 等众多协议,还支持POST、cookies、认证、从指定偏移处下载部分文件、用户代理字符串、限速、文 件大小、进度条等特征。

测试不携带cookie和携带cookie的情况

image-20221217124050989

  1. Header Route Predicate

    ​ 要求请求头中要有必要的属性和属性规则

    image-20221217124551710

  2. Host Route Predicate

image-20221217125036490

image-20221217125207680

  1. Method Route Predicate

​ 指定请求方式:

image-20221217125303217

  1. Path Route Predicate

​ 就是之前一直使用的指定路径

  1. Query Route Predicate

    指定要含有参数名

image-20221217125506600

12.10Gateway网关的Filter

12.10.1Filter

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

Filter生命周期:

  • pre
  • post

Filter种类:

  • GatewayFilter
  • GlobleFilter

12.10.2自定义过滤器

自定义过滤器要实现GlobalFilter, Ordered接口,下面过滤器是判断是否携带参数名为username的参数。

@Component
@Slf4j
public class GlobalGatewayLogFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("*******come in GlobalGatewayLogFilter:" + new Date());
        String username = exchange.getRequest().getQueryParams().getFirst("username");
        if (!StringUtils.hasText(username)){
            log.info("用户名为空。");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

不携带参数时:

image-20221217134207466

携带参数时:

image-20221217134236738

13.服务配置(SpringCloud Config)

13.1服务配置出现的原因

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。==由于每个服务都需要必要的配置信息才能运行,但是每个服务都有一个不同的配置文件,如果微服务数量太多的话,配置文件管理起来就会很麻烦。==所以一套集中式的、动态的配置管理设施是必不可少的。

13.2SpringCloud Config概述

image-20221217193957889

SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

SpringCloud Config分为服务端和客户端两部分。

服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口

客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式

13.3SpringCloud Config的作用

  1. 集中管理配置文件
  2. 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
  3. 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
  4. 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
  5. 将配置信息以REST接口的形式暴露

13.4Springcloud Config配置中心搭建(服务端)

由于SpringCloud Config默认使用Git来存储配置文件,使用的是http/https访问的形式

  1. 创建SpringCloud Config配置中心远程仓库,并创建yaml文件

其中配置文件的命名方式应该按照官网指定的形式命名:

image-20221217215910488

image-20221217211121745

文件内容:

image-20221217211502022

  1. 新建服务配置模块
  2. pom文件
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</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.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>
  1. yaml文件
server:
  port: 3344
spring:
  application:
    name: cloud-config-centre
  cloud:
    config:
      server:
        git:
          #          gitee仓库地址
          uri: https://gitee.com/xu-huaiang/springcloud-config-center.git
          #          用户名
          username: 13783239983
          #          密码
          password: xuhuaiang123
          #          指定分支
        default-label: master

eureka:
  instance:
    prefer-ip-address: true
    instance-id: cloud-config-centre-3344
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  1. 主启动类
@SpringBootApplication
@EnableConfigServer
public class ConfigCenter3344Main {
    public static void main(String[] args) {
        SpringApplication.run(ConfigCenter3344Main.class,args);
    }
}
  1. 测试

方式1:直接读取文件内容

ip+端口+分支+文件名(或ip+端口+文件名),查看是否能够读取到文件内容

/{label}/{application}-{profile}.yml

/{application}-{profile}.yml

image-20221217213510525

image-20221217214314941

image-20221217214328243

方式2:以JSON数据格式读取文件内容

/{application}/{profile}[/{label}]

image-20221217215223060

{
    "name":"config",
    "profiles":[
        "dev"
    ],
    "label":"master",
    "version":"2dd0e089b05987b31a6061e7a2922177c4583f6e",
    "state":null,
    "propertySources":[
        {
            "name":"https://gitee.com/xu-huaiang/springcloud-config-center.git/file:C:\\Users\\TONY贾~1\\AppData\\Local\\Temp\\config-repo-735025406705343047\\config-dev.yaml",
            "source":{
                "config.info":"master branch, springcloud-config-center/config-dev.yml version=1"
            }
        }
    ]
}

13.5SpringCloud Config客户端配置与测试

  1. 新建模块

  2. pom文件

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</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.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>
  1. 配置文件

​ 这里要使用的配置文件为bootstrap.yaml文件,什么时bootstrap.yaml文件?

applicaiton.yml是用户级的资源配置项

bootstrap.yml是系统级的,优先级更加高

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

Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap contextApplication Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap ContextApplication Context配置的分离。

要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的

因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml

其中高版本的SpringCloud禁用了bootstrap,如果需要使用bootstrap需要自己手动引入对应的依赖

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
server:
  port: 3355
spring:
  application:
    name: cloud-config-client
  cloud:
    config:
      label: master #    分支名
      name: config  #    配置文件名称
      profile: dev  #    后缀名称
      uri: http://localhost:3344 #    config配置中心地址
  #   上述3个综合:master分支上config-dev.yml的配置文件被读取http://localhost:3344/master/config-dev.yml

eureka:
  instance:
    #    是否显示ip
    prefer-ip-address: true
    #    实例id
    instance-id: cloud-config-client-3355
  client:
    #    是否将自己注册进EurekaServer
    register-with-eureka: true
    #    是否从Eureka中抓取已有的注册信息
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

  1. 主启动类
@SpringBootApplication
@EnableEurekaClient
public class ConfigClient3355Main {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClient3355Main.class,args);
    }
}
  1. 业务类

从config配置中心读取配置文件内容:config.info

@RequestMapping("/client")
@RestController
public class ConfigClientController {

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/configInfo")
    public String getConfigInfo() {
        return configInfo;
    }
}
  1. 测试

​ 注册中心:

image-20221218110644148

​ Config配置中心自测通过:

image-20221218110816261

​ Config客户端访问Config配置中心获取到配置文件内容:

image-20221218110938360

在bootstrap配置文件中修改配置可以读取到不同分支下的不同的文件内容

13.7分布式的配置动态刷新问题

对于上一节的配置出现的问题:

当修改Git上的配置文件内容后,对于Config配置中心,发现ConfigServer配置中心立刻响应改变。而Config客户端却没有任何响应,除非自己重启或者重新加载。

避免每次更新配置都要重启客户端配置

  1. Config客户端添加actutor依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 修改yaml文件,暴露监控的端点
# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
  1. 业务类上添加@RefreshScope注解
image-20221218112944945
  1. 测试

    修改读取的配置文件内容,查看Config配置中心和Config客户端的响应情况

Config配置中心显示得配置文件内容改变

image-20221218115114698

Config客户端读取得配置文件内容并未改变

image-20221218115058776

对于这种情况需要对Config客户端发送Post请求

http://config客户端ip:端口/actuator/refresh

image-20221218115545545

再次访问即可:

image-20221218115606899

然而,这样得方式还是很麻烦,因为每次修改后都要发送post请求,如果config客户端过多,修改起来还是很麻烦,所以就引出了下一节的内容:消息总线

14.消息总线(SpringCloud Bus)

14.1总线Bus概述

  1. 什么是总线

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

  1. 基本原理

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

14.1Spring Cloud Bus的概念

Spring Cloud Bus配合Spring Cloud Config,使用可以实现配置的动态刷新。

Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。

Bus支持两种消息代理:RabbitMQ和Kafka

image-20221218123314910

14.2Spring Cloud Bus的特点

Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。

image-20221218123730342

14.3RabbitMQ环境搭建

  1. 拉取RabbitMQ镜像

​ 使用这种镜像rabbitmq中无需安装管理插件就能实现Channels节点的UI统计信息功能。

docker pull rabbitmq:management
  1. 开发15672端口

​ 15672端口是rabbitmq管理界面ui端口

firewall-cmd --zone=public --add-port=15672/tcp --permanent
  1. 在命令行交互模式下,根据镜像创建容器实例
docker run -d -p 15672:15672 -p 5672:5672 --name rabbitmq1.0 rabbitmq:latest
  1. 访问ip + 15672端口

​ 默认Username和Password都是guest

image-20221014205942964

image-20221218125456186

14.4SpringCloud Bus动态刷新全局广播

14.4.1新建Config客户端配置模块3366

与之前的Config客户端配置模块3355相同,目的是为了让SpringCloud Bus能够对多个客户端进行广播通知。

14.4.2Bus全局广播思想

  1. 方式1

​ 利用消息总线触发一个客户端/bus/refresh,而刷新所有户端的配置

image-20221218142319823
  1. 方式2

    利用消息总线触发一个服务端ConfigServer的/bus/refresh端点, 而刷新所有客户端的配置

image-20221218142517762

这种方式实现一次修改,广播通知,处处生效

  1. 最佳方式

而Bus全局广播思想最好的是方式2,方式1不适合的原因是:

  1. 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责。
  2. 破坏了微服务各节点的对等性。
  3. 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改

14.4.3Bus全局广播实现步骤

14.4.3.1config配置中心添加消息总线支持
  1. 添加消息总线RabbitMQ相关依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    
  2. 添加RabbitMQ和暴露bus刷新配置端点的配置

server:
  port: 3344
spring:
  application:
    name: cloud-config-centre
  cloud:
    config:
      server:
        git:
          #          gitee仓库地址
          uri: https://gitee.com/xu-huaiang/springcloud-config-center.git
          #          用户名
          username: 13783239983
          #          密码
          password: xuhuaiang123
          #          指定分支
        default-label: master
#        rabbitmq的相关配置
  rabbitmq:
    host: 192.168.26.142
    port: 5672
    username: guest
    password: guest
#    暴露bus刷新配置的端点
management:
  endpoint:
    web:
      exposure:
        include: 'busrefresh'

eureka:
  instance:
    prefer-ip-address: true
    instance-id: cloud-config-centre-3344
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
14.4.3.2config客户端添加消息总线支持
  1. 添加消息总线RabbitMQ相关依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    
  2. 添加RabbitMQ的配置

server:
  port: 3355
spring:
  application:
    name: cloud-config-client
  cloud:
    config:
      label: master #    分支名
      name: config  #    配置文件名称
      profile: dev  #    后缀名称
      uri: http://localhost:3344 #    config配置中心地址
  #   上述3个综合:master分支上config-dev.yml的配置文件被读取http://localhost:3344/master/config-dev.yml
  rabbitmq:
    host: 192.168.26.142
    port: 5672
    username: guest
    password: guest

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

eureka:
  instance:
    #    是否显示ip
    prefer-ip-address: true
    #    实例id
    instance-id: cloud-config-client-3355
  client:
    #    是否将自己注册进EurekaServer
    register-with-eureka: true
    #    是否从Eureka中抓取已有的注册信息
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
14.4.3.3测试
  1. 启动消息总线配置中心和客户端

image-20221218152636159

  1. 未修改前测试

image-20221218153216912

  1. 修改gitee上的配置文件内容

image-20221218150747692

  1. 向Bus配置中心发送post请求

ip:端口/actuator/busrefresh

查看Bus配置总线和客户端响应结果

image-20221218160054342

image-20221218160032468

  1. 查看RabbitMQ

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

image-20221218160405806

14.5SpringCloud Bus动态刷新定点通知

不想全部通知,只想定点通知如:只通知3355,不通知3366。

指定具体某一个实例生效而不是全部。

公式: http://localhost:配置中心的端口号/actuator/busrefresh/{destination}

/bus/refresh请求不再发送到具体的服务实例上,而是发给configserver,并通过destination参数类指定需要更新配置的服务或实例。其中destination就是**Config客户端服务名+端口号**

image-20221218161428284

测试:

  1. 修改git当中的配置文件

  2. 通知Config客户端3355

image-20221218163958477

  1. 查看Config配置中心3344、config客户端3355和config客户端3366

可以发现定点通知成功

image-20221218164136307

image-20221218164218547

15.消息驱动(SpringCloud Stream)

15.1问题引入

目前主流的消息中间件有:

  • RabbitMQ
  • ActiveMQ
  • RocketMQ
  • Kafka

如果一个系统的不同平台使用了不同的消息中间件,如一个购物商城平台(使用的消息中间件为RabbitMQ)将数据交由大数据处理平台(使用的消息中间件为Kafka)。这就会产生很多问题如:

  • 切换
  • 维护
  • 开发

有没有一种技术,让我们不再关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动在各种MQ内切换。

15.2SpringCloud Stream概述

SpringCloud Stream是一个构建消息驱动的微服务框架。其就是为了屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。

​ 应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream中binder对象交互。**通过我们配置来binding(绑定) ,而 Spring Cloud Stream 的 binder对象负责与消息中间件交互。**所以,我们只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。

通过使用Spring Integration来连接消息代理中间件以实现消息事件驱动。

SpringCloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。

Stream中的消息通信方式为发布订阅模式

目前仅支持RabbitMQ、Kafka。

15.3为什么使用SpringCloud Stream

比方说我们用到了RabbitMQ和Kafka,由于这两个消息中间件的架构上的不同,

像RabbitMQ有exchange,kafka有Topic和Partitions分区

image-20221221123827360

这些中间件的差异性导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我想往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的,因为它跟我们的系统耦合了,这时候springcloud Stream给我们提供了一种解耦合的方式。

15.4Stream自定义绑定器Binder

在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程

  • 通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。
  • 通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件实现。
  • 通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。

Binder:

  • INPUT对于消费者
  • OUPUT对于生产者

image-20221221131548671

通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。

Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。

15.5SpringCloud Stream工作流程

  • Binder:很方便的连接中间件,屏蔽差异
  • Channel:信道在消息队列当中就是实现存储和转发的媒介
  • Source和Sink:简单的可以理解为参照对象是SpringCloud Stream自身,从Stream发布消息就是输出,接收消息就是输入。

15.7编码API和常用注解(Stream3.1版本之前)

image-20221221134128330

组成说明
Middleware中间件,目前只支持RabbitMQ和Kafaka
BinderBinder是应用与消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过
Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,
RabbitMQ的exchange),这些都可以通过配置文件来实现
@Input注解标识输入通道,通过该输入通道接收到的消息进入应用程序
@Output注解标识输出通道,发布的消息将通过该通道离开应用程序
@StreamListener监听队列,用于消费者的队列的消息接收
@EnableBinding指通道channel和exchange绑定在一起

15.8Stream版本说明

自Spring Cloud 2020版本开始,Spring Cloud Stream的版本升级至3.1.0以上版本,目前最新版本为3.1.5。
自此版本开始@StreamListener@EnableBinding上面就增加@Deprecated注解,不赞成使用,有可能接下来的版本会删除掉。==官网推荐以函数式编程的方式代替StreamListener的方法。==所以下面的案例就采用函数式编程来实现。

15.8案例说明

15.8.1模块说明

  • 生产者进行发消息模块:cloud-stream-rabbitmq-provider8801
  • 消息接收模块1:cloud-stream-rabbitmq-consumer8802
  • 消息接收模块2:cloud-stream-rabbitmq-consumer8803

15.8.2生产者模块

  1. 新建模块
  2. pom文件
<dependencies>
    <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.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </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>
  1. yaml文件
#子模块端口号
server:
  port: 8801

spring:
  application:
    name: cloud-stream-provider
  rabbitmq:
    host: 192.168.26.151
    port: 5672  #mq调用的端口为5672
    username: guest
    password: guest
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息;
        defaultRabbit: # 表示定义的名称,用于于binding整合
          type: rabbit # 消息组件类型

eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
    #主机名称
    instance-id: cloud-stream-rabbitmq-provider-8801
    #显示ip地址
    prefer-ip-address: true

  1. 主启动类
@SpringBootApplication
public class StreamRabbitMQ8801Main {
    public static void main(String[] args) {
        SpringApplication.run(StreamRabbitMQ8801Main.class,args);
    }
}
  1. Service层

​ 发送消息的接口:

public interface IMessageProvider {
    public String send();
}

​ 接口实现类:

@Component
public class IMessageProviderImpl implements IMessageProvider {

    @Resource
    private StreamBridge streamBridge;

    @Override
    public String send() {
        String uuid = IdUtil.simpleUUID();
        //这里说明一下这个 streamBridge.send 方法的参数 第一个参数是exchange或者topic 就是主题名称(bindingName)
        //默认的主题名称是通过
        //输入:    <方法名> + -in- + <index>
        //输出:    <方法名> + -out- + <index>
        //这里我们接收的时候就要用send方法 参数是consumer<String>接收  详情看8802的controller
        //consumer的参数类型是这里message的类型
        streamBridge.send("send-in-0",uuid);
        System.out.println("******uuid:" + uuid);
        return null;
    }
}
  1. 控制层
@RestController
@RequestMapping("/provider")
public class MessageProviderController {

    @Resource
    private IMessageProvider messageProvider;

    @GetMapping("/sendMessage")
    public String sendMessage(){
        return messageProvider.send();
    }
}
  1. 测试

​ 启动模块,查看注册到RabbitMQ当中的交换机send-in-0

image-20221221200656082

​ 向消息队列中添加消息:

image-20221221170619428

image-20221221170555786

15.8.3消费者模块

  1. 新建模块
  2. pom文件
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </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>
  1. yaml文件
#子模块端口号
server:
  port: 8802

spring:
  application:
    name: cloud-stream-consumer
  rabbitmq:
    host: 192.168.26.151
    port: 5672  #mq调用的端口为5672
    username: guest
    password: guest
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息;
        defaultRabbit: # 表示定义的名称,用于于binding整合
          type: rabbit # 消息组件类型


eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
    #主机名称
    instance-id: cloud-stream-rabbitmq-consumer-8802
    #显示ip地址
    prefer-ip-address: true

  1. 主启动类
@SpringBootApplication
public class StreamRabbitMQ8802Main {
    public static void main(String[] args) {
        SpringApplication.run(StreamRabbitMQ8802Main.class,args);
    }
}
  1. 控制层
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;

import java.util.function.Consumer;
@Service
public class ReceivedMessageController {

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

    @Bean
        //这里接收rabbitmq的条件是参数为Consumer 并且 方法名和supplier方法名相同
        //这里的返回值是一个匿名函数 返回类型是consumer 类型和提供者的类型一致
        //supplier发送的exchange是 send-in-0 这里只需要用send方法名即可
    public Consumer<String> send() {
        return str -> {
            System.out.println("我是消费者" + serverPort + ",我收到了消息:" + str);
        };
    }
}
  1. 测试生产者模块发布消息,消费者模块处理消息

image-20221221202955845

image-20221221203006372

15.9分组消费与持久化

15.9.1分组消费

15.9.1.1问题引出

按照8802再创建一个相同的模块8803也作为消费者模块,

image-20221221203844318

此时有一个生产者两个消费者。

测试:生产者发布消息,查看消息的消费情况

可以发现存在的问题:消息被重复消费

导致原因:默认分组group是不同的,组流水号不一样,被认为不同组,可以消费

GIF 2022-12-21 20-39-43

15.9.1.2分为相同组

在配置文件中进行添加,指定bindings的交换机名称,再分为不同的组。

image-20221221205832328

image-20221221205748318

image-20221221205708957

但是这样还是会出现重复消费问题

多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了消费组的概念。

将两个消费者模块改为一个组

image-20221221210740063

测试:生产者发送4条信息

GIF 2022-12-21 21-11-05

15.9.2Stream持久化

Stream的消息持久化依赖于group属性

处于同一组中的消费者,当其中的任何一个或者多个宕机或者分组情况改变而重启的,而此时恰好生产者发送消息,就会造成消息的丢失。

但是处于同一分组的消费者,如果有未消费的消息,就会重新创建队列被剩余的消费者消费。

16.链路跟踪(SpringCloud Sleuth)

16.1问题引出

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

16.2链路跟踪是什么

Spring Cloud Sleuth提供了一完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并且兼容支持了zipkin

image-20221221213955858

16.3链路跟踪环境搭建

  1. 启动zipkin-server

​ SpingCloud从F版起已不需要自己构建ZIpkin Sever了,只需调用jar包即可

​ 下载地址:https://repo1.maven.org/maven2/io/zipkin/zipkin-server/

image-20221221215606251

image-20221221215629668

image-20221221215919677

本地启动zipkin:

java -jar jar包名

image-20221221220758586

访问zipkin可视化界面

ip:9411/zipkin/

image-20221221220708943

16.4完整调用链路

表示一请求链路,一条链路通过**Trace Id唯一标识**,Span标识发起的请求信息,各span通过parent id 关联起来

image-20221221220949968

image-20221221221017455

image-20221221221024586

16.5链路追踪测试

说明:采用8001支付模块和其对应的80消费者模块进行链路追踪测试

16.5.1支付模块8001

  1. 添加sleuth和zipkin依赖
        <!--包含了sleuth+zipkin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
  1. yaml配置文件
image-20221221221720941
#子模块端口号
server:
  port: 8001

#子模块名称
spring:
  application:
    name: cloud-provider-server
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      #采样率值介于 0 到 1 之间,1 则表示全部采集
      probability: 1
  #    mysql配置信息
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springcloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=Asia/Shanghai
    username: root
    password: xu.123456

#    mybatis映射文件地址和pojo路径
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.xha.springcloud.pojo

eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
    #主机名称
    instance-id: cloud-provider-server-8001
    #显示ip地址
    prefer-ip-address: true
#    Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
    lease-expiration-duration-in-seconds: 2
#      Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认为30s)
    lease-renewal-interval-in-seconds: 1
  1. 控制层
@GetMapping("/zipkin")
public String paymentZipkin()
{
    return "Hi ,I'am paymentzipkin server fall back,welcome to xha,O(∩_∩)O哈哈~";
}

16.5.2消费模块80

  1. 添加sleuth和zipkin依赖
        <!--包含了sleuth+zipkin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
  1. yaml配置文件
image-20221221222308105
server:
  port: 80

#子模块名称
spring:
  application:
    name: cloud-consumer-server
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      probability: 1

eureka:
  client:
    #    是否将当前模块注册进Eureka注册中心
    register-with-eureka: true
    #    是否抓取在EurekaServer中已经存在的注册信息
    fetch-registry: true
    #    Eureka服务地址
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
    instance-id: cloud-consumer-server-80
    prefer-ip-address: true
  1. 控制层
@GetMapping("/zipkin")
public String paymentZipkin()
{
    String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
    return result;
}

16.5.3测试

启动各模块,通过80访问8001,点击多次

image-20221221223059558

查看zipkin服务的UI界面,查找链路:

image-20221221223251806

image-20221221223433157

可以看出是consumer服务调用payment服务

image-20221221223628159

17.SpringCloud Alibaba

17.1SpringCloud Alibaba简介

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

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里分布式应用解决方案,通过阿里中间件来迅速搭建分布式应用系统。

项目github地址:https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

17.2SpringCloud Alibaba的功能

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

18.Alibaba Nacos服务注册和配置中心

18.1Nacos简介

Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

Nacos就是注册中心 + 配置中心的组合。即Nacos = Eureka + Config + Bus

image-20221222105039495

Nacos官网:https://nacos.io/zh-cn/

image-20221222105657581

点击版本说明,到github中查看稳定版

image-20221222110645220

image-20221222110716822

18.2Nacos安装

18.2.1windows系统下安装包安装

  1. 下载安装包,并将安装包解压到指定位置
  2. 进入nacos的bin目录下,输入命令启动nacos服务

startup.cmd -m standalone

image-20221222111532363
  1. 访问nacos服务UI界面

ip:8848/nacos

image-20221222111802144

18.2.2Linux操作系统安装

  1. 将安装包放到指定目录下并解压

image-20230302122202859

  1. 进入到nacos的bin目录下启动nacos
cd nacos/bin
./startup.sh -m standalone 

image-20230302122107738

  1. 开发端口(和服务器安全组规则)
firewall-cmd --zone=public --add-port=8848/tcp --permanent
systemctl restart firewalld.service

18.2.3docker安装nacos

  1. 拉取nacos镜像和MySQL镜像
docker pull nacos/nacos-server:v2.1.2
docker pull mysql:8.0.19
  1. 在官网找到对应版本的sql文件

image-20221223124141364

  1. 新建数据库nacos_config

image-20221223124606765

  1. 启动nacos容器实例
docker run -d -p 8848:8848 \
--restart=always \
-e MODE=standalone \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=ip \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_DB_NAME=nacos_config \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=xu.123456 \
--name nacos nacos/nacos-server:v2.1.2

​ 查看容器的日志信息:

image-20221223135251792
  1. 开放8848端口,重启防火墙
firewall-cmd --zone=public --add-port=8848/tcp --permanent
systemctl restart firewalld.service

如果是云服务器记得开放对应的安全组规则

  1. 访问Nacos的UI界面

ip:8848/nacos

  1. 添加配置信息,查看数据库中是否有对应的数据

image-20221223135044148

image-20221223135053894

18.3Nacos服务注册中心对比

服务注册与发现框架CAP控制台管理社区活跃度
EurekaAP支持
ZookeeperCP不支持
ConsulCP支持
NacosAP/CP支持

18.4Nacos之服务注册中心

18.4.1基于Nacos的服务提供者

官网上有详细的配置步骤:

image-20221222123423535
  1. 父pom添加依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>${alibaba-version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
  1. 新建模块

  2. pom文件

<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <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.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>
  1. yaml文件
server:
  port: 9001

spring:
  application:
    name: cloud-provider-nacos
#    注册到nacos当中
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
#     暴露监控的内容
management:
  endpoints:
    web:
      exposure:
        include: '*'
  1. 主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderNacos9001Main {
    public static void main(String[] args) {
        SpringApplication.run(ProviderNacos9001Main.class,args);
    }
}
  1. 控制层
@RestController
@RequestMapping("/payment")
public class NacosController {

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

    @GetMapping("/nacos/{id}")
    public String getMessage(@PathVariable("id") Integer id){
        return "nacos register:" + id + ",serverPort:" + serverPort;
    }
}
  1. 启动消费模块,查看Nacos服务UI界面的服务列表

image-20221222124301031

  1. 再创建一个新的模块为9002,启动模块

image-20221222125054171

  1. 查看Nacos,发现服务提供者实例变成两个

image-20221222125207492

18.4.2基于Nacos的服务消费者

  1. 新建模块
  2. pom文件
<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.xha.springcloud</groupId>
        <artifactId>cloud-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <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.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>
  1. yaml文件
server:
  port: 80

spring:
  application:
    name: cloud-consumer-nacos
#    注册到nacos当中
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
# 要访问的微服务名称
server-url:
  nacos-user-service: http://cloud-provider-nacos
  1. 主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerNacos80Main {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerNacos80Main.class,args);
    }
}
  1. RestTemplate配置类
@Configuration
public class ApplicationConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
  1. 控制层
@RestController
@RequestMapping("/consumer")
public class NacosController {
    @Resource
    private RestTemplate restTemplate;

    @Value("${server-url.nacos-user-service}")
    private String serverURL;

    @GetMapping("/nacos/{id}")
    public String paymentInfo(@PathVariable("id") Long id)
    {
        return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
    }
}
  1. 将消费者模块注册进nacos

image-20221222131844075

  1. 测试

    ​ 因为spring-cloud-starter-alibaba-nacos-discovery依赖中还有ribbon,所以能够实现负载均衡

    image-20221222132426124

GIF 2022-12-22 13-21-43

18.5Nacos之服务配置中心

18.5.1基础配置

  1. 新建模块

  2. pom文件

	<dependencies>        
		<!--nacos-config-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--nacos-discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--web + actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
     		<groupId>org.springframework.cloud</groupId>
     		<artifactId>spring-cloud-starter-bootstrap</artifactId>
		</dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </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>
  1. yaml文件

​ 需要两个配置文件,分别为application.yamlbootstrap.yaml

Nacos同springcloud-config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。

springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application

高版本的Springcloud默认禁用了bootstarp.yaml文件,需要添加依赖spring-cloud-starter-bootstrap

bootstrap.yaml文件

server:
  port: 3377
spring:
  application:
    name: cloud-config-nacos
  cloud:
    nacos:
#      nacos服务注册
      discovery:
        server-addr: localhost:8848
#        nacos服务配置
      config:
        server-addr: localhost:8848
        file-extension: yaml

application.yaml文件

spring:
  profiles:
#    表示开发环境
    active: dev
  1. 主启动类

  2. 业务类

@RequestMapping("/client")
@RestController
// 实现配置的自动更新
@RefreshScope
public class ConfigClientController {

    @Value("${config.info}")
    private String configInfo;

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

官网说明:

image-20221222172943586

  1. 在Nacos中添加配置信息

Nacos中的配置管理dataid的组成格式及与SpringBoot配置文件中的匹配规则一致

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

​ 官网的说明:

image-20221222172835895

对于配置文件,对应的dataid组成格式为:

image-20221222174122940

在nacos中添加配置,注意dataid的格式

image-20221222174337135

image-20221222174425311

  1. 启动配置中心模块,测试

​ 读取配置文件

image-20221222175949181

​ 配置文件动态刷新

修改配置文件

image-20221222180114150

image-20221222180134626

18.5.2分类配置

18.5.2.1问题引入

问题1:

实际开发中,通常一个系统会准备

dev开发环境

test测试环境

prod生产环境。

如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?

问题2:

一个大型分布式微服务系统会有很多微服务子项目,

每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…

那怎么对这些微服务配置进行管理呢?

18.5.2.2Nacos中命名空间、Group和DataId

类似Java里面的package名和类名

最外层的namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象。

image-20221222180951833

默认情况:

Namespace=public,Group=DEFAULT_GROUP, 默认Cluster是DEFAULT

  1. Namespace主要用来实现隔离

比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。

  1. Group可以把不同的微服务划分到同一个分组里面去
  2. Service就是微服务

一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。

比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,

这时就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。

  1. Instance,就是微服务的实例
18.5.2.3三种方案加载配置
  1. DataID方案

​ 通过spring.profile.active属性就能进行多环境下配置文件的读取

image-20221222185313083

​ 新建nacos配置:

image-20221222185510728

​ 重启模块,访问配置文件信息

image-20221222185656707

  1. Group方案

​ 通过Group实现环境区分

新建两个分组,但是是两个相同的文件名

image-20221222190359598

在配置文件中指定分组和当前环境:

image-20221222190635417

image-20221222190722073

  1. Namespace方案

​ 新建命名空间:

image-20221222191123262

image-20221222191219733

配置管理中显现的有命名空间:

image-20221222191415179

选中DEV_NAMESPACES命名空间,找到命名空间ID并配置到配置文件当中

image-20221222192239174

image-20221222192303802

在此命名空间下创建三个配置,文件名相同,分别位于不同的组

image-20221222192931578

此时的配置文件

即查找此命名空间下的位于DEV_GROUP组中的profiledevyaml文件

image-20221222193149482

重启模块测试:

image-20221222193619390

18.6Nacos集群和持久化

18.6.1Nacos支持的三种部署模式

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

18.6.2Nacos集群说明

默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。

为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。

image-20221222200218561

18.6.3Nacos的持久化

Nacos默认自带的是嵌入式数据库derby。

在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力。

单机模式下更换数据源详见官网:https://nacos.io/zh-cn/docs/deployment.html

image-20221222202232758

19.Alibaba Sentinel服务熔断与限流

19.1Sentinel简介

官网地址:https://sentinelguard.io/zh-cn/

Github地址:https://github.com/alibaba/Sentinel

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件,主要以流量为切入点,从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维度来帮助开发者保障微服务的稳定性。

即Sentinel就是面向云原生微服务的流量控制、熔断降级组件。

19.2Sentinel的功能

19.2.1流量控制

流量控制在网络传输中是一个常用的概念,它用于调整网络包的发送数据。然而,从系统稳定性角度考虑,在处理请求的速度上,也有非常多的讲究。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:

image-20221223142518824

流量控制有以下几个角度:

  • 资源的调用关系,例如资源的调用链路,资源和资源之间的关系;
  • 运行指标,例如 QPS、线程池、系统负载等;
  • 控制的效果,例如直接限流、冷启动、排队等。

Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

19.2.2熔断降级

Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高) , 对这个资源的调用进行限制让请求快速失败,避免影响到其它的资源而导致级联错误。

什么是熔断降级

除了流量控制以外,降低调用链路中的不稳定资源也是 Sentinel 的使命之一。由于调用关系的复杂性,如果调用链路中的某个资源出现了不稳定,最终会导致请求发生堆积。这个问题和 Hystrix 里面描述的问题是一样的。

image-20221223142635879

Sentinel 和 Hystrix 的原则是一致的: 当调用链路中某个资源出现不稳定,例如,表现为 timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。

熔断降级设计理念

在限制的手段上,Sentinel 和 Hystrix 采取了完全不一样的方法。

Hystrix 通过线程池的方式,来对依赖(在我们的概念中对应资源)进行了隔离。这样做的好处是资源和资源之间做到了最彻底的隔离。缺点是除了增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

Sentinel 对这个问题采取了两种手段:

  • 通过并发线程数进行限制

和资源池隔离的方法不同,Sentinel 通过限制资源并发线程的数量,来减少不稳定资源对其它资源的影响。这样不但没有线程切换的损耗,也不需要您预先分配线程池的大小。当某个资源出现不稳定的情况下,例如响应时间变长,对资源的直接影响就是会造成线程数的逐步堆积。当线程数在特定资源上堆积到一定的数量之后,对该资源的新请求就会被拒绝。堆积的线程完成任务后才开始继续接收请求。

  • 通过响应时间对资源进行降级

除了对并发线程数进行控制以外,Sentinel 还可以通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

熔断降级策略

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

19.3Sentinel使用说明

Sentinel 的使用可以分为两个部分:

  • 核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持(见 主流框架适配)。
  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

19.4Sentinel对Endpoint 支持

在使用 Endpoint 特性之前需要在 Maven 中添加 spring-boot-starter-actuator 依赖,并在配置中允许 Endpoints 的访问。

  • Spring Boot 1.x 中添加配置 management.security.enabled=false。暴露的 endpoint 路径为 /sentinel
  • Spring Boot 2.x 中添加配置 management.endpoints.web.exposure.include=*。暴露的 endpoint 路径为 /actuator/sentinel

Sentinel Endpoint 里暴露的信息非常有用。包括当前应用的所有规则信息、日志目录、当前实例的 IP,Sentinel Dashboard 地址,Block Page,应用与 Sentinel Dashboard 的心跳频率等等信息。

19.5安装Sentinel

19.5.1docker安装Sentinel

  1. 来到Docker Hub查找镜像源

image-20221223144156072

  1. 拉取镜像
docker pull bladex/sentinel-dashboard:1.7.2
  1. 创建容器
docker run -d -p 8858:8858 --restart=always --name sentinel bladex/sentinel-dashboard:1.7.2
  1. 开放端口,重启防火墙
firewall-cmd --zone=public --add-port=8858/tcp --permanent
  1. 访问8858端口登录sentinel

image-20221223145325378

19.5.2Linux安装Sentinel

  1. sentinel安装地址

Releases · alibaba/Sentinel (github.com)

  1. 将对应的jar包发送到指定目录下并在指定的端口8858运行
nohup java -jar sentinel-dashboard-1.8.3.jar --server.port=8858 &
  1. 开发对应8080端口(服务器同时开放安全组规则)
firewall-cmd --zone=public --add-port=8858/tcp --permanent
systemctl restart firewalld

19.6案例演示

  1. 新建模块
  2. pom文件
<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
    <dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件+actuator -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>4.6.3</version>
    </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>
  1. yaml文件

将模块注册进nacos,使用sentinel做服务降级、服务熔断和服务限流

server:
  port: 8401

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

management:
  endpoints:
    web:
      exposure:
        include: '*'
  1. 启动类
@SpringBootApplication
@EnableDiscoveryClient
public class AlibabaSentinel8401Main {
    public static void main(String[] args) {
        SpringApplication.run(AlibabaSentinel8401Main.class,args);
    }
}
  1. 业务类
@RestController
@RequestMapping("/sentinel")
public class SentinelController {

    @GetMapping("/testA")
    public String testA(){
        return "--------testA";
    }

    @GetMapping("/testB")
    public String testB(){
        return "--------testB";
    }
    
}
  1. 启动测试

​ 一:访问接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WYGgzzAH-1676542416305)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223154228321.png)]

​ 二:查看服务是否注册到nacos

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CPO7FsUV-1676542416305)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223153744955.png)]

​ 三:查看sentinel

​ 查看实时监控:

image-20221223154501042

19.7sentinel流控规则

19.7.1sentinel流控规则说明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7sFRFXV-1676542416306)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223170748287.png)]

  • 资源名:唯一名称,默认请求路径

  • 针对来源: Sentinel可以针对调用者进行限流, 填写微服务名,默认default (不区分来源)

  • 阈值类型/单机阈值:

    • QPS (每秒钟的请求数量) :当调用该api的QPS达到阈值的时候,进行限流
    • 线程数:当调用该api的线程数达到阈值的时候,进行限流
  • 是否集群:不需要集群

  • 流控模式:

    • 直接: api达到限流条件时,直接限流
    • 关联:当关联的资源达到阈值时,就限流自己
    • 链路:只记录指定链路.上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流) [api级别的针对来源
  • 流控效果:

    • 快速失败:直接失败,抛异常
    • Warm Up:根据codeFactor (冷加载因子,默认3)的值,从阈值codeFactor, 经过预热时长,才达到设
      置的QPS阈值
    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效

对/sentinel/testA进行流控操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YgsAZJoB-1676542416306)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223171936156.png)]

19.7.2流控阀值类型(QPS)

QPS就是每秒钟的请求数量。当调用该api的QPS达到阈值的时候,进行限流

测试:对于请求,要求每秒请求的次数为1次,如果大于1次,就快速失败。

​ 流控规则如下:

image-20221223172011936

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GKK0R6pG-1676542416306)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/GIF%202022-12-23%2017-21-48.gif)]

19.7.3流控阀值类型(线程数)

设置线程数,当调用该api的线程数达到阈值的时候,进行限流。

19.7.4流控模式(关联)

当关联的资源达到阈值时,就限流自己,如当支付接口到达阈值后就限流下单接口

下面就是当/sentinel/testB到达阈值后就限流/sentinel/testA

image-20221223174216282

19.7.5流控效果(warm up)

Warm Up ( RuleConstant. CONTROL BEHAVIOR _WARM_UP )方式,即预热/冷启动方式。==当系统长期处于
低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值 上限,给冷系统一个预热的时间, 避免冷系统被压垮。==详细文档可以参考流量控制- Warm Up文档,具体的例子可以参见WarmUpFlowDemo。

image-20221223180048315

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

下面的意思就是:设置初始的QPS数为3(因为coldFactor为3,10/3=3),在5秒的时间后QPS数升到10

image-20221223184120075

19.7.6流控效果(排队等待)

下面的效果就是如果超过设置的阈值,就排队等待(等待时间为设置的超时时间)

image-20221223190639088

19.8sentinel熔断降级规则

19.8.1熔断降级说明

熔断降级策略

  • 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),==请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。==经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
  • 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  • 异常数 (ERROR_COUNT):==当单位统计时长内的异常数目超过阈值之后会自动进行熔断。==经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o0tuzQx6-1676542416307)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223195946975.png)]

Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高) , 对这个资源的调用进行限制让请求快速失败,避免影响到其它的资源而导致级联错误。

19.8.2降级策略(RT)

  1. 新增业务类
@GetMapping("/testC")
public String testC(){
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("测试RT");
    return "--------testC";
}
  1. 实现服务限流,设置最大访问数
image-20221223204428999
  1. 设置RT为0.8秒,即访问为慢调用
image-20221223204223934
  1. 使用Jmeter进行测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7cKorpLg-1676542416307)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223202801938.png)]

19.8.2降级策略(异常比例)

  1. 设置降级规则
image-20221223204528782
  1. 使用Jmeter进行测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CRGPPp7U-1676542416307)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223204639664.png)]

19.8.3降级策略(异常数)

超过规定的异常数就会发生降级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dkc2kVvK-1676542416307)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221223211049398.png)]

19.9sentinel热点限流规则

19.9.1热点简介

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

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

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

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

image-20221224083838741

19.9.2热点限流与服务降级

  1. 新增控制层方法
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
                         @RequestParam(value = "p2",required = false) String p2){
    return "testHotKey";
}

public String deal_testHotKey(String p1, String p2, BlockException blockException){
    return "deal_testHotKey";
}

/testHotKey接受两个参数,参数非必须。

@SentinelResource指定资源名,指定的降级方法为deal_testHotKey

  1. 新增热点限流规则

    即对于testHotKey指定的资源,第一个参数的访问限制为1s访问1次,超过就会进行降级。

image-20221224085936929

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xnCADUiD-1676542416308)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/GIF%202022-12-24%209-01-51.gif)]

而p2不会出现限流,因为在设置热点规则的时候只是对参数位为0的进行限流。

19.9.3参数例外项

参数例外项就是设置热点参数的例外值,当处于例外值得时候可以重新设置其限流阈值。

image-20221224091053987

测试:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qXw1Etrr-1676542416308)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/GIF%202022-12-24%209-14-08.gif)]

19.10sentinel系统规则

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 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 达到阈值即触发系统保护。
image-20221224092553184

19.11@SentinelResource注解服务限流降级说明

19.11.1按资源名称限流+服务降级

  1. 新增控制类
@RestController
@RequestMapping("/sentinel")
public class SentinelResourceController {

    @GetMapping("/resource")
    @SentinelResource(value = "resource",blockHandler = "handlerDemote")
    public CommonResult resource(){
        return new CommonResult(200,"按资源名称限流测试OK");
    }

    public CommonResult handlerDemote(BlockException blockException){
        return new CommonResult(200,blockException.getClass().getCanonicalName() + "服务不可用!");
    }
}
  1. 增加限流控制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0GZoE5Qf-1676542416308)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221224101424123.png)]

image-20221224101443279
  1. 测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3B9lipBW-1676542416309)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/GIF%202022-12-24%2010-15-24.gif)]

19.11.2按url限流+服务降级

  1. 新增方法
@GetMapping("/url")
@SentinelResource(value = "url4")
public CommonResult url(){
    return new CommonResult(200,"按url限流测试OK");
}
  1. 新增限流规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8eJUpOAP-1676542416309)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221224101939446.png)]

image-20221224102051017

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0U6Nj9Cv-1676542416309)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/GIF%202022-12-24%2010-21-41.gif)]

19.11.3自定义全局限流降级处理类

对于之前的案例,存在和Hystrix中相同的问题,即每个方法都要有一个处理服务降级的方法,代码耦合度太高。所以可以采用自定义限流降级处理类。

  1. 自定义全局限流降级
public class DemoteHandler {
    public static CommonResult handlerDemote(BlockException blockException){
        return new CommonResult(200,"全局自定义限流降级处理");
    }
}
  1. 在方法上的@SentinelResource中使用blockHandlerClass属性指定自定义全局限流降级规则类,使用blockHandler属性指定服务降级方法
@GetMapping("/personal")
@SentinelResource(value = "personal",
        blockHandlerClass = DemoteHandler.class,
        blockHandler = "handlerDemote")
public CommonResult url(){
    return new CommonResult(200,"按全局自定义限流降级测试OK");
}
  1. 测试

    使用资源名进行限流

image-20221224174504402

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6bF9Foch-1676542416309)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/GIF%202022-12-24%2017-46-34.gif)]

19.12sentinel服务熔断

19.12.1sentinel服务熔断说明

sentinel整合ribbon+ openfeign + fallback

19.12.2Ribbon系列

19.12.2.1提供者
  1. 新建模块9003/9004

  2. pom文件

<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.xha.springcloud</groupId>
        <artifactId>cloud-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <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.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>
  1. yaml文件
server:
  port: 9003

spring:
  application:
    name: cloud-provider-alibaba-sentinel-ribbon
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.26.154:8848 #配置Nacos地址

management:
  endpoints:
    web:
      exposure:
        include: '*'
  1. 主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class AlibabaSentinelRibbon {
    public static void main(String[] args) {
        SpringApplication.run(AlibabaSentinelRibbon.class,args);
    }
}
  1. 业务类
import com.xha.springcloud.entities.CommonResult;
import com.xha.springcloud.entities.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;

@RestController
public class SentinelRibbonController {

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

    public static HashMap<Long,Payment> hashMap = new HashMap<>();
    static
    {
        hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181"));
        hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));
        hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));
    }

    @GetMapping("/paymenySQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable Long id){
        Payment payment = hashMap.get(id);
        CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort:  "+serverPort,payment);
        return result;
    }

}
19.12.2.2消费者
  1. 新建模块80

  2. pom文件

<dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--SpringCloud ailibaba sentinel -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.xha.springcloud</groupId>
        <artifactId>cloud-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <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.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>
  1. yaml文件
server:
  port: 80


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


#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://cloud-provider-alibaba-sentinel-ribbon
  1. 主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class AlibabaSentinelRibbon80Main {
    public static void main(String[] args) {
        SpringApplication.run(AlibabaSentinelRibbon80Main.class,args);
    }
}
  1. 配置类
@Configuration
public class RestTemplateConfig {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}
  1. 业务类

​ 通过服务名调用服务提供者模块

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.xha.springcloud.entities.CommonResult;
import com.xha.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class CircleBreakerController {

    public static final String SERVER_URL = "http://cloud-provider-alibaba-sentinel-ribbon";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVER_URL + "/paymenySQL/" + id, CommonResult.class, id);
        if (id == 4){
            throw new IllegalArgumentException("非法参数异常!");
        }else if(result.getData() == null){
            throw new IllegalArgumentException("该ID没有对应的数据!");
        }
        return result;
    }
}
19.12.2.3测试
  1. 启动服务

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mSjQPhgy-1676542416310)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221224211926463.png)]

  1. 服务提供者注册进nacos

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OjDSbtyF-1676542416310)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221224212302278.png)]

  1. 消费者注册进sentinel
image-20221224213549981
  1. 通过80访问9003和9004

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rMqsaCJw-1676542416310)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221224213918797.png)]

但是如果访问id不存在得就会直接返回500错误,这样就不太好

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ivimohLt-1676542416311)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221224214014623.png)]

19.12.2.4服务熔断,服务降级(配置fallback)
  1. 业务类

​ 当出现异常得时候服务降级

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.xha.springcloud.entities.CommonResult;
import com.xha.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class CircleBreakerController {

    public static final String SERVER_URL = "http://cloud-provider-alibaba-sentinel-ribbon";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback",fallback = "handlerFallback")
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVER_URL + "/paymenySQL/" + id, CommonResult.class, id);
        if (id == 4){
            throw new IllegalArgumentException("非法参数异常!");
        }else if(result.getData() == null){
            throw new IllegalArgumentException("该ID没有对应的数据!");
        }
        return result;
    }

    public CommonResult handlerFallback(@PathVariable Long id){
        Payment payment = new Payment(id, null);
        return new CommonResult(444,"服务熔断,服务降级。",payment);
    }
}

测试没有得数据,查看是否会进行服务熔断,服务降级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BejKS8hy-1676542416311)(https://imagebed-xuhuaiang.oss-cn-shanghai.aliyuncs.com/typora/image-20221224215804563.png)]

19.12.2.5服务熔断,服务降级(配置blockHandler)

blockHandler只负责sentinel的配置规则的违规情况

根据资源名/fallback进行服务降级

image-20221224220248369

注意:sentinel的blockHandler服务降级方法需要添加BlockException参数

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.xha.springcloud.entities.CommonResult;
import com.xha.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class CircleBreakerController {

    public static final String SERVER_URL = "http://cloud-provider-alibaba-sentinel-ribbon";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
//    @SentinelResource(value = "fallback",fallback = "handlerFallback")
    @SentinelResource(value = "fallback",blockHandler = "toBlockHandler")
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVER_URL + "/paymenySQL/" + id, CommonResult.class, id);
        if (id == 4){
            throw new IllegalArgumentException("非法参数异常!");
        }else if(result.getData() == null){
            throw new IllegalArgumentException("该ID没有对应的数据!");
        }
        return result;
    }

//    public CommonResult handlerFallback(@PathVariable Long id){
//        Payment payment = new Payment(id, null);
//        return new CommonResult(444,"服务熔断,服务降级。",payment);
//    }
    public CommonResult toBlockHandler(@PathVariable Long id, BlockException blockException){
        Payment payment = new Payment(id, null);
        return new CommonResult(444,"sentinel限流,服务降级。异常信息为:" + blockException.getMessage(),payment);
    }

}
19.12.2.6服务熔断,服务降级(配置fallback和blockHandler)

若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.xha.springcloud.entities.CommonResult;
import com.xha.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;

@RestController
@Slf4j
public class CircleBreakerController {

    public static final String SERVER_URL = "http://cloud-provider-alibaba-sentinel-ribbon";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
//    @SentinelResource(value = "fallback",fallback = "handlerFallback")
    @SentinelResource(value = "fallback",blockHandler = "toBlockHandler",fallback = "handlerFallback")
    public CommonResult<Payment> fallback(@PathVariable Long id){
        CommonResult result = restTemplate.getForObject(SERVER_URL + "/paymenySQL/" + id, CommonResult.class, id);
        if (id == 4){ 
            throw new IllegalArgumentException("非法参数异常!");
        }else if(result.getData() == null){
            throw new IllegalArgumentException("该ID没有对应的数据!");
        }
        return result;
    }

    public CommonResult handlerFallback(@PathVariable Long id){
        Payment payment = new Payment(id, null);
        return new CommonResult(444,"服务熔断,服务降级。",payment);
    }
    public CommonResult toBlockHandler(@PathVariable Long id, BlockException blockException){
        Payment payment = new Payment(id, null);
        return new CommonResult(444,"sentinel限流,服务降级。异常信息为:" + blockException.getMessage(),payment);
    }

}
19.12.2.7Sentinel—Expection Ignore

sentinel能够控制忽略异常

image-20221224224821042

19.12.3Feign系列

  1. 新建模块

  2. pom添加feign依赖

    <dependencies>
        <!--SpringCloud ailibaba nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.xha.springcloud</groupId>
            <artifactId>cloud-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- SpringBoot整合Web组件 -->
        <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.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>
  1. yaml文件添加sentinel对openfeign的支持
server:
  port: 80

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


#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
  nacos-user-service: http://cloud-provider-alibaba-sentinel-ribbon
  
#  开启sentinel对openfeign的支持
feign:
  sentinel:
    enabled: true
  1. 主启动类添加EnableFeignClients注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class AlibabaSentinelOpenfeign80Main {
    public static void main(String[] args) {
        SpringApplication.run(AlibabaSentinelOpenfeign80Main.class,args);
    }
}
  1. 业务类

​ 采用openfeign进行服务调用和服务降级

service层接口:

@FeignClient(value = "cloud-provider-alibaba-sentinel-ribbon",
        fallback = PaymentServiceImpl.class)
public interface PaymentService {

    @GetMapping("/paymenySQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable Long id);
}

接口实现类做服务降级

@Component
public class PaymentServiceImpl implements PaymentService {
    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
        return new CommonResult<>(444,"服务降级返回,没有该流水信息",new Payment(id, "errorSerial......"));
    }
}

控制层调用service层

@RestController
public class PaymentController {

    @Resource
    private PaymentService paymentService;

    @GetMapping("/consumer/openfeign/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable Long id){
        if (id == 4){
            throw new RuntimeException("没有该id");
        }
        return paymentService.paymentSQL(id);
    }
}

19.13Sentinel持久化规则

19.13.1问题引入

当我们为某个模块中的资源添加Sentinel服务规则的时候,再次重启模块,那么该模块的服务规则就会消失。

解决方案:

将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址, sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401.上sentinel上的流控规则持续有效。

19.13.3Sentinel持久化配置

  1. 8401模块添加依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
  1. yaml配置
image-20221225124817208
server:
  port: 8401

spring:
  application:
    name: cloud-alibaba-sentinel
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心地址
        server-addr: 192.168.26.156:8848
    sentinel:
      transport:
        #配置Sentinel dashboard地址
        dashboard: 192.168.26.156:8858
        #sentinel监控服务,默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
        port: 8719
      datasource:
        ds1:
          nacos:
            server-addr: 192.168.26.156
            dataId: ${spring.application.name}
            groupId: DEFAULT_GROUP
            data-type: json
            rule-type: flow

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

  1. nacos新增配置(在nacos当中配置限流降级规则

dataId就是刚刚配置的微服务名

​ 配置内容解释:

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

image-20221225122912558
  1. 测试,访问对应的资源,查看sentinel当中是否已经存在限流规则
    image-20221225125253991

GIF 2022-12-25 12-51-16

19.14服务熔断与服务降级的区别

服务熔断:服务熔断就是当服务内部出现错误时,该服务会自动断开对外的服务,防止当其他服务再此调用该服务时,导致系统崩溃。

服务降级:服务降级就是当系统内部出现错误时,该服务就会降低服务质量,但是不会断开服务,从而保证系统的稳定性。

20.SpringCloud Alibaba Seata处理分布式事务

20.1分布式事务的问题

单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。

即一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

image-20221225131945815

20.2Seata简介

Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

官网地址:https://seata.io/zh-cn/

image-20221225133801248

20.3Seata的AT模式

前提

  • 基于支持本地 ACID 事务的关系型数据库。
  • Java 应用,通过 JDBC 访问数据库。

整体机制

两阶段提交协议的演变:

  • 一阶段(加载):业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

在一阶段,Seata 会拦截“业务 SQL”,

  1. 解析 SQL 语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,

  2. 执行“业务 SQL”更新业务数据,在业务数据更新之后,

  3. 其保存成“after image”,最后生成行锁。

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。

image-20221226165143962

​ 行锁表:

  • 二阶段(提交+回滚)

    • 提交异步化,非常快速地完成。

      ​ 二阶段如是顺利提交的话,

      因为“业务 SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

    image-20221226165600412
    • 回滚通过一阶段的回滚日志进行反向补偿。

      二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。

      回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理。

    image-20221226165805971

写隔离

  • 一阶段本地事务提交前,需要确保先拿到 全局锁
  • 拿不到 全局锁 ,不能提交本地事务。
  • 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。

image-20221226172949500

20.3一个典型的分布式事务过程

20.3.1分布式事务处理过程的一ID+三组件模型

  • 全局唯一的事务ID

  • TC (Transaction Coordinator) - 事务协调者

​ 维护全局和分支事务的状态,驱动全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器

​ 定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器

​ 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

image-20221225160352560

20.3.2处理过程

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

一开始TM向TC申请,准备开启一个全局事务(无论事务中涉及到多少分支处理,有阈值限制),然后TC返回一个XID,XID必须要是全局的,因为需要在各个上下文传播(一般也就是redis)如果有RM出现了问题,RM会通知TC有事务失败,TC会及时通知TM进行全局事务回滚。

image-20221225135512166

20.4Docker安装seata

Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。

搭建Server端

  1. 拉取seata镜像
docker pull seataio/seata-server:1.4.2
  1. 根据镜像启动容器
docker run -d -p 8091:8091 --name seata-server seataio/seata-server:1.4.2
  1. 创建本地文件夹,拷贝容器文件到本地文件
mkdir -p /home/dockerdata
docker cp seata-server:/seata-server  /home/dockerdata/seata
  1. 停止并删除容器
docker stop seata-server
docker rm seata-server
  1. 创建数据库,导入sql文件

    创建数据库:seata

    sql文件github地址

    image-20221225185314394

    ​ 导入sql文件

  2. 在nacos中创建命名空间

image-20221225205620067

  1. 修改file.conf文件

​ 进入/home/dockerdata/seata/resources目录下,修改file.conf文件

  • 修改mode为db
  • 修改数据库的配置信息

image-20221225211000853

  1. 修改registry.conf文件
  • 修改注册中心

image-20221225211400470

  • 修改配置中心

image-20221225211609911

  1. 下载seata-server文件

​ 进入到/home/dockerdata/seata目录

cd /home/dockerdata/seata

​ 在github官网上获取到对应版本的.gz文件,并传输到当前目录下

image-20221225211924588

## 解压文件
tar -zxvf seata-server-1.4.2.tar.gz
#删除tar包
rm -rf seata-server-1.4.2.tar.gz
  1. 修改seata-server中的配置文件

​ 进入到/home/dockerdata/seata/seata/seata-server-1.4.2/conf目录下

再次修改file.conf文件和registry.conf文件。修改内容和7、8节的相同。

image-20221225212351735

  1. 新建config.txt文件

​ 在/home/dockerdata/seata/seata/seata-server-1.4.2文件下创建config.txt文件,文件内容如下:

将store.mode=file 改为store.mode=db ,将数据库改为自己数据库的配置

transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableClientBatchSendRequest=false
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=false
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
store.mode=db
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://192.168.1.7:3306/seata?useUnicode=true
store.db.user=root
store.db.password=123456
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
client.undo.dataValidation=true
client.undo.logSerialization=jackson
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.log.exceptionRate=100
transport.serialization=seata
transport.compressor=none
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
  1. 新建script文件夹,并创建 nacos-config.sh文件

​ 在/home/dockerdata/seata/seata/seata-server-1.4.2文件下创建script文件夹

image-20221225212801367

​ 进入script文件夹,并创建nacos-config.sh文件,文件内容为:

#!/usr/bin/env bash
# Copyright 1999-2019 Seata.io Group.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at、
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
 
while getopts ":h:p:g:t:" opt
do
  case $opt in
  h)
    host=$OPTARG
    ;;
  p)
    port=$OPTARG
    ;;
  g)
    group=$OPTARG
    ;;
  t)
    tenant=$OPTARG
    ;;
  ?)
    echo " USAGE OPTION: $0 [-h host] [-p port] [-g group] [-t tenant] "
    exit 1
    ;;
  esac
done
 
if [[ -z ${host} ]]; then
    host=localhost
fi
if [[ -z ${port} ]]; then
    port=8848
fi
if [[ -z ${group} ]]; then
    group="SEATA_GROUP"
fi
if [[ -z ${tenant} ]]; then
    tenant=""
fi
 
nacosAddr=$host:$port
contentType="content-type:application/json;charset=UTF-8"
 
echo "set nacosAddr=$nacosAddr"
echo "set group=$group"
 
failCount=0
tempLog=$(mktemp -u)
function addConfig() {
  curl -X POST -H "${1}" "http://$2/nacos/v1/cs/configs?dataId=$3&group=$group&content=$4&tenant=$tenant" >"${tempLog}" 2>/dev/null
  if [[ -z $(cat "${tempLog}") ]]; then
    echo " Please check the cluster status. "
    exit 1
  fi
  if [[ $(cat "${tempLog}") =~ "true" ]]; then
    echo "Set $3=$4 successfully "
  else
    echo "Set $3=$4 failure "
    (( failCount++ ))
  fi
}
 
count=0
for line in $(cat $(dirname "$PWD")/config.txt | sed s/[[:space:]]//g); do
  (( count++ ))
	key=${line%%=*}
  value=${line#*=}
	addConfig "${contentType}" "${nacosAddr}" "${key}" "${value}"
done
 
echo "========================================================================="
echo " Complete initialization parameters,  total-count:$count ,  failure-count:$failCount "
echo "========================================================================="
 
if [[ ${failCount} -eq 0 ]]; then
	echo " Init nacos config finished, please start seata-server. "
else
	echo " init nacos config fail. "
fi
  1. 修改文件权限为可执行
chmod u+x *.sh
  1. 执行命令,将配置文件初始化到nacos配置中心
sh nacos-config.sh -h nacos的IP地址 -p 8848 -g DEFAULT_GROUP -t 命名空间ID

出现下面表示seata初始化nacos配置完成

image-20221225210458264

查看nacos配置中对应的命名空间下是否含有对应的配置

image-20230209222933499

出现下面错误时,添加对应模块的配置:

image-20230209223017106

image-20230209222848384

  1. 启动容器,并设置容器为自动重启
docker run -d -p 8091:8091 --restart always --name seata-server -v /home/dockerdata/seata:/seata-server -e SEATA_IP=自己seata-server的IP -e SEATA_PORT=8091 seataio/seata-server:1.2.0 

image-20221225213627282

  1. 开放对应的8091端口,并重启防火墙
firewall-cmd --zone=public --add-port=8091/tcp --permanent
systemctl restart firewalld.service
  1. 两个文件的作用

config.txt就是seata各种详细的配置,执行 nacos-config.sh 即可将这些配置导入到nacos,这样就不需要将file.conf和registry.conf放到我们的项目中了,需要什么配置就直接从nacos中读取。

20.5seata的分布式事务的解决方案

20.5.1微服务示例

用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:

  • 仓储服务:对给定的商品扣除仓储数量。
  • 订单服务:根据采购需求创建订单。
  • 帐户服务:从用户帐户中扣除余额。

这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

架构图

image-20221226104109842

20.5.2seata的分布式交易解决方案

image-20221226104335660

我们只需要使用一个 @GlobalTransactional 注解在业务方法上:

  @GlobalTransactional
    public void purchase(String userId, String commodityCode, int orderCount) {
        ......
    }

20.5.3建立数据库和表

  • seata_order:存储订单的数据库

    • t_order表
    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 '金额',
    
      `status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结' 
    
    ) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
    
    
    
  • seata_storage:存储库存的数据库

    • t_storage表
    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');
    
  • seata_account:存储账户信息的数据库

    • t_account表
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', '1', '1000', '0', '1000');

每个数据库下再增加回滚日志表:日志表位置

image-20221226131854716
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

image-20221226132541985

20.5.4新建库存模块

  1. 新建模块
  2. pom文件
<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>1.4.2</version>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--web-actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--mysql-druid-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
  1. yaml文件
server:
  port: 2002

spring:
  application:
    name: seata-storage-service
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.26.156:8848
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的对应
        tx-service-group: SEATA_GROUP
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:3306/seata_order
    username: root
    password: xu.123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml
  1. 在资源文件中添加file.conf文件和registry.conf文件

  2. 主启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.xha.springcloud.mapper")
// 开启seata
@EnableAutoDataSourceProxy
public class SeataStorageService2002Main {
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageService2002Main.class,args);
    }
}
  1. 业务类

​ service层接口:

public interface StorageService extends IService<Storage> {
    /**
     * 减少存储
     *
     * @param productId 产品id
     * @param count     数
     * @return {@link CommonResult}
     */
    @PostMapping("/storage/decrease")
    CommonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

service层实现类:

@Service
public class StorageServiceImpl extends ServiceImpl<StorageMapper, Storage>
    implements StorageService{

    /**
     * 减少存储
     *
     * @param productId 产品id
     * @param count     数
     * @return {@link CommonResult}
     */
    @Override
    public CommonResult decreaseStorage(Long productId, Integer count) {
//        1.根据productId查询当前商品
        Storage storage = getById(productId);
//        2.更新商品库存
        storage = storage
                .setResidue(storage.getResidue() - count)
                .setUsed(storage.getUsed() + count);
//        3.更新商品库存信息
        updateById(storage);
        return new CommonResult(200,"更新库存完成!");
    }
}
  1. controller层
@RestController
public class StorageController {

    @Resource
    private StorageService storageService;

    /**
     * 减少存储
     *
     * @param productId 产品id
     * @param count     数
     * @return {@link CommonResult}
     */
    @PostMapping("/storage/decrease")
    CommonResult decreaseStorage(@RequestParam("productId") Long productId, @RequestParam("count") Integer count){
        return storageService.decreaseStorage(productId,count);
    }
}

20.5.5新建账户模块

  1. 新建模块
  2. pom文件
<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>1.4.2</version>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--web-actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--mysql-druid-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
  1. yaml文件
server:
  port: 2003

spring:
  application:
    name: seata-account-service
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.26.156:8848
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的对应
        tx-service-group: SEATA_GROUP
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:3306/seata_order
    username: root
    password: xu.123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml
  1. 在资源文件中添加file.conf文件和registry.conf文件

  2. 主启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.xha.springcloud.mapper")
@EnableAutoDataSourceProxy
public class SeataAccountService2003Main {
    public static void main(String[] args) {
        SpringApplication.run(SeataAccountService2003Main.class,args);
    }
}
  1. 业务类

​ service层接口:

public interface AccountService extends IService<Account> {
    /**
     * 扣除余额
     *
     * @param userId 用户id
     * @param money  钱
     * @return {@link CommonResult}
     */
    @PostMapping("/account/decrease")
    CommonResult decreaseMoney(@RequestParam("userId") Long userId, @RequestParam("money") Integer money);

}

service层实现类:

@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account>
    implements AccountService{

    /**
     * 扣除余额
     *
     * @param userId 用户id
     * @param money  钱
     * @return {@link CommonResult}
     */
    @Override
    public CommonResult decreaseMoney(Long userId, Integer money) {
        //        1.根据userId查询当前用户
        Account account = getById(userId);
//        2.更新商品库存
        account = account
                .setResidue(account.getResidue() - money)
                .setUsed(account.getUsed() + money);
//        3.更新商品库存信息
        updateById(account);
        return new CommonResult(200,"更新库存完成!");
    }
}

  1. controller层
@RestController
public class AccountController {

    @Resource
    private AccountService accountService;

    /**
     * 扣减库存
     *
     * @param userId 用户id
     * @param money  钱
     * @return {@link CommonResult}
     */
    @PostMapping("/account/decrease")
    public CommonResult decreaseMoney(@RequestParam("userId") Long userId, @RequestParam("money") Integer money){
        return accountService.decreaseMoney(userId, money);
    }
}

20.5.6新建订单模块

  1. 新建模块
  2. pom文件
    <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>1.4.2</version>
        </dependency>
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--web-actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--mysql-druid-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
  1. yaml文件

image-20221226113402075

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.26.156:8848
    alibaba:
      seata:
        #自定义事务组名称需要与seata-server中的对应
        tx-service-group: SEATA_GROUP
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:3306/seata_order
    username: root
    password: xu.123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml

  1. 在资源文件中添加file.conf文件和registry.conf文件
  2. 主启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.xha.springcloud.mapper")
// 开启seata
@EnableAutoDataSourceProxy    
public class SeataOrderService2001Main {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderService2001Main.class,args);
    }
}
  1. 使用MybatisX逆向生成entities、mapper、mapper.xml以及service层

  2. 添加统一响应实体

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * JSON封装体CommonResult
 */
@Data
@NoArgsConstructor
public class CommonResult<T> {
    /**
     * 状态码
     */
    private Integer code;

    /**
     * 提示信息
     */
    private String message;

    /**
     * 返回的数据
     */
    private T data;

    /**
     * 不含data的有参构造
     */
    public CommonResult(Integer code, String message){
        this.code = code;
        this.message = message;
    }

    /**
     * 含有data的有参构造
     */
    public CommonResult(Integer code, String message, T data){
        this.code = code;
        this.message = message;
        this.data = data;
    }
}
  1. service接口以及实现类

​ 实现创建订单的业务

使用OpenFeign实现模块之间的调用,创建库存模块和账户模块对应的接口,指定模块服务名**调用库存模块扣减库存,调用账户模块扣减余额。**

image-20221226134340251

OrderService:

import com.xha.springcloud.entities.Order;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * 订单服务
 *
 * @author Xu Huaiang
 * @date 2022/12/26
 */
public interface OrderService extends IService<Order> {

    /**
     * 创建订单
     *
     * @param order 订单
     */
    public void createOrder(Order order);
}

StorageService:

@FeignClient(value = "seata-storage-service")
public interface StorageService {

    /**
     * 减少存储
     *
     * @param productId 产品id
     * @param count     数
     * @return {@link CommonResult}
     */
    @PostMapping("/storage/decrease")
    CommonResult decreaseStorage(@RequestParam("productId") Long productId,@RequestParam("count") Integer count);
}

AccountService:

@FeignClient(value = "seata-account-service")
public interface AccountService {

    /**
     * 扣除余额
     *
     * @param userId 用户id
     * @param money  钱
     * @return {@link CommonResult}
     */
    @PostMapping("/account/decrease")
    CommonResult decreaseMoney(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

OrderServiceImpl实现类

@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order>
    implements OrderService{

    @Resource
    private StorageService storageService;

    @Resource
    private AccountService accountService;

    /**
     * 创建订单
     *
     * @param order 订单
     */
    @Override
    public void createOrder(Order order) {
        log.info("创建订单");
//        1.创建订单
        save(order);
        log.info("调用库存模块,扣减库存");
//        2.扣减库存
        storageService.decreaseStorage(order.getProductId(),order.getCount());
        log.info("调用账户模块,扣减余额");
//        3.账户扣减余额
        accountService.decreaseMoney(order.getUserId(),order.getMoney());
    }
}
  1. 控制层
@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    @PostMapping("/order/create")
    public CommonResult createOrder(Order order){
        orderService.createOrder(order);
        return new CommonResult(200,"订单创建完成!");
    }
}

20.5.7测试

当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1。而且由于feign的重试机制,账户余额还有可能被多次扣减

添加分布式事务控制,当出现异常的回滚数据

image-20221226161855215
出现以下错误表示数据库字段transaction_service_group指定的长度太短,可以修改对应的字段长度。

image-20230209225045104

image-20230209225212838

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值