SpringCloud微服务架构学习

SpringCloud

1. 微服务架构理论入门

1.1. 微服务架构概述

微服务架构是一种架构模式,它提倡将单一应用程序划分为一组小的服务,服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务于服务之间采用轻量级的通信机制互相协作(通常是基于HTTP协议的RESTful API)。每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具对其进行构建。

1.2. SpringCloud简介

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

SpringCloud微服务架构学习_第1张图片

1.3. SpringCloud技术栈

在这里插入图片描述

SpringCloud微服务架构学习_第2张图片

2. SpringBoot和SpringCloud版本选型

SpringCloud Hoxton.SR1
SpringBoot 2.2.2.RELEASE
SpringCloud Alibaba 2.1.0.RELEASE
Java Java8
Maven 3.5+
Mysql 5.7+

3. 关于Cloud各种组件的停更/升级/替换

SpringCloud微服务架构学习_第3张图片

SpringCloud英文文档:https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/

SpringCloud中文文档:https://www.bookstack.cn/read/spring-cloud-docs/docs-index.md

SpringBoot英文文档:https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/

4. 微服务架构编码实现

4.1 微服务cloud整体聚合父工程Project

约定 > 配置 > 编码

创建父maven工程



<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.0modelVersion>

  <groupId>com.wei.springcloudgroupId>
  <artifactId>springcloud2020artifactId>
  <version>1.0-SNAPSHOTversion>
  <packaging>pompackaging>

  
  <properties>
    <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    <maven.compiler.source>1.8maven.compiler.source>
    <maven.compiler.target>1.8maven.compiler.target>
    <junit.version>4.12junit.version>
    <log4j.version>1.2.17log4j.version>
    <lombok.version>1.16.18lombok.version>
    <mysql.version>5.1.47mysql.version>
    <druid.version>1.1.16druid.version>
    <mybatis.spring.boot.version>1.3.0mybatis.spring.boot.version>
  properties>

  
  
  
  
  
  <dependencyManagement>
    <dependencies>
      
      <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-bootartifactId>
        <version>2.2.2.RELEASEversion>
        <type>pomtype>
        <scope>importscope>
      dependency>
      
      <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-dependenciesartifactId>
        <version>Hoxton.SR1version>
        <type>pomtype>
        <scope>importscope>
      dependency>
      
      <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-alibaba-dependenciesartifactId>
        <version>2.1.0.RELEASEversion>
        <type>pomtype>
        <scope>importscope>
      dependency>
      <dependency>
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
        <version>${mysql.version}version>
      dependency>
      <dependency>
        <groupId>com.alibabagroupId>
        <artifactId>druidartifactId>
        <version>${druid.version}version>
      dependency>
      <dependency>
        <groupId>org.apache.maven.pluginsgroupId>
        <artifactId>maven-project-info-reports-pluginartifactId>
        <version>3.0.0version>
      dependency>
    dependencies>
  dependencyManagement>
project>

4.2 支付模块构建

  • 建module

新建maven的module工程

观察父工程pom的变化:

  <modules>
    <module>cloud-provider-payment8001module>
  modules>
  • 改pom

子类pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud2020artifactId>
        <groupId>com.wei.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>
    
    <artifactId>cloud-provider-payment8001artifactId>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-jdbcartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
        dependency>
    dependencies>
project>
  • 写YML

在resource下面创建application.yml

server:
  port: 8001

spring:
  application:
    name: cloud-payment-server
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource     # 当前数据源操作类型
    driver-class-name: org.gjt.mm.mysql.Driver       # mysql驱动包
    url: jdbc:mysql://81.70.20.18:3306/db2021?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.wei.springcloud.entity
  • 主启动

创建PaymentMain8001.java作为主启动类

@SpringBootApplication
public class PaymentMain8001 {
     
    public static void main(String[] args) {
     
        SpringApplication.run(PaymentMain8001.class, args);
    }
}
  • 业务类

  • 建表sql

    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;
    

INSERT INTO payment (id, serial) VALUES (31, ‘aaabbb01’);


* entity实体类

1. 主题类Payment

   ```java
   @Data
   @AllArgsConstructor
   @NoArgsConstructor
   public class Payment implements Serializable {
       private long id;
       private String serial;
   }
  1. JSON封装体CommenResult

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class CommonResult<T> {
           
        // 404 not found
        private Integer code;
        private String message;
        private T data;
    
        public CommonResult(Integer code, String message){
           
            this(code, message, null);
        }
    }
    
  • dao

    @Mapper
    @Repository
    public interface PaymentDao {
           
        int add(Payment payment);
        Payment getPaymentById(@Param("id") Long id);
    }
    
    
    
    <mapper namespace="com.wei.springcloud.dao.PaymentDao">
        <resultMap type="com.wei.springcloud.entity.Payment" id="baseResultMap">
            <id column="id" property="id" jdbcType="BIGINT"/>
            <id column="serial" property="serial" jdbcType="VARCHAR"/>
        resultMap>
        <insert id="add" 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>
    
  • service

    public interface PaymentServie {
           
        int add(Payment payment);
        Payment getPaymentById(@Param("id") Long id);
    }
    
    @Service
    public class PaymentServiceImpl implements PaymentServie {
           
        @Autowired
        PaymentDao paymentDao;
        @Override
        public int add(Payment payment) {
           
            return paymentDao.add(payment);
        }
        @Override
        public Payment getPaymentById(Long id) {
           
            return paymentDao.getPaymentById(id);
        }
    }
    
  • controller

    @RestController
    @Slf4j
    public class PaymentController {
           
        @Autowired
        PaymentServie paymentServie;
        @PostMapping(value = "/payment/create")
        public CommonResult add(Payment payment){
           
            int result = paymentServie.add(payment);
            log.info("*****插入结果:" + result);
            if(result > 0) {
           
                return new CommonResult(200, "插入数据成功", result);
            }else {
           
                return new CommonResult(444, "插入数据失败", null);
            }
        }
        @GetMapping(value = "/payment/get/{id}")
        public CommonResult getPaymentById(@PathVariable("id") Long id){
           
            Payment payment = paymentServie.getPaymentById(id);
            log.info("*****查询结果:" + payment);
            if(null != payment) {
           
                return new CommonResult(200, "查询成功", payment);
            }else {
           
                return new CommonResult(444, "查询失败", null);
            }
        }
    }
    
    
  • 4.2.6 测试

使用postman工具进行测试

SpringCloud微服务架构学习_第4张图片

在这里插入图片描述

4.3 热部署 Devtools

  • 添加devtools的jar包到工程
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
  • 添加插件
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-maven-pluginartifactId>
        <configuration>
          <fork>truefork>
          <addResources>trueaddResources>
        configuration>
      plugin>
    plugins>
  build>
  • 开启自动构建选项

SpringCloud微服务架构学习_第5张图片

  • 开启热注册,更新值

    快捷键 Ctrl + Shift + Alt + /

SpringCloud微服务架构学习_第6张图片

  • 重启IDEA

4.4 订单模块

  • 4.4.1 建module、改pom、写yml、主启动

spring-consumer-order80

  • 4.4.2 业务类

  • 配置restTemplate

@Configuration
public class ApplicationContextConfig {
     
    @Bean
    public RestTemplate getRestTemplate(){
     
        return new RestTemplate();
    }
}
  • controller
@RestController
@Slf4j
public class OrderController {
     
    private static final String PAYMENT_URL= "http://localhost:8001";
    @Autowired
    RestTemplate restTemplate;
    @GetMapping(value = "/consumer/payment/create")
    public CommonResult<Payment> create(Payment payment){
     
        return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
    }
    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult getPayment(@PathVariable("id") Long id){
     
        return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
    }
}
  • entity

    同4.2 支付模块的entity

4.5 项目重构

项目中存在相同的entity代码,存在代码冗余问题

  • 建module

spring-api-commons

  • 改pom

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud2020artifactId>
        <groupId>com.wei.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>
    <artifactId>cloud-api-commonsartifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>cn.hutoolgroupId>
            <artifactId>hutool-allartifactId>
            <version>5.1.0version>
        dependency>
    dependencies>
project>
  • .5.3 业务类

将上面两个工程的entity迁移

maven install上传本地仓库

改写其他引用此module的pom文件

        <dependency>
            <groupId>com.wei.springcloudgroupId>
            <artifactId>cloud-api-commonsartifactId>
            <version>${project.version}version>
        dependency>

SpringCloud微服务架构学习_第7张图片

5. Eureka 注册与发现

5.1 Eureka基础知识

  • 什么是服务治理

    SpringCloud 封装了Netflix公司开发的Eureka模块来实现服务治理。

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

  • 什么是服务注册与发现

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

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

SpringCloud微服务架构学习_第8张图片

  • Eureka的两个组件

    Eureka Server 和 Eureka Client

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

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

5.2 Eureka服务端安装

5.2.1 建module
<artifactId>cloud-eureka-server7001artifactId>
5.2.2 改pom
  • 1.X 和 2.X 的区别

    以前的老版本(2018):

            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-eurekaartifactId>
            dependency>
    

    现在的新版本(2020):

            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
            dependency>
    

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud2020artifactId>
        <groupId>com.wei.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>
    <artifactId>cloud-eureka-server7001artifactId>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
        dependency>
        <dependency>
            <groupId>com.wei.springcloudgroupId>
            <artifactId>cloud-api-commonsartifactId>
            <version>${project.version}version>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-devtoolsartifactId>
            <scope>runtimescope>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
        dependency>
    dependencies>
project>
5.2.3 主启动
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {
     
    public static void main(String[] args) {
     
        SpringApplication.run(EurekaMain7001.class, args);
    }
}
5.2.4 测试

浏览器访问 http://localhost:7001/

SpringCloud微服务架构学习_第9张图片

5.2.5 将cloud-provider-payment8001注册进eureka

改造8001

  • pom导入Eureka client依赖
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
  • 改写yml
eureka:
  client:
    register-with-eureka: true     # false表示不向注册中心注册自己
    fetch-registry: true           # false表示自己就是注册中心,我的职责就是维护服务实例,并不需要检索服务
    service-url:
      # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
      defaultZone: http://localhost:7001/eureka/
  • 主启动类增加 @EnableEurekaClient 注解

SpringCloud微服务架构学习_第10张图片

5.2.6 将cloud-consumer-order80入驻eureka
  • 改pom
  • 改yml
  • 主启动类

5.3 Eureka 集群搭建

5.3.1 Eureka集群原理

SpringCloud微服务架构学习_第11张图片

高可用!

原理:互相注册,相互守望

SpringCloud微服务架构学习_第12张图片

5.3.2 集群搭建步骤

参考7001新建7002

  • 建module

  • 改pom

  • 修改映射配置

    找到C:\Windows\System32\drivers\etc路径下的hosts文件

    127.0.0.1 eureka7001.com
    127.0.0.1 eureka7002.com
    
5.3.3 将支付8001模块和订单80模块注册进集群

SpringCloud微服务架构学习_第13张图片

5.3.4 支付提供者8001集群环境构建

参考8001,构建8002

  • 修改80订单Config配置,增加@LoadBalanced注解,赋予RestTemplate负载均衡的能力
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
     
        return new RestTemplate();
    }
  • 修改80订单OrderController类,将写死的URL地址改为微服务提供者名称
   @RestController
    @Slf4j
    public class OrderController {
     
        private static final String PAYMENT_URL= "http://CLOUD-PAYMENT-SERVICE";
        @Autowired
        RestTemplate restTemplate;
        @GetMapping(value = "/consumer/payment/create")
        public CommonResult<Payment> create(Payment payment){
     
            return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
        }
        @GetMapping(value = "/consumer/payment/get/{id}")
        public CommonResult getPayment(@PathVariable("id") Long id){
     
            return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        }
    }
  • 测试
{
     "code":200,"message":"查询成功serverPort: 8001","data":{
     "id":32,"serial":"weichengjie"}}
{
     "code":200,"message":"查询成功serverPort: 8002","data":{
     "id":32,"serial":"weichengjie"}}

–默认轮询–

SpringCloud微服务架构学习_第14张图片

5.4 actuator 微服务信息完善

5.4.1 主机名称服务名称的修改

修改8001和8002的 application.yml

  instance:
    instance-id: payment8001

修改前:

在这里插入图片描述

修改后:

在这里插入图片描述

5.4.2 访问信息有IP显示

修改8001和8002的application.yml

  instance:
    instance-id: payment8001
    prefer-ip-address: true

SpringCloud微服务架构学习_第15张图片

5.5 服务发现Discovery

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

  • 修改8001

修改controller

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping(value = "/payment/discovery")
    public Object discovery(){
     
        List<String> services = discoveryClient.getServices();
        services.forEach(item -> {
     
            System.out.println(item);
        });
        List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
        instances.forEach(item -> {
     
            System.out.println("getServiceId::" + item.getServiceId() + "::" + item.getHost() + ":::"
                    + item.getPort() + ":::" + item.getUri());
        });
        return this.discoveryClient;
    }

修改启动类,增加注解 @EnableDiscoveryClient注解

自测
在这里插入图片描述

在这里插入图片描述

5.6 Eureka 自我保护

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

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

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

什么原因导致?

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

属于CAP里面的AP分支。

什么是自我保护模式?

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

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

综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,额可以让Eureka集群更加的健壮、稳定。

怎么禁止使用自我保护?

在7001server的yml中eureka添加如下配置

  server:
    # 关闭自我保护机制,保证不可用服务被及时剔除
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 2000

在8001provider中添加如下配置

  instance:
    instance-id: payment8001
    # 访问路径可以显示IP地址
    prefer-ip-address: true
    # Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
    lease-renewal-interval-in-seconds: 1
    # Eureka服务端在收到最后一次心跳等待的时间上限,单位为秒(默认在90),超时将剔除
    lease-expiration-duration-in-seconds: 2

SpringCloud微服务架构学习_第16张图片

在就存在,不再立即剔除

6. Zookeeper服务注册与发现

5.1 注册中心Zookeeper

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

SpringCloud微服务架构学习_第17张图片

5.2 服务提供者

  • 新建cloud-provider-payment8004 module项目

  • 改pom

    新增:

            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
            dependency>
    
  • yml

    # 8004表示注册到zookeeper服务器的支付服务提供者服务端口
    server:
      port: 8004
    # 服务别名 -- 注册zookeeper到注册中心名称
    spring:
      application:
        name: cloud-provider-payment
      cloud:
        zookeeper:
          connect-string: 81.70.20.18:2181
    
  • 主启动类

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

    @RestController
    @Slf4j
    public class PaymentController {
           
        @Value("server.port")
        private String serverPort;
        @RequestMapping(value = "/payment/zk")
        private String paymentzk() {
           
            return "springcloud with zookeeper: " + serverPort + "\t" + UUID.randomUUID().toString();
        }
    }
    
  • 启动8004注册进zookeeper

SpringCloud微服务架构学习_第18张图片

  • 测试

SpringCloud微服务架构学习_第19张图片

  • 思考

    服务节点是临时节点还是持久节点? 临时

5.3 服务消费者

  • 新建cloud-consumerzk-order80

  • pom 和服务提供者一模一样

  • yml

  • 主启动

  • 业务类

    @Configuration
    public class ApplicationConfig {
           
        @Bean
        @LoadBalanced
        public RestTemplate getRestTemplate(){
           
            return new RestTemplate();
        }
    }
    
    @RestController
    @Slf4j
    public class OrderZKController {
           
        public static final String INVOKE_URL = "http://cloud-provider-payment";
        @Autowired
        RestTemplate restTemplate;
        @RequestMapping(value = "/consumer/payment/zk")
        public String paymentInfo() {
           
            String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
            return result;
        }
    }
    

7. Consul服务注册与发现

7.1 Consul简介

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

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

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

Consul具有哪些特点?

  • 服务发现(Service Discovery):Consul提供了通过DNS或者HTTP接口的方式来注册服务和发现服务。一些外部的服务通过Consul很容易的找到它所依赖的服务。
  • 健康检查(Health Checking):Consul的Client可以提供任意数量的健康检查,既可以与给定的服务相关联(“webserver是否返回200 OK”),也可以与本地节点相关联(“内存利用率是否低于90%”)。操作员可以使用这些信息来监视集群的健康状况,服务发现组件可以使用这些信息将流量从不健康的主机路由出去。
  • Key/Value存储:应用程序可以根据自己的需要使用Consul提供的Key/Value存储。 Consul提供了简单易用的HTTP接口,结合其他工具可以实现动态配置、功能标记、领袖选举等等功能。
  • 安全服务通信:Consul可以为服务生成和分发TLS证书,以建立相互的TLS连接。意图可用于定义允许哪些服务通信。服务分割可以很容易地进行管理,其目的是可以实时更改的,而不是使用复杂的网络拓扑和静态防火墙规则。
  • 多数据中心:Consul支持开箱即用的多数据中心. 这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域。

7.2 安装并运行Consul

docker pull consul
docker run --name consul -d -p 8500:8500 consul

SpringCloud微服务架构学习_第20张图片

7.3 服务提供者

  • 新建module:cloud-providerconsul-payment8006

  • pom

            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-consul-discoveryartifactId>
            dependency>
    
  • yml

    server:
      port: 8006
    
    spring:
      application:
        name: consul-provider-payment
    # consul注册中心地址
      cloud:
        consul:
          host: 81.70.20.18
          port: 8500
          discovery:
            service-name: ${
           spring.application.name}
            prefer-ip-address: true
            heartbeat:
              enabled: true
    
  • 主启动类

    @RestController
    @Slf4j
    public class PaymentController {
           
        @Value("server.port")
        private String serverPort;
    
        @RequestMapping(value = "/payment/consul")
        private String paymentzk() {
           
            return "springcloud with consul: " + serverPort + "\t" + UUID.randomUUID().toString();
        }
    }
    
  • 启动测试

SpringCloud微服务架构学习_第21张图片

7.4 服务消费者

  • 新建cloud-consumerconsul-order80
  • 改pom
  • yml
  • 主启动
  • controller
  • 测试

三个注册中心对比:

组件名 语言 CAP 服务健康检查 对外暴露接口 SpringCloud集成
Eureka Java AP 可配支持 HTTP 已集成
Zookeeper Java CP 支持 HTTP/DNS 已集成
Consul Go CP 支持 客户端 已集成

SpringCloud微服务架构学习_第22张图片

C:Consistency(强一致性)

A:Availability(可用性)

P:Partition tolerance(分区容错性)

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

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

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

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

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

CP - 满足一致性、分区容错性的系统,通常性能不是特别高;

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

8. Ribbon负载均衡服务调用

8.1 概述

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

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

Ribbon目前也进入了维护模式。。。

  • LB负载均衡是什么?

    将用户的请求平摊到多个服务上,从而达到系统的HA(高可用)。

    常见的负载均衡有软件Nginx、LVS,硬件F5等。

  • Ribbon本地负载均衡客户端和Nginx服务端负载均衡的区别:

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

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

  • 集中式LB

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

  • 进程内LB

    将LB逻辑集成放消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合格的服务器。

    Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务的提供方的地址。

Ribbon:负载均衡 + TestTemplate调用

8.2 Ribbon负载均衡演示

Ribbon其实就是一个软负载均衡的客户端组件。

它可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。

SpringCloud微服务架构学习_第23张图片

Ribbon在工作时分成两步:

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

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

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

  • pom

    之前写样例时候没有引入spring-cloud-starter-ribbon也可以使用ribbon。

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-ribbonartifactId>
    dependency>
    

    猜测spring-cloud-starter-netflix-eureka-client只带了spring-cloud-starter-ribbon的引用:

SpringCloud微服务架构学习_第24张图片

  • RestTemplate

    getForObject、postForObject : 返回json

    getForEntity、postForEntity : 返回ResponseEntity对象,包含了一些相应信息

8.3 Ribbon 核心组件IRule

8.3.1 IRule

根据特定算法中从服务列表中选取一个要访问的服务。

SpringCloud微服务架构学习_第25张图片

  • 如何替换

    自定义配置类不能放到@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有Ribbon客户端共享,达不到特殊定制化的目的。

    新建com.wei.myrule包

    @Configuration
    public class MySelfRule {
           
        public IRule myRule(){
           
            return new RandomRule();
        }
    }
    

    主启动类添加@RibbonClients注解

    @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
    

8.4 Ribbon负载均衡算法

8.5 手写轮询算法

@Component
public class MYLB implements LoadBalance{
     
    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement() {
       // 利用CAS和自旋锁
        int current;
        int next;
        do {
     
            current = atomicInteger.get();
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;
        }while(!this.atomicInteger.compareAndSet(current, next));
        return next;
    }

    @Override
    public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
     
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

9. OpenFeign

9.1 概述

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

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

  • Feign能干什么?

    Feign旨在使编码Java Http客户端变得更容易。

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

    Feign继承了Ribbon

    利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而于Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
    SpringCloud微服务架构学习_第26张图片

9.2 OpenFeign使用步骤

  • pom

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-openfeignartifactId>
    dependency>
    
  • yml

    server:
      port: 80
    spring:
      application:
        name: cloud-order-feign-service
    eureka:
      client:
        register-with-eureka: false     # false表示不向注册中心注册自己
        fetch-registry: true           # false表示自己就是注册中心,我的职责就是维护服务实例,并不需要检索服务
        service-url:
          # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
          defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka
    
  • 主启动

    @SpringBootApplication
    @EnableFeignClients  // 使用Feign
    @EnableEurekaClient
    public class OrderFeignMain80 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(OrderFeignMain80.class, args);
        }
    }
    
  • controller

    @RestController
    @Slf4j
    public class OrderFeignController {
           
        @Autowired
        private PaymentFeignService paymentFeignService;
        @GetMapping(value = "/consumer/payment/get/{id}")
        public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
           
            return paymentFeignService.getPaymentById(id);
        }
    }
    
  • service

    @Component
    @FeignClient(value = "CLOUD-PAYMENT-SERVICE")
    public interface PaymentFeignService {
           
        @GetMapping(value = "/payment/get/{id}")
        CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
    }
    

9.3 超时控制

OpenFeign默认等待1秒,超过后报错。

yml文件中需要开启OpenFeign客户端超时控制。

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

9.4 日志打印

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

对Feign接口的调用情况进行监控和输出。

  • 日志级别:

    None:默认,不显示任何日志

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

    HEADERS:除了BASIC之外,还有请求和相应的头信息

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

  • 配置日志bean:

@Configuration
public class FeignConfig {
     
    Logger.Level feignLoggerLevel(){
     
        return Logger.Level.FULL;
    }
}
  • yml
logging:
  level:
    # feign 日志以什么级别监控哪个接口
    com.wei.springcloud.service.PaymentFeignService: debug

10. Hystrix 服务降级

10.1 概述

  • 分布式系统面临的问题

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

SpringCloud微服务架构学习_第27张图片

  • 服务雪崩

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

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

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

10.1.1 Hystrix
  • 是什么

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

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

  • 能干嘛

    1. 服务降级
    2. 服务熔断
    3. 接近实时的监控
    4. 。。。。。
10.1.2 Hystrix官宣 , 停更进维

10.2 Hystrix重要概念

10.2.1 服务降级 Fallback

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

  • 哪些情况会发生服务降级??
    1. 程序运行异常
    2. 超时
    3. 服务熔断触发服务降级
    4. 线程池/信号量打满也会导致服务降级
10.2.2 服务熔断 Break

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

就是保险丝:服务降级 -> 进而熔断 -> 恢复调用链路

10.2.3 服务限流 Flowlimit

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

10.3 Hystrix案例

10.3.1 构建
  • 搭建cloud-provider-hystrix-payment8001

  • pom

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
    dependency>
    
  • yaml

    server:
      port: 8001
    spring:
      application:
        name: cloud-payment-hystrix-service
    eureka:
      client:
        register-with-eureka: true     # false表示不向注册中心注册自己
        fetch-registry: true           # false表示自己就是注册中心,我的职责就是维护服务实例,并不需要检索服务
        service-url:
          # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
          defaultZone: http://eureka7001.com:7001/eureka/
    
  • 主启动

  • service

    @Service
    public class PaymentService {
           
        public String paymentInfo_OK(Integer id) {
           
            return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK, ID: " + id;
        }
        public String paymentInfo_Timeout(Integer id) {
           
            // 暂停几秒钟
            try{
           
                TimeUnit.SECONDS.sleep(3);
            }catch (Exception e){
           
                e.printStackTrace();
            }
            return "线程池:" + Thread.currentThread().getName() + "paymentInfo_Timeout, ID: " + id + ",耗时:3秒钟" ;
        }
    }
    
  • controller

    @RestController
    @Slf4j
    public class PaymentController {
           
        @Autowired
        PaymentService paymentService;
        @Value("${server.port}")
        private String port;
        @GetMapping("/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id) {
           
            String result = paymentService.paymentInfo_OK(id);
            log.info("*****result:" + result);
            return result;
        }
        @GetMapping("/payment/hystrix/timeout/{id}")
        public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
           
            String result = paymentService.paymentInfo_Timeout(id);
            log.info("******result" + result);
            return result;
        }
    }
    
  • 测试

10.3.2 高并发测试

上述在非高并发情形下,还能勉强满足 but…

  • Jmeter压力测试

    开启Jmeter,来20000个并发压死8001,20000个请求都去访问paymentInfo_Timeout;

    再来一个请求去访问paymentInfo_OK,结果paymentInfo_OK也变慢了!!!

    tomcat的默认线程数被打满了,没有多余的线程来分解压力和处理。非常的危险!!!

10.3.3 80加入测试
  • cloud-cunsumer-feign-hystrix-order80

  • pom

  • yml

  • controller

    @RestController
    @Slf4j
    public class OrderHystrixController {
           
        @Autowired
        PaymentHystrixService paymentHystrixService;
    
        @Value("${server.port}")
        private String port;
    
        @GetMapping("/consumer/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id) {
           
            String result = paymentHystrixService.paymentInfo_OK(id);
            log.info("*****result:" + result);
            return result;
        }
    
        @GetMapping("/consumer/payment/hystrix/timeout/{id}")
        public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
           
            String result = paymentHystrixService.paymentInfo_Timeout(id);
            log.info("******result" + result);
            return result;
        }
    }
    
  • service

    @Service
    @FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE")
    public interface PaymentHystrixService {
           
    
        @GetMapping("/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id);
    
        @GetMapping("/payment/hystrix/timeout/{id}")
        public String paymentInfo_Timeout(@PathVariable("id") Integer id);
    }
    
  • 正常测试 http://localhost/consumer/payment/hystrix/ok/31

  • 高并发压力测试

    2W个线程压8001

    消费端80微服务再去访问正常的OK微服务

    80也转圈圈…

10.3.4 上述结论

正因为有上述故障或不佳表现,才有了降级/容错/限流等技术的诞生!

10.3.5 如何解决
  • 超时导致服务器变慢(转圈)

    超时不再等待

  • 出错(宕机或者程序运行出错)

出错要有兜底

  • 解决

    对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级;

    对方服务(8001)down机了,调用者(80)不能一直卡死等待,必须有服务降级;

    对方服务(8001)正常,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者)

10.4 服务降级

10.4.1 降级配置

@HystrixCommand注解

设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,做服务降级fallback

10.4.2 8001 fallback
  • 业务类启用 @HystrixCommand

    一旦调用服务方法失败并抛出错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法。

        @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = {
           
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
        })
        public String paymentInfo_Timeout(Integer id) {
           
            // 暂停几秒钟
            try{
           
                TimeUnit.SECONDS.sleep(5);
            }catch (Exception e){
           
                e.printStackTrace();
            }
            return "线程池:" + Thread.currentThread().getName() + "paymentInfo_Timeout, ID: " + id + ",耗时:5秒钟" ;
        }
        // 兜底的方法
        public String paymentInfo_TimeoutHandler(Integer id) {
           
            return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeoutHandler, ID: " + id + "^_^" ;
        }
    
  • 主启动类启用

    @SpringBootApplication
    @EnableEurekaClient
    @EnableCircuitBreaker  // 开启
    public class PaymentHystrixMain8001 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(PaymentHystrixMain8001.class, args);
        }
    }
    
10.4.3 80 fallback
  • yaml

    feign:
      hystrix:
        enabled: true
    
  • 主启动

    @SpringBootApplication
    @EnableFeignClients
    @EnableHystrix
    public class OrderHystrixMain80 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(OrderHystrixMain80.class, args);
        }
    }
    
  • 业务类

    @RestController
    @Slf4j
    public class OrderHystrixController {
           
        @Autowired
        PaymentHystrixService paymentHystrixService;
    
        @Value("${server.port}")
        private String port;
    
        @GetMapping("/consumer/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id) {
           
            String result = paymentHystrixService.paymentInfo_OK(id);
            log.info("*****result:" + result);
            return result;
        }
    
        @GetMapping("/consumer/payment/hystrix/timeout/{id}")
        @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = {
           
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
        })
        public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
           
            String result = paymentHystrixService.paymentInfo_Timeout(id);
            log.info("******result" + result);
            return result;
        }
    
        public String paymentInfo_TimeoutHandler(@PathVariable("id") Integer id) {
           
            return "洞拐洞拐,我是80,对方支付繁忙请稍后再试,o(╥﹏╥)o";
        }
    }
    
10.4.4 目前问题
  • 每个业务方法对应一个兜底的方法,代码膨胀。。。。。。。能不能有个global兜底方法?

    @DefaultProperties(defaultFallBack = “”) ,除了个别重要核心业务有专属,其他都统一处理结果。

    @Service
    @DefaultProperties(defaultFallback = "paymentGlobalFallbackMethod")
    public class PaymentService {
           
    
        public String paymentInfo_OK(Integer id) {
           
            return "线程池:" + Thread.currentThread().getName() + "paymentInfo_OK, ID: " + id;
        }
    
    //    @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = {
           
    //            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
    //    })
        @HystrixCommand
        public String paymentInfo_Timeout(Integer id) {
           
            // 暂停几秒钟
    		int i = 10/0;
            return "线程池:" + Thread.currentThread().getName() + "paymentInfo_Timeout, ID: " + id + ",耗时:3秒钟" ;
        }
    
        public String paymentInfo_TimeoutHandler(Integer id) {
           
            return "线程池:" + Thread.currentThread().getName() + "paymentInfo_TimeoutHandler, 8001系统繁忙,请稍后再试,ID: " + id + "^_^" ;
        }
    
        // 兜底全局fallback方法
        public String paymentGlobalFallbackMethod(){
           
            return "全局异常信息处理,请稍后再试!";
        }
    }
    
  • 统一和自定义的分开。

    @Service
    @FeignClient(value = "CLOUD-PAYMENT-HYSTRIX-SERVICE", fallback = PaymentHystrixServiceImpl.class)
    public interface PaymentHystrixService {
           
        @GetMapping("/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id);
    
        @GetMapping("/payment/hystrix/timeout/{id}")
        public String paymentInfo_Timeout(@PathVariable("id") Integer id);
    }
    
    @Component
    public class PaymentHystrixServiceImpl implements PaymentHystrixService {
           
        @Override
        public String paymentInfo_OK(Integer id) {
           
            return "全局。。。paymentInfo_OK。。";
        }
    
        @Override
        public String paymentInfo_Timeout(Integer id) {
           
            return "全局。。。paymentInfo_Timeout。。";
        }
    }
    

10.5 服务熔断

10.5.1 断路器

家里的保险丝

10.5.2 熔断是什么

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

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

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

10.5.3 实操
  • 修改cloud-provider-hystrix-payment8001

  • service

    //  ========= 服务熔断 =============
    @HystrixCommand(fallbackMethod = "paymentCircuitBreak_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"),    // 失败率达到多少后跳闸
        // 默认10秒内超过20次请求,超过50%以上失败!
    })
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
           
        if(id < 0) {
           
            throw new RuntimeException("****** id 不能为负数 ********");
        }
        String serialNumber = IdUtil.simpleUUID();
        return Thread.currentThread().getName() + "\t 调用成功,流水号:" + serialNumber;
    }
    
    public String paymentCircuitBreak_fallback(@PathVariable("id") Integer id) {
           
        return "id 不能为负数,请稍后再试!!!!!!, id:" + id;
    }
    
  • controller

    //  ========= 服务熔断 =============
    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
           
        String reslut = paymentService.paymentCircuitBreaker(id);
        log.info("result ********** :" + reslut);
        return reslut;
    }
    
  • 测试

    多次错误,然后慢慢正确,发现刚开始不满足条件,就算是正确的访问也不能进行,过段时间后(默认5秒钟)恢复正常。

10.5.4 总结

SpringCloud微服务架构学习_第28张图片

  • 熔断类型

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

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

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

  • 断路器打开

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

    2. 原来的主逻辑如何恢复??自动恢复功能

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

10.6 服务限流

到alibaba的Sentinel讲解

10.7 Hystrix处理流程

Hystrix整个工作流如下:

  1. 构造一个 HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数;
  2. 执行命令,Hystrix提供了4种执行命令的方法,后面详述;
  3. 判断是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动;
  4. 判断熔断器是否打开,如果打开,跳到第8步;
  5. 判断线程池/队列/信号量是否已满,已满则跳到第8步;
  6. 执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳到第8步;否则,跳到第9步;
  7. 统计熔断器监控指标;
  8. 走Fallback备用逻辑
  9. 返回请求响应

10.8 服务监控 HystrixDashboard

10.8.1 概述

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

10.8.2 仪表盘9001
  • 新建module cloud-consumer-hystrix-dashboard9001
  • pom
<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
  • yaml

  • 主启动

    @SpringBootApplication
    @EnableHystrixDashboard
    public class HystrixDashboard9001 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(HystrixDashboard9001.class, args);
        }
    }
    
  • 启动测试 http://localhost:9001/hystrix

SpringCloud微服务架构学习_第29张图片

10.8.3 断路器演示(服务监控hystrixDashboard)
  • 修改8001
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
     
    public static void main(String[] args) {
     
        SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

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

SpringCloud微服务架构学习_第30张图片

SpringCloud微服务架构学习_第31张图片

监控测试

SpringCloud微服务架构学习_第32张图片

11. 服务网关 Gateway

Zuul内部发生重大分歧和变化,不再学习~~~

11.1 概述简介

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

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/

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

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

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

  • 能干嘛,为什么要用网关?

    1. 反向代理
    2. 鉴权
    3. 流量控制
    4. 熔断
    5. 日志监控
    6. 。。。。。。
  • 微服务架构中的网关在哪里?

SpringCloud微服务架构学习_第33张图片

  • 有Zuul为什么还要用gateway?

    • 我们为什么选择gateway

      1. Netflix不太靠谱,Zuul 2.x一直跳票,迟迟不发布

      2. Spring Gateway具有以下特征:

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

      3. SpringCloud Gateway 与 Zuul的区别

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

        (1)Zuul 1.x,是一个基于阻塞I/O的API gatway;

        (2)Zuul 1.x基于Servlet 2.5使用阻塞架构它不支持任何长连接(如Websocket),Zuul的设计模式和Nginx比较像,每次I/O操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul性能相对较差。

        (3)Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul 2.x的性能较Zuul 1.x有较大的提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway 的 RPS(每秒请求书)是Zuul的1.6倍。

        (4)Spring Cloud Gateway 建立在Spring Framework 5、Project Reactor 和 Springboot 2.0之上,使用非阻塞额API。

        (5)Spring Cloud Gateway 还支持Websocket,并且与Spring紧密集成拥有更好的开发体验。

    • Zuul 1.x 模型

      Spring Cloud所继承的Zuul版本,所采用的是Tomcat容器,传统的Servlet IO处理模型。

SpringCloud微服务架构学习_第34张图片

  • Gateway 模型

    Gateway基于异步非阻塞模型,性能强大。

    Webflux

SpringCloud微服务架构学习_第35张图片

11.2 三大核心概念

  • Route(路由)

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

  • Predicate(断言)

    参考java8的java.util.function.Predicate

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

  • Filter(过滤)

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

总结:

web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。

predicate就是我们的匹配条件;而filter可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由。

11.3 Gateway工作流程

在这里插入图片描述

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

Handler再通过制定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑。

Filter在 “pre” 类型的过滤器可以做参数校验、权限校验、流量监控、协议转换等,

在 “post” 类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有这非常重要的作用。

核心逻辑:路由转发 + 执行过滤器链

11.4 入门配置

  • 新建module cloud-gateway-gatway9527

  • pom

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-gatewayartifactId>
    dependency>
    

    需要移除web和actuator依赖,否则启动时会报错!!!!

  • yml

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway
    
    eureka:
      instance:
        hostname: cloud-gateway-service
      client:
        register-with-eureka: true     # false表示不向注册中心注册自己
        fetch-registry: true           # false表示自己就是注册中心,我的职责就是维护服务实例,并不需要检索服务
        service-url:
          # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
          defaultZone: http://eureka7001.com:7001/eureka/
    
  • 业务类

  • 主启动

    @SpringBootApplication
    @EnableEurekaClient
    public class GatewayMain9527 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(GatewayMain9527.class, args);
        }
    }
    
  • 9527网关如果做路由映射?

    我们目前不希望暴露8001端口,希望在8001外面套一层9527

  • yml新增网关配置

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          routes:
            - id: payment_routh # payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
              uri: http://localhost:8001           # 匹配后提供服务的路由地址
              predicates:
                - Path=/payment/get/**             # 断言,路径相匹配进行路由
    
            - id: payment_routh2 # payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
              uri: http://localhost:8001           # 匹配后提供服务的路由地址
              predicates:
                - Path=/payment/lb/**             # 断言,路径相匹配进行路由
    
    eureka:
      instance:
        hostname: cloud-gateway-service
      client:
        register-with-eureka: true     # false表示不向注册中心注册自己
        fetch-registry: true           # false表示自己就是注册中心,我的职责就是维护服务实例,并不需要检索服务
        service-url:
          # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
          defaultZone: http://eureka7001.com:7001/eureka/
    
  • 测试成功

在这里插入图片描述

  • gateway网关路由有两种配置方式:

    1. 在配置文件yml中配置,见前面的步骤

    2. 代码中注入RouteLocator的Bean

      • 业务要求:通过9527网关访问到外网的百度新闻网址

        @Configuration
        public class GatewayConfig {
                   
            @Bean
            public RouteLocator customRouteLocation(RouteLocatorBuilder builder) {
                   
                RouteLocatorBuilder.Builder routes = builder.routes();
                routes.route("path_route_wei1",
                        r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
                return routes.build();
            }
        }
        
      • 测试

在这里插入图片描述

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

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

  • 启动: 一个7001和 8001、8002

  • yml

    server:
      port: 9527
    
    spring:
      application:
        name: cloud-gateway
      cloud:
        gateway:
          discovery:
            locator:
              enabled: true                        # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
          routes:
            - id: payment_routh # payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
              #uri: http://localhost:8001           # 匹配后提供服务的路由地址
              uri: lb://cloud-payment-service           # 匹配后提供服务的路由地址
              predicates:
                - Path=/payment/get/**             # 断言,路径相匹配进行路由
    
            - id: payment_routh2 # payment_route    # 路由的ID,没有固定规则但要求唯一,建议配合服务名
              uri: lb://cloud-payment-service           # 匹配后提供服务的路由地址
              predicates:
                - Path=/payment/lb/**             # 断言,路径相匹配进行路由
    
    eureka:
      instance:
        hostname: cloud-gateway-service
      client:
        register-with-eureka: true     # false表示不向注册中心注册自己
        fetch-registry: true           # false表示自己就是注册中心,我的职责就是维护服务实例,并不需要检索服务
        service-url:
          # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
          defaultZone: http://eureka7001.com:7001/eureka/
    
  • 测试

    动态路由负载均衡测试成功!

11.6 Predicate 的使用

启动9527

SpringCloud微服务架构学习_第36张图片

Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分。

Spring Cloud Gateway包括许多内置的Route Predicate工厂。所有这些Predicate都与Http请求的不同属性匹配。多个Route Predicate工厂可以进行组合。

Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。

所有这些谓词都匹配http请求的不同属性。多个谓词工厂可以组合,并通过逻辑and。

SpringCloud微服务架构学习_第37张图片

10.6.1 常用的Route Predicate
  1. After Route Predicate

    - After=2021-01-24T17:42:47.789+08:00[Asia/Shanghai]
    
  2. Before Route Predicate

    - Before=2021-01-24T17:42:47.789+08:00[Asia/Shanghai]
    
  3. Between Route Predicate

  4. Cookie Route Predicate

    • 带cookie访问
    • 不带cookie访问
    - Cookie=chocolate, ch.p
    

    Cookie Route Predicate需要两个参数,一个是Cookie name,一个是正则表达式。

    路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配则不执行。

  5. Header Route Predicate

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

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

  6. Host Route Predicate

    - Host=**.somehost.org,**.anotherhost.org
    

    接收一组参数,一组匹配的域名列表,这个模板是一个ant分割的模板,用.号作为分隔符。

    它通过参数中的主机地址作为匹配规则

  7. Method Route Predicate

    - Method=GET
    
  8. Path Route Predicate

  9. Query Route Predicate

    - Query=username, \d+   # 要有参数名username并且值还要是整数的才能路由
    
  10. 小总结

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

10.7 Filter的使用

10.7.1 概念

路由过滤器可用于修改进入的http请求和返回的http响应,路由过滤器只能指定路由进行使用。

Spring Cloud Gateway 内置了多种路由过滤器,他们都是由GatewayFilter工厂类产生。

10.7.2 Spring Cloud Gateway的Filter
  • 生命周期

    pre、post

  • 种类

    GatewayFilter 单一(31种)

    GlobalFilter 全局(10种)

    filters:
    - AddRequestHeader=X-Request-red, blue
    
10.7.2 自定义全局过滤器

两个接口:GlobalFilter、Ordered

  • 能干嘛

    全局日志记录、统一网关鉴权、。。

@Component
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
     
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
     
        System.out.println("******** come in global filter ********" + new Date());
        String uname = exchange.getRequest().getQueryParams().getFirst("uname");
        if(null == uname) {
     
            System.out.println("用户名为null,非法用户");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    // 优先级,越大优先级越高
    @Override
    public int getOrder() {
     
        return 0;
    }
}
  • 测试成功

    http://localhost:9527/payment/lb/?uname=99 带着uname才可以访问成功!!!!

12. 分布式配置中心 SpringCloud Config

12.1 概述

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

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

    SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yaml,上百个配置文件的管理就会非常复杂。

  • 是什么?

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

SpringCloud微服务架构学习_第38张图片

  • 怎么玩?

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

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

  • 能干嘛?

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

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

  • 官网

    https://cloud.spring.io/spring-cloud-config/reference/html/

12.2 Config服务端配置与测试

  1. 在Github新建一个名为springcloud-config的新Repository

  2. 得到Git地址: https://github.com/wchj08/springcloud-config.git

  3. 本地硬盘目录上新建git仓库并clone (命令:git clone https://github.com/wchj08/springcloud-config.git)

  4. 在本地目录下新建 config-dev.yml, config-test.yml, config-prod.yml文件

    git常用命令 git add . ; git commit -m “init yml” ; git push origin master

  5. 新建module模块cloud-config-center-3344 它即为Cloud的配置中心模块cloudConfig Center

  6. pom

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-config-serverartifactId>
    dependency>
    
  7. yml

    server:
      port: 3344
    
    spring:
      application:
        name: cloud-config-server    # 注册进eureka的微服务名
      cloud:
        config:
          server:
            git:
              uri: https://github.com/wchj08/springcloud-config.git   # github上仓库的地址
              # 搜索目录
              search-paths: springcloud-config
          # 读取分支
          label: master
    
    eureka:
      client:
        service-url:
          # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
          defaultZone: http://eureka7001.com:7001/eureka/
    
  8. 主启动

    @SpringBootApplication
    @EnableConfigServer
    public class ConfigCenterMain3344 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(ConfigCenterMain3344.class, args);
        }
    }
    
  9. windows下修改hosts配置文件,增加映射

    127.0.0.1 config-3344.com
    
  10. 测试通过Config微服务是否可以从Github上获取配置内容

    启动微服务3344

    http://config-3344.com:3344/master/config-dev.yml

SpringCloud微服务架构学习_第39张图片

  1. 配置读取规则

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k8dPu3FT-1615514689579)(SpringCloud笔记.assets/image-20210202105348377.png)]

SpringCloud微服务架构学习_第40张图片

在这里插入图片描述

  1. 成功实现用SpringCloud Config读取Github获取配置信息

12.3 Config客户端配置与测试

  1. 新建cloud-config-client-3355

  2. pom

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-configartifactId>
    dependency>
    
  3. bootstrap.yml

    什么玩意?

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

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

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

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

    因为boostrap.yml是比application.yml先加载的。

    server:
      port: 3355
    
    spring:
      application:
        name: config-client
      cloud:
        # Config客户端配置
        config:
          label: master  # 分支名称
          name: config   # 配置文件名称
          profile: dev   # 读取后缀名称,上述三个综合:master分支上config-dev.yml的配置文件被读取到http://config-3344.com:3344/master/config-dev.yml
          uri: http://localhost:3344/
    
    eureka:
      client:
        service-url:
          # 设置与Eureka Server交互的地址查询服务与注册服务都需要依赖这个地址。
          defaultZone: http://eureka7001.com:7001/eureka/
    
  4. 主启动

    @SpringBootApplication
    @EnableEurekaClient
    public class ConfigClientMain3355 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(ConfigClientMain3355.class, args);
        }
    }
    
  5. 业务类

    @RestController
    public class ConfigInfoController {
           
    
        @Value(value = "${config.info}")
        private String configInfo;
    
        @RequestMapping(value = "/configInfo")
        public String getConfigInfo() {
           
            return configInfo;
        }
    }
    
  6. 测试

    http://localhost:3355/configInfo

    成功通过3355客户端访问SpringCloud Config3344通过Github获取配置信息。

  7. 问题

    Linux运维修改Github上的配置文件内容,刷新3344配置中心立即响应,刷新3355客户端没有任何响应。难道每次运维修改文件,都需要修改3355?

12.4 客户端动态刷新

避免每次修改配置文件都要修改3355

  1. 修改3355模块

  2. pom引入actuator监控

  3. 修改yml,暴露监控端口

    management:
      endpoints:
        web:
          exposure:
            include: "*"
    
  4. 业务类controller增加@RefreshScope注解

    @RestController
    @RefreshScope
    public class ConfigInfoController {
           
    
        @Value(value = "${config.info}")
        private String configInfo;
    
        @RequestMapping(value = "/configInfo")
        public String getConfigInfo() {
           
            return configInfo;
        }
    }
    
  5. 再次修改测试!

    没生效!GG!

  6. How 怎么办?

    需要运维人员发送post请求刷新3355

    curl -X POST “http://localhost:3355/actuator/refresh”

  7. 再次测试成功!

  8. 还有问题

    可否通过广播,一次通知处处生效 引入BUS消息总线!

14. 消息总线 Bus

14.1 概述

想实现分布式自动刷新配置功能,SpringCloud Bus配合SpringCloud Config可以实现配置的动态刷新。

  • 是什么

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

  • 能干嘛

SpringCloud微服务架构学习_第41张图片

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

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

SpringCloud微服务架构学习_第42张图片

  • 为何被称为总线?

    • 什么是总线?

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

  • 基本原理

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

14.2 RabbitMQ环境配置

用docker装吧

docker run -dit --name Myrabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:managemen

14.3 Bus动态刷新全局广播

  1. 以3355为模板新建一个3366

  2. 设计思想

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

SpringCloud微服务架构学习_第43张图片

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

SpringCloud微服务架构学习_第44张图片

思想(2)显然更加适合,(1)不适合的原因如下:

(a) 打破了微服务的职责单一性,因为微服务本身是业务模块,他不应该承担配置刷新的职责;

(b) 破坏了微服务各节点的对等性;

© 有一定的局限性,例如:在微服务迁移时,它的网络地址常常会发生改变,此时如果要做到自动刷新,就要在增加更多修改。

  1. 给cloud-config-server-3344服务端增加消息总线的支持

    pom

    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-bus-amqpartifactId>
    dependency>
    

    yml

    # rabbitmq的配置
    rabbitmq:
      host: 81.70.20.18
      port: 5672
      username: admin
      password: admin
    
    # rabbitmq相关配置,暴露bus刷新配置的端点
    management:
      endpoints:  # 暴露bus刷新配置的端点
        web:
          exposure:
            include: 'bus-refresh'
    
  2. 给客户端3355和3366增加消息总线的支持

    pom

    
    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-bus-amqpartifactId>
    dependency>
    

    yml

      # rabbitmq的配置
      rabbitmq:
        host: 81.70.20.18
        port: 5672
        username: admin
        password: admin
    
  3. 测试

    修改github上的版本号,发送请求 curl -X POST “http://localhost:3344/actuator/bus-refresh”

    测试成功,一次发送处处生效!

14.4 Bus动态刷新定点通知

  • 不想全部通知,只想定点通知

    只通知3355,不通知3366

    指定某个具体实例生效

  • http://localhost:3344/actuator/bus-refresh/{destination}

    config server通过destination参数类指定需要更新配置的服务和实例

    http://localhost:3344/actuator/bus-refresh/config-client:3355

14.5 总结

SpringCloud微服务架构学习_第45张图片

15. 消息驱动 Stream

15.1 消息驱动概述

  • 是什么

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

    官网:https://spring.io/projects/spring-cloud-stream#overview

SpringCloud微服务架构学习_第46张图片

应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象交互。

通过我们配置来binding(绑定),而Spring Cloud Stream的binder对象负责与消息中间件交互。

所以,我们只需要搞清楚如何与Spring Cloud Stream交互就可以方便使用消息驱动的方式。

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

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

目前仅支持 RabbitMQ、Kafka。

  • 设计思想

    • 标准MQ

SpringCloud微服务架构学习_第47张图片

生产者/消费者之间靠消息媒介传递消息内容;

消息必须走特定的通道;

消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅。
  • 为什么用SpringCloud Stream

SpringCloud微服务架构学习_第48张图片

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

* Stream凭什么可以统一底层差异?

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

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

  Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为Kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。

* Binder

  Input    --> 消费者

  Output --> 生产者

SpringCloud微服务架构学习_第49张图片

  • Stream中的消息通信方式遵循了发布-订阅模式

    Topic主题进行广播,在RabbitMQ就是Exchange,在Kafka中就是Topic。

  • SpringCloud Stream标准流程套路

SpringCloud微服务架构学习_第50张图片

Binder:很方便的连接中间件,屏蔽差异;

Channel:通道,是队列Queue的一种抽象,在消息通信系统中就是实现存储和转发的媒介,通过Channel对队列进行配置。

Source和Sink:输入输出

  • 编码API和常用注解

SpringCloud微服务架构学习_第51张图片

15.2 案例说明

新建三个子模块:

  1. cloud-stream-rabbitmq-provider8801 生产者
  2. cloud-stream-rabbitmq-consumer8802 消费者1
  3. cloud-stream-rabbitmq-consumer8803 消费者2

15.3 消息驱动之生产者

  • 新建module: cloud-stream-rabbitmq-provider8801 生产者

  • pom

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-stream-rabbitartifactId>
    dependency>
    
  • yml

    server:
      port: 8801
    
    spring:
      application:
        name: cloud-stream-provider
      cloud:
        stream:
          binders:                  # 在此配置要绑定的rabbitmq的服务信息
            defaultRabbit:          # 表示定义的名称,用于binding整合
              type: rabbit          # 消息组件的名称
              environment:          # 设置mq先关的环境配置
                spring:
                  rabbitmq:
                    host: 81.70.20.18
                    port: 5672
                    username: admin
                    password: admin
          bindings:                  # 服务的整合处理
            output:                  # 这个名字是一个通道的名称
              destination: studyExchange          # 表示要使用的Exchange名称定义
              content-type: application/json      # 设置消息类型,本次使用json,文本类型为:text/plain
              binder: defaultRabbit               # 设置要绑定的消息服务的具体设置
    eureka:
      client:   # 客户端进行Eureka注册的配置
        service-url:
          defaultZone: http://localhost:7001/eureka
      instance:
        lease-renewal-interval-in-seconds: 2     # 设置心跳的时间间隔(默认30秒)
        lease-expiration-duration-in-seconds: 5  #
        instance-id: send8801.com                # 在信息列表时显示主机名称
        prefer-ip-address: true                  # 访问的路径变为IP地址
    
  • 主启动

    @SpringBootApplication
    public class StreamMain8801 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(StreamMain8801.class, args);
        }
    }
    
  • 业务类

    • 发送消息接口

      public interface IMessage {
               
          String send();
      }
      
    • 发送接口实现类

      @EnableBinding(Source.class)    // 定义消息的推送广告
      public class MessageProviderImpl implements IMessage{
               
      
          @Resource
          private MessageChannel output;     // 消息发送管道
      
          @Override
          public String send() {
               
              String serial = UUID.randomUUID().toString();
              output.send(MessageBuilder.withPayload(serial).build());
              System.out.println("********发送成功**********");
              return null;
          }
      }
      
    • Controller

      @RestController
      public class SendMessageController {
               
      
          @Autowired
          private IMessage iMessage;
      
          @GetMapping(value = "/send")
          public String send(){
               
              return iMessage.send();
          }
      }
      
  • 测试成功

    http://localhost:8801/send

15.4 消费驱动之消费者

  • 新建模块 cloud-stream-rabbitmq-consumer8802

  • pom

  • yml

    server:
      port: 8802
    
    spring:
      application:
        name: cloud-stream-consumer
      cloud:
        stream:
          binders:                  # 在此配置要绑定的rabbitmq的服务信息
            defaultRabbit:          # 表示定义的名称,用于binding整合
              type: rabbit          # 消息组件的名称
              environment:          # 设置mq先关的环境配置
                spring:
                  rabbitmq:
                    host: 81.70.20.18
                    port: 5672
                    username: admin
                    password: admin
          bindings:                  # 服务的整合处理
            input:                  # 这个名字是一个通道的名称
              destination: studyExchange          # 表示要使用的Exchange名称定义
              content-type: application/json      # 设置消息类型,本次使用json,文本类型为:text/plain
              binder: defaultRabbit               # 设置要绑定的消息服务的具体设置
    
    eureka:
      client:   # 客户端进行Eureka注册的配置
        service-url:
          defaultZone: http://eureka7001.com:7001/eureka/
      instance:
        lease-renewal-interval-in-seconds: 2     # 设置心跳的时间间隔(默认30秒)
        lease-expiration-duration-in-seconds: 5  #
        instance-id: consumer8802.com                # 在信息列表时显示主机名称
        prefer-ip-address: true                  # 访问的路径变为IP地址
    
  • 主启动

  • 业务类

    @Component
    @EnableBinding(Sink.class)
    public class ReceiveLisenterContrller {
           
        @Value(value = "server.port")
        private String serverPort;
    
        @StreamListener(Sink.INPUT)
        public void input(Message<String> message) {
           
            System.out.println("消费者1号!收到的消息:" + message.getPayload() + "\t端口号:" + serverPort);
        }
    }
    

15.5 分组消费和持久化

  • 依照8802,clone出一份8803

  • 启动消费者8803

  • 运行后有两个问题:

    1. 重复消费
    2. 消息持久化
  • 消费

    8802和8803同时收到了消息,存在重复消费的问题

    使用分组和持久化属性group解决

  • 分组

    • 原理

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

    • 8802和8803都变成不同组,两个不同group

      bindings:                  # 服务的整合处理
        input:                  # 这个名字是一个通道的名称
        destination: studyExchange          # 表示要使用的Exchange名称定义
        content-type: application/json      # 设置消息类型,本次使用json,文本类型为:text/plain
        binder: defaultRabbit               # 设置要绑定的消息服务的具体设置
        group: group2                       # 增加组名
      
    • 8802和8803都变成相同组,一个group

  • 持久化

    通过上述配置,解决了重复消费的问题,再看看持久化的问题

    增加group分组后,可以实现消息持久化,消费到持久化的消息,避免消息丢失!

16. 分布式请求链路跟踪 SpringCloud Sleuth

16.1 概述

  • 为什么出现该技术,解决哪些问题?

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

SpringCloud微服务架构学习_第52张图片

  • 是什么?

    SpringCloud Sleuth提供了一套完整的服务跟踪的解决方案

    在分布式系统中提供追踪解决方案并且兼容支持了zipkin

  • 解决

16.2 搭建链路监控步骤

16.2.1 zipkin
  • 下载 https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/ zipkin-server-2.12.9-exec.jar

  • 运行jar java -jar zipkin-server-2.12.9-exec.jar

  • 运行控制台 http://localhost:9411/zipkin

SpringCloud微服务架构学习_第53张图片

  • 术语:

    完整的调用链路

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

SpringCloud微服务架构学习_第54张图片

SpringCloud微服务架构学习_第55张图片

Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识。

Span: 标识调用链路来源,通俗的理解span就是一次请求信息。

16.2.2 服务提供者
  • 改造cloud-provider-payment8001

  • pom

    <dependency>
        <groupId>org.springframework.cloudgroupId>
        <artifactId>spring-cloud-starter-zipkinartifactId>
    dependency>
    
  • yml

    spring:
      application:
        name: cloud-payment-service
      zipkin:       # 新增
        base-url: http://localhost:9411
      sleuth:
        sampler:
          probability: 1     # 采样值介于0到1之间,1则表示全部采集
    
  • 业务类 PaymentController

    @GetMapping(value = "/payment/zipkin")
    public String paymentZipkin() {
           
        return "paymentZipkin  哈哈^_^O(∩_∩)O哈哈~";
    }
    
16.2.3 服务消费方
  • 改造cloud-consumer-order80

  • pom

  • yml

  • 业务类 OrderController

    @GetMapping(value = "/consumer/payment/zipkin/")
    public String paymentZipkin() {
           
        String result = restTemplate.getForObject("http://localhost:8001/payment/zipkin/", String.class);
        return result;
    }
    
16.2.4 测试

浏览器访问 http://localhost:9411

SpringCloud微服务架构学习_第56张图片

17. SpringCloud Alibaba入门简介

17.1 为什么会出现SpringCloud Alibaba?

SpringCloud Netflix项目进入维护模式

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

17.2 SpringCloud Alibaba带来了什么?

  • 是什么?

    18年10月份,SpringCloud Alibaba正式入驻SpringCloud官方孵化器,并在Maven中央库发布了第一个版本。

    SpringCloud Alibaba,它是由一些阿里巴巴的开源组件和云产品组成的。这个项目的目的就是为了让大家所熟知的Spring框架,其优秀的设计模式和抽象概念,以给使用阿里巴巴产品的Java开发者带来使用Spiring Boot 和Spring Cloud的更多遍历。

  • 能干什么?

    服务限流降级:默认支持Servlet、Feign、RestTemplate、Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看先留降级Metrics监控。

    服务注册与发现:适配Spring Cloud服务注册与发现标准,默认继承了Ribbon的支持。

    分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。

    消息驱动能力:基于Spring Cloud Stream为微服务应用构建消息驱动能力。

    阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。

    分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有Worker(schedulerx-client)上执行。

  • 官方文档

    https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

  • 怎么玩?

SpringCloud微服务架构学习_第57张图片

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

18.1 Nacos简介

  • 为什么叫Nacos?

    前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service。

  • 是什么?

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

    Nacos:Dynamic Naming and Configruation Service

    注册中心 + 配置中心的组合, 等价于 Nacos = Eureka + Config + Bus

  • 能干嘛?

    替代Eureka做服务注册中心;

    替代Config做服务配置中心。

  • 去哪下?

    https://github.com/alibaba/Nacos

    https://nacos.io/zh-cn/

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

18.2 安装并运行Nacos

下载安装,访问 http://localhost:8848/nacos,默认账号密码nacos

18.3 Nacos作为服务注册中心

18.3.1 官网文档
18.3.2 基于Nacos的服务提供者
  • 新建module:cloudalibaba-provider-payment9001

  • pom

    父pom

    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-alibaba-dependenciesartifactId>
        <version>2.1.0.RELEASEversion>
        <type>pomtype>
        <scope>importscope>
    dependency>
    

    子pom

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    dependency>
    
  • yml

    server:
      port: 9001
    
    spring:
      application:
        name: nacos-payment-provider
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848    # 配置nacos地址
    
    management:
      endpoints:
        web:
          exposure:
            include: '*'
    
  • 主启动

    @SpringBootApplication
    @EnableDiscoveryClient
    public class PaymentMain9001 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(PaymentMain9001.class, args);
        }
    }
    
  • 业务类

    @RestController
    public class PaymentController {
           
    
        @Value(value = "${server.port}")
        private String serverPort;
    
        @GetMapping(value = "/payment/nacos/{id}")
        public String getPayment(@PathVariable("id") Integer id) {
           
            return "nacos registry, serverPort:" + serverPort + "\t id: " + id;
        }
    }
    
  • 测试

在这里插入图片描述

  • 参照9001,新建module 9002
18.3.3 基于Nacos的服务消费者
  • 新建module:cloudalibaba-consumer-nacos-order83

  • pom

  • yml

    server:
      port: 83
    
    spring:
      application:
        name: nacos-order-consumer
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
    
    # 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
    service-url:
      nacos-user-service: http://nacos-payment-provider
    
  • 主启动

    @SpringBootApplication
    @EnableDiscoveryClient
    public class OrderNacosMain83 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(OrderNacosMain83.class, args);
        }
    }
    
  • 业务类

    @Configuration
    public class ApplicationContextConfig {
           
    
        @Bean
        @LoadBalanced
        public RestTemplate getRestTemplate() {
           
            return new RestTemplate();
        }
    }
    
    @RestController
    @Slf4j
    public class OrderNacosController {
           
        @Value(value = "${service-url.nacos-user-service}")
        private String serverURL;
    
        @Resource
        private RestTemplate restTemplate;
    
        @GetMapping(value = "/consumer/payment/nacos/{id}")
        public String paymentInfo(@PathVariable("id") Integer id) {
           
            return restTemplate.getForObject(serverURL + "/payment/nacos/" +id, String.class);
        }
    }
    
  • 测试

    83访问,9001/9002轮询负载(Nacos底层整合了Ribbon)

18.4 服务注册中心的对比

在这里插入图片描述

Nacos支持CP和AP模式的切换!

SpringCloud微服务架构学习_第58张图片

18.5 Nacos作为服务配置中心

18.5.1 基础配置
  • 新建module:cloudalibaba-config-nacos-client3377

  • pom

    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
    dependency>
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    dependency>
    
  • yml

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

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

    bootstrap.yml:

    server:
      port: 3377
    
    spring:
      application:
        name: nacos-payment-provider
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848    # 配置nacos地址
          config:
            server-addr: localhost:8848    # 配置nacos地址
            file-extension: yaml           # 指定yaml格式的配置
    

    application.yml:

    spring:
      profiles:
        active: dev     # 表示开发环境
    
  • 主启动

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

    @RestController
    @RefreshScope   // 支持Nacos的动态刷新
    public class NacosConfigController {
           
        @Value(value = "${config.info}")
        private String configInfo;
        public String getConfigInfo() {
           
            return configInfo;
        }
    }
    
  • 在Nacos中添加配置信息

    Nacos配置规则:

    Nacos中的dataid的组成格式与SpringBoot配置文件中的匹配规则:

    p r e f i x − {prefix}- prefix{spring.profiles.active}.${file-extension}

    nacos-config-client-dev.yml
    SpringCloud微服务架构学习_第59张图片

配置新增:

SpringCloud微服务架构学习_第60张图片

  • 测试成功

    http://localhost:3377/config

18.5.2 分类配置

SpringCloud微服务架构学习_第61张图片

  • Namespace、Group、DataID三者之间的关系?为什么这么设计?

    类似Java里面的package名和类名。最外层的namespace是可以区分部署环境的,Group和DataID逻辑上区分两个目标对象。

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

SpringCloud微服务架构学习_第62张图片

Nacos默认的命名空间是public,Namespace主要用来实现隔离。

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

Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分等到同一个分组里面去。

Service就是微服务;一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。比如说为了容灾,将Servcie微服务分别部署在杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让一个机房的微服务互相调用,以提升性能。

Instance,就是微服务的实例。

  • 三种方案加载配置

    • DataID方案

    • Group方案

    SpringCloud微服务架构学习_第63张图片

    • Namespace方案

SpringCloud微服务架构学习_第64张图片

18.6 Nacos集群和持久化配置(重要)

SpringCloud微服务架构学习_第65张图片

VIP是什么啊?虚拟IP

在这里插入图片描述

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

SpringCloud微服务架构学习_第66张图片

  • 持久化配置解释

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

    2. derby到mysql切换配置步骤:

SpringCloud微服务架构学习_第67张图片

​ 在mysql中新建库,执行nacos-mysql.sql脚本。

​ 在application.properties中添加:

spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://81.70.20.18:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456

配置完成,重启Nacos,再增加配置值,数据会保存到Mysql中!

  • Linux系统下配置

    预计需要:1个Nginx + 3个nacos注册中心 + 1个mysql

    1. Linux服务器上mysql数据库配置;
    2. application.properties配置;
    3. Linux服务器上的nacos的集群配置cluster.conf

SpringCloud微服务架构学习_第68张图片

  1. 编辑nacos的启动脚本startup.sh,可以实现传递不同的端口号生成不同的实例。

在这里插入图片描述

SpringCloud微服务架构学习_第69张图片
启动:
在这里插入图片描述

  1. Nginx的配置,由它作为负载均衡器
  2. 配置完成,访问测试!

19. 实现熔断和限流 SpringCloud Alibaba Sentinel

19.1 Sentinel : 分布式系统的流量防卫兵

  • Hystrix
    1. 需要我们手工搭建监控平台
    2. 没有一套web界面进行更加细粒度化的配置 流控、速率控制、服务熔断、服务降级
  • Sentinel
    1. 单独一个组件,可以独立出来
    2. 直接界面化的细粒度统一配置
  • Sentinel是什么?

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

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

SpringCloud微服务架构学习_第70张图片

Sentinel 的开源生态:

19.2 安装Sentinel控制台

sentinel组件由两部分组成: 后台 前台

Sentinel 分为两个部分:

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

安装步骤:

  1. 下载 https://github.com/alibaba/Sentinel/releases/tag/1.7.1,下载到本地sentinel-dashboard-1.7.1.jar
  2. 运行命令 java -jar sentinel-dashboard-1.7.1.jar
  3. 访问sentinel管理界面 http://localhost:8080,账号密码是sentinel

19.3 初始化演示工程

  1. 启动Nacos 8848

  2. Module

    • 新建cloudalibaba-sentinel-service8401

    • pom

      <dependency>
          <groupId>com.alibaba.cspgroupId>
          <artifactId>sentinel-datasource-nacosartifactId>
      dependency>
      <dependency>
          <groupId>com.alibaba.cloudgroupId>
          <artifactId>spring-cloud-alibaba-sentinelartifactId>
      dependency>
      <dependency>
          <groupId>org.springframework.cloudgroupId>
          <artifactId>spring-cloud-starter-openfeignartifactId>
      dependency>
      
    • yml

      server:
        port: 8401
      
      spring:
        application:
          name: cloudalibaba-sentinel-service
        cloud:
          nacos:
            discovery:
              server-addr: localhost:8848
          sentinel:
            transport:
              # 配置Sentinel dashborad地址
              dashboard: localhost:8080
              # 默认8719端口,假如被占用会自动从8719开始一次+1扫描,直到找到未被占用的端口
              port: 8719
      
      management:
        endpoints:
          web:
            exposure:
              include: '*'
      
    • 主启动

      @SpringBootApplication
      @EnableDiscoveryClient
      public class MainApp8401 {
               
          public static void main(String[] args) {
               
              SpringApplication.run(MainApp8401.class, args);
          }
      }
      
    • 业务类

      @RestController
      public class FlowLimitController {
               
      
          @GetMapping(value = "/testA")
          public String testA() {
               
              return "TestA --- "
          }
      
          @GetMapping(value = "/testB")
          public String testB() {
               
              return "TestB ---";
          }
      }
      
  3. 启动Sentinel8080

  4. 启动微服务8401

  5. 访问测试

    sentinel采用的是懒加载机制,需要执行一次访问:http://localhost:8401/testA
    在这里插入图片描述

19.4 流控规则

  • 基本介绍

SpringCloud微服务架构学习_第71张图片

SpringCloud微服务架构学习_第72张图片

​ 排队等待:让请求以均匀的速度通过,阈值类型必须设成QPS,否则无效。

请求超过1秒1次时报错:Blocked by Sentinel (flow limiting)

19.5 降级规则

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

SpringCloud微服务架构学习_第73张图片

  • RT(平均响应时间,秒级)

    平均响应时间超出阈值,且在时间窗口内通过的请求>=5,两个条件同时满足后触发降级

    窗口期过后关闭熔断器

    RT最大4900(更大的需要铜过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)

  • 异常比例(秒级)

    QPS >= 5,且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

  • 异常数(分钟级)

    异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级。

Sentinel断路器没有半开状态。

SpringCloud微服务架构学习_第74张图片

19.6 热点key限流

热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次Top N数据,并对其访问进行限制。

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

  • 自定义的兜底方法

    从HystrixCommand 到 @SentinelResource

  • 测试

    @GetMapping(value = "/testhotKey")
    @SentinelResource(value = "testhotKey", blockHandler = "deal_testhotKey")
    public String testhotKey(@RequestParam(value = "p1", required = false) String p1,
                             @RequestParam(value = "p2", required = false) String p2) {
           
        return "-----testhotKey-----";
    }
    
    // 自定义的兜底方法
    public String deal_testhotKey(String p1, String p2, BlockException blockException) {
           
        return "deal_testhotKey blockException---  o(╥﹏╥)o";  // Sentinel的默认提示:Blocked by Sentinel(flow limiting)
    }
    

SpringCloud微服务架构学习_第75张图片

19.7 系统规则

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

系统规则支持以下的模式:

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

19.8 @SentinelResource

  1. 按资源名称限流 + 后续处

    @RestController
    public class RateLimitController {
           
    
        @GetMapping("/byResource")
        @SentinelResource(value = "byResource", blockHandler = "handleException")
        public CommonResult byResource() {
           
            return new CommonResult(200, "按资源名称限流测试成功!", new Payment(2020L, "serial001"));
        }
    
        public CommonResult handleException(BlockException exception) {
           
            return new CommonResult(404, exception.getClass().getCanonicalName() + "\t服务不可用");
        }
    }
    

    关闭服务8401,Sentinel控制台的流控规则消失了。。。(临时)

  2. 按照Url地址限流 + 后续处理

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

SpringCloud微服务架构学习_第76张图片

  1. 上面兜底方案面临的问题

    (1)系统默认的,没有体现业务要求;

    (2)依照现有条件,自定义的处理方法和业务代码耦合在一起,不直观;

    (3)每个业务方法都添加一个兜底的,代码膨胀加速;

    (4)全局统一的处理方法没有体现。

  2. 客户自定义限流处理逻辑

    (1)创建CustomerBlockHandler类用于自定义限流处理逻辑

    (2)自定义限流处理类CustomerBlockHandler

    public class CustomerBlockHandler {
           
        public static CommonResult handlerException(BlockException exception) {
           
            return new CommonResult(404, "按用户自定义Url - 1\t服务不可用");
        }
        public static CommonResult handlerException2(BlockException exception) {
           
            return new CommonResult(404, "按用户自定义Url - 2\t服务不可用");
        }
    }
    

    (3)RateLimitController

    @GetMapping("/ratelimit/customerBlockHandler")
    @SentinelResource(value = "customerBlockHandler",
                      blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
    public CommonResult customerBlockHandler() {
           
        return new CommonResult(200, "按客户自定义测试成功!",
                                new Payment(2020L, "serial003"));
    }
    

    (4)启动微服务后先调用一次

    (5)Sentinel控制台配置

    (6)测试成功

19.9 服务熔断功能

  1. sentinel整合ribbon + openfeign + fallback

  2. Ribbon系列

    • 启动nacos和sentinel

    • 提供者9003和9004

      @RestController
      public class PaymentController {
               
          @Value(value = "${server.port}")
          private String serverPort;
      
          public static HashMap<Long, Payment> hashMap = new HashMap<>();
          static{
               
              hashMap.put(1L, new Payment(1L, "1231243763qfdafwrgavdaa"));
              hashMap.put(2L, new Payment(2L, "ascjaosncoafdafwrgavdaa"));
              hashMap.put(3L, new Payment(3L, "ASDASDASSAAfdafwrgavdaa"));
          }
          @GetMapping(value = "/paymentSQL/{id}")
          public CommonResult paymentSQL(@PathVariable("id") Long id) {
               
              Payment payment = hashMap.get(id);
              CommonResult<Payment> result = new CommonResult<>(200, "from mysql, serverPort:" + serverPort);
              return result;
          }
      }
      
    • 消费者84

      • yaml
      server:
        port: 84
      spring:
        application:
          name: nacos-order-consumer
        cloud:
          nacos:
            discovery:
              server-addr: localhost:8848   # 配置Nacos地址
          sentinel:
            transport:
              dashboard: localhost:8080      # 配置sentinel
              port: 8719
      service-url:
        nacos-user-service: http://nacos-payment-provider
      management:
        endpoints:
          web:
            exposure:
              include: '*'
      
      • controller
      @RestController
      @Slf4j
      public class CircleBreakController {
               
          public static String SERVICE_URL = "http://nacos-payment-provider";
      
          @Autowired
          private RestTemplate restTemplate;
      
          @GetMapping(value = "/consumer/fallback/{id}")
      //    @SentinelResource(value = "fallback", fallback = "handlerFallback")  // java异常归fallback
          @SentinelResource(value = "fallback",   // java异常归fallback
                  fallback = "handlerFallback",   // blockHandler只负责sentinel控制台违规
                  blockHandler = "blockHandler",  // 降级需要忽略的异常
                  exceptionsToIgnore = IllegalArgumentException.class) 
          public CommonResult<Payment> fallback(@PathVariable("id") Long id) {
               
              CommonResult<Payment> result =
                      restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
              if(id == 4) {
               
                  throw new IllegalArgumentException("IllegalArgumentException, 非法参数异常");
              }else if(result.getData() == null) {
               
                  throw new NullPointerException("NullPointerException - 空参数异常。。。。。");
              }
              return result;
          }
      
          public CommonResult handlerFallback(@PathVariable("id") Long id, Throwable e) {
               
              Payment payment = new Payment(id, null);
              return new CommonResult(444, "兜底方法,出现了异常!!!!" + e.getMessage(), payment);
          }
      
          public CommonResult blockHandler(@PathVariable("id") Long id, BlockException e) {
               
              Payment payment = new Payment(id, null);
              return new CommonResult(444, "blockHandler限流!!!!" + e.getMessage(), payment);
          }
      }
      
  3. Feign系列

    • yml
    # 激活Sentinel对Feign的支持
    feign:
      sentinel:
        enabled: true
    
    • 主启动
    @SpringBootApplication
    @EnableDiscoveryClient
    @EnableFeignClients
    public class OrderMain84 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(OrderMain84.class, args);
        }
    }
    
    • controller

      @Autowired
      PaymentService paymentService;
      
      @GetMapping(value = "/consumer/paymentSQL/{id}")
      public CommonResult<Payment> paymentSQL(@PathVariable(value = "id") Long id) {
               
          return paymentService.paymentSQL(id);
      }
      
      
    • service

      @FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
      public interface PaymentService {
               
      
          @GetMapping(value = "/paymentSQL/{id}")
          CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
      }
      
      @Component
      public class PaymentFallbackService implements PaymentService{
               
          @Override
          public CommonResult<Payment> paymentSQL(Long id) {
               
              return new CommonResult(444, "服务降级返回!");
          }
      }
      

19.10 规则持久化

  • 是什么?

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

  • 怎么用?

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

  • 步骤

    1. 修改cloudalibaba-sentinel-service8401

    2. pom

      <dependency>
          <groupId>com.alibaba.cspgroupId>
          <artifactId>sentinel-datasource-nacosartifactId>
      dependency>
      
    3. yml

            datasource:
              ds1:
                nacos:
                  server-addr: localhost:8848
                  dataId: cloudalibaba-sentinel-service
                  groupId: DEFAULT_GROUP
                  data-type: json
                  rule-type: flow
      
    4. Nacos增加配置

SpringCloud微服务架构学习_第77张图片

 resource:  资源名称

 limitApp: 来源应用

 grade: 阈值类型,0表示线程数,1表示QPS

 count: 单机阈值

 strategy: 流控模式,0表示直接,1表示关联,2表示链路

 controlBehavior: 流控效果,0表示快速失败,1表示Warm Up,2表示排队等待

 clusterMode: 是否集群
  1. 测试成功!

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

20.1 分布式事务问题

分布式之前,单机单库没有问题。

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

SpringCloud微服务架构学习_第78张图片

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

20.2 Seata简介

  • 是什么?

    Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

    http://seata.io/zh-cn/docs/overview/what-is-seata.html

  • 能干嘛?

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

    TC (Transaction Coordinator) - 事务协调者

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

    TM (Transaction Manager) - 事务管理器

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

    RM (Resource Manager) - 资源管理器

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

  • 处理过程

SpringCloud微服务架构学习_第79张图片

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

    https://github.com/seata/seata/releases

    http://seata.io/zh-cn/blog/download.html

  • 怎么玩?

    本地 @Transactional

    全局 @GlobalTransactional

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

20.3 Seata的安装

  1. 下载

  2. 解压,修改conf下的file.conf配置文件

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

​ file.conf

service {
     
  #vgroup->rgroup
  vgroup_mapping.my_test_tx_group = "fsp_tx_group"
  #only support single node
  default.grouplist = "127.0.0.1:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
}

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

  2. 修改 registry.conf配置

    type = "nacos"
    
    nacos {
           
    	serverAddr = "localhost:8848"
    	namespace = ""
    	cluster = "default"
    }
    
  3. 启动nacos、再启动seata

20.4 订单/库存/账户业务数据库准备

  • 业务说明:

    订单服务 -> 库存服务 -> 账户服务,用到三个库,存在分布式事务问题!

  • 创建业务数据库

    seata_order:存储订单数据库

    seata_stroage:存储库存数据库

    seata_account:存储账户信息数据库

  • 分别建表

    t_order、t_stroage、t_account

  • 3个库分别建对应的回滚日志表,在/conf下面db_undo_log.sql

  • 新建订单 Order-Module

    1. pom

      <dependency>
          <groupId>com.alibaba.cloudgroupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
      dependency>
      <dependency>
          <groupId>com.alibaba.cloudgroupId>
          <artifactId>spring-cloud-starter-alibaba-seataartifactId>
          <exclusions>  
              <exclusion>
                  <groupId>seata-allgroupId>
                  <artifactId>io.seataartifactId>
              exclusion>
          exclusions>
      dependency>
      <dependency>
          <groupId>io.seatagroupId>
          <artifactId>seata-allartifactId>
          <version>0.9.0version>
      dependency>
      <dependency>
          <groupId>org.springframework.cloudgroupId>
          <artifactId>spring-cloud-starter-openfeignartifactId>
      dependency>
      
    2. yml

      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://81.70.20.18:3306/seata_order
          username: root
          password: 123456
      
      feign:
        hystrix:
          enabled: true
          
      logging:
        level:
          root: info
      
      mybatis:
        mapperLocations: classpath:mapper/*.xml
      
    3. file.conf

      将seata中的file.conf 复制到项目中

    4. registry.conf

      将seata中的registry.conf复制到项目中

    5. domain

      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class Order {
               
          private Long id;
          private Long userId;
          private Long productId;
          private Integer count;
          private BigDecimal money;
          private Integer status;  // 订单状态: 0创建中,1已完结
      }
      
    6. dao

      @Mapper
      public interface OrderDao {
               
          // 1. 新建订单
          void create(Order order);
          // 2. 修改订单状态 0->1
          void update(@Param("userId") Long id, @Param("status") Integer status);
      }
      
    7. mapper

      
      
      <mapper namespace="com.wei.springcloud.dao.OrderDao">
          <resultMap id="BaseResultMap" type="com.wei.springcloud.domain.Order">
              <id column="id" property="id" jdbcType="BIGINT">id>
              <result column="user_id" property="userId" jdbcType="BIGINT">result>
              <result column="product_id" property="productId" jdbcType="BIGINT">result>
              <result column="count" property="count" jdbcType="INTEGER">result>
              <result column="money" property="money" jdbcType="DECIMAL">result>
              <result column="status" property="status" jdbcType="INTEGER">result>
          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

      @Service
      @Slf4j
      public class OrderServiceImpl implements OrderService {
               
          @Autowired
          private OrderDao orderDao;
          @Autowired
          private StorageService storageService;
          @Autowired
          private AccountService accountService;
          @Override
          public void create(Order order) {
               
              log.info("---------开始新建订单。。。。");
              orderDao.create(order);
              log.info("---------订单微服务开始调用库存扣减操作。。。。");
              storageService.decrease(order.getProductId(), order.getCount());
              log.info("---------订单微服务调用库存扣减操作结束了。。。。");
              log.info("---------订单微服务开始调用账户扣钱扣减操作。。。。");
              accountService.decrease(order.getUserId(), order.getMoney());
              log.info("---------订单微服务调用账户扣钱扣减操作结束了。。。。");
              // 修改订单的状态,从0到1
              log.info("---------修改订单状态开始。。。。");
              update(order.getUserId(), 0);
              log.info("---------修改订单状态结束。。。。");
              log.info("---------下订单结束了。。。。");
          }
          @Override
          public void update(Long id, Integer status) {
               
              orderDao.update(id, status);
          }
      }
      
      @FeignClient(value = "seata-storage-service")
      public interface StorageService {
               
          @PostMapping(value = "/storage/decrease")
          CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
      }
      
      @FeignClient(value = "seata-account-service")
      public interface AccountService {
               
          @PostMapping(value = "/account/decrease")
          CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal count);
      }
      
    9. controller

      @RestController
      public class OrderController {
               
          @Autowired
          private OrderService orderService;
      
          @GetMapping(value = "/order/create")
          public CommonResult create(Order order) {
               
              orderService.create(order);
              return new CommonResult(200, "订单创建成功!!!!");
          }
      }
      
    10. config

      @Configuration
      @MapperScan({
               "com.wei.springcloud.dao"})
      public class MybatisConfig {
               
      }
      
      @Configuration
      public class DatasourceProxyConfig {
               
          @Value(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 sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
               
              SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
              sqlSessionFactoryBean.setDataSource(dataSourceProxy);
              sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
              sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
              return sqlSessionFactoryBean.getObject();
          }
      }
      
    11. 主启动

      @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建
      @EnableFeignClients
      @EnableDiscoveryClient
      public class SeataOrderMainApp2001 {
               
          public static void main(String[] args) {
               
              SpringApplication.run(SeataOrderMainApp2001.class, args);
          }
      }
      
  • 新建库存 Storage-Module

    跟上面的module差不多事!

  • 新建账户Account-Module

    跟上面的module差不多事!

20.5 测试

  • 测试成功
{
     "code":200,"message":"订单创建成功!!!!","data":null}
  • 超时异常,没加 @GlobalTransactional

    在account中模拟超时,当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从0改为1。并且由于feign的重试机制,账户余额还有可能被多次扣减!

  • 超时异常,添加@GlobalTransactional

    @Override
    @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) // 发生任何异常,统统回滚!
    public void create(Order order) {
           }
    

20.6 补充

  • seata

    2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案;

    Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架

  • TC/TM/RM三大组件

SpringCloud微服务架构学习_第80张图片

SpringCloud微服务架构学习_第81张图片

  • AT模式如何做到对业务的无侵入

    1. 一阶段加载

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

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

      (2)执行业务SQL更新业务数据,在业务数据更新之后,

      (3)将其保存成 “after image”, 最后生成行锁;

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

SpringCloud微服务架构学习_第82张图片

  1. 二阶段提交

    二阶段如果顺利提交的话,

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

SpringCloud微服务架构学习_第83张图片

  1. 二阶段回滚

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

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

在这里插入图片描述

  • debug

SpringCloud微服务架构学习_第84张图片

21. 完结撒花!

   log.info("---------修改订单状态开始。。。。");
         update(order.getUserId(), 0);
         log.info("---------修改订单状态结束。。。。");
         log.info("---------下订单结束了。。。。");
     }
     @Override
     public void update(Long id, Integer status) {
         orderDao.update(id, status);
     }
 }
 ```

 ```java
 @FeignClient(value = "seata-storage-service")
 public interface StorageService {
     @PostMapping(value = "/storage/decrease")
     CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
 }
 ```

 ```java
 @FeignClient(value = "seata-account-service")
 public interface AccountService {
     @PostMapping(value = "/account/decrease")
     CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal count);
 }
 ```
  1. controller

    @RestController
    public class OrderController {
           
        @Autowired
        private OrderService orderService;
    
        @GetMapping(value = "/order/create")
        public CommonResult create(Order order) {
           
            orderService.create(order);
            return new CommonResult(200, "订单创建成功!!!!");
        }
    }
    
  2. config

    @Configuration
    @MapperScan({
           "com.wei.springcloud.dao"})
    public class MybatisConfig {
           
    }
    
    @Configuration
    public class DatasourceProxyConfig {
           
        @Value(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 sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
           
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSourceProxy);
            sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
            sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
            return sqlSessionFactoryBean.getObject();
        }
    }
    
  3. 主启动

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // 取消数据源的自动创建
    @EnableFeignClients
    @EnableDiscoveryClient
    public class SeataOrderMainApp2001 {
           
        public static void main(String[] args) {
           
            SpringApplication.run(SeataOrderMainApp2001.class, args);
        }
    }
    
  • 新建库存 Storage-Module

    跟上面的module差不多事!

  • 新建账户Account-Module

    跟上面的module差不多事!

20.5 测试

  • 测试成功
{
     "code":200,"message":"订单创建成功!!!!","data":null}
  • 超时异常,没加 @GlobalTransactional

    在account中模拟超时,当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从0改为1。并且由于feign的重试机制,账户余额还有可能被多次扣减!

  • 超时异常,添加@GlobalTransactional

    @Override
    @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) // 发生任何异常,统统回滚!
    public void create(Order order) {
           }
    

20.6 补充

  • seata

    2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案;

    Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架

  • TC/TM/RM三大组件

    [外链图片转存中…(img-oXH4vQPN-1615514689596)]

  • AT模式如何做到对业务的无侵入

    1. 一阶段加载

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

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

      (2)执行业务SQL更新业务数据,在业务数据更新之后,

      (3)将其保存成 “after image”, 最后生成行锁;

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

      [外链图片转存中…(img-YiUG8L1l-1615514689596)]

    2. 二阶段提交

      二阶段如果顺利提交的话,

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

      [外链图片转存中…(img-ZQih4CVN-1615514689597)]

    3. 二阶段回滚

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

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

      [外链图片转存中…(img-in7gncc1-1615514689598)]

  • debug

[外链图片转存中…(img-UlrY0Inc-1615514689598)]

21. 完结撒花!

你可能感兴趣的:(Java,java)