父工程搭建

  1. 创建 Maven 工程

    image-20220809092840896

  2. 字符编码

    image-20220809093904178

  3. 注解激活生效

    image-20220809094142769

  4. 删除项目中的 src 文件夹

  5. 修改 porm.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    <?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.cuc</groupId>
    <artifactId>SpringCloud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 打包方式 -->
    <packaging>pom</packaging>

    <!--统一管理jar包版本-->
    <properties>
    <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>
    <log4j.version>1.2.17</log4j.version>
    <lombok.version>1.16.18</lombok.version>
    <mysql.version>5.1.47</mysql.version>
    <druid.version>1.1.16</druid.version>
    <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    </properties>
    <!--子模块继承之后,提供作用:锁定版本+子module不用写groupId和version-->
    <dependencyManagement>
    <dependencies>
    <!--springboot2.2.2-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.2.2.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <!--springcloudHoxton.SR1-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>Hoxton.SR1</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <!--springcloudalibaba2.1.0.RELEASE-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.1.0.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>${mysql.version}</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>${druid.version}</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>${mybatis.spring.boot.version}</version>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>${junit.version}</version>
    </dependency>
    <dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>${log4j.version}</version>
    </dependency><dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
    <optional>true</optional>
    </dependency>
    </dependencies>
    </dependencyManagement>
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
    <fork>true</fork>
    <addResources>true</addResources>
    </configuration>
    </plugin>
    </plugins>
    </build>
    </project>

    Maven常用 dependencyManagement元素来提供一种管理依赖版本号的方式。通常在一个项目的最顶层父POM中看到 dependencyManagement 元素。它能让所有子项目引入依赖而不需要显式列出版本号。只要子项目中没有指定版本,Maven 会向上找,直到找到拥有 dependencyManagement 元素的项目,然后就会使用 dependencyManagement 元素中指定的版本,包括scope都取自父pom。dependencyManagement 只是声明版本,并不真正引入。

  6. 跳过单元测试

    点击闪电图标,打包不会将单元测试打进去。

    image-20220810105000680

模块构建

微服务提供者

  1. 新建 maven 模块 cloud-provider-payment8001

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    <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.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!--热部署(可省去)-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
    </dependency>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>
  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    server:
    port: 8001

    spring:
    application:
    name: cloud-payment-service
    datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db_springcloud?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

    mybatis:
    mapper-locations: classpath:mapper/*.xml
    type-aliases-package: com.cuc.springcloud.entities # 所有Entity类所在包
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.cuc.springcloud;

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class PaymentMain8001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentMain8001.class,args);
    }
    }
  5. 建表SQL

    1
    2
    3
    4
    5
    CREATE TABLE `payment` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
    `serial` varchar(200) DEFAULT '',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
  6. 实体类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.cuc.springcloud.entities;

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

    import java.io.Serializable;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Payment implements Serializable {
    private Long id;
    private String serial;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.cuc.springcloud.entities;


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

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class CommonResult<T> {
    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code,String message){
    this(code,message,null);
    }
    }
  7. 接口Dao

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.cuc.springcloud.dao;

    import com.cuc.springcloud.entities.Payment;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Param;

    @Mapper
    public interface PaymentDao {
    int create(Payment payment);
    Payment getPaymentById(@Param("id") Long id);
    }
  8. 映射文件

    路径:resource/mapper/PaymentMapper.xml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <?xml version ="1.0" encoding ="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    <mapper namespace ="com.cuc.springcloud.dao.PaymentDao" >

    <resultMap id ="BaseResultMap" type ="com.cuc.springcloud.entities.Payment" >
    <id column ="id" property ="id" jdbcType ="BIGINT" />
    <result column ="serial" property ="serial" jdbcType ="VARCHAR" />
    </resultMap>
    <insert id ="create" parameterType ="Payment" useGeneratedKeys ="true" keyProperty ="id" >
    INSERT INTO payment(SERIAL) VALUES(#{serial});
    </insert>
    <select id ="getPaymentById" parameterType ="Long" resultMap ="BaseResultMap" >
    SELECT * FROM payment WHERE id=#{id};
    </select>

    </mapper>
  9. 接口 Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.cuc.springcloud.service;

    import com.cuc.springcloud.entities.Payment;
    import org.apache.ibatis.annotations.Param;

    public interface PaymentService {

    int create(Payment payment);
    Payment getPaymentById(@Param("id") Long id);
    }
  10. Service 实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.cuc.springcloud.service.impl;

    import com.cuc.springcloud.dao.PaymentDao;
    import com.cuc.springcloud.entities.Payment;
    import com.cuc.springcloud.service.PaymentService;
    import org.springframework.stereotype.Service;

    import javax.annotation.Resource;

    @Service
    public class PaymentServiceImpl implements PaymentService {

    @Resource
    private PaymentDao paymentDao;

    @Override
    public int create(Payment payment) {
    return paymentDao.create(payment);
    }

    @Override
    public Payment getPaymentById(Long id) {
    return paymentDao.getPaymentById(id);
    }
    }
  11. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    package com.cuc.springcloud.controller;


    import com.cuc.springcloud.entities.CommonResult;
    import com.cuc.springcloud.entities.Payment;
    import com.cuc.springcloud.service.PaymentService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.*;

    import javax.annotation.Resource;

    @Slf4j
    @RestController
    public class PaymentController {

    @Resource
    private PaymentService paymentService;

    @PostMapping("/payment/create")
    public CommonResult create(@RequestBody Payment payment){
    int result = paymentService.create(payment);
    log.info("插入操作返回结果:"+result);
    if(result>0){
    return new CommonResult(200,"插入成功",result);
    }else{
    return new CommonResult(444,"插入失败",null);
    }
    }

    @GetMapping("/payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id") Long id){
    Payment payment = paymentService.getPaymentById(id);
    log.info("查询结果:{}",payment);
    if (payment!=null){
    return new CommonResult(200,"查询成功",payment);
    }else{
    return new CommonResult(444,"查询失败",null);
    }
    }
    }
  12. 测试

    image-20220810124405687

    image-20220810131009306

客户端消费者

  1. 新建 maven 模块 cloud-consumer-order80

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <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.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>
  3. 配置文件

    1
    2
    server:
    port: 80
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    package com.cuc.springcloud;


    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;

    @SpringBootApplication
    public class OrderMain80 {
    public static void main(String[] args) {
    SpringApplication.run(OrderMain80.class,args);
    }
    }
  5. 实体类

    将服务提供者 entities 复制过来即可。

  6. 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.cuc.springcloud.config;

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

    @Configuration
    public class ApplicationContextConfig {

    /**
    * 是什么?
    * RestTemplate提供了多种便捷访问远程Http服务的方法
    * 是Spring提供的用于访问Rest服务的 客户端模板工具集
    * 使用
    * 使用restTemplate访问restful接口非常的简单粗暴无脑
    * (url, requestMap, ResponseBean.class)这三个参数分别代表 REST请求地址、请求参数、返回结果。
    */
    @Bean
    public RestTemplate restTemplate(){
    return new RestTemplate();
    }
    }
  7. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    package com.cuc.springcloud.controller;


    import com.cuc.springcloud.entities.CommonResult;
    import com.cuc.springcloud.entities.Payment;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;

    import javax.annotation.Resource;

    @Slf4j
    @RestController
    public class OderController {

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

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment){
    return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
    }

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
    return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
    }
    }
  8. 测试

    启动 服务提供者和客户端消费者进行测试。

    image-20220810201947893

    image-20220810202425523

    这里不用写不写80端口无所谓,因为浏览器默认访问的就是80端口。

工程重构

在上面两个模块中,实体类完全一样,造成代码冗余。

以下通过新建cloud-api-commons项目解决此问题。

  1. 新建 maven 模块 cloud-api-commons

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
    </dependency>
    <dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.1.0</version>
    </dependency>
    </dependencies>
  3. 创建跟两个项目一样的实体类,并将两个项目的实体类删除

  4. 打包(clean、intall)

    image-20220810205904904

  5. 两个项目中引入打好的包

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
    </dependency>

    重构完成。

  6. 目前工程样图

    image-20220810210046404

服务注册中心

Eureka

概述

Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来 实现服务治理。类似 Dubbo 架构。

具体流程:

  1. 启动eureka注册中心
  2. 启动服务提供者,会将自身信息(比如服务地址以别名的方式注册进eureka)
  3. 消费者在需要调用接口时,使用服务别名去注册中心获取实际的RPC远程调用地址
  4. 消费者获取调用地址后,底层实际利用HttpClient技术实现远程调用
  5. 消费者获得服务地址后会缓存在本地jvm内存中,默认每隔三十秒更新一次服务调用地址

image-20220811124716735

Eureka 包含两个组件:Eureka Server 和 Eureka Client :

  • Eureka Server 提供服务注册服务

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

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

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

单机构建

EurekaServer
  1. 创建 maven 模块 cloud-eureka-server7001

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <dependencies>
    <!--eureka-server-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <!--引入自己定义的api通用包-->
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
    </dependency>
    <!--bootwebactuator-->
    <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>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    </dependency>
    </dependencies>
  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server:
    port: 7001
    eureka:
    instance:
    hostname: localhost # eureka server 实例名称
    client:
    # false 表示不向注册中心注册自己
    register-with-eureka: false
    # false 表示自己就是注册中心,职责就是维护服务实例,并不需要去检索服务
    fetch-registry: false
    service-url:
    # 设置 与Eureka Server交互的地址, 查询服务和注册服务都需要依赖这个地址
    defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.cuc.springcloud;


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

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaMain7001 {

    public static void main(String[] args) {
    SpringApplication.run(EurekaMain7001.class,args);
    }
    }
  5. 启动测试

    访问 localhost:7001

    image-20220811131709026

8001模块
  1. 添加 pom 依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. 添加配置

    1
    2
    3
    4
    5
    6
    7
    8
    eureka:
    client:
    # 表示是否将自己注册进 EurekaServer 默认为 true
    register-with-eureka: true
    # 是否从 EurekaServer 抓取已有的注册信息,默认为 true 。单节点无所谓,集群必须设置为 true 才能配合 ribbon 使用负载均衡
    fetch-registry: true
    service-url:
    defaultZone: http://localhost:7001/eureka
  3. 主启动添加注解

    1
    @EnableEurekaClient
  4. 测试

    先启动7001服务注册中心再启动当前项目

    image-20220811133432015

    其中红框内对应配置文件的配置:

    image-20220811133519001

80模块
  1. 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. 添加配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    spring:
    application:
    name: cloud-order-service

    eureka:
    client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
    defaultZone: http://localhost:7001/eureka
  3. 主启动添加注解 @EnableEurekaClient

  4. 测试

    先启动7001,再启动8001,最后启动80

    image-20220811134624565

    访问 localhost/consumer/payment/get/1 进行测试。

集群构建

  • 搭建注册中心集群
  • 搭建服务提供者集群
注册中心集群
  1. 新建 maven 项目 cloud-eureka-server7002

  2. 引入依赖

    cloud-eureka-server7001

  3. 添加映射配置

    路径 /etc/hosts(Mac)

    image-20220811175434892

  4. 配置文件

    注意:7001以及7002都要修改。

    7001:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 7001
    eureka:
    instance:
    hostname: eureka7001.com
    client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
    defaultZone: http://eureka7002.com:7002/eureka/

    7002:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    server:
    port: 7002
    eureka:
    instance:
    hostname: eureka7002.com
    client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka/
  5. 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.cuc.springcloud;

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

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaMain7002 {
    public static void main(String[] args) {
    SpringApplication.run(EurekaMain7002.class,args);
    }
    }
  6. 修改80以及8001配置文件

    1
    2
    - defaultZone: http://localhost:7001/eureka
    + defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  7. 测试

    先启动7001和7002,然后启动8001,最后启动80即可访问测试。

    其中可以通过访问hosts配置的域名映射访问界面(感觉没啥用,就是localhsot加上不同的端口)

    image-20220811194813818

服务提供者集群

两个提供相同功能的服务提供者,在注册中心共同使用一个服务别名。

  1. 新建 maven 项目 cloud-provider-payment8002

    代码都跟8001一样,除了配置文件的端口。注意配置文件中的服务名称是一样的。

  2. 修改8001以及8002的Controller

    为了方便下面负载均衡的测试,在控制器类中修改代码以看出调用的哪个服务。

    image-20220811202056193

    image-20220811202134421

  3. 负载均衡

    1)修改80中的Controller

    1
    2
    3
    4
    - public static final String PAYMENT_URL = "http://localhost:8001";

    //通过在eureka上注册过的微服务名称调用
    + public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

    2)使用@LoadBalanced注解赋予RestTemplate负载均衡的能力,在80配置类中修改。

    image-20220811203558474

  4. 测试

    启动顺序不多说了,之后浏览器一直访问 http://localhost/consumer/payment/get/1,发现请求会轮流调用两个提供者的接口。

Actuator

微服务信息完善

修改8001配置文件

image-20220811205444759

可以看到注册中心中8001相应名称替换掉了

image-20220811205554545

但是鼠标移上去浏览器还是会显示

image-20220811205554545

好像再配置下面这行就可以,试过没起作用🫠。

1
2
3
eureka:
instance:
prefer-ip-address: false # 访问路径是否显示ip地址

Discovery

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

  1. 在8001的Controller添加如下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Resource
    private DiscoveryClient discoveryClient;

    @GetMapping("/payment/discovery")
    public Object discovery(){
    List<String> services = discoveryClient.getServices();
    for (String service:services){
    System.out.println(service);
    }

    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
    for (ServiceInstance instance:instances){
    System.out.println(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
    }
    return this.discoveryClient;
    }
  2. 在8001主启动类添加注解@EnableDiscoveryClient

  3. 测试

    image-20220811214821647

    image-20220811214834209

自我保护

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

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

image-20220811215602469

默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当EurekaServer节点在短时间内丢失过多微服务实例心跳时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式,不会剔除该服务。

它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。

在注册中心的配置文件使用:

1
eureka.server.enable-self-preservation = false # 可以禁用自我保护模式,默认开启

在服务提供者的配置文件中也可以修改:

1
2
eureka.instance.lease-renewal-interval-in-seconds=30 # 客户端向服务端发送心跳的时间
eureka.instance.lease-expiration-duration-in-seconds=90 # 服务端在收到最后一次心跳后等待时间上限,超时将剔除服务

Zookeeper

ZK注册中心

在Linux下搭建zookeeper。

服务提供者
  1. 新建 maven 项目 cloud-provider-payment8004

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <dependencies>
    <!--SpringBoot整合Web组件-->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--SpringBoot整合zookeeper客户端-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </dependency>
    <!--zookeeper版本跟搭建的一致-->
    <dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.5.7</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>
  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 8004
    spring:
    application:
    name: cloud-provider-payment
    cloud:
    zookeeper:
    connect-string: 192.168.1.101:2181
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableDiscoveryClient // 该注解用于向使用 consul 或者 zookeeper 作为注册中心时注册服务
    public class PaymentMain8004 {

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class PaymentController {

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

    @RequestMapping("/payment/zk")
    public String paymentZK(){
    return "SpringCloud with Zookeeper"+serverPort+"\t"+ UUID.randomUUID();
    }
    }
  6. 测试

    现将zookeeper服务启动,再启动8004。启动后会在zookeeper多出 /services 节点

    image-20220812075444816

服务消费者
  1. 新建 maven 模块 cloud-consumerzk-order80

  2. 引入依赖

    同服务提供者

  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 80
    spring:
    application:
    name: cloud-consumer-order
    cloud:
    zookeeper:
    connect-string: 192.168.1.101:2181
  4. 主启动

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class OrderZK80 {
    public static void main(String[] args) {
    SpringApplication.run(OrderZK80.class,args);
    }
    }
  5. 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class ApplicationContextBean {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
    return new RestTemplate();
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RestController
    public class OrderZKController {

    public static final String INVOKE_URL = "http://cloud-provider-payment";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/payment/zk")
    public String paymentInfo(){
    String result = restTemplate.getForObject( INVOKE_URL + "/payment/zk" , String.class );
    System.out.println("消费者调用支付服务 (zookeeper)--->result:" + result);
    return result;
    }
    }
  7. 测试

    image-20220812101246119

    image-20220812101258961

注意服务在 ZK 创建的节点为临时节点。

Consul

Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司 用 Go 语言开发 。下载地址怎么玩

安装运行Consul

安装

使用开发模式启动:

1
consul agent -dev

停止代理:

1
consul leave

之后访问Consul的首页:http://localhost:8500

image-20220812110405692

服务提供者
  1. 新建 maven 模块 cloud-providerconsul-payment8006

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <dependencies>
    <!--SpringCloud consul-server -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
    <groupId>org.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>
  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server:
    port: 8006
    spring:
    application:
    name: consul-provider-payment

    cloud:
    consul:
    host: localhost
    port: 8500
    discovery:
    service-name: ${spring.application.name}
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableDiscoveryClient
    public class PaymentMain8006 {

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class PaymentController {

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

    @GetMapping("/payment/consul")
    public String paymentInfo(){
    return "SpringCloud with Consul: " + serverPort + " \t\t " + UUID. randomUUID ();
    }
    }
  6. 运行

    image-20220812112552325

服务消费者
  1. 新建模块 cloud-consumerconsul-order80

  2. 引入依赖

    同提供者。

  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server:
    port: 80
    spring:
    application:
    name: consul-consumer-order

    cloud:
    consul:
    host: localhost
    port: 8500
    discovery:
    service-name: ${spring.application.name}
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableDiscoveryClient
    public class OrderConsulMain80 {

    public static void main(String[] args) {
    SpringApplication.run(OrderConsulMain80.class,args);
    }
    }
  5. 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class ApplicationContextBean {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
    return new RestTemplate();
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @RestController
    public class OrderConsulController {

    public static final String INVOKE_URL = "http://consul-provider-payment" ;

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/consumer/payment/consul")
    public String paymentInfo(){
    String result = restTemplate.getForObject(INVOKE_URL+"/payment/consul",String.class);
    return result;
    }
    }
  7. 运行测试

    image-20220812113838587

    image-20220812113846414

异同点

组件 语言 CAP 服务健康检查 暴露接口 SpringCloud集成
Eureka Java AP 可配支持 HTTP 已集成
Consul Go CP 支持 HTTP/DNS 已集成
Zookeeper Java CP 支持 客户端 已集成
  • C:强一致性
  • A:可用性
  • P:分区容错性

CAP理论关注粒度是数据,而不是系统整体设计。

服务调用

Ribbon

概述

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

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

image-20220812182921776

  1. 先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server。
  2. 再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。

Ribbon VS Nginx

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

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

引入

spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用。

image-20220812183545620

RestTemplate

上面已经演示过了。get/post 代表请求方法

  • getForObject/postForObject

    返回对象为响应体中数据转化成的对象,基本上可以理解为Json。

  • getForEntity/postForEntity

    返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。

IRule

Ribbon核心组件,根据特定算法从服务列表中选取一个要访问的服务。

  • RoundRobinRule:轮询

  • RandomRule:随机

  • RetryRule

    先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。

  • WeightedResponseTimeRule

    对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择。

  • BestAvailableRule

    会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。

  • AvailabilityFilteringRule

    先过滤掉故障实例,再选择并发较小的实例。

  • ZoneAvoidanceRule

    默认规则,复合判断server所在区域的性能和server的可用性选择服务器。

如何替换?

修改 cloud-consumer-order80

  1. 新建 package

    image-20220812194637216

    为什么要新建包?

    Ribbon配置类不能放在@ComponentScan所扫描的当前包下以及子包下,也就是不能放在主启动类的目录或子目录下。

    否则配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。

  2. 在myrule包下新建配置类

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class MySelfRule {

    @Bean
    public IRule myRule(){
    return new RandomRule(); // 定义为随机
    }
    }
  3. 主启动添加注解 @RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)

  4. 测试

    按顺序启动7001、8001、8002、80。之后多次访问 http://localhost/consumer/payment/get/1 即可看到请求随机分配到8001和8002。

负载均衡算法

原理

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

如: List [0] instances = 127.0.0.1:8001

   List [1] instances = 127.0.0.1:8002

按照轮询算法原理:

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

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

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

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

如此类推……

手写

试着写一个本地负载均衡器。以下均在cloud-consumer-order80模块进行改造。

  1. ApplicationContextConfig 去掉注解 @LoadBalanced

    image-20220812202046148

  2. 新建 LoadBalancer 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.cuc.springcloud.lb;

    import org.springframework.cloud.client.ServiceInstance;

    import java.util.List;

    public interface LoadBalancer {
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
    }
  3. 实现接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package com.cuc.springcloud.lb;

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

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


    @Component
    public class MyLB implements LoadBalancer{

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement(){
    int current;
    int next;
    do {
    current = this.atomicInteger.get();
    next = current >= 2147483647 ? 0 : current + 1 ;
    }while (!this.atomicInteger.compareAndSet(current, next));
    System.out.println("*****next:"+next);
    return next;
    }

    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
    int index = getAndIncrement() % serviceInstances.size();
    return serviceInstances.get(index);
    }
    }
  4. 在 OrderController 添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 可以获取注册中心上的服务列表
    @Resource
    private DiscoveryClient discoveryClient;
    @Resource
    private LoadBalancer loadBalancer;

    @GetMapping("/consumer/payment/lb/{id}")
    public CommonResult<Payment> getPaymentLB(@PathVariable("id") Long id){
    List<ServiceInstance> instances = discoveryClient.getInstances( "CLOUD-PAYMENT-SERVICE");
    if (instances==null || instances.size()<=0){
    return null;
    }
    ServiceInstance instance = loadBalancer.instances(instances);
    URI uri = instance.getUri();
    return restTemplate.getForObject(uri+"/payment/get/"+id,CommonResult.class);
    }
  5. 测试

    一直访问 http://localhost/consumer/payment/lb/5

OpenFeign

概述

Feign 是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可

Feign能干什么?

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

Feign集成了Ribbon

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

使用

  1. 新建 cloud-consumer-feign-order80

  2. 引入依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <dependencies>
    <!-- openfeign -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
    </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>
  3. 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 80

    eureka:
    client:
    register-with-eureka: false
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableFeignClients
    public class OrderFeignMain80 {
    public static void main(String[] args) {
    SpringApplication.run(OrderFeignMain80.class,args);
    }
    }
  5. Service

    1
    2
    3
    4
    5
    6
    @Component
    @FeignClient(value = "CLOUD-PAYMENT-SERVICE")
    public interface PaymentFeignService {
    @GetMapping("/payment/get/{id}")
    CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class OrderFeignController {

    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping("/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
    return paymentFeignService.getPaymentById(id);
    }
    }
  7. 测试

    访问 http://localhost/consumer/payment/get/1

  8. 总结

    image-20220812210749765

超时控制

演示超时情况:

  1. 8001 Controller

    1
    2
    3
    4
    5
    6
    @GetMapping("/payment/feign/timeout")
    public String paymentFeignTimeOut() throws InterruptedException {
    // 暂停三秒
    TimeUnit.SECONDS.sleep(3);
    return serverPort;
    }
  2. 80 Service

    1
    2
    @GetMapping("/payment/feign/timeout")
    String paymentFeignTimeOut();
  3. 80 Controller

    1
    2
    3
    4
    5
    @GetMapping("/consumer/payment/feign/timeout")
    public String paymentFeignTimeOut(){
    // 默认等待 1 秒
    return paymentFeignService.paymentFeignTimeOut();
    }
  4. 测试

    image-20220812212334994

Feign客户端默认只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。

为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制。

配置文件开启配置:

在80模块配置

1
2
3
4
5
6
# 设置 feign 客户端超时时间(OpenFeign默认支持 ribbon )
ribbon:
# 指的是建立连接所用的时间,适用于网络状况正常的情况下, 两端连接所用的时间
ReadTimeout: 5000
# 指的是建立连接后从服务器读取到可用资源所用的时间
ConnectTimeout: 5000

日志打印

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

日志级别:

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

使用:

  1. 配置日志Bean

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel(){
    return Logger.Level.FULL;
    }
    }
  2. 配置文件

    1
    2
    3
    4
    logging:
    level:
    # feign 日志以什么级别监控哪个接口
    com.cuc.springcloud.service.PaymentFeignService: debug
  3. 测试

    image-20220812220604661

Hystrix断路器

问题

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

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

概述

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

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

重要概念

1)服务降级

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

哪些情况会出发降级?

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

2)服务熔断

类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。

3)服务限流

秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行。

案例里都有说明。

案例

构建服务提供者
  1. 新建cloud-provider-hystrix-payment8001

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    <dependencies>
    <!--hystrix-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
    </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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    server:
    port: 8001
    spring:
    application:
    name: cloud-provider-hystrix-payment

    eureka:
    client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableEurekaClient
    public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentHystrixMain8001.class,args);
    }
    }
  5. Service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Service
    public class PaymentService {

    /**
    * 正常访问,一切 OK
    */
    public String paymentInfo_OK(Integer id){
    return "线程池:"+Thread.currentThread().getName()+"paymentInfo_OK,id:"+id+"\t"+"O(∩_∩)O";
    }

    /**
    * 超时访问,演示降级
    */
    public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
    TimeUnit.SECONDS.sleep(3);
    return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOut,id:"+id+"\t"+"O(∩_∩)O耗费 3 秒";
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @RestController
    public class PaymentController {

    @Autowired
    private PaymentService paymentService;

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

    @GetMapping("/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
    String result = paymentService.paymentInfo_OK(id);
    return result;
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) throws InterruptedException {
    String result = paymentService.paymentInfo_TimeOut(id);
    return result;
    }
    }
构建服务消费者
  1. 创建 cloud-consumer-feign-hystrix-order80

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <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>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-consumer-order80</artifactId>
    <version>1.0-SNAPSHOT</version>
    </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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    server:
    port: 80

    eureka:
    client:
    register-with-eureka: false
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka/
  4. 主启动

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableFeignClients
    public class OrderHystrixMain80 {
    public static void main(String[] args) {
    SpringApplication.run(OrderHystrixMain80.class,args);
    }
    }
  5. Service 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component
    @FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
    public interface PaymentHystrixService {

    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

    @GetMapping("/payment/hystrix/timeout/{id}")
    String paymentInfo_TimeOut(@PathVariable("id") Integer id);
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @RestController
    public class OrderHystirxController {

    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/ok/{id}")
    public String paymentInfo_OK(@PathVariable("id") Integer id){
    return paymentHystrixService.paymentInfo_OK(id);
    }

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
    return paymentHystrixService.paymentInfo_TimeOut(id);
    }
    }
高并发测试

使用 jmeter 并发40000个请求访问 paymentInfo_TimeOut

image-20220813141915525

image-20220813135153762

运行这些请求,然后浏览器访问 80 的 ok 路由,正常情况下会立刻返回数据,但是高并发进行时,要么访问变慢要么访问超时报错(如下)。

image-20220813141843066

服务降级

如何解决?

  • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,服务降级
  • 对方服务(8001)down机了,调用者(80)不能一直卡死等待,服务降级
  • 对方服务(8001)OK,调用者(80)自己出故障或有自我要求(例如设有最大等待时间),服务降级

==针对 8001==

  1. 修改接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    		/**
    * 超时访问,演示降级
    */
    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
    // 超过 2 秒则调用 paymentInfo_TimeOutHandler。
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")
    })
    public String paymentInfo_TimeOut(Integer id) throws InterruptedException {
    TimeUnit.SECONDS.sleep(3);
    return "线程池:"+Thread.currentThread().getName()+"paymentInfo_TimeOut,id:"+id+"\t"+"O(∩_∩)O耗费 3 秒";
    }

    public String paymentInfo_TimeOutHandler(Integer id){
    return "/( ㄒ o ㄒ )/调用接口超时或异常"+id;
    }
  2. 主启动添加注解 @EnableCircuitBreaker

  3. 测试

    image-20220813153459572

通过测试知道处理时间超过2秒会调用paymentInfo_TimeOutHandler函数返回结果,需要注意的是如果没有超时,但是在处理的时候报错,也是会调用paymentInfo_TimeOutHandler函数的。

==针对 80==

服务降级一般放在消费端,👆只是为了说明服务降级也可以用在服务端。

  1. 撤回服务端降级代码,使消费者能正常接受

  2. YML

    1
    2
    3
    feign:
    hystrix:
    enabled: true
  3. 主启动添加注解 @EnableHystrix

  4. 修改Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
    // 超过 2 秒则调用 paymentInfo_TimeOutHandler。
    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "2000")
    })
    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
    return paymentHystrixService.paymentInfo_TimeOut(id);
    }

    public String paymentInfo_TimeOutHandler(Integer id){
    return "/( ㄒ o ㄒ )/调用接口超时或异常"+id;
    }
  5. 测试

    image-20220813155658099

==问题及解决==

如果有多个业务需要服务降级,每个业务方法对应一个兜底的方法,会导致代码膨胀。并且兜底方法与业务方法放在一块,耦合度高。

解决代码膨胀:

image-20220813162531500

image-20220813162646129

解决耦合度高:

只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。根据 80 已经有的 PaymentHystrixService 接口,重新新建一个类 PaymentFallbackService 实现该接口,统一为接口里面的方法进行异常处理。

  1. 将 80 控制器方法进行还原,使之不出现异常,能正常调用接口。

  2. 接口实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Component
    public class PaymentFallbackService implements PaymentHystrixService{

    @Override
    public String paymentInfo_OK(Integer id) {
    return "paymentInfo_OK接口异常";
    }

    @Override
    public String paymentInfo_TimeOut(Integer id) {
    return "paymentInfo_TimeOut接口异常";
    }
    }
  3. 修改Service接口

    image-20220813164502427

  4. 测试

    由上方代码可知当服务接口异常时,会调用实现类里头相应的方法。当然只能对服务提供者(8001)提供的接口错误进行fallback,并不能对自身(80)例如控制器方法错误的fallback。

    启动顺序不多说了,启动后,当 8001 的接口出现异常或者处理超时又或者 8001 宕机都会调用实现类里的fallback函数。

服务熔断

==熔断机制概述==

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

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

==案例==

  1. 在 8001 的Service接口添加如下内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //========服务熔断
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",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") // 失败率达到多少后跳闸
    })
    public String paymentCircuitBreaker(Integer id){
    if (id<0){
    throw new RuntimeException(); // 自定义异常
    }
    return "success";
    }

    public String paymentCircuitBreaker_fallback(Integer id){
    return "false";
    }

    即在时间窗口期内,至少有10次请求过来,并且这些请求中至少有60%请求失败则进行跳闸(使服务不能使用)。

  2. 在 8001 的Controller添加如下内容

    1
    2
    3
    4
    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id){
    return paymentService.paymentCircuitBreaker(id);
    }
  3. 测试

    访问 http://localhost:8001/payment/circuit/33(正确)http://localhost:8001/payment/circuit/-33(错误)

    多次访问错误的路径,之后访问正确的路径发现返回false,继续访问正确路径多次后才会返回success。

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

==熔断类型==

  • 熔断关闭

    熔断关闭则对服务进行正常调用。

  • 熔断打开

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

  • 熔断半开

    会释放一个请求到原来的服务。如果成功,断路器会关闭,若失败,继续开启并重新计时休眠时间。

==所有配置==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@HystrixCommand(fallbackMethod="str_fallbackMethod",groupKey="strGroupCommand",commandKey="strCommand",
threadPoolKey="strThreadPool",commandProperties={
//设置隔离策略,THREAD表示线程池SEMAPHORE:信号池隔离
@HystrixProperty(name="execution.isolation.strategy",value="THREAD"),
//当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
@HystrixProperty(name="execution.isolation.semaphore.maxConcurrentRequests",value="10"),
//配置命令执行的超时时间
@HystrixProperty(name="execution.isolation.thread.timeoutinMilliseconds",value="10"),
//是否启用超时时间
@HystrixProperty(name="execution.timeout.enabled",value="true"),
//执行超时的时候是否中断
@HystrixProperty(name="execution.isolation.thread.interruptOnTimeout",value="true"),
//执行被取消的时候是否中断
@HystrixProperty(name="execution.isolation.thread.interruptOnCancel",value="true"),
//允许回调方法执行的最大并发数
@HystrixProperty(name="fallback.isolation.semaphore.maxConcurrentRequests",value="10"),
//服务降级是否启用,是否执行回调函数
@HystrixProperty(name="fallback.enabled",value="true"),
//是否启用断路器
@HystrixProperty(name="circuitBreaker.enabled",value="true"),
//该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为20的时候,
//如果滚动时间窗(默认10秒)内仅收到了19个请求,即使这19个请求都失败了,断路器也不会打开。
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value="20"),
//该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过
//circuitBreaker.requestVolumeThreshold的情况下,如果错误请求数的百分比超过50,
//就把断路器设置为"打开"状态,否则就设置为"关闭"状态。
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="50"),
//该属性用来设置当断路器打开之后的休眠时间窗。休眠时间窗结束之后,
//会将断路器置为"半开"状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为"打开"状态,
//如果成功就设置为"关闭"状态。
@HystrixProperty(name="circuitBreaker.sleepWindowinMilliseconds",value="5000"),
//断路器强制打开
@HystrixProperty(name="circuitBreaker.forceOpen",value="false"),
//断路器强制关闭
@HystrixProperty(name="circuitBreaker.forceClosed",value="false"),
//滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
@HystrixProperty(name="metrics.rollingStats.timeinMilliseconds",value="10000"),
//该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据
//设置的时间窗长度拆分成多个"桶"来累计各度量值,每个"桶"记录了一段时间内的采集指标。
//比如10秒内拆分成10个"桶"收集这样,所以timeinMilliseconds必须能被numBuckets整除。否则会抛异常
@HystrixProperty(name="metrics.rollingStats.numBuckets",value="10"),
//该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为false,那么所有的概要统计都将返回-1。
@HystrixProperty(name="metrics.rollingPercentile.enabled",value="false"),
//该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
@HystrixProperty(name="metrics.rollingPercentile.timeInMilliseconds",value="60000"),
//该属性用来设置百分位统计滚动窗口中使用“桶”的数量。
@HystrixProperty(name="metrics.rollingPercentile.numBuckets",value="60000"),
//该属性用来设置在执行过程中每个“桶”中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
//就从最初的位置开始重写。例如,将该值设置为100,滚动窗口为10秒,若在10秒内一个“桶”中发生了500次执行,
//那么该“桶”中只保留最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
@HystrixProperty(name="metrics.rollingPercentile.bucketSize",value="100"),
//该属性用来设置采集影响断路器状态的健康快照(请求的成功、错误百分比)的间隔等待时间。
@HystrixProperty(name="metrics.healthSnapshot.intervalinMilliseconds",value="500"),
//是否开启请求缓存
@HystrixProperty(name="requestCache.enabled",value="true"),
//HystrixCommand的执行和事件是否打印日志到HystrixRequestLog中
@HystrixProperty(name="requestLog.enabled",value="true"),},threadPoolProperties={
//该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
@HystrixProperty(name="coreSize",value="10"),
//该参数用来设置线程池的最大队列大小。当设置为-1时,线程池将使用SynchronousQueue实现的队列,
//否则将使用LinkedBlockingQueue实现的队列。
@HystrixProperty(name="maxQueueSize",value="-1"),
//该参数用来为队列设置拒绝阈值。通过该参数,即使队列没有达到最大值也能拒绝请求。
//该参数主要是对LinkedBlockingQueue队列的补充,因为LinkedBlockingQueue
//队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
@HystrixProperty(name="queueSizeRejectionThreshold",value="5")})
服务限流

后面alibaba的Sentinel说明。

工作流程

image-20220813183218387

服务监控

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

仪表盘9001
  1. 新建 cloud-consumer-hystrix-dashboard9001

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <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>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>
  3. YML

    1
    2
    server:
    port: 9001
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableHystrixDashboard
    public class HystrixDashboardMain9001 {
    public static void main(String[] args) {
    SpringApplication.run(HystrixDashboardMain9001.class,args);
    }
    }
  5. 测试

    启动后访问 http://localhost:9001/hystrix

    image-20220813185102958

监控测试
  1. 8001需要依赖(之前应该就有)

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 修改 8001 主启动类(注入一个Bean)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @SpringBootApplication
    @EnableEurekaClient
    @EnableCircuitBreaker
    public class PaymentHystrixMain8001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentHystrixMain8001.class,args);
    }

    /**
    * 服务监控配置
    */
    @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;
    }
    }
  3. 监控 8001

    image-20220813204948604

    Delay:该参数用来控制服务器上轮询监控信息的延迟时间,默认为2000毫秒,可以通过配置该属性来降低客户端的网络和CPU消耗。

    Title:该参数对应了头部标题Hystrix Stream之后的内容,默认会使用具体监控实例的URL,可以通过配置该信息来展示更合适的标题。

之后通过访问正确与错误路径可以直观看到断路器的运作。

image-20220813205719782

image-20220813210058112

image-20220813210242365

服务网关

Zuul

不是重点,重点在Gateway。

Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。

API 网关为微服务架构中的服务提供了统一的访问入口,客户端通过API网关访问相关服务。它相当于整个微服务架构中的门面,所有客户端的访问都通过它来进行路由及过滤。它实现了请求路由、负载均衡、校验过滤、服务容错、服务聚合等功能。

image-20220813212443611

学 Gateway 去了🫡

Gateway

概述

Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关,但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul, 那就是SpringCloud Gateway 。

image-20220813213636704

image-20220813213312330

路由转发+执行过滤器链

客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。 Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等, 在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

能干嘛?

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

三大核心

==Route 路由==

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

==Predicate 断言==

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

==Filter 过滤==

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

配置

  1. 新建 cloud-gateway-gateway9527

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    <dependencies>
    <!--gateway-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</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>
  3. YML

    Gateway 也可以用Bean注入方式进行配置,不一定使用YML。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    server:
    port: 9527
    spring:
    application:
    name: cloud-gateway
    cloud:
    gateway:
    routes:
    - id: payment_route # 路由的 ID ,没有固定规则但要求唯一
    uri: http://localhost:8001 # 匹配后提供服务的路由地址
    predicates:
    - Path=/payment/get/** # 断言,路径相匹配则进行路由
    - id: payment_route2
    uri: http://localhost:8001
    predicates:
    - Path=/payment/feign/timeout


    eureka:
    instance:
    hostname: cloud-gateway-service
    client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableEurekaClient
    public class GateWayMain9527 {
    public static void main(String[] args) {
    SpringApplication.run(GateWayMain9527.class,args);
    }
    }
  5. 测试

    启动7001、cloud-provider-payment8001、9527,之后访问 http://localhost:9527/payment/get/5 进行测试。

  6. 新建配置类

    演示使用Bean注入方式配置route。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Configuration
    public class GateWayConfig {
    /**
    * 当访问地址 http://localhost:9527/guonei 时会自动转发到地址: http://news.baidu.com/guonei
    * 当访问地址 http://localhost:9527/guoji 时会自动转发到地址: http://news.baidu.com/guoji
    */
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder){
    RouteLocatorBuilder.Builder routes = builder.routes();
    routes.route("route3",r->r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
    return routes.build();
    }

    @Bean
    public RouteLocator customRouteLocator2(RouteLocatorBuilder builder){
    RouteLocatorBuilder.Builder routes = builder.routes();
    routes.route("route4",r->r.path("/guoji").uri("http://news.baidu.com/guoji")).build();
    return routes.build();
    }
    }
  7. 测试

    分别访问 http://localhost:9527/guoneihttp://localhost:9527/guoji

    image-20220814084036802

动态路由

以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

  1. 修改YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    server:
    port: 9527
    spring:
    application:
    name: cloud-gateway
    cloud:
    gateway:
    discovery:
    locator:
    enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
    routes:
    - id: payment_route
    # uri: http://localhost:8001
    uri: lb://cloud-payment-service # 需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。
    predicates:
    - Path=/payment/get/**
    - id: payment_route2
    # uri: http://localhost:8001
    uri: lb://cloud-payment-service
    predicates:
    - Path=/payment/feign/timeout


    eureka:
    instance:
    hostname: cloud-gateway-service
    client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
    defaultZone: http://eureka7001.com:7001/eureka
  2. 测试

    启动7001、8001、8002、9527,多次访问 http://localhost:9527/payment/get/5 。会发现轮流调用8001和8002的接口。

Predicate

Predicate就是为了实现一组匹配规则

当启动9527模块控制台:

image-20220814091510619

其中 Path 在上面已经使用过了。

==常用 Predicate==

  • After Route Predicate

    image-20220814103006117

    时间格式可通过编写代码获得:

    image-20220814103836432

  • Before Route Predicate

    与After相反,给定时间之前才能对路由访问。

  • Between Route Predicate

    代表在给定时间范围内才能对路由访问。

    1
    2
    3
    4
    predicates:
    - Path=/payment/get/**
    # - After=2020-02-05T15:10:03.685+08:00[Asia/Shanghai]
    - Between=2020-02-02T17:45:06.206+08:00[Asia/Shanghai],2021-03-25T18:59:06.206+08:00[Asia/Shanghai]
  • Cookie Route Predicate

    两个参数:一个cookie名称,一个是正则表达式或属性值。

    1
    2
    3
    predicates:
    - Path=/payment/get/**
    - Cookie=username,swj

    image-20220814105550002

  • Header Route Predicate

    两个参数:一个是属性名称和一个正则表达式或属性值。

    1
    - Header=X-Request-Id, \d+  #  请求头要有 X-Request-Id 并且值为整数的正则表达式 

    image-20220814110115203

  • Host Route Predicate

    它通过参数中的主机地址作为匹配规则。 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板。

    1
    - Host=**.sscarf.com,www.baidu.com

    image-20220814110851717

  • Method Route Predicate

    匹配请求方法。

    1
    - Method=GET
  • Path Route Predicate

    匹配访问路径,之前已经演示了。

  • Query Route Predicate

    匹配参数。支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式。

    1
    - Query=username, \d+   #  要有参数名 username 并且值还要是整数才能路由

Filter

路由过滤器可用于修改进入的HTTP请求(pre)或返回的HTTP响应(post),路由过滤器只能指定路由进行使用。

==种类==

  • GatewayFilter

    31种之多,地址

  • GlobalFilter

    image-20220814111847767

==GatewayFilter 示例==

1
2
- AddRequestParameter=X-Request-Id,1024 # 过滤器工厂会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 1024
- AddResponseHeader=X-Response-Red, Blue # 给响应的内容加上请求头

image-20220814112746209

image-20220814112700343

其它请查看官网。

==自定义过滤器==

自定义全局 GlobalFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class MyLogGateWayFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("time:"+new Date()+"\t执行了自定义的全局过滤器:"+"MyLogGateWayFilter"+"hello");
String username = exchange.getRequest().getQueryParams().getFirst("username");
if (username == null){
System.out.println("用户名为 null 无法登陆");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

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

image-20220814113845849

Config

概述

分布式配置中心。

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。

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

image-20220814162907730

SpringCloud Config分为 服务端客户端两部分:

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

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

==能干嘛?==

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

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

服务端配置

  1. 在 github 新建仓库 springcloud-config(公开)

  2. 克隆到本地

    1
    git clone git@github.com:keep-out-the-cold/springcloud-config.git
  3. 新建文件

    在本地仓库新建 config.yml 和 config-test.yml 文件添加一些内容并上传到 github。文件格式必须为UTF-8。

    上传:

    1
    2
    3
    git add .
    git commit -m "..."
    git push
  4. 新建模块 cloud-config-center-3344

  5. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <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>
  6. YML

    仓库不公开还要配置账号密码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    server:
    port: 3344
    spring:
    application:
    name: cloud-config-center
    cloud:
    config:
    server:
    git:
    uri: https://github.com/keep-out-the-cold/springcloud-config.git # 仓库(ssh报错,所以用https)
    # 目录
    search-paths:
    - springcloud-config
    default-label: main # 如果是 gitee 则改为 master

    eureka:
    client:
    service-url:
    defaultZone: http://localhost:7001/eureka
  7. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableConfigServer
    public class ConfigCenterMain3344 {
    public static void main(String[] args) {
    SpringApplication.run(ConfigCenterMain3344.class,args);
    }
    }
  8. 测试(启动7001、3344)

    访问 http://localhost:3344/main/config-dev.yml

    image-20220814190611854

    访问 http://localhost:3344/main/config-111.yml

    image-20220814190801124

    访问 http://localhost:3344/main/config-test.yml

    image-20220814190828404

    访问规则如下:

    1
    2
    3
    4
    5
    /{application}/{profile}[/{label}]
    /{application}-{profile}.yml
    /{label}/{application}-{profile}.yml # 上面使用的规则
    /{application}-{profile}.properties
    /{label}/{application}-{profile}.properties

    又新增了 cloud-config-center-dev.yml 文件(第二个规则)测试了一下,在测试的时候并没有重启idea项目,将文件push到github后直接浏览器访问就能看到。说明服务不需要重启即可感知到配置的变化并应用新的配置。

    image-20220814192221524

客户端配置

  1. 新建模块 cloud-config-client-3355

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</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>
  3. bootstrap.yml

    applicaiton.yml是用户级的资源配置项;bootstrap.yml是系统级的,优先级更加高。

    要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的, 因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    server:
    port: 3355
    spring:
    application:
    name: config-client
    cloud:
    config:
    label: main # 分支
    name: config # 配置文件名称
    profile: test # 读取后缀
    uri: http://localhost:3344 # 配置中心地址
    # 综上,http://localhost:3344/main/config-test.yml
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:7001/eureka
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableEurekaClient
    public class ConfigClientMain3355 {
    public static void main(String[] args) {
    SpringApplication.run(ConfigClientMain3355.class,args);
    }
    }
  5. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class ConfigClientController {

    @Value("${hello}") // hello为github上对应配置文件里头的配置信息
    private String configInfo;

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

    启动7001、3344、3355,访问 http://localhost:3355/configInfo

    image-20220814211439662

  7. 问题

    当修改了github上的配置文件,服务端(3344)会同步修改,但是客户端(3355)却没有同步修改。

    image-20220814212732350

客户端动态刷新

解决上述问题

  1. 确保引入下面依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
  2. 修改YML

    添加如下内容:

    1
    2
    3
    4
    5
    6
    # 暴露监控端点
    management:
    endpoints:
    web:
    exposure:
    include: "*"
  3. Controller添加注解

    image-20220814215458058

  4. 测试

    重启3355,修改github上配置文件,访问 http://localhost:3355/configInfo 还是没有变化,需要先POST访问 http://localhost:3355/actuator/refresh,再访问才能有变化。

    image-20220814215709205

    image-20220814215717910

    还是有点鸡肋💩

Bus

消息总线

概述

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

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

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

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

配置RabbitMQ

全局广播

演示广播效果,以3355为模板再制作一个3366

  1. 新建模块 cloud-config-client-3366

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    server:
    port: 3366
    spring:
    application:
    name: config-client
    cloud:
    config:
    label: main # 分支
    name: config # 配置文件名称
    profile: test # 读取后缀
    uri: http://localhost:3344 # 配置中心地址
    # 综上,http://localhost:3344/main/config-test.yml
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:7001/eureka
    # 暴露监控端点
    management:
    endpoints:
    web:
    exposure:
    include: "*"
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableEurekaClient
    public class ConfigClientMain3366 {
    public static void main(String[] args) {
    SpringApplication.run(ConfigClientMain3366.class,args);
    }
    }
  5. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @RestController
    @RefreshScope
    public class ConfigClientController {

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

    @Value ( "${hello}" )
    private String configInfo ;

    @GetMapping("/configInfo")
    public String configInfo(){
    return "serverPort:"+serverPort+"\tconfigInfo:"+configInfo;
    }
    }

设计细想

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

image-20220815092940979

给 cloud-config-center-3344 配置中心服务端添加消息总线支持

  1. 添加依赖

    1
    2
    3
    4
    5
    <!-- 添加消息总线 RabbitMQ 支持 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
  2. 添加配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    spring:
    # rabbitmq 相关配置
    rabbitmq:
    host: 120.48.54.126
    port: 5672
    username: admin
    password: admin
    # rabbitmq 相关配置 , 暴露 bus 刷新配置的端点
    management:
    endpoints:
    web:
    exposure:
    include: 'bus-refresh'

给 cloud-config-client-3355 和 cloud-config-client-3366 客户端添加消息总线支持

  1. 依赖

    同上

  2. 配置

    同上,需要注意的是客户端的 include 为 “*”,也就包含了 ‘bus-refresh’,所以不用改。

测试

  1. 启动

    7001、3344、3355、3366

  2. 修改 Github 上的配置文件

    此时访问 http://localhost:3344/main/config-test.yml 可以看到更改后的内容;

    但是访问 http://localhost:3355/configInfohttp://localhost:3366/configInfo 则还没有更改。

  3. 发送 POST 请求

    1
    http://localhost:3344/actuator/bus-refresh

    image-20220815095530706

  4. 测试

    再次访问 http://localhost:3355/configInfohttp://localhost:3366/configInfo 内容已经刷新。

定点通知

例:只通知 3355 不通知 3366

==公式==

1
http://localhost:配置中心端口号/actuator/bus-refresh/{destination}

==测试==

  1. 修改 Github 配置文件

  2. POST 访问 http://localhost:3344/actuator/bus-refresh/config-client:3355

  3. 效果

    image-20220815100527389

Stream

消息驱动

概述

屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。应用程序通过 inputs 或者 outputs 来与 Spring Cloud Stream中binder 对象交互。 通过我们配置来binding(绑定) ,而 Spring Cloud Stream 的 binder 对象负责与消息中间件交互。所以,只需要搞清楚如何与 Spring Cloud Stream 交互就可以方便使用消息驱动的方式。就像 jdbc 可以操作 mysql、oracle、sqlserver等数据库,就是对各类 MQ 操作的封装。目前仅支持RabbitMQ、KafkaStream中的消息通信方式遵循了发布-订阅模式

如果用了两个消息队列的其中一种,因为后面的业务需求,需要往另外一种消息队列进行迁移,这时候无疑就是一个灾难性的, 一大堆东西都要重新推倒重做,因为它跟系统耦合了,这时候 springcloud Stream 就提供了一种解耦合的方式。

==stream凭什么可以统一底层差异?==

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

在没有绑定器这个概念的情况下,SpringBoot 应用要直接与消息中间件进行信息交互的时候, 由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,实现了应用程序与消息中间件细节之间的隔离。 通过向应用程序暴露统一的 Channel 通道,使应用程序不需要再考虑各种不同的消息中间件实现。

==应用模型==

image-20220815114937737

image-20220815111148485

  • Middleware:各类消息中间件

  • Binder

    用来与中间件连接,不同的 Binder 对应不同的中间件,比如Kafka的实现KafkaMessageChannelBinder,RabbitMQ的实现RabbitMessageChannelBinder。Binder 可以生成 Binding,Binding 用来绑定消息容器的生产者和消费者,它有两种类型,INPUT 和 OUTPUT,INPUT 对应于消费者,OUTPUT 对应于生产者。

  • Application Core:生产者、消费者

  • inputs:消息输入通道

  • outputs:消息输出通道

生产者

  1. 新建 cloud-stream-rabbitmq-provider8801

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    server:
    port: 8801
    spring:
    application:
    name: cloud-stream-provider
    rabbitmq:
    host: 120.48.54.126
    port: 5672
    username: admin
    password: admin
    cloud:
    stream:
    binders:
    defaultRabbit:
    type: rabbit
    bindings:
    output:
    destination: studyExchange # 表示要使用的 Exchange 名称定义
    content-type: application/json # 设置消息类型
    binder: defaultRabbit
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:7001/eureka
    instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是 30 秒)
    lease-expiration-duration-in-seconds: 5 # 超时时间(默认是 90 秒),超时未收到心跳则移除instance
    instance-id: send-8801.com # 在信息列表时显示主机名称
    prefer-ip-address: true # 访问的路径变为 IP 地址
  4. 主启动类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class StreamMQMain8801 {
    public static void main(String[] args) {
    SpringApplication.run(StreamMQMain8801.class,args);
    }
    }
  5. Service 接口

    1
    2
    3
    public interface IMessageProvider {
    String send();
    }
  6. 接口实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package com.cuc.springcloud.service.impl;

    import com.cuc.springcloud.service.IMessageProvider;
    import org.springframework.cloud.stream.annotation.EnableBinding;
    import org.springframework.cloud.stream.messaging.Source;
    import org.springframework.integration.support.MessageBuilder;
    import org.springframework.messaging.MessageChannel;

    import javax.annotation.Resource;
    import java.util.UUID;

    @EnableBinding(Source.class)
    public class MessageProviderImpl implements IMessageProvider {

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

    @Override
    public String send() {
    String serial = UUID.randomUUID().toString();
    this.output.send(MessageBuilder.withPayload(serial).build()); // 创建并发送消息
    return serial;
    }
    }
  7. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class SendMessageController {

    @Resource
    private IMessageProvider messageProvider;

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

    启动 MQ、7001、8801,访问 http://localhost:8801/sendMessage

    image-20220815122947783

消费者

  1. 新建 cloud-stream-rabbitmq-consumer8802

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    server:
    port: 8802
    spring:
    application:
    name: cloud-stream-consumer
    rabbitmq:
    host: 120.48.54.126
    port: 5672
    username: admin
    password: admin
    cloud:
    stream:
    binders:
    defaultRabbit:
    type: rabbit
    bindings:
    input:
    destination: studyExchange # 表示要使用的 Exchange 名称定义
    content-type: application/json # 设置消息类型
    binder: defaultRabbit
    eureka:
    client:
    service-url:
    defaultZone: http://localhost:7001/eureka
    instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是 30 秒)
    lease-expiration-duration-in-seconds: 5 # 超时时间(默认是 90 秒),超时未收到心跳则移除instance
    instance-id: receive-8802.com # 在信息列表时显示主机名称
    prefer-ip-address: true # 访问的路径变为 IP 地址
  4. 主启动类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class StreamMQMain8802 {
    public static void main(String[] args) {
    SpringApplication.run(StreamMQMain8802.class,args);
    }
    }
  5. 业务类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package com.cuc.springcloud.service;

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.stream.annotation.EnableBinding;
    import org.springframework.cloud.stream.annotation.StreamListener;
    import org.springframework.cloud.stream.messaging.Sink;
    import org.springframework.messaging.Message;
    import org.springframework.stereotype.Component;

    @Component
    @EnableBinding(Sink.class)
    public class ReceiveMessageListener {

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

    @StreamListener(Sink.INPUT)
    public void input(Message<String> message){
    System.out.println(serverPort+"接受到的消息:"+message.getPayload());
    }
    }
  6. 测试

    访问 http://localhost:8801/sendMessage

    image-20220815124913017

    image-20220815124923797

分组与持久化

依照8802,clone出来一份运行8803

  1. 新建 cloud-stream-rabbitmq-consumer8803

  2. 代码同 8802,改下端口号啥的

  3. 测试

    image-20220815125734491

    image-20220815125746580

    image-20220815125801955

  4. 问题

    • 重复消费
    • 消息持久化问题(看下面持久化就懂了)

分组

微服务应用放置于同一个 group 中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。

  1. 修改YML,将 8802、8803 分在不同组

    下图为修改位置:

    image-20220815140439968

  2. 重启项目

    可以看到不同组分在不同队列,故 studyExchange 交换机会将消息发送到这两个队列上,从而导致重复消费,默认应该就是分在不同组的。

    image-20220815140408506

  3. 修改YML,将 8802、8803 分在同一组

  4. 测试发现 8002、8003 会轮询消费队列上的消息,并没有重复消费。

持久化

  1. 停止 8802、8803 并去除掉 8802 的分组配置
  2. 8801 先发送4条消息到 rabbitmq
  3. 先启动 8802(无分组属性配置),后台没有打印消息
  4. 再启动 8803(有分组属性配置),后台打出来了MQ上的消息

综上,想要持久化配置分组即可。

Sleuth

分布式请求链路跟踪

概述

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

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

image-20220815144740843

搭建链路监控

Zipkin

  1. 下载 zipkin

    地址

    image-20220815151252467

  2. 运行

    1
    java -jar zipkin-server-2.23.18-exec.jar

    image-20220815151125915

  3. 访问 http://localhost:9411/zipkin/

    image-20220815151330202

服务提供者,修改cloud-provider-payment8001

  1. 添加依赖

    1
    2
    3
    4
    5
    <!-- 包含了 sleuth+zipkin-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
    </dependency>
  2. 添加配置

    1
    2
    3
    4
    5
    6
    7
    spring:
    zipkin:
    base-url: http://localhost:9411
    sleuth:
    sampler:
    # 采样率值介于 0 到 1 之间,1 表示全部采集
    probability: 1
  3. 添加 Controller

    1
    2
    3
    4
    @GetMapping("/payment/zipkin")
    public String paymentZipkin(){
    return "PaymentZipkin server fall back";
    }

服务消费者,修改cloud-consumer-order80

  1. 添加依赖(同上)

  2. 添加配置(同上)

  3. 添加 Controller

    1
    2
    3
    4
    5
    @GetMapping("/consumer/payment/zipkin")
    public String paymentZipkin(){
    String result = restTemplate.getForObject("http://localhost:8001/payment/zipkin/", String.class);
    return result;
    }

测试

启动 7001、8001、80,多次访问 http://localhost/consumer/payment/zipkin

image-20220815153749764

可进一步查看详情。。

Alibaba

为什么出现 Spring Cloud Alibaba?

Spring Cloud Netflix 项目进入维护模式,这意味着将模块置于维护模式, Spring Cloud 团队将不会再向模块添加新功能。


以下均是 Spring Cloud Alibaba 内容。

Nacos

概述

Nacos就是注册中心 + 配置中心的组合。一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos = Eureka+Config +Bus,Nacos 支持APCP模式的切换。

安装

基于 Mac

  1. 下载

    https://github.com/alibaba/nacos/releases

    image-20220816075937875

  2. 解压

    1
    tar -zxvf 压缩包名称
  3. bin 目录下,启动

    单机启动命令:

    1
    sh startup.sh -m standalone
  4. 访问

    http://127.0.0.1:8848/nacos ,账号/密码:nacos

  5. bin 目录下,停止

    1
    sh shutdown.sh 

注册中心

服务提供者
  1. 新建 cloudalibaba-provider-payment9001

  2. 父POM

    之前应该导过了。

    1
    2
    3
    4
    5
    6
    7
    8
    <!--springcloudalibaba2.1.0.RELEASE-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.1.0.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
  3. 本模块 POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
    <groupId>org.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>
  4. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    server:
    port: 9001
    spring:
    application:
    name: nacos-payment-provider
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 # 配置 Nacos 地址
    management:
    endpoints:
    web:
    exposure:
    include: "*"
  5. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableDiscoveryClient
    public class PaymentMain9001 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentMain9001.class,args);
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class PaymentController {

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

    @GetMapping("/payment/nacos/{id}")
    public String getPayment(@PathVariable("id") Integer id){
    return "nacos registry,serverPort:"+serverPort+"\tid"+id;
    }
    }
  7. 测试

    启动 9001,访问 http://localhost:9001/payment/nacos/22 进行测试,同时:

    image-20220816083217485

    此时 注册中心 和 服务提供者 都已经完成了。

  8. 为了下一章节演示 nacos 的负载均衡,参照9001新建9002(略)

服务消费者
  1. 新建 cloudalibaba-consumer-nacos-order83

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
    </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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server:
    port: 83
    spring:
    application:
    name: nacos-order-consumer
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848

    # 消费者将要去访问的微服务名称
    service-url:
    nacos-user-service: http://nacos-payment-provider
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableDiscoveryClient
    public class OrderNacosMain83 {

    public static void main(String[] args) {
    SpringApplication.run(OrderNacosMain83.class,args);
    }
    }
  5. 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class ApplicationContextBean {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
    return new RestTemplate();
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @RestController
    public class OrderNacosController {

    @Resource
    private RestTemplate restTemplate;

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

    @GetMapping("/consumer/payment/nacos/{id}")
    public String paymentInfo(@PathVariable("id") Integer id){
    return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);
    }
    }
  7. 测试

    启动 nacos、9001、9002、83,多次访问 http://localhost:83/consumer/payment/nacos/66 ,发现轮询9001和9002,故负载均衡👌。

    为什么 nacos 支持负载均衡?

    image-20220816085610182

配置中心

基础配置
  1. 新建 cloudalibaba-config-nacos-client3377

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <dependencies>
    <!--nacos-config-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
    <groupId>org.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>
  3. YML

    1)bootstrap.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # nacos 配置
    server:
    port: 3377
    spring:
    application:
    name: nacos-config-client
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 # Nacos 服务注册中心地址
    config:
    server-addr: localhost:8848 # Nacos 作为配置中心地址
    file-extension: yaml # 指定 yaml 格式的配置(仅支持 yaml和 properties)

    # ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
    # nacos-config-client-dev.yaml 后缀不能是 yml

    2)application.yml

    1
    2
    3
    spring:
    profiles:
    active: dev # 表示开发环境

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

  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableDiscoveryClient
    public class NacosConfigClientMain3377 {
    public static void main(String[] args) {
    SpringApplication.run(NacosConfigClientMain3377.class,args);
    }
    }
  5. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    @RefreshScope // 使当前类下的配置支持 Nacos 的动态刷新功能。
    public class ConfigClientController {

    @Value("${hello}")
    private String configInfo;

    @GetMapping("/config/info")
    public String getConfigInfo(){
    return configInfo;
    }
    }
  6. 在 Nacos 中添加配置信息

    image-20220816101655568

    image-20220816101717960

  7. 测试

    启动 3377,自带动态刷新,修改下Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经刷新。

    image-20220816101819933

分类配置

问题:

  • 实际开发中,通常一个系统会准备 dev开发环境、test测试环境、prod生产环境。如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件?
  • 一个大型分布式微服务系统会有很多微服务子项目, 每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境,那怎么对这些微服务配置进行管理?

==Nacos的图形化管理界面==

image-20220816102717871

默认情况: Namespace=public,Group=DEFAULT_GROUP,默认 Cluster 是 DEFAULT

  • Namespace 主要用来实现隔离。 比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个 Namespace,不同的Namespace 之间是隔离的。
  • Group 默认是 DEFAULT_GROUP,Group 可以把不同的微服务划分到同一个分组里面去
  • Service 就是微服务。一个 Service 可以包含多个 Cluster(集群),Nacos 默认 Cluster 是 DEFAULT,Cluster 是对指定微服务的一个虚拟划分。
  • Instance,就是微服务的实例。

==三种方案加载配置==

  • DataID(Nacos上 具体配置文件)

    通过在 YML 配置指定配置文件的 DataID 来使不同环境下读取不同的配置。

  • Group

    image-20220816104114725

    image-20220816104306383

  • Namespace

    1)新增命名空间

    image-20220816104503675

    2)回到服务列表查看

    image-20220816104539906

    3)YML 配置

    image-20220816104712162

集群和持久化

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

==支持三种部署模式:==

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

==环境准备==

  • 安装好 JDK,需要 1.8 及其以上版本
  • 建议: 2核 CPU / 4G 内存 及其以上
  • 建议: 生产环境 3 个节点 及其以上

==集群部署==

Linux 下 Nacos + MySQL 配置。部署手册

要求:

1个Nginx + 3个nacos注册中心 + 1个mysql

  1. 安装 nginx、nacos、mysql

  2. 在 nacos/conf 的 application.properties 文件添加如下内容:

    1
    2
    3
    4
    5
    spring.datasource.platform=mysql
    db.num=1
    db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
    db.user=root
    db.password=root
  3. 在 nacos/conf 下配置 cluster.conf

    1
    cp cluster.conf.example cluster.conf

    cluster.conf 内容如下:

    1
    2
    3
    120.48.54.126:3333
    120.48.54.126:4444
    120.48.54.126:5555

    给三台 nacos 配置端口,取消默认的 8848。ip 不能写 localhost 或 127.0.0.1。

  4. mysql 建表

    1)本地 Navicat 连接 Linux 下的 mysql,自行百度

    2)创建 nacos_config 数据库(urf8)

    3)在数据库运行 nacos/conf 目录下的 nacos-mysql.sql 脚本

    image-20220816141502087

  5. 编辑 nacos/bin 目录下的启动脚本 startup.sh,使它能够接受不同的启动端口

    添加o:,其它字母也行,只要不跟前面的重复:

    image-20220816143743891

    image-20220816144049656

    之后可通过一下命令启动指定 nacos:

    1
    startup.sh -p 端口号
  6. 配置 Nginx,由它作为负载均衡器

    修改 nginx.conf

    image-20220816144907301

  7. 测试

    访问 http://120.48.54.126:1111/nacos ,访问报错😓。等哪天Vmware适配了m1,在虚拟机上再试试,也可以试试其它搭建方法。

==集群和持久化很重要==

Sentinel

实现熔断与限流。等同于 Hystrix

安装

  1. 下载

    地址

    image-20220816164213290

  2. 启动

    1
    java -jar sentinel-dashboard-1.8.5.jar
  3. 访问 http://localhost:8080,账号/密码:sentinel

演示工程

  1. 新建 cloudalibaba-sentinel-service8401

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <dependencies>
    <!--SpringCloud ailibaba sentinel -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--sentinel-datasource-nacos 后续做持久化用到 -->
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
    <!--openfeign-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
    </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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    server:
    port: 8401
    spring:
    application:
    name: cloudalibaba-sentinel-service
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848
    sentinel:
    transport:
    dashboard: localhost:8080
    # 默认 8719 端口,假如被占用会自动从 8719 开始依次 +1 扫描 , 直至找到未被占用的端口
    port: 8719
    management:
    endpoints:
    web:
    exposure:
    include: "*"
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableDiscoveryClient
    public class MainApp8401 {

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RestController
    public class FlowLimitController {

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

    @GetMapping("/testB")
    public String testB(){
    return "TestB";
    }
    }
  6. 测试

    启动 nacos、sentinel、8401,这时候查看sentinel控制台还是空的,因为sentinel采用的懒加载。

    先访问一下路由:http://localhost:8401/testA。再访问sentinel控制台:

    image-20220816170545708

    此时 sentinel8080 正在监控微服务8401。

流控规则

image-20220816175115908

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

==流控模式==

直接+快速失败

image-20220816181701215

浏览器在一秒内超过一次访问 http://localhost:8401/testA ,会抛异常:

image-20220816181032348

关联+直接失败

image-20220816181537123

当与A关联的资源B达到阀值后,限流A。postman 模拟并发密集访问testB(不然手动速度没那么快):

  1. 新建多线程集合组

    image-20220816182727801

  2. 将访问地址添加进线程组

    image-20220816182858565

    image-20220816182938326

  3. Run

    image-20220816183118046

    image-20220816183200556

    image-20220816183229964

链路+直接失败

例如有两条请求链路:

1
2
/test1/common
/test2/common

如果只希望统计从 /test2 进入到 /common 的请求,对 /test2 进行限流,则可以这样配置:

image-20220816185829742

自行测试。

==流控效果==

直接失败

Blocked by Sentinel (flow limiting)

Warm Up

QPS一开始为阈值除以coldFactor(默认值为3),一旦触发,经过预热时长后才会达到设定阈值。

image-20220816191529518

上图为例,一开始 QPS=10/3=3,也就是每秒请求数最多3个,超过则限流,但是与此同时,在之后的 5 秒内,QPS 会慢慢上升直到 10。

应用场景

秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值。

排队等待

匀速排队,阈值必须设置为QPS。

以下图为例,当QPS为2时,每隔500ms才允许通过下一个请求。其它未通过请求则进行等待。最多等待20秒。

image-20220816195454844

image-20220816200131837

每0.3秒访问一次,一秒请求数超过了2次,因为请求会进行等待,所以不会报错。

image-20220816200322023

可以看到10个请求都成功。

熔断规则

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

==慢调用比例==

image-20220817083255462

慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。

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

==异常比例==

image-20220817084432544

异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

==异常数==

image-20220817085132256

异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。

热点规则

热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作 。

image-20220817091051845

image-20220817091338129

1
2
3
4
5
6
7
8
9
10
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey",blockHandler = "dealHandler_testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2){
return "testHotKey";
}

public String dealHandler_testHotKey(String p1, String p2, BlockException exception){
return "dealHandler_testHotKey";
}

上面的抓图代表第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用 dealHandler_testHotKey 支持方法。

试着去访问:

1
2
3
http://localhost:8401/testHotKey?p1=aaa
http://localhost:8401/testHotKey?p2=aaa
http://localhost:8401/testHotKey?p1=aaa&p2=bbb

==参数例外项==

image-20220817092300983

期望p1参数当它是某个特殊值时,它的限流值和平时不一样,当p1的值等于5时,它的阈值可以达到200。

系统规则

image-20220817092758662

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

系统保护规则是应用系统维度的,而不是资源维度,并且仅对入口流量生效。入口流量指的是进入应用的流量,比如 WEB 服务或 Dubbo 服务端接受的请求,都属于入口流量。

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

@SentinelResource

按资源名称限流
  1. 8401 新增 Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RestController
    public class RateLimitController {

    @GetMapping("/byResource")
    @SentinelResource(value = "byResource",blockHandler = "handleException")
    public CommonResult byResource(){
    return new CommonResult(200,"按资源名称限流测试 OK",new Payment(2022L,"serial001"));
    }

    public CommonResult handleException(BlockException exception){
    return new CommonResult(444,exception.getClass().getCanonicalName()+"\t服务不可用");
    }
    }
  2. 添加流控规则

    image-20220817101821723

  3. 测试

    每秒超过1次访问:

    image-20220817101920655

按URL地址限流

通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息

  1. Controller

    1
    2
    3
    4
    5
    @GetMapping("/rateLimit/byUrl")
    @SentinelResource(value = "byUrl")
    public CommonResult byUrl(){
    return new CommonResult(200,"按 url 限流测试 OK");
    }
  2. 配置流控规则

    image-20220817102814704

  3. 测试

    每秒超过一次访问:

    image-20220817102851736

面临的问题
  • 系统默认的,没有体现自己的业务要求
  • 依照现有条件,自定义的处理方法又和业务代码耦合在一块
  • 每个业务方法都添加一个兜底的,那代码膨胀加剧
  • 全局统一的处理方法没有体现
自定义限流处理逻辑
  1. 自定义限流处理类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.cuc.springcloud.handler;

    import com.alibaba.csp.sentinel.slots.block.BlockException;
    import com.cuc.springcloud.entities.CommonResult;

    public class CustomerBlockHandler {

    public static CommonResult handleException2(BlockException exception){
    return new CommonResult(2022,"定义的限流处理");
    }
    }
  2. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    // 自定义处理逻辑
    @GetMapping("/rateLimit/customerBlockHandler")
    @SentinelResource(value = "customerBlockHandler",
    blockHandlerClass = CustomerBlockHandler.class,
    blockHandler = "handleException2")
    public CommonResult customerBlockHandler(){
    return new CommonResult(200,"客户自定义限流处理逻辑");
    }
  3. 自行测试

属性说明

注意:注解方式埋点不支持 private 方法。

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

  • value:资源名称,必需项(不能为空)
  • entryType:entry 类型,可选项(默认为 EntryType.OUT
  • blockHandler / blockHandlerClass: blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • fallback / fallbackClass:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

服务熔断功能

sentinel 整合 ribbon+openFeign

服务提供者
  1. 新建 cloudalibaba-provider-payment9003

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    <dependencies>
    <!--SpringCloud ailibaba nacos -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
    </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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    server:
    port: 9003
    spring:
    application:
    name: nacos-payment-provider
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848
    management:
    endpoints:
    web:
    exposure:
    include: "*"
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableDiscoveryClient
    public class PaymentMain9003 {
    public static void main(String[] args) {
    SpringApplication.run(PaymentMain9003.class,args);
    }
    }
  5. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @RestController
    public class PaymentController {

    @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("/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
    Payment payment = hashMap.get(id);
    CommonResult<Payment> result = new CommonResult(200, "from mysql,serverPort:" + serverPort, payment);
    return result;
    }
    }
  6. 同理再新建 cloudalibaba-provider-payment9004

服务消费者
  1. 新建 cloudalibaba-consumer-nacos-order84

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    <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>com.cuc</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
    </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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    server:
    port: 84
    spring:
    application:
    name: nacos-order-consumer
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848
    sentinel:
    transport:
    dashboard: localhost:8080
    port: 8719
  4. 主启动类

    1
    2
    3
    4
    5
    6
    7
    @SpringBootApplication
    @EnableDiscoveryClient
    public class OrderNacosMain84 {
    public static void main(String[] args) {
    SpringApplication.run(OrderNacosMain84.class,args);
    }
    }
  5. 配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Configuration
    public class ApplicationContextConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
    return new RestTemplate();
    }
    }
  6. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @RestController
    public class CircleBreakerController {

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

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")
    public CommonResult<Payment> fallback(@PathVariable("id") Long id){
    CommonResult result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class);
    if (id==4){
    throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常 ....");
    }else if (result.getData()==null){
    throw new NullPointerException("NullPointerException, 该 ID 没有对应记录 , 空指针异常");
    }
    return result;
    }
    }
  7. 测试

    image-20220817135012867

    image-20220817135022570

Ribbon系列
  1. 修改 Controller

    image-20220817135640501

  2. 测试

    image-20220817135657109

    image-20220817135707849

  3. 再次修改Controller

    此时fallbackblockHandler同时存在

    image-20220817140424410

  4. 配置流控规则

    image-20220817140644864

  5. 测试

    访问 http://localhost:84/consumer/fallback/4 ,刚开始走抛异常逻辑,若每秒请求超过一次,则走限流逻辑。

  6. 继续修改

    image-20220817141720142

  7. 测试

    image-20220817141838403

Feign系列
  1. 添加依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. YML添加配置

    1
    2
    3
    4
    # 激活 Sentinel 对 Feign 的支持
    feign:
    sentinel:
    enabled: true
  3. Service 接口

    1
    2
    3
    4
    5
    6
    @FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
    public interface PaymentService {

    @GetMapping("/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
    }
  4. 接口实现

    1
    2
    3
    4
    5
    6
    7
    @Component
    public class PaymentFallbackService implements PaymentService{
    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
    return new CommonResult(444,"服务降级返回",new Payment(id,null));
    }
    }
  5. Controller

    1
    2
    3
    4
    5
    6
    7
    @Resource
    private PaymentService paymentService;

    @GetMapping("/consumer/openfeign/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
    return paymentService.paymentSQL(id);
    }
  6. 主启动类添加注解 @EnableFeignClients

  7. 测试

    启动84,启动后将所有服务提供者都关掉,之后访问 http://localhost:84/consumer/openfeign/1

    image-20220817144032173

规则持久化

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

  1. 修改 cloudalibaba-sentinel-service8401

  2. 添加依赖(之前就有了)

    1
    2
    3
    4
    <dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
    </dependency>
  3. YML 添加配置

    image-20220817145528309

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    spring:
    cloud:
    sentinel:
    datasource:
    ds1:
    nacos:
    server-addr: localhost:8848
    data-id: cloudalibaba-sentinel-service
    group-id: DEFAULT_GROUP
    data-type: json
    rule-type: flow
  4. Nacos 新增配置

    image-20220817145711607

    1
    2
    3
    4
    5
    6
    7
    resource:资源名称;
    limitApp:来源应用;
    grade:阈值类型,0表示线程数,1表示QPS;
    count:单机阈值;
    strategy:流控模式,0表示直接,1表示关联,2表示链路;
    controlBehavior:流控效果,0表示快速失败,1表示Warm Up,2表示排队等待;
    clusterMode:是否集群。
  5. 启动8401刷新sentinel(如果没有,先快速访问被限流的地址,直到限流)

    image-20220817145819646

  6. 停止8401,sentinel上的规则消失

  7. 重启8401,规则还是没有,快速访问8401直到限流,刷新sentinel,规则又出现了。(sentinel懒加载)

Seata

分布式问题

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

概述

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

image-20220817160749541

  • XID:全局唯一的事务ID
  • TC:事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚
  • TM:控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议
  • RM:控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚

==处理过程==

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

==怎么玩?==

  • 本地 @Transactional
  • 全局 @GlobalTransactional

安装

  1. 下载(本次版本0.9.0)

    下载地址

  2. 解压并修改conf目录下的file.conf配置文件

    主要修改:自定义事务组名称+事务日志存储模式为db+数据库连接信息

    1)service模块

    image-20220817213437559

    2)store模块

    image-20220817220802346

  3. mysql5.7 数据库新建库 seata

  4. seata 库执行 conf 目录的 db_store.sql 脚本

    image-20220817162737552

  5. 修改 conf 目录下的 registry.conf

    image-20220817162942303

  6. 先启动Nacos,再启动 seata-server

    bin 目录下执行:

    1
    sh seata-server.sh 

数据库准备

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

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

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

  1. 创建数据库

    1
    2
    3
    seata_order
    seata_storage
    seata_account
  2. 建表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    USE seata_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 CHARSET=utf8;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    USE seata_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=7 CHARSET=utf8;
    INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    USE seata_account;
    CREATE TABLE t_account(
    id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    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=7 CHARSET=utf8;
    INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
  3. 按照上述3库分别建回滚日志表

    conf 目录的 db_undo_log.sql 脚本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    CREATE TABLE `undo_log` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `branch_id` bigint(20) NOT NULL,
    `xid` varchar(100) NOT NULL,
    `context` varchar(128) NOT NULL,
    `rollback_info` longblob NOT NULL,
    `log_status` int(11) NOT NULL,
    `log_created` datetime NOT NULL,
    `log_modified` datetime NOT NULL,
    `ext` varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

微服务准备

订单模块
  1. 新建 seata-order-service2001

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    <dependencies>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
    <exclusion>
    <groupId>seata-all</groupId>
    <artifactId>io.seata</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    <dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>0.9.0</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    server:
    port: 2001
    spring:
    application:
    name: seata-order-service
    cloud:
    alibaba:
    seata:
    # 自定义事务组名称需要与 seata-server 中的对应
    tx-service-group: fsp_tx_group
    nacos:
    discovery:
    server-addr: localhost:8848
    datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    feign:
    hystrix:
    enabled: false
    logging:
    level:
    io:
    seata: info
    mybatis:
    mapperLocations: classpath:mapper/*.xml
  4. 拷贝 seata/conf 目录下的 file.conf、registry.conf 到模块的 resources 目录下

    image-20220817220417720

  5. Domain

    1)CommonResult

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class CommonResult<T> {

    private Integer code;
    private String message;
    private T data;

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

    2)Order

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Order {

    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status;
    }
  6. OrderDao 接口

    1
    2
    3
    4
    5
    6
    7
    8
    @Mapper
    public interface OrderDao {
    // 创建订单
    void create(Order order);

    // 修改订单状态
    void update(@Param("userId")Long userId,@Param("status")Integer status);
    }
  7. OrderMapper.xml

    resources/mapper 目录下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <?xml version ="1.0" encoding ="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    <mapper namespace ="com.cuc.springcloud.dao.OrderDao" >

    <resultMap id="BaseResultMap" type="com.cuc.springcloud.domain.Order">
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="user_id" property="userId" jdbcType="BIGINT" />
    <result column="product_id" property="productId" jdbcType="BIGINT" />
    <result column="count" property="count" jdbcType="INTEGER" />
    <result column="money" property="money" jdbcType="DECIMAL" />
    <result column="status" property="status" jdbcType="INTEGER" />
    </resultMap>

    <insert id="create">
    INSERT INTO `t_order` (`id`, `user_id`, `product_id`, `count`, `money`, `status`)
    VALUES (NULL, #{userId}, #{productId}, #{count}, #{money}, 0);
    </insert>

    <update id="update">
    UPDATE `t_order`
    SET status = 1
    WHERE user_id = #{userId} AND status = #{status};
    </update>
    </mapper>
  8. Service

    1)OrderService

    1
    2
    3
    4
    public interface OrderService {
    // 创建订单
    void create(Order order);
    }

    2)StorageService

    1
    2
    3
    4
    5
    6
    @FeignClient(value = "seata-storage-service")
    public interface StorageService {
    // 减库存
    @PostMapping("/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count);
    }

    3)AccountService

    1
    2
    3
    4
    5
    6
    @FeignClient(value = "seata-account-service")
    public interface AccountService {
    // 扣减账户余额
    @PostMapping("/account/decrease")
    CommonResult decrease(@RequestParam("userId")Long userId, @RequestParam("money")BigDecimal money);
    }
  9. OrderService 实现类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    @Service
    @Slf4j
    public class OrderServiceImpl implements OrderService {

    @Resource
    private OrderDao orderDao;

    @Resource
    private StorageService storageService;

    @Resource
    private AccountService accountService;

    //第二个参数表示发生任何异常都进行回滚
    @Override
    @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
    public void create(Order order) {
    log.info("-------> 下单开始");
    // 创建订单
    orderDao.create(order);
    // 远程调用库存服务扣减库存
    log.info("------->order-service 中扣减库存开始");
    storageService.decrease(order.getProductId(),order.getCount());
    log.info("------->order-service 中扣减库存结束");
    // 远程调用账户服务扣减余额
    log.info("------->order-service 中扣减余额开始");
    accountService.decrease(order.getUserId(),order.getMoney());
    log.info("------->order-service 中扣减余额结束");
    // 修改订单状态为已完成
    log.info("------->order-service 中修改订单状态开始");
    orderDao.update(order.getUserId(),0);
    log.info("------->order-service 中修改订单状态结束");
    log.info( "-------> 下单结束");
    }
    }
  10. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("/order/create")
    public CommonResult create(Order order){
    orderService.create(order);
    return new CommonResult(200,"订单创建成功");
    }
    }
  11. 配置类

    1)MyBatisConfig

    1
    2
    3
    4
    @Configuration
    @MapperScan({"com.cuc.springcloud.dao"})
    public class MyBatisConfig {
    }

    2)DataSourceProxyConfig

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package com.cuc.springcloud.config;


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

    import javax.sql.DataSource;

    /**
    * 使用 Seata 对数据源进行代理
    */
    @Configuration
    public class DataSourceProxyConfig {

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

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
    return new DruidDataSource();
    }

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

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy)throws Exception{
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSourceProxy);
    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
    sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
    return sqlSessionFactoryBean.getObject();
    }
    }
  12. 主启动类

    1
    2
    3
    4
    5
    6
    7
    8
    @EnableDiscoveryClient
    @EnableFeignClients
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建
    public class SeataOrderMainApp2001 {
    public static void main(String[] args) {
    SpringApplication.run(SeataOrderMainApp2001.class,args);
    }
    }
库存模块
  1. 新建 seata-storage-service2002

  2. POM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    <dependencies>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    <exclusions>
    <exclusion>
    <groupId>seata-all</groupId>
    <artifactId>io.seata</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
    <dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-all</artifactId>
    <version>0.9.0</version>
    </dependency>
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.10</version>
    </dependency>
    <dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</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>
  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    server:
    port: 2002
    spring:
    application:
    name: seata-storage-service
    cloud:
    alibaba:
    seata:
    tx-service-group: fsp_tx_group
    nacos:
    discovery:
    server-addr: localhost:8848
    datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_storage?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    logging:
    level:
    io:
    seata: info
    mybatis:
    mapperLocations: classpath:mapper/*.xml
  4. 拷贝 seata/conf 目录下的 file.conf、registry.conf 到模块的 resources 目录下

  5. Domain

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class CommonResult<T> {

    private Integer code;
    private String message;
    private T data;

    public CommonResult(Integer code, String message){
    this(code,message,null);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Storage {
    private Long id;
    private Long productId; // 产品ID
    private Integer total; // 总库存
    private Integer used; // 已用库存
    private Integer residue; // 剩余库存
    }
  6. Dao接口及实现

    1
    2
    3
    4
    @Mapper
    public interface StorageDao {
    void decrease(@Param( "productId" ) Long productId,@Param ( "count" ) Integer count);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?xml version ="1.0" encoding ="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    <mapper namespace ="com.cuc.springcloud.dao.StorageDao" >

    <resultMap id="BaseResultMap" type="com.cuc.springcloud.domain.Storage">
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="product_id" property="productId" jdbcType="BIGINT" />
    <result column="total" property="total" jdbcType="INTEGER" />
    <result column="used" property="used" jdbcType="INTEGER" />
    <result column="residue" property="residue" jdbcType="INTEGER" />
    </resultMap>

    <update id="decrease">
    UPDATE t_storage
    SET used = used + #{count}, residue = residue - #{count}
    WHERE product_id = #{productId}
    </update>
    </mapper>
  7. Service接口及实现

    1
    2
    3
    public interface StorageService {
    void decrease(Long productId, Integer count);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.cuc.springcloud.service.impl;

    import com.cuc.springcloud.dao.StorageDao;
    import com.cuc.springcloud.service.StorageService;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;

    import javax.annotation.Resource;

    @Service
    public class StorageServiceImpl implements StorageService {

    private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);

    @Resource
    private StorageDao storageDao;

    @Override
    public void decrease(Long productId, Integer count) {
    LOGGER.info( "------->storage-service 中扣减库存开始 " );
    storageDao.decrease(productId,count);
    LOGGER.info( "------->storage-service 中扣减库存结束 " );
    }
    }
  8. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    public class StorageController {

    @Autowired
    private StorageService storageService ;

    @RequestMapping("/storage/decrease")
    public CommonResult decrease(Long productId,Integer count){
    storageService.decrease(productId, count);
    return new CommonResult(200," 扣减库存成功!");
    }
    }
  9. Config

    1
    2
    3
    4
    @Configuration
    @MapperScan({"com.cuc.springcloud.dao"})
    public class MyBatisConfig {
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    package com.cuc.springcloud.config;


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

    import javax.sql.DataSource;

    /**
    * 使用 Seata 对数据源进行代理
    */
    @Configuration
    public class DataSourceProxyConfig {

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

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
    return new DruidDataSource();
    }

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

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy)throws Exception{
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    sqlSessionFactoryBean.setDataSource(dataSourceProxy);
    sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
    sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
    return sqlSessionFactoryBean.getObject();
    }
    }
  10. 主启动

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @EnableFeignClients
    @EnableDiscoveryClient
    public class SeataStorageServiceApplication2002 {
    public static void main(String[] args) {
    SpringApplication.run(SeataStorageServiceApplication2002.class,args);
    }
    }
账户模块
  1. 新建 seata-account-service2003

  2. POM

    同上

  3. YML

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    server:
    port: 2003
    spring:
    application:
    name: seata-account-service
    cloud:
    alibaba:
    seata:
    tx-service-group: fsp_tx_group
    nacos:
    discovery:
    server-addr: localhost:8848
    datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_account?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    logging:
    level:
    io:
    seata: info
    mybatis:
    mapperLocations: classpath:mapper/*.xml
  4. 拷贝 seata/conf 目录下的 file.conf、registry.conf 到模块的 resources 目录下

  5. Domain

    CommonResult 同上

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Account {

    private Long id;
    private Long userId; // 用户id
    private BigDecimal total; // 总额度
    private BigDecimal used; // 已用额度
    private BigDecimal residue; // 剩余额度
    }
  6. Dao接口及实现

    1
    2
    3
    4
    @Mapper
    public interface AccountDao {
    void decrease(@Param( "userId" ) Long userId,@Param ( "money" ) BigDecimal money);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <?xml version ="1.0" encoding ="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

    <mapper namespace ="com.cuc.springcloud.dao.AccountDao" >

    <resultMap id="BaseResultMap" type="com.cuc.springcloud.domain.Account">
    <id column="id" property="id" jdbcType="BIGINT" />
    <result column="user_id" property="userId" jdbcType="BIGINT" />
    <result column="total" property="total" jdbcType="DECIMAL" />
    <result column="used" property="used" jdbcType="DECIMAL" />
    <result column="residue" property="residue" jdbcType="DECIMAL" />
    </resultMap>

    <update id="decrease">
    UPDATE t_account
    SET residue = residue - #{money}, used = used - #{money}
    WHERE user_id = #{userId}
    </update>
    </mapper>
  7. Service接口及实现

    1
    2
    3
    public interface AccountService {
    void decrease(@RequestParam( "userId" ) Long userId,@RequestParam ( "money" ) BigDecimal money);
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Service
    public class AccountServiceImpl implements AccountService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);


    @Resource
    private AccountDao accountDao;

    @Override
    public void decrease(Long userId, BigDecimal money) {
    LOGGER .info( "------->account-service 中扣减账户余额开始 " );
    accountDao .decrease(userId,money);
    LOGGER .info( "------->account-service 中扣减账户余额结束 " );
    }
    }
  8. Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    public class AccountController {

    @Resource
    private AccountService accountService;

    @RequestMapping( "/account/decrease" )
    public CommonResult decrease(@RequestParam( "userId" ) Long userId, @RequestParam ( "money" ) BigDecimal money){
    accountService.decrease(userId, money);
    return new CommonResult(200,"扣减账户余额成功!");
    }
    }
  9. Config

    同上

  10. 主启动

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataAccountMainApp2003 {
    public static void main(String[] args) {
    SpringApplication.run(SeataAccountMainApp2003.class,args);
    }
    }

测试

启动 nacos、seata、2001、2002、2003,访问 http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

之后可去数据库查看相应数据。

image-20220817213837686

修改 2003,添加如下内容:

image-20220817214421674

重启2003,再次访问:

image-20220817214618224

提示超时错误,查看数据库,会发现错误之前的操作回滚了,数据库的数据并没有被修改。

image-20220817215419776

也就是说注解了@GlobalTransactional,方法里头的任意微服务调用出错,会将方法中所有数据库操作进行回滚。