SpringCloud微服务笔记

写在前面

  • 官方代码地址:https://github.com/zzyybs/atguigu_spirngcloud2020
  • 本文地址:https://blog.csdn.net/hancoder/article/details/109063671
  • 本文markdown下载地址:会在一周内提供

文章目录


集中什么是微服务架构:

1597213385700

SpringCloud 是微服务一站式服务解决方案,微服务全家桶。它是微服务开发的主流技术栈。它采用了名称,而非数字版本号。

s方法gCloud 和 springCloud Alibaba 目前是最主流的微服务框架组合。

版本选择:

选用 springboot 和 springCloud 版本有约束,不按照它的约束会有冲突。

版本问题

本次学习的各种软件的版本:

  • boot使用的是数字作为版本。官网强烈建议升级到2.0以上
  • cloud使用的是字母作为版本,伦敦地铁站站名
Cloud Release Train Boot Version
Hoxton 2.2.x, 2.3.x (Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.x

查看版本对应关系:https://start.spring.io/actuator/info

"spring-cloud": {
    
    
    "Finchley.M2": "Spring Boot >=2.0.0.M3 and <2.0.0.M5",
    "Finchley.M3": "Spring Boot >=2.0.0.M5 and <=2.0.0.M5",
    "Finchley.M4": "Spring Boot >=2.0.0.M6 and <=2.0.0.M6",
    "Finchley.M5": "Spring Boot >=2.0.0.M7 and <=2.0.0.M7",
    "Finchley.M6": "Spring Boot >=2.0.0.RC1 and <=2.0.0.RC1",
    "Finchley.M7": "Spring Boot >=2.0.0.RC2 and <=2.0.0.RC2",
    "Finchley.M9": "Spring Boot >=2.0.0.RELEASE and <=2.0.0.RELEASE",
    "Finchley.RC1": "Spring Boot >=2.0.1.RELEASE and <2.0.2.RELEASE",
    "Finchley.RC2": "Spring Boot >=2.0.2.RELEASE and <2.0.3.RELEASE",
    "Finchley.SR4": "Spring Boot >=2.0.3.RELEASE and <2.0.999.BUILD-SNAPSHOT",
    "Finchley.BUILD-SNAPSHOT": "Spring Boot >=2.0.999.BUILD-SNAPSHOT and <2.1.0.M3",
    "Greenwich.M1": "Spring Boot >=2.1.0.M3 and <2.1.0.RELEASE",
    "Greenwich.SR6": "Spring Boot >=2.1.0.RELEASE and <2.1.18.BUILD-SNAPSHOT",
    "Greenwich.BUILD-SNAPSHOT": "Spring Boot >=2.1.18.BUILD-SNAPSHOT and <2.2.0.M4",
    "Hoxton.SR8": "Spring Boot >=2.2.0.M4 and <2.3.5.BUILD-SNAPSHOT",
    "Hoxton.BUILD-SNAPSHOT": "Spring Boot >=2.3.5.BUILD-SNAPSHOT and <2.4.0.M1",
    "2020.0.0-M3": "Spring Boot >=2.4.0.M1 and <=2.4.0.M1",
    "2020.0.0-SNAPSHOT": "Spring Boot >=2.4.0.M2"
},

尚硅谷阳哥教程版本:

cloud Hoxton.SR1
boot 2.2.2.RELEASE
cloud alibaba 2.1.0.RELEASE
java java8
maven 3.5及以上
mysql 5.7及以上

cloud版本决定了boot版本

停更

1,Eureka停用,可以使用zk作为服务注册中心

2,服务调用,Ribbon准备停更,代替为LoadBalance

3,Feign改为OpenFeign

4,Hystrix停更,改为resilence4j

​ 或者阿里巴巴的sentienl

5.Zuul改为gateway

6,服务配置Config改为 Nacos

7,服务总线Bus改为Nacos

1597213783265

Cloud简介

参考资料,尽量去官网

https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/

工程建造

写一个下图的Hello World

1597225177689

构建父工程,后面的项目模块都在此工程中:

1597214225785

设置编码:Settings -> File Encodings

注解激活:

1597214602636

Java版本确定:

1597214699619

父工程pom配置

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

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

    <groupId>com.dkf.cloud</groupId>
    <artifactId>cloud2020</artifactId>
    <version>1.0-SNAPSHOT</version>
    <!-- 第一步 打包方式pom-->
    <packaging>pom</packaging>

    <!-- 统一管理 jar 包版本 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
        <mysql.version>5.1.47</mysql.version>
        <druid.version>1.1.16</druid.version>
        <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    </properties>

    <!-- 子块基础之后,提供作用:锁定版本 + 子module不用写 groupId 和 version -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-project-info-reports-plugin</artifactId>
                <version>3.0.0</version>
            </dependency>

            <!-- 下面三个基本是微服务架构的标配 -->
            <!--spring boot 2.2.2-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.2.2.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud Hoxton.SR1-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud 阿里巴巴-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--mysql-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
                <scope>runtime</scope>
            </dependency>
            <!-- druid-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.spring.boot.version}</version>
            </dependency>
            <!--junit-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <!--log4j-->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

上面配置的解释:

首先要加 <packaging>pom</packaging> 这个。

为了让项目顺利的运行,我们必须使用统一的版本号;
1、dependencyment
(1)在我们项目中,我们会发现在父模块的pom文件中常常会出现dependencyMent元素,这是因为我们可以通过其来管理子模块的版本号,也就是说我们在父模块中声明号依赖的版本,但是并不实现引入
2、dependencies
(1)上面说到dependencyment只是声明一个依赖,而不实现引入,故我们在子模块中也需要对依赖进行声明,倘若不声明子模块自己的依赖,是不会从父模块中继承的;只有子模块中也声明了依赖。并且没有写对应的版本号它才会从父类中继承;并且version和scope都是取自父类;此外要是子模块中自己定义了自己的版本号,是不会继承自父类的。
3、总结
dependencyment只是用来管理依赖,规定未添加版本号的子模块依赖继承自它,dependencies是用来声明子模块自己的依赖,可以在其中来写自己需要的版本号

聚合版本依赖,dependencyManagement 只声明依赖,并不实现引入,所以子项目还需要写要引入的依赖。

可以统一版本

父工程创建完成执行mvn:install将父工程发布到仓库方便子工程继承

第一个微服务架构

创建一个module后(只能改a),在父工程的pom里多了个<modules>

在模块的pom里没有gv,只有a。

模块里的<dependencies>里的依赖只有ga,没有v

提供者

cloud-provider-payment8001 子工程的pom文件:

这里面的 lombok 这个包,引入以后,实体类不用再写set 和 get

可以如下写实体类:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
     
     
private Integer id;
private String serial;
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>com.dkf.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-provider-payment8001</artifactId>
    <dependencies>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <!--这个和web要写到一块-->
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

cloud-provider-payment8001 子工程的yml文件:

server:
  port: 8001

spring:
  application:
    name: cloud-provider-payment8001
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.gjt.mm.mysql.Driver
    url: jdbc:mysql://localhost:3306/cloud2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.dkf.springcloud.entities  # 所有Entity 别名类所在包

cloud-provider-payment8001 子工程的主启动类:

package com.dkf.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

下面的常规操作:

  • ①建表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
    
    select * from payment;
    
entities
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data   //set/get方法
@AllArgsConstructor //有参构造器
@NoArgsConstructor  //无参构造器
public class Payment implements Serializable {
    
    
    private long id;//数据库是bigint
    private String serial;
}
 import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

//返回给前端的通用json数据串
@Data   //set/get方法
@AllArgsConstructor //有参构造器
@NoArgsConstructor  //无参构造器
public class CommonResult<T> {
    
    
    private Integer code;
    private String message;
    private T data; //泛型,对应类型的json数据

    //自定义两个参数的构造方法
    public CommonResult(Integer code, String message){
    
    
        this(code, message, null);
    }
}
dao
@Mapper // 是ibatis下面的注解 //@Repositoty有时候会有问题
public interface PaymentDao {
    
    
    int create(Payment payment);

    Payment getPaymentById(@Param("id") Long id);
}

resource下创建mapper文件夹,新建PaymentMapper.xml。在yml里有所有entity别名类所在包,所有payment不用写全类名

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xzq.springcloud.dao.PaymentDao">
    <resultMap id="BaseResultMap" type="com.xzq.springcloud.entities.Payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <id column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>

    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial) values (#{serial})
    </insert>

    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
        select * from payment where id = #{id}
    </select>
</mapper>

@Param注解:https://blog.csdn.net/qq_39505065/article/details/90550705

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

public interface PaymentService {
    
    
  int create(Payment payment);

  Payment getPaymentById(@Param("id") Long id);
}
import com.xzq.springcloud.dao.PaymentDao;
import com.xzq.springcloud.entities.Payment;
import com.xzq.springcloud.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PaymentServiceImpl implements PaymentService {
    
    

  @Autowired
  private PaymentDao paymentDao;

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

  @Override
  public Payment getPaymentById(Long id) {
    
    
      return paymentDao.getPaymentById(id);
  }
}
controller
package com.dkf.springcloud.controller;


import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import com.dkf.springcloud.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController   //必须是这个注解,因为是模拟前后端分离的restful风格的请求,要求每个方法返回 json
@Slf4j
public class PaymentController {
    
    

  @Resource
  private PaymentService paymentService;

  @PostMapping(value = "/payment/create")
  //	    注意这里的 @RequestBody  是必须要写的,虽然 MVC可以自动封装参数成为对象,
  //      但是当消费者项目调用,它传参是 payment 整个实例对象传过来的, 即Json数据,因此需要写这个注解
  // https://blog.csdn.net/weixin_38004638/article/details/99655322
  public CommonResult create(@RequestBody Payment payment){
    
    
      int result = paymentService.create(payment);
      log.info("****插入结果:" + result);
      if(result > 0){
    
    
          return new CommonResult(200, "插入数据库成功", result);
      }
      return new CommonResult(444, "插入数据库失败", null);
  }

  @GetMapping(value = "/payment/{id}")
  public CommonResult getPaymentById(@PathVariable("id")Long id){
    
    
      Payment result = paymentService.getPaymentById(id);
      log.info("****查询结果:" + result);
      if(result != null){
    
    
          return new CommonResult(200, "查询成功", result);
      }
      return new CommonResult(444, "没有对应id的记录", null);
  }
}

对应POST方式的请求,要学会用POSTMAN工具

微服务多了之后就使用run dashboard

不但编译有个别地方会报错,启动也会报错,但是测试两个接口都是没问题的,推测启动报错是因为引入了下面才会引入的jar包,目前不影响。

热部署配置devtools

代码改动后希望自动生效

  1. 具体模块里添加Jar包到工程中,上面的pom文件已经添加上了
<dependency>    
    <groupId>org.springframework.boot</groupId>    
    <artifactId>spring-boot-devtools</artifactId>    
    <scope>runtime</scope>    
    <optional>true</optional>
</dependency>
  1. 添加plus到父工程的pom文件中:上i按也已经添加好了
<build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <fork>true</fork>
          <addResources>true</addResources>
        </configuration>
      </plugin>
    </plugins>
  </build>
  1. 1597222225568

  2. shift + ctrl + alt + / 四个按键一块按,选择Reg项:

1597222341651

重启IDEA

热部署只允许在开发阶段使用

消费者

新建模块cloud-consumer-order80

消费者现在只模拟调用提供者的Controller方法,没有持久层配置,只有Controller和实体类

当然也要配置主启动类和启动端口

pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>com.dkf.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-customer-order80</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--<dependency>&lt;!&ndash; 引入自己定义的api通用包,可以使用Payment支付Entity &ndash;&gt;-->
        <!--<groupId>com.atguigu.springcloud</groupId>-->
        <!--<artifactId>cloud-api-commons</artifactId>-->
        <!--<version>${project.version}</version>-->
        <!--</dependency>-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

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

把CommonResult 和 Payment 两个 实体类也创建出来

  • config/ApplicationContextCOnfig.java
  • controller/OrderController.java
  • entities/CommonResult.java
  • entities/Payment.java
  • OrderMain80.java

application.yml

server:
	port:80 # 80端口就可以省略了

OrderMain80.java

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

entites包中的类也拷贝到本项目中

  • entities/CommonResult.java
  • entities/Payment.java
配置RestTemplate

ApplicationContextConfig 内容:

package com.dkf.springcloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;//网络客户端

@Configuration
public class ApplicationContextConfig {
    
    

    @Bean
    public RestTemplate getRestTemplate(){
    
    
        return new RestTemplate();
        /*
        RestTemplate提供了多种便捷访问远程http服务的方法,
        是一种简单便捷的访问restful服务模板类,是spring提供的用于rest服务的客户端模板工具集
        */
    }
}
Controller
package com.dkf.springcloud.controller;

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

import javax.annotation.Resource;

@RestController
@Slf4j
public class OrderController {
    
    

    //远程调用的 地址
    public static final String PAYMENY_URL = "http://localhost:8001";

    @Resource
    private RestTemplate restTemplate;

    @PostMapping("customer/payment/create")
    public CommonResult<Payment> create (Payment payment){
    
    

        return restTemplate.postForObject(PAYMENY_URL + "/payment/create",//请求地址
                                          payment,//请求参数
                                          CommonResult.class);//返回类型
    }

    @GetMapping("customer/payment/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id")Long id){
    
    
        return restTemplate.getForObject(PAYMENY_URL + "/payment/" + id,//请求地址
                                         CommonResult.class);//返回类型
    }
}

如果 runDashboard 控制台没有出来,右上角搜索 即可

运用spring cloud框架基于spring boot构建微服务,一般需要启动多个应用程序,在idea开发工具中,多个同时启动的应用

需要在RunDashboard运行仪表盘中可以更好的管理,但有时候idea中的RunDashboard窗口没有显示出来,也找不到直接的开启按钮

idea中打开Run Dashboard的方法如下

view > Tool Windows > Run Dashboard

如果上述列表找不到Run Dashboard,则可以在工程目录下找到.idea文件夹下的workspace.xml,在其中相应位置加入以下代码(替换)即可:

<component name="RunDashboard">
    <option name="configurationTypes">
        <set>
            <option value="SpringBootApplicationConfigurationType"/>
        </set>
    </option>
    <option name="ruleStates">
        <list>
            <RuleState>
                <option name="name" value="ConfigurationTypeDashboardGroupingRule"/>
            </RuleState>
            <RuleState>
                <option name="name" value="StatusDashboardGroupingRule"/>
            </RuleState>
        </list>
    </option>
</component>

工程重构

上面 两个子项目,有多次重复的 导入 jar,和重复的 Entity 实体类。可以把 多余的部分,加入到一个独立的模块中,将这个模块打包,并提供给需要使用的 module

  1. 新建一个 cloud-api-commons 子模块
  2. 将 entities 包里面的实体类放到这个子模块中,也将 pom 文件中,重复导入的 jar包放到这个新建的 模块的 pom 文件中。如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud2020</artifactId>
        <groupId>com.dkf.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-api-commons</artifactId>

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

        <!-- 这个是新添加的,之前没用到,后面会用到。关于这个hutool 是个功能强大的工具包,官网:https://hutool.cn/ -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.1.0</version>
        </dependency>
    </dependencies>

</project>

mvn跳过test,mvc clean,mvn install

将此项目打包 install 到 maven仓库。

  1. 将 提供者 和 消费者 两个项目中的 entities 包删除,并删除掉加入到 cloud-api-commons 模块的 依赖配置。
  2. 将 打包到 maven 仓库的 cloud-api-commons 模块,引入到 提供者 和 消费者的 pom 文件中,如下所示
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <groupId>com.dkf.cloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
</dependency>

服务注册中心

如果是上面只有两个微服务,通过 RestTemplate ,是可以相互调用的,但是当微服务项目的数量增大,就需要服务注册中心。目前没有学习服务调用相关技术,使用 SpringCloud 自带的 RestTemplate 来实现RPC

1597213783265

Eureka

什么是服务治理

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

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

什么是服务注册与发现

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

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

1597291856582

官方停更不停用,以后可能用的越来越少。

EurekaNetflix 开发的,一个基于 REST 服务的,服务注册与发现的组件,以实现中间层服务器的负载平衡和故障转移。

Eureka 分为 Eureka Server 和 Eureka Client及服务端和客户端。Eureka Server为注册中心,是服务端,而服务提供者和消费者即为客户端,消费者也可以是服务者,服务者也可以是消费者。同时Eureka Server在启动时默认会注册自己,成为一个服务,所以Eureka Server也是一个客户端,这是搭建Eureka集群的基础。

  • Eureka Client:一个Java客户端,用于简化与 Eureka Server 的交互(通常就是微服务中的客户端和服务端)。通过注册中心进行访问。是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(roundrobin)负载算氵去的负载均衡器
    在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)
  • Eureka Server:提供服务注册服务,各个微服务节,通过配置启动后,会在Eureka Serverc中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点信息,服务节点的信息可以在界面中直观看到。

服务在Eureka上注册,然后每隔30秒发送心跳来更新它们的租约。如果客户端不能多次续订租约,那么它将在大约90秒内从服务器注册表中剔除。注册信息和更新被复制到集群中的所有eureka节点。来自任何区域的客户端都可以查找注册表信息(每30秒发生一次)来定位它们的服务(可能在任何区域)并进行远程调用

服务提供者向注册中心注册服务,并每隔30秒发送一次心跳,就如同人还活着存在的信号一样,如果Eureka在90秒后还未收到服务提供者发来的心跳时,那么它就会认定该服务已经死亡就会注销这个服务。这里注销并不是立即注销,而是会在60秒以后对在这个之间段内“死亡”的服务集中注销,如果立即注销,势必会对Eureka造成极大的负担。这些时间参数都可以人为配置。

Eureka还有自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,所以不会再接收心跳,也不会删除服务。

客户端消费者会向注册中心拉取服务列表,因为一个服务器的承载量是有限的,所以同一个服务会部署在多个服务器上,每个服务器上的服务都会去注册中心注册服务,他们会有相同的服务名称但有不同的实例id,所以拉取的是服务列表。我们最终通过负载均衡来获取一个服务,这样可以均衡各个服务器上的服务。

在这里插入图片描述

单机版Eureka构建:

消费者端口80,提供者端口8001。

Eureka端口7001

Server模块

pom

	<artifactId>cloud-eureka-server7001</artifactId>

<dependencies>
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
  </dependency>
    
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
  </dependency>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>com.dkf.cloud</groupId>
      <artifactId>cloud-api-commons</artifactId>
      <version>${project.version}</version>
  </dependency>
</dependencies>

版本说明:

<!--新版本2020.02-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>


<!--旧版本2018-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

application.yml

server:
	port: 7001

eureka:
	instance:
		hostname: localhost  # eureka 服务端的实例名称

	client:
# false 代表不向服务注册中心注册自己,因为它本身就是服务中心
		register-with-eureka: false
# false 代表自己就是服务注册中心,自己的作用就是维护服务实例,并不需要去检索服务
		fetch-registry: false
		service-url:
# 设置与 Eureka Server 交互的地址,查询服务 和 注册服务都依赖这个地址
			defaultZone: http://${
    
    eureka.instance.hostname}:${
    
    server.port}/eureka/
@EnableEurekaServer

最后写主启动类,如果启动报错,说没有配置 DataSource ,就在 主启动类的注解加上 这样的配置:

// exclude :启动时不启用 DataSource的自动配置检查
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableEurekaServer   // 表示它是服务注册中心
public class EurekaServerMain7001 {
    
    
    public static void main(String[] args){
    
    
        SpringApplication.run(EurekaServerMain7001.class, args);
    }
}

启动测试,访问 7001 端口

提供者

这里的提供者,还是使用 上面的 cloud-provider-payment8001 模块,做如下修改:

  1. 在 pom 文件的基础上引入 eureka 的client包,pom 的全部依赖如下所示:
	<artifactId>cloud-provider-payment8001</artifactId>
    <dependencies>
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
@EnableEurekaClient
  1. 主启动类 加上注解 : @EnableEurekaClient
  2. yml 文件添加关于 Eureka 的配置:
spring:
  application:
    name: cloud-payment-service # 项目名,也是注册的名字

eureka:
  client:
	# 注册进 Eureka 的服务中心
    register-with-eureka: true
    # 检索 服务中心 的其它服务
    fetch-registry: true
    service-url:
      # 设置与 Eureka Server 交互的地址
      defaultZone: http://localhost:7001/eureka/
消费者

这里的消费者 也是上面 的 cloud-customer-order80 模块

  1. 修改 pom 文件,加入Eureka 的有关依赖, 全部 pom 依赖如下:
	<artifactId>cloud-customer-order80</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
@EnableEurekaClient
  1. 主启动类 加上注解 : @EnableEurekaClient
  2. yml 文件必须添加的内容:
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://localhost:7001/eureka/
spring:
  application:
    name: cloud-order-service

Eureka 集群

  • 1先启动eureka注册中心
  • 2启动服务提供者payment支付服务
  • 3支付服务启动后会把自身信息化 服务以别名方式注册进eureka
  • 4消费者order服务在要调用接囗时,使用服务别名去注册中心取实际的RPC远程调用地址
  • 5消费者获得调用地址后,底层实际是利用HttpClient技术实现呈调用
  • 6消费者获得服务地址后会存jvm内存中,默认每间隔30s更新一次服务调用地址

Eureka Server在设计的时候就考虑了高可用设计,在Eureka服务治理设计中,所有节点既是服务的提供方,也是服务的消费方,服务注册中心也不例外。

Eureka Server的高可用实际上就是将自己做为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。

Eureka Server的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。可以采用两两注册的方式实现集群中节点完全对等的效果,实现最高可用性集群,任何一台注册中心故障都不会影响服务的注册与发现。

问题:微服务RPC远程服务调用最核心的是什么:

高可用,试想你的注册中心只有一个。onlyone,它出故障了那就呵呵了,会导致整个为服务环境不可用,所以要搭建Eureka注册中心集群,实现负载均衡+故障容错

Eureka 集群的原理:相互注册,互相守望。每台Eureka服务器都有集群里其他Eureka服务器地址的信息

开始构建Eureka集群:

现在创建 cloud-eureka-server7002 ,也就是第二个 Eureka 服务注册中心,pom 文件和 主启动类,与第一个Server一致。

模拟多个 为了不用输出C:\Windows\System32\drivers\etc\hosts 添加如下:

127.0.0.1 eureka7001.com

127.0.0.1 eureka7002.com

现在修改这两个 Server 的 yml 配置:

7001 端口的Server yml文件:

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com  # eureka 服务器的实例地址

  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
    ## 一定要注意这里的地址,这是搭建集群的关键。反过来写,写的是集群中其他Eureka服务器的地址
      defaultZone: http://eureka7002.com:7002/eureka/

7002 端口的Server yml文件:

server:
  port: 7002

eureka:
  instance:
    hostname: eureka7002.com  # eureka 服务器的实例地址

  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
    ## 一定要注意这里的地址 这是搭建集群的关键
      defaultZone: http://eureka7001.com:7001/eureka/

eureka.instance.hostname 才是启动以后 本 Server 的注册地址,而 service-url 是 map 类型,只要保证 key:value 格式就行,它代表 本Server 指向了那些 其它Server 。利用这个,就可以实现Eureka Server 相互之间的注册,从而实现集群的搭建。

提供者集群

上面配置了多个Eureka作为集群,接下来要配置的是提供者集群,让提供者高可用

为提供者cloud-provider-payment8001 模块创建集群,新建模块名为 cloud-provider-payment8002

即两个提供者8001和8002

其余配置都一致,需要配置集群的配置如下:

配置区别要点:

  • 集群中多个提供者的spring:application:name:要一致
  • 启动类添加*@EnableDiscoveryClient或者@EnableEurekaClient*
    1,@EnableDiscoveryClient注解是基于spring-cloud-commons依赖,并且在classpath中实现;
    2,@EnableEurekaClient注解是基于spring-cloud-netflix依赖,只能为eureka作用;
    如果你的classpath中添加了eureka,则它们的作用是一样的。

消费者(一般需要连接其他微服务的服务或者gateway/zuul)

# 提供者
server:
  port: 8001  # 端口号不一样

spring:
  application:
    name: cloud-provider-service  # 这次重点是这里,两个要写的一样,这是这个集群的关键
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.gjt.mm.mysql.Driver
    url: jdbc:mysql://localhost:3306/cloud2020?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: 123456

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.dkf.springcloud.entities  

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

注意在 Controller 返回不同的消息,从而区分者两个提供者的工作状态。(只是为了学习测试才这么做,生产环境直接复制即可)

在提供者的controller中

@Value("${server.port}")
private String serverPort;
消费者的配置

此时消费者一旦消费完之后,他以后访问的还是那台提供者。明显不对,原因在于消费者并没有去Rureka里找服务,而是自己找的

就是消费者如何访问 由这两个提供者组成的集群?

Eureka Server 上的提供者的服务名称如下:

@RestController
@Slf4j
public class OrderController {
    
    
    
    // 重点是这里,改成 提供者在Eureka 上的名称,而且无需写端口号	
    public static final String PAYMENY_URL = "http://CLOUD-PROVIDER-SERVICE";//取决于我们在提供者出配置的name,CLOUD-PAYMENY-SERVICE,//同时要注意使用@LoadBalanced注解赋予RestTemplate负载均衡能力

    @Resource
    private RestTemplate restTemplate;

    @PostMapping("customer/payment/create")
    public CommonResult<Payment> create (Payment payment){
    
    
        return restTemplate.postForObject(PAYMENY_URL + "/payment/create", payment, CommonResult.class);
    }

    @GetMapping("customer/payment/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id")Long id){
    
    
        return restTemplate.getForObject(PAYMENY_URL + "/payment/" + id, CommonResult.class);
    }

}
server:
  port: 80

spring:
    application:
        name: cloud-order-service
    zipkin:
      base-url: http://localhost:9411
    sleuth:
      sampler:
        probability: 1

eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: false
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #单机
      #defaultZone: http://localhost:7001/eureka
      # 集群
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka  # 集群版
@LoadBalanced

还有,消费者里面对RestTemplate配置的config文件,需要更改成如下:(就是加一个注解 @LoadBalanced

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

@Configuration
public class ApplicationContextConfig {
    
    

    @Bean
    @LoadBalanced  //这个注解,就赋予了RestTemplate 负载均衡的能力
    public RestTemplate getRestTemplate(){
    
    
        return new RestTemplate();
    }
}

这时候,消费者消费的提供者多次访问就会变化了(这就是Ribbon的负载平衡功能)

actuator让Eureka显示ip

为了在微服务Eureka控制台能看到我们的某个具体服务是在哪台服务器上部署的,我们需要配置一些内容。

修改 提供者在Eureka 注册中心显示的 主机名:即修改eureka:instance:instance-id:eureka:instance:prefer-ip-address:

# 提供者
server:
  port: 8001  # 端口号不一样

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
  instance: #重点,和client平行
    instance-id: payment8001 # 每个提供者的id不同,显示的不再是默认的项目名
    prefer-ip-address: true # 可以显示ip地址

1597301299700

1597301396971

服务发现Discovery@EnableDiscoveryClient

对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息。(即我们前面可视化页面的信息)

  1. 在主启动类上添加注解:@EnableDiscoveryClient
  2. 在 Controller 里面打印信息:
@Resource
private DiscoveryClient discoveryClient;

@GetMapping("/customer/discovery")
public Object discovery(){
    
    
    //获得服务清单列表
    List<String> services = discoveryClient.getServices();
    for(String service: services){
    
    
        log.info("*****service: " + service);
    }
    // 根据具体服务进一步获得该微服务的信息
    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-ORDER-SERVICE");
    for(ServiceInstance serviceInstance:instances){
    
    
        log.info(serviceInstance.getServiceId() + "\t" + serviceInstance.getHost()
                 + "\t" + serviceInstance.getPort() + "\t" + serviceInstance.getUri());
    }
    return this.discoveryClient;
}

Eureka 自我保护机制

某时刻某一个微服务不可用了,Eureka不会立即清理,依旧会对该微服务的信息进行保存。属于CAP里的AP分支

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

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

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自我保护机制?

为了防止Eureka Client可以正常运行但是与Eureka Server网络不通情况下,Eureka Server不会立刻将Eureka Client服务剔除

什么是自我保护模式?

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

自我保护机制:默认情况下Eureka CIient定时向Eureka Server端发送心跳包。

如畀Eureka在server端在一定时间内(默认90秒)没有收到Eureka Client发送心跳包,便会直接从服务注册列表中剔除该服务,但是在短时间(90秒中)内丢矢了大量的服务实例心跳,这时Eureka Server会开启目我保护机制,不令剔除该服务(该现象可能出现如果网络不通但是Eureka Client出现宕机,此时如果别的注册中心如果一定时间内没有收到心跳会将剔除该服务这样就出现了严重失误,因为客户端还能正常发送心跳,只是网络延迟问题,而保护机制是为了解决此问韙而产生的

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

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

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

禁止自我保护:(如果想)

在 Eureka Server 的模块中的 yml 文件进行配置:

server:
port: 7001

eureka:
instance:
 hostname: eureka7001.com

client:
 register-with-eureka: false
 fetch-registry: false
 service-url:
   defaultZone: http://eureka7002.com:7002/eureka/
server: # 与client平行
	# 关闭自我保护机制,保证不可用该服务被及时剔除
	enable-self-preservation: false
	eviction-interval-timer-in-ms: 2000

修改 Eureka Client 模块的 心跳间隔时间:

# 提供者
server:
  port: 8001  # 端口号不一样

eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url: # 集群
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
  instance: #重点,和client平行
    instance-id: payment8001 # 每个提供者的id不同,显示的不再是默认的项目名
    prefer-ip-address: true # 可以显示ip地址
    # Eureka客户端像服务端发送心跳的时间间隔,单位s,默认30s
    least-renewal-interval-in-seconds: 1
    # Rureka服务端在收到最后一次心跳后等待时间上线,单位为s,默认90s,超时将剔除服务
    least-expiration-duration-in-seconds: 2
eureka配置项解读:

在注册服务之后,服务提供者会维护一个心跳用来持续高速Eureka Server,“我还在持续提供服务”,否则Eureka Server的剔除任务会将该服务实例从服务列表中排除出去。我们称之为服务续约。
面是服务续约的两个重要属性:

(1)eureka.instance.lease-expiration-duration-in-seconds
leaseExpirationDurationInSeconds,表示eureka server至上一次收到client的心跳之后,等待下一次心跳的超时时间,在这个时间内若没收到下一次心跳,则将移除该instance。
默认为90秒
如果该值太大,则很可能将流量转发过去的时候,该instance已经不存活了。
如果该值设置太小了,则instance则很可能因为临时的网络抖动而被摘除掉。
该值至少应该大于leaseRenewalIntervalInSeconds

(2)eureka.instance.lease-renewal-interval-in-seconds
leaseRenewalIntervalInSeconds,表示eureka client发送心跳给server端的频率。如果在leaseExpirationDurationInSeconds后,server端没有收到client的心跳,则将摘除该instance。除此之外,如果该instance实现了HealthCheckCallback,并决定让自己unavailable的话,则该instance也不会接收到流量。
默认30秒

*eureka.client.registry-fetch-interval-seconds* :表示eureka client间隔多久去拉取服务注册信息,默认为30秒,对于api-gateway,如果要迅速获取服务注册状态,可以缩小该值,比如5秒
*eureka.server.enable-self-preservation*
是否开启自我保护模式,默认为true。
默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。
Eureka通过“自我保护模式”来解决这个问题——当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
*eureka.server.eviction-interval-timer-in-ms*
eureka server清理无效节点的时间间隔,默认60000毫秒,即60秒

Eureka停更说明:

2.0后停更了。

Zookeeper

springCloud 整合 zookeeper

  • zookeeper是一个分布式协调工具,可以实现注册中心功能
  • 关闭Linux服务器防火墙后动zookeeper服务器
  • zookeeper服务器取代Eureka服务器,zk作为服务注册中心
提供者

新建模块cloud-provider-payment8004

pom文件如下:

	<artifactId>cloud-provider-payment8004</artifactId>

    <dependencies>
        <!--springcloud 整合 zookeeper 组件-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <!--zk发现-->
            <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.zookeeper</groupId>
                    <artifactId>zookeeper</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.9</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yaml

server:
  port: 8004

spring:
  application:
    name: cloud-provider-service
  cloud:
    zookeeper:
      connect-string: 192.168.40.100:2181 # zk地址

主启动类:

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

@SpringBootApplication
@EnableDiscoveryClient	 // 以后用这个就可以了,不用eureka了
public class PaymentMain8004 {
    
    
    public static void main(String[] args){
    
    
        SpringApplication.run(PaymentMain8004.class, args);
    }
}

Controller 打印信息:

@RestController
@Slf4j
public class PaymentController {
    
    

    @Resource
    private PaymentService paymentService;

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

    @RequestMapping("/payment/zk")
    public String paymentzk(){
    
    
        return "springcloud with zookeeper :" + serverPort + "\t" + UUID.randomUUID().toString();
    }
}

如果 zookeeper 的版本和导入的jar包版本不一致,启动就会报错,由zk-discovery和zk之间的jar包冲突的问题。

解决这种冲突,需要在 pom 文件中,排除掉引起冲突的jar包,添加和服务器zookeeper版本一致的 jar 包,

但是新导入的 zookeeper jar包 又有 slf4j 冲突问题,于是再次排除引起冲突的jar包

<!--springcloud 整合 zookeeper 组件-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    <!-- 排除与zookeeper版本不一致到导致 冲突的 jar包 -->
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 添加对应版本的jar包 -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.9</version>
    <!-- 排除和 slf4j 冲突的 jar包 -->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

启动测试:

# 在zk客户端
ls /services # 输入
[cloud-provider-service] #输出
# 继续向下查看
ls /services/cloud-provider-service
# 继续向下,然后get,返回了个json

我们在zk上注册的node是临时节点,当我们的服务一定时间内没有发送心跳,那么zk就会将这个服务的znode删除了。没有自我保护机制。重新建立连接后znode-id号也会变

消费者

创建测试zookeeper作为服务注册中心的 消费者 模块 cloud-customerzk-order80

主启动类、pom文件、yml文件和提供者的类似

config类,注入 RestTemplate

@SpringBootConfiguration
public class ApplicationContextConfig {
    
    
    @Bean
    @LoadBalanced // 继续加上这个
    public RestTemplate getTemplate(){
    
    
        return new RestTemplate();
    }
}

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

controller层也是和之前类似:

@RestController
@Slf4j
public class CustomerZkController {
    
    

    public static final String INVOKE_URL="http://cloud-provider-service"; //和原来一样

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/customer/payment/zk")
    public String paymentInfo(){
    
    
        String result = restTemplate.getForObject(INVOKE_URL + "/payment/zk",String.class);
        return result;
    }
}

然后就在zk里查到consumer信息了。

关于 zookeeper 的集群搭建,目前使用较少,而且在 yml 文件中的配置也是类似,以列表形式写入 zookeeper 的多个地址即可,而且zookeeper 集群,在 hadoop的笔记中也有记录。总而言之,只要配合zookeeper集群,以及yml文件的配置就能完成集群搭建

后面会用ribbon代替RestTemplate

Consul

consul也是服务注册中心的一个实现,是由go语言写的。官网地址: https://www.consul.io/intro 中文地址: https://www.springcloud.cc/spring-cloud-consul.html
Consul是一套开源的分布式服务发现和配置管理系统。
提供了微服务系统中的服务治理,配置中心,控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网络。

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

安装并运行

下载地址:https://www.consul.io/downloads.html

打开下载的压缩包,只有一个exe文件,实际上是不用安装的,在exe文件所在目录打开dos窗口使用即可。

使用开发模式启动:consul agent -dev

访问8500端口,即可访问首页

提供者

新建提供者模块:cloud-providerconsul-service8006

pom 文件:

	<artifactId>cloud-providerconsul-service8006</artifactId>
    <dependencies>
        <!--springcloud consul server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>

        <!-- springboot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 日常通用jar包 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml 文件:

server:
  port: 8006
spring:
  application:
    name: consul-provider-service
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:    # 指定注册对外暴露的服务名称
        service-name: ${
    
    spring.application.name}

主启动类:

@SpringBootApplication
@EnableDiscoveryClient // 提供者
public class ConsulProviderMain8006 {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(ConsulProviderMain8006.class,args);
    }
}

controller也是简单的写一下就行。

消费者

新建 一个 在82端口的 消费者模块。pom和yml和提供者的类似,主启动类不用说,记得注入RestTemplate

@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class OrderConsulMain80
{
    
    
    public static void main(String[] args) {
    
    
            SpringApplication.run(OrderConsulMain80.class, args);
    }
}


@Configuration
public class ApplicationContextConfig
{
    
    
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate()
    {
    
    
        return new RestTemplate();
    }
}

controller层:

@RestController
public class CustomerConsulController {
    
    

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

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/customer/payment/consul")
    public String paymentInfo(){
    
    
        String result = restTemplate.getForObject(INVOKE_URL + "/payment/consul",String.class);
        return result;
    }
}

总结

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

CAP:

  • C:Consitency 强一致性
  • A:Available 可用性
  • P:Partition tolerance 分区容错性

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

15973845082911597384554249

服务调用

RestTemplate不是给我们提供远程调用了吗,那还要学其他的做什么。答案是负载均衡,在发送请求时通过负载均衡算法从提供者列表中选择一个。

都是使用在 client端,即有 ”消费者“ 需求的模块中。
1597213783265

Ribbon

我们这里提前启动好之前在搭建的 eureka Server 集群(5个模块)

简介

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

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

LB负载均衡(LoadBalance)是什么?

简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。

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

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

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

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

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

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

1597385250463

Ribbon在工作时分成两步:

  • 第一步先选择Eureka Server,它优先选择在同一个区域内负载较少的server
  • 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
    • 其中Ribbon提供了多种策略:比如轮询、随相和根据响应时间加权。

上面在eureka时,确实实现了负载均衡机制,那是因为 eureka-client包里面自带着ribbon

一句话,Ribbon 就是 负载均衡 + RestTemplate 调用。实际上不止eureka的jar包有,zookeeper的jar包,还有consul的jar包都包含了他,就是上面使用的服务调用。

1597385486515

如果自己添加,在 模块的 pom 文件中引入:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
RestTemplate

对于RestTemplate 的一些说明:

有两种请求方式:post和get ,还有两种返回类型:object 和 Entity

  • getForObject()/getForEntity()
    • Object:返回对象响应体中数据转化成的对象,基本上可以理解成json
    • Entity:返回对象是ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
    • 返回的entity.getBody()即得到了Object
  • postForObject()/postForEntity()
package com.atguigu.springcloud.controller;

import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.lb.LoadBalancer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.net.URI;
import java.util.List;

@RestController
@Slf4j
public class OrderController{
    
    
    //public static final String PAYMENT_URL = "http://localhost:8001";

    public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private LoadBalancer loadBalancer;
    @Resource
    private DiscoveryClient discoveryClient;

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

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

    @GetMapping("/consumer/payment/getForEntity/{id}")
    public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
    
    
       //   ResponseEntity是spring中的类
        ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);

        if(entity.getStatusCode().is2xxSuccessful()){
    
    
            return entity.getBody();//获取json
        }else{
    
    
            return new CommonResult<>(444,"操作失败");
        }
    }

    @GetMapping(value = "/consumer/payment/lb")
    public String getPaymentLB(){
    
    
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");

        if(instances == null || instances.size() <= 0){
    
    
            return null;
        }
        ServiceInstance serviceInstance = loadBalancer.instances(instances);
        URI uri = serviceInstance.getUri();

        return restTemplate.getForObject(uri+"/payment/lb",String.class);
    }

    // ====================> zipkin+sleuth
    @GetMapping("/consumer/payment/zipkin")
    public String paymentZipkin(){
    
    
        String result = restTemplate.getForObject("http://localhost:8001"+"/payment/zipkin/", String.class);
        return result;
    }
}

负载均衡

IRule:根据特定算法从服务列表中选择一个要访问的服务

Ribbon 负载均衡规则类型:

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

配置负载均衡规则:

官方文档明确给出了警告:

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

注意上面说的,而Springboot主启动类上的 @SpringBootApplication 注解,相当于加了@ComponentScan注解,会自动扫描当前包及子包,所以注意不要放在SpringBoot主启动类的包内。

创建包:

  • java
    • com.hh
      • myrule
        • MySelfRule.java
      • springcloud

在这个包下新建 MySelfRule类:

package com.dkf.myrule;

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

@Configuration
public class MySelfRule {
    
    
    @Bean
    public IRule myrule(){
    
    
        return new RandomRule(); //负载均衡规则定义为随机
    }
}

然后在主启动类上添加如下注解 @RibbonClient:

package com.dkf.springcloud;

import com.dkf.myrule.MySelfRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;

@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@RibbonClient(name="CLOUD-PROVIDER-SERVICE", configuration = MySelfRule.class)//指定该负载均衡规则对哪个提供者服务使用 , 加载自定义规则的配置类
public class OrderMain80 {
    
    

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

轮询算法原理

负载均衡轮询算法

rest接口第几次请求次数 % 服务器集群总数量 = 实际调用服务器位置下标

每次服务器重启后,rest接口计数从1开始。

1597387609476

ribbon源码:

private AtomicInteger nextServerCyclicCounter;

public Server choose(ILoadBalancer lb, Object key) {
    
    

    Server server = null;
    int count = 0;
    while (server == null && count++ < 10) {
    
    
        List<Server> reachableServers = lb.getReachableServers();
        List<Server> allServers = lb.getAllServers();
        int upCount = reachableServers.size();
        int serverCount = allServers.size();

        int nextServerIndex = incrementAndGetModulo(serverCount);
        server = allServers.get(nextServerIndex);

        if (server == null) {
    
    
            /* Transient. */
            Thread.yield();
            continue;
        }

        if (server.isAlive() && (server.isReadyToServe())) {
    
    
            return (server);
        }

        // Next.
        server = null;
    }

    if (count >= 10) {
    
    
        log.warn("No available alive servers after 10 tries from load balancer: "  + lb);
    }
    return server;
}
private int incrementAndGetModulo(int modulo) {
    
    
    for (;;) {
    
    
        int current = nextServerCyclicCounter.get();//获取原子的值
        int next = (current + 1) % modulo;
        if (nextServerCyclicCounter.compareAndSet(current, next)) //CAS
            return next;
    }
}

手写负载算法:cas+自旋

首先8001、8002服务controller层加上

@GetMapping("/payment/lb")
public String getPaymentLB() {
    
    
    return SERVER_PORT;
}

LoadBalancer接口:

import org.springframework.cloud.client.ServiceInstance;
import java.util.List;

public interface LoadBalancer {
    
    
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

实现

import org.springframework.cloud.client.ServiceInstance;
import java.sql.SQLOutput;

@Component
public class MyLB implements LoadBalancer {
    
    

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    private final int getAndIncrement() {
    
    
        int current;
        int next;

        do {
    
    
            current = this.atomicInteger.get();
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;
        } while (!atomicInteger.compareAndSet(current, next));
        System.out.println("第几次访问,次数next:" + next);
        return next;
    }

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

controller类中添加:

@GetMapping("/consumer/payment/lb")
public String getPaymentLB() {
    
    
    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");//获得总的提供者数
    if (instances == null || instances.size() <= 0) {
    
    
        return null;
    }

    ServiceInstance serviceInstance = loadBalancer.instances(instances);//传入总的实例数
    URI uri = serviceInstance.getUri();

    return restTemplate.getForObject(uri + "/payment/lb", String.class);
}

OpenFeign

概述

这里和之前学的dubbo很像,例如消费者的controller 可以调用提供者的 service层方法,但是不一样,它貌似只能调用提供者的 controller,即写一个提供者项目的controller的接口,消费者来调用这个接口方法,就还是相当于是调用提供者的 controller ,和RestTemplate 没有本质区别

Feign能干什么:

Feign旨在使编写JavaHttp客户端变得更容易。

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

Feign集成了Ribbon

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

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

消费端

新建cloud-consumer-feign-order80模块

feign用在消费端,feign自带负载均衡配置,所以不用手动配置

pom :

	<dependencies>
        <!-- Open Feign,他里面也有ribbon,所以有负载均衡 -->
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- eureka Client -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

yaml

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
@EnableFeignClients

主启动类:

@SpringBootApplication
@EnableFeignClients   //关键注解
public class CustomerFeignMain80 {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(CustomerFeignMain80.class, args);
    }
}
@FeignClient

新建一个service

这个service还是 customer 模块的接口,和提供者没有任何关系,不需要包类名一致。它使用起来就相当于是普通的service。

推测大致原理,对于这个service 接口,读取它某个方法的注解(GET或者POST注解不写报错),知道了请求方式和请求地址,而抽象方法,只是对于我们来讲,调用该方法时,可以进行传参等。

@Component //别忘了添加这个
@FeignClient(value = "CLOUD-PROVIDER-SERVICE")  //服务名称,要和eureka上面的一致才行
public interface PaymentFeignService
{
    
    
    //这个就是provider 的controller层的方法定义。
    @GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);

    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeout();
}
/*
在提供端有:
	@GetMapping(value = "/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id)
    {
        Payment payment = paymentService.getPaymentById(id);

        if(payment != null)
        {
            return new CommonResult(200,"查询成功,serverPort:  "+serverPort,payment);
        }else{
            return new CommonResult(444,"没有对应记录,查询ID: "+id,null);
        }
    }
*/

Controller层:

//使用起来就相当于是普通的service。
@RestController
public class CustomerFeignController {
    
    

    @Resource
    private PaymentFeignService paymentFeignService;//动态代理

    @GetMapping("/customer/feign/payment/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){
    
    
        return paymentFeignService.getPaymentById(id);
    }
}

超时控制

超时设置,故意设置超时演示出错情况:

  • 服务提供方8001故意写暂停程序
  • 服务消费方80添加超时方法PaymentFeignService
  • 服务消费方80添加超时方法OrderFeignControIIer
  • 测试:http://localhost/consumer/payment/feign/timeout
    错误页面

Openfeign默认超时等待为一秒,在消费者里面配置超时时间

//8001提供方 
@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout(){
    
    
    // 业务逻辑处理正确,但是需要耗费3秒钟
    try {
    
     TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
    return serverPort;
}
//消费方80 
@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout(){
    
    
    // OpenFeign客户端一般默认等待1秒钟
    return paymentFeignService.paymentFeignTimeout();
}
eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

开启日志打印

Feign共了日志打印功能,我们可以诵过配置来调整日志级别,从而了解Feign中Tttp请求的细节。

说白了就是对Feign接口的调用情况进行监控和输出。

日志级别:

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

首先写一个config配置类:

package com.atguigu.springcloud.config;

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

@Configuration
public class FeignConfig{
    
    
    @Bean
    Logger.Level feignLoggerLevel(){
    
    
        return Logger.Level.FULL;
    }
}

然后在yml文件中开启日志打印配置:

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
#指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.atguigu.springcloud.service.PaymentFeignService: debug

中级部分

主要是服务降级、服务熔断、服务限流的开发思想和框架实现

1597213783265

Hystrix 断路器

官方地址:https://github.com/Netflix/Hystrix/wiki/How-To-Use

概述

服务雪崩:

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

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

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

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

Hystrix停止更新,进入维护阶段:https://github.com/Netflix/Hystrix

https://github.com/Netflix/Hystrix/wiki/How-To-Use

服务降级:

fallback

服务器忙碌或者网络拥堵时,不让客户端等待并立刻返回一个友好提示,fallback。

对方系统不可用了,你需要给我一个兜底的方法,不要耗死。

向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

降低发生的情况:

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

break

  • 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
  • 就是保险丝:服务的降级->进熔断->恢复调用链路
服务限流:

flowlimit

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

可见,上面的技术不论是消费者还是提供者,根据真实环境都是可以加入配置的。

案例

首先构建一个eureka作为服务中心的单机版微服务架构 ,这里使用之前eureka Server 7001模块,作为服务中心

新建 提供者 cloud-provider-hystrix-payment8001 模块:

pom 文件:

	<dependencies>
        <!-- hystrix -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
        
        <!--eureka-client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

下面主启动类、service、和controller代码都很简单普通。

主启动类:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableEurekaClient
public class PaymentMain8001 {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(PaymentMain8001.class,args);
    }
}

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){
    
    
        int interTime = 3;
        try{
    
    
            TimeUnit.SECONDS.sleep(interTime);//模拟超时
        }catch (Exception e){
    
    
            e.printStackTrace();
        }
        return "线程池:" + Thread.currentThread().getName() + "--paymentInfo_Timeout,id:" + id + 
            "耗时" + interTime + "秒钟--";
    }
}

controller层:

@RestController
@Slf4j
public class PaymentController {
    
    

    @Resource
    private PaymentService paymentService;

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

    @GetMapping("/payment/hystrix/{id}")
    public String paymentInfo_OK(@PathVariable("id")Integer id){
    
    
        log.info("paymentInfo_OKKKKOKKK");
        return paymentService.paymentinfo_Ok(id);
    }

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id")Integer id){
    
    
        log.info("paymentInfo_timeout");
        return paymentService.paymentinfo_Timeout(id);
    }
}

模拟高并发

JMeter

这里使用一个新东西 JMeter 压力测试器,模拟多个请求

下载压缩包,解压,双击 /bin/ 下的 jmeter.bat 即可启动

15974716834321597471750363

ctrl + S 保存后,输入请求地址开始压测。

1597472029967

从测试可以看出,当模拟的超长请求被高并发以后,访问普通的小请求速率也会被拉低。

tomcat的默认工作线程数被打满了,没有多余的线程来分解压力和处理。

上面还是服务8001自己测试,加入此时外部的消费者80页来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖死。

测试可见,当启动高并发测试时,消费者访问也会变得很慢,甚至出现超时报错。

演示问题:8001端口自身已经被打满了,80还要访问8001,80也响应慢了。

  • 超时导致服务器变慢(转圈):超时不再等待
  • 出错(宕机或程序运行出错):出错要有兜底

解决思路:

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

服务降级案例

新建消费者 cloud-customer-feign-hystrix-order80 模块:以feign为服务调用,eureka为服务中心的模块,

<dependencies>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--hystrix-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <!--eureka client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
server:
  port: 80

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

feign:
  hystrix:
    enabled: true

主启动类

@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80{
    
    //消费端
    public static void main(String[] args)
    {
    
    
        SpringApplication.run(OrderHystrixMain80.class,args);
    }
}

一般服务降级放在客户端,即 消费者端 ,但是提供者端一样能使用。

首先提供者,即8001 先从自身找问题,设置自身调用超时的峰值,峰值内正常运行,超出峰值需要有兜底的方法处理,作服务降级fallback

服务端降级@HystrixCommand

首先 对 8001的service进行配置(对容易超时的方法进行配置) :

降级配置:@HystrixCommand,可以在里面指定超时/出错的回调方法,作为兜底方法

提供方 service方法:演示超时

// 服务端service
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",//超时后回调方法
                commandProperties = {
    
    
                    @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",//时间单位
                                     value="3000")})//超时时间
public String paymentInfo_TimeOut(Integer id){
    
    
    try {
    
     TimeUnit.MILLISECONDS.sleep(5000); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
    return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(秒): ";
}

// 兜底方法
public String paymentInfo_TimeOutHandler(Integer id){
    
     // 回调函数向调用方返回一个符合预期的、可处理的备选响应
    return "线程池:  "+Thread.currentThread().getName()+"  8001系统繁忙或者运行报错,请稍后再试,id:  "+id+"\t"+"o(╥﹏╥)o";
}

提供方 主启动类:

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker//加上这个
public class PaymentHystrixMain8001{
    
    //提供者
    public static void main(String[] args) {
    
    
            SpringApplication.run(PaymentHystrixMain8001.class, args);
    }

上面演示的是超时,下面演示上面service出错:同样走一样的回调方法

// 服务端service
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",//超时后回调方法,出错后也走
                commandProperties = {
    
    
                    @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",//时间单位
                                     value="3000")})//超时时间
public String paymentInfo_TimeOut(Integer id){
    
    
    int age = 10/0;
    return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(秒): ";
}

// 兜底方法
public String paymentInfo_TimeOutHandler(Integer id){
    
     // 回调函数向调用方返回一个符合预期的、可处理的备选响应
    return "线程池:  "+Thread.currentThread().getName()+"  8001系统繁忙或者运行报错,请稍后再试,id:  "+id+"\t"+"o(╥﹏╥)o";
}

消费端降级

上面的案例是服务端降级,现在我们服务端处理3s,然后返回。但是消费端等1s就等不住了,这时候就需要消费端也有降级方法

80的降级。原理是一样的,上面的@HystrixCommand降级可以放在服务端,也可以放在消费端。但一般放在客户端。

注意:我们自己配置过的热部署方式对java代码的改动明显,但对@HystrixCommand内属性的修改建议重启微服务。

server:
  port: 80

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

feign:
  hystrix:
    enabled: true
@SpringBootApplication
@EnableFeignClients
@EnableHystrix // 加上这个  //注意区别我们在提供端添加的注解是@EnableCircuitBreaker
public class OrderHystrixMain80{
    
    
    public static void main(String[] args){
    
    
        SpringApplication.run(OrderHystrixMain80.class,args);
    }
}

然后对 80 进行服务降级:很明显 service 层是接口,所以我们对消费者,在它的 controller 层进行降级。继续使用@HystrixCommand注解指定方法超时后的回调方法

controller

@RestController
@Slf4j
public class OrderHystirxController{
    
    
    @Resource
    private PaymentHystrixService paymentHystrixService;

    

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",
                    commandProperties = {
    
    
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
                             value="1500") })//只等1.5s
    //@HystrixCommand
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
    
    
        // int age = 10/0;
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;
    }
    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
    
    
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试 或者 自己运行出错请检查自己,o(╥﹏╥)o";
    }
}
//然后我们把上面的 int age=10/0;打开,出错后也会调用它的兜底方法

service

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" ,fallback = PaymentFallbackService.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);
}

目前问题:

  • 每个业务方法对应一个兜底的方法,代码膨胀
  • 同样和自定义分开

我们定义一个全局的兜底方法,这样就不用每个方法都得写兜底方法了。

全局兜底@DefaultProperties
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") //添加了这个
public class OrderHystirxController{
    
    
    @Resource
    private PaymentHystrixService paymentHystrixService;

    @GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",
                    commandProperties = {
    
    
            @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",
                             value="1500") })//只等1.5s
    public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
    
    
        // int age = 10/0;
        String result = paymentHystrixService.paymentInfo_TimeOut(id);
        return result;
    }
    public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
    
    
        return "我是消费者80,对方支付系统繁忙请10秒钟后再试 或者 自己运行出错请检查自己,o(╥﹏╥)o";
    }

    // 下面是全局fallback方法
    public String payment_Global_FallbackMethod(){
    
    
        return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
    }
}
// 自己指定了兜底方法的话就走自己的兜底方法,否则走全局的兜底方法
service降级@FeignClient

下面解决业务逻辑混在一起的问题(解耦):我们改在service层进行服务降级

服务降级,客户端去调用服务端,碰上服务端宕机或关闭。

本次案例降级处理是在客户端80实现完成的,与服务端8001没有关系。只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。

在这种方式一般是在客户端,即消费者端,首先上面再controller中添加的 @HystrixCommand 和 @DefaultProperties 两个注解去掉。就是保持原来的controller

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT" ,
             fallback = PaymentFallbackService.class)//指定的是回调的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);
}

service回调方法:是service接口的实现类。此时就不需要在controller上写fallback方法了

@Component
public class PaymentFallbackService implements PaymentHystrixService{
    
     // 统一为该接口异常处理
    @Override
    public String paymentInfo_OK(Integer id){
    
    
        return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
    }

    @Override //兜底方法,根据上述配置,程序内发生异常、或者运行超时,都会执行该兜底方法
    public String paymentInfo_TimeOut(Integer id){
    
    
        return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
    }
}

此时的yml文件配置

server:
  port: 80
spring:
  application:
    name: cloud-customer-feign-hystrix-service
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

# 用于服务降级 在注解@FeignClient 中添加 fallback 属性值
feign:
  hystrix:
    enabled: true  # 在feign中开启 hystrix
  1. 修改service 接口:
@Component											// 这里是重点
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = OrderFallbackService.class)
public interface OrderService {
    
    

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

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

}
  1. fallback 指向的类:
package com.dkf.springcloud.service;

import org.springframework.stereotype.Component;

@Component						//注意这里,它实现了service接口
public class OrderFallbackService implements  OrderService{
    
    


    @Override
    public String paymentInfo_OK(Integer id) {
    
    
        return "OrderFallbackService --发生异常";
    }

    @Override
    public String paymentInfo_Timeout(Integer id) {
    
    
        return "OrderFallbackService --发生异常--paymentInfo_Timeout";
    }
}

新问题,这样配置如何设置超时时间?

首先要知道 下面两个 yml 配置项:

hystrix.command.default.execution.timeout.enable=true    ## 默认值

## 为false则超时控制有ribbon控制,为true则hystrix超时和ribbon超时都是用,但是谁小谁生效,默认为true

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000  ## 默认值

## 熔断器的超时时长默认1秒,最常修改的参数

看懂以后,所以:

只需要在yml配置里面配置 Ribbon 的 超时时长即可。注意:hystrix 默认自带 ribbon包。

ribbon:
	ReadTimeout: xxxx
	ConnectTimeout: xxx

服务熔断案例

实际上服务熔断 和 服务降级 没有任何关系,就像 java 和 javaScript

服务熔断,有点自我恢复的味道

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

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

熔断机制概述:

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

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

在SpringCloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的情况,

当失败的调用到一定阈值(缺省是5秒内20次调用失败),就会启动熔断机制。熔断机制的注解是@HystrixCommand

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

修改cloud-provider-hystrix-payment8001模块

package com.atguigu.springcloud.service;

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;

@Service
public class PaymentService{
    
    

    //=====服务熔断
    @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",
                    commandProperties = {
    
    
            @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),// 是否开启断路器
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),// 请求次数
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"), // 时间窗口期
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60"),// 失败率达到多少后跳闸
    }) // 在10s内10次请求有60%失败 // 先看次数,再看百分比
    public String paymentCircuitBreaker(@PathVariable("id") Integer id){
    
    
        if(id < 0) {
    
    
            throw new RuntimeException("******id 不能负数");
        }
        String serialNumber = IdUtil.simpleUUID();// 等价于UUID.randomUUID().toString(); //pom中有hutool-all

        return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
    }
    public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){
    
    //服务降级
        return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " +id;
    }

}

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

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

The precise way that the circuit opening and closing occurs is as follows:

  1. Assuming the volume across a circuit meets a certain threshold (HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())…
  2. And assuming that the error percentage exceeds the threshold error percentage (HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())…
  3. Then the circuit-breaker transitions from CLOSED to OPEN.
  4. While it is open, it short-circuits all requests made against that circuit-breaker.
  5. After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next single request is let through (this is the HALF-OPEN state). If the request fails, the circuit-breaker returns to the OPEN state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions to CLOSED and the logic in 1. takes over again.经过一段时间后,如果有1个尝试成功了,就慢慢尝试恢复

参考:https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker

controller:

	//====服务熔断
    @GetMapping("/payment/circuit/{id}")
    public String paymentCircuitBreaker(@PathVariable("id")Integer id){
    
    
        return paymentService.paymentCircuitBreaker(id);
    }

实验效果为,多次出错调用fallback后,调用正常的也出错调用fallback。过了一会又自己恢复了。
展望:以后用的是Sentinel代替。

关于解耦以后的全局配置说明:

例如上面提到的全局服务降级,并且是feign+hystrix整合,即 service 实现类的方式,如何做全局配置?

上面有 做全局配置时,设置超时时间的方式,我们可以从中获得灵感,即在yml文件中 进行熔断配置:

hystrix:
command:
default:
circuitBreaker:
  enabled: true
  requestVolumeThreshold: 10
  sleepWindowInMilliseconds: 10000
  errorThresholdPercentage: 60

Hystrix DashBoard

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s08qjlCg-1602605634458)(images\1597556913055.png)]除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(HystrixDashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream实现了对以上指标的监控。SpringCloud也提供了HystrixDashboard的整合,对监控内容转化成可视化界面。

新建模块 cloud-hystrix-dashboard9001 :

pom 文件:

	<dependencies>
        <!-- hystrix Dashboard-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
        </dependency>
        
        <!-- 常规 jar 包 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml文件只需要配置端口号,主启动类加上这样注解:@EnableHystrixDashboard

启动测试:访问 http://ocalhost:9001/hystrix

监控实战

下面使用上面 9001 Hystrix Dashboard 项目,来监控 8001 项目 Hystrix 的实时情况:

注意8001被监控的时候pom里要actuator依赖,此外还要有

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9UT1qhv-1602605634460)(images\1597557620438.png)]

@Bean
public ServletRegistrationBean getServlet(){
    
    
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(streamServlet);
        servletRegistrationBean.setLoadOnStartup(1);
        servletRegistrationBean.addUrlMappings("/hystrix.stream");
        servletRegistrationBean.setName("HystrixMetricsStreamServlet");
        return servletRegistrationBean;
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jf1q8jOi-1602605634461)(images\1597558323714.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pq9A9S1j-1602605634463)(images\1597558486510.png)]

服务网关

1597213783265

Gateway

内容过多,开发可参考 https://docs.spring.io/ 官网文档

简介

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

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

springCloudGateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

一方面因为Zuul 1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的是亲儿子产品,值得信赖。而且很多功能比zull用起来都简单便捷。

Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的Zuul2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?

多方面综合考虑Gateway是很理想的网关选择。

1597559787064

SpringCloudGateway与Zuul的区别:

在SpringCloudFinchley正式版之前,SpringCloud推荐的网关是Netflix提供的Zuul:

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

Zuull.x
springcloud中所集成的zuul版本,采用的是tomcat容器,使用的是传统的servlet IO处理模型。

学过尚硅谷web中期课程都知道一个题目,Servlet的生命周期,servlet由servlet container进行生命周期管理

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

上述模式的缺点:

servlete—个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。

在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势

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

传统的Web框架比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。

但是,在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使
用java8)

SpringWebFlux是Spring5.0引入的新的响应式框架区别于SpringMVC,它不需要依赖ServletAPI,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。

Gateway是什么

Gateway特性:

  • 基于SpringFramework5,ProjectReactor和SpringBoot 2.0进行构建;
  • 动态路由:能够匹任何请求属性;
  • 可以对路由指定Predicate(断言)和Filter(过滤器)·
  • 集成Hystrix的断路器功能;
  • 集成SpringCloud服务发现功能;
  • 易于编写的Predicate(断言)和Filter(过滤器)·
  • 请求限流功能;
  • 支持路径重写。

GateWay的三大核心概念:

  • Route(路由):路由是构建网关的基本模块,它由ID、目标URI、一系列的断言和过滤器组成,如果断言为true则匹配该路由
  • Predicate(断言):参考的是Java8的java.util.function.predicate。开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
  • Filter(过滤):指的是spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
  • 例子:通过断言虽然进来了,但老师要罚站10min(过滤器操作),然后才能正常坐下听课

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

Spring Cloud Gateway Diagram

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

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

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

Filter在pre类型的过滤器可以做参数校验,权限校验,流量监听,日志输出,协议转换等,

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

入门配置

新建模块 cloud-gateway-gateway9527

现在实现,通过Gateway (网关) 来访问其它项目,这里选择之前8001项目,要求注册进Eureka Server 。其它没要求。

pom文件:

<dependencies>
        <!--gateway-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <!--eureka-client gateWay网关作为一种微服务,也要注册进服务中心。哪个注册中心都可以,如zk-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    <!-- gateway和spring web+actuator不能同时存在,即web相关jar包不能导入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml文件:

server:
  port: 9527
spring:
  application:
    name: cloud-gateway
  ## GateWay配置
  cloud:
    gateway: 
      routes: #多个路由
        - id: payment_routh  # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
          uri: http://localhost:8001  # 匹配后提供服务的路由地址 #uri+predicates  # 要访问这个路径得先经过9527处理
          predicates:
            - Path=/payment/get/**  # 断言,路径相匹配的进行路由

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

# 注册进 eureka Server # 网关他本身也是一个微服务,也要注册进注册主中心
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
    register-with-eureka: true
    fetch-registry: true

如果IDEA出现了bug,yml配置文件没有变为小叶子,就去structure里设置下模块的spring,选择该yml

主启动类

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

8001看看controller的访问地址,我们目前不想暴露8001端口,希望在8001外面套一层9527。这样别人就攻击不了8001,有网关挡着

访问测试:

  • 1 启动eureka Server,
  • 2 启动 8001 项目,
  • 3 启动 9527(Gateway项目)

可见,当我们访问 http://localhost:9527/payment/get/1 时,即访问网关地址时,会给我们转发到 8001 项目的请求地址,以此作出响应。

加入网关前:http://localhost:8001/payment/get/1

加入网关后:http://localhost:9527/payment/get/1

上面是以 yml 文件配置的路由,也有使用config类配置的方式:

@Configuration
public class GateWayConfig{
    
    
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
    
    
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();

        // 分别是id,本地址,转发到的地址
        routes.route("path_route_atguigu",
                r -> r.path("/guonei").uri("http://news.baidu.com/guonei")
                    ).build();//JDK8新特性

        return routes.build();
    }
}
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter,Ordered {
    
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    
    
        log.info("***********come in MyLogGateWayFilter:  "+new Date());

        String uname = exchange.getRequest().getQueryParams().getFirst("uname");

        if(uname == null) {
    
    
            log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

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

动态配置

这里所谓的动态配置就是利用服务注册中心,来实现 负载均衡 的调用 多个微服务。

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

注意,这是GateWay 的负载均衡

对yml进行配置:让其先通过gateway,再通过gateway去注册中心找提供者

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: payment_routh  # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
         # uri: http://localhost:8001  # 匹配后提供服务的路由地址 
          uri: lb://CLOUD-PROVIDER-SERVICE # lb 属于GateWay 的关键字,代表是动态uri,即代表使用的是服务注册中心的微服务名,它默认开启使用负载均衡机制 
          predicates:
            - Path=/payment/get/**  # 断言,路径相匹配的进行路由

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

# uri: lb://CLOUD-PROVIDER-SERVICE  解释:lb 属于GateWay 的关键字,代表是动态uri,即代表使用的是服务注册中心的微服务名,它默认开启使用负载均衡机制 

eureka:
  instance:
    hostname: cloud-gateway-service
  client: #服务提供者provider注册进eureka服务列表内
    service-url:
      register-with-eureka: true
      fetch-registry: true
      defaultZone: http://eureka7001.com:7001/eureka

下面可以开启 8002 模块,并将它与8001同微服务名,注册到 Eureka Server 进行测试。

Gateway:Predicate

注意到上面yml配置中,有个predicates 属性值。

  • 1 After Route Predicate
  • 2 Before Route Predicate
  • 3 Between Route Predicate
  • 4 Cookie Route Predicate
  • 5 Header Route Predicate
  • 6 Host Route Predicate
  • 7 Method Route Predicate
  • 8 Path Route Predicate
  • 9 Query Route Predicate

具体使用:

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: http://localhost:8001          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service #匹配后提供服务的路由地址
          predicates:
            - Path=/payment/lb/**         # 断言,路径相匹配的进行路由
            #- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
            #- Cookie=username,zzyy
            #- Header=X-Request-Id, \d+  # 请求头要有X-Request-Id属性并且值为整数的正则表达式

predicates下面可以有多个属性,表示多个属性与操作为true这个路由才生效

predicates属性下的After属性

		predicates:
            - Path=/payment/lb/** 
            - After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai] # 会在这个时间之后这个路由才生效

predicates属性下的Cookie属性

predicates:
	- Cookie=username,zzyy,ch.p # 需要有这个Cookie值才生效  最后一个是正则表达式
	# curl 地址 --cookie "a=b"

predicates属性下的Header属性

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: https://example.org
        predicates:
        - Header=X-Request-Id, \d+
spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: https://example.org
        predicates:
        - Host=**.somehost.org,**.anotherhost.org  # Host Route Predicate Factory接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则
        
        
        # 放爬虫思路,前后端分离的话,只限定前端项目主机访问,这样可以屏蔽大量爬虫。

例如我加上: - Host=localhost:**       ** 代表允许任何端口

就只能是主机来访

更多属性可以参考:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#configuring-route-predicate-factories-and-gateway-filter-factories

需要注意的是value部分写的是正则表达式

高并发工具:jmeter、postman、curl

配置错误页面:

注意,springboot默认/static/error/ 下错误代码命名的页面为错误页面,即 404.html

而且不需要导入额外的包,Gateway 里面都有。

Gateway:Filter

生命周期:

  • pre:
  • post:

种类:

  • GatewayFilter:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories
  • GlobalFilter:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#global-filters
spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        filters:
        - AddRequestHeader=X-Request-red, blue # 添加了个请求头X-Request-red

主要是配置全局自定义过滤器,其它的小配置具体看官网吧

2个主要接口:implements GlobalFilter,Ordered

场景:

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

自定义全局过滤器配置类:

@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter,Ordered {
    
    

    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
        GatewayFilterChain chain) {
    
    

        log.info("********** come in MyLogGateWayFilter:  "+new Date());

        String uname = exchange.getRequest().getQueryParams().getFirst("uname");

        //合法性检验
        if(uname == null) {
    
    
            log.info("*******用户名为null,非法用户,o(╥﹏╥)o,请求不被接受");
            //设置 response 状态码   因为在请求之前过滤的,so就算是返回NOT_FOUND 也不会返回错误页面
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            //完成请求调用
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);//过滤链放行
    }

    // 返回值是加载顺序,一般全局的都是第一位加载
    @Override
    public int getOrder() {
    
    
        return 0;
    }
}

服务配置

1597213783265

Config

SpringCloud Config 分布式配置中心

概述

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

springCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理。比如数据库的信息,我们可以写到一个统一的地方。

  • config+bus
  • alibaba
  • 携程 阿波罗

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

怎么玩:
SpringCloudConfig分为服务端和客户端两部分。

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

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

能干嘛:

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

服务端配置

首先在github上新建一个仓库 springcloud-config

[email protected]:名字/项目.git

然后使用git命令克隆到本地,命令:git clone https://github.com/LZXYF/springcloud-config

注意上面的操作不是必须的,只要github上有就可以,克隆到本地只是修改文件。

新建 cloud-config-center3344 模块:

pom文件:

	<dependencies>
        <!-- config Server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
        </dependency>

        <!--eureka-client config Server也要注册进服务中心-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml 配置:

server:
  port: 3344
spring:
  application:
    name: cloud-config-center  # 注册进eureka Server 的微服务名
  cloud:
    config:
      server:
        git:
          uri: https://github.com/LZXYF/springcloud-config # github 仓库位置
          ## 搜索目录
          search-paths:
            - springcloud-config
          # 读取的分支
          label: master
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

主启动类:

@SpringBootApplication
@EnableConfigServer   //关键注解
public class ConfigCenterMain3344 {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(ConfigCenterMain3344.class,args);
    }
}

添加模拟映射:【C:\Windows\System32\drivers\etc\hosts】文件中添加: 127.0.0.1 config-3344.com

启动微服务3344,访问http://config-3344.com:3344/master/config-dev.yml 文件(注意,要提前在git上弄一个这文件)

文件命名和访问的规则:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pHkwyddB-1602605634467)(images\1597646186970.png)]

不加分支名默认是master:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWprQb1X-1602605634469)(images\1597646308915.png)]

客户端配置

这里的客户端指的是,使用 Config Server 统一配置文件的项目。既有之前说的消费者,又有提供者

新建 cloud-config-client-3355 模块:

pom文件:

	<dependencies>
        <!-- config Client 和 服务端的依赖不一样 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

        <!--eureka-client config Server也要注册进服务中心-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

**** bootstrap.yml文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsVbqpwh-1602605634470)(images\1597646809831.png)]

内容:

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    # config 客户端配置
    config:
      label: master # 分支名称
      name: config # 配置文件名称,文件也可以是client-config-dev.yml这种格式的,这里就写 client-config 
      profile: dev # 使用配置环境
      uri: http://config-3344.com:3344  # config Server 地址
      # 综合上面四个 即读取配置文件地址为: http://config-3344.com:3344/master/config-dev.yml

eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

主启动类,极其普通:

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

controller层,测试读取配置信息

package com.dkf.springcloud.controller;

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

@RestController
public class ConfigClientController {
    
    

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

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

启动测试完成!如果报错,注意github上的 yml 格式有没有写错!

动态刷新

问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-747vScMB-1602605634472)(images\1597649646383.png)]

就是github上面配置更新了,config Server 项目上是动态更新的,但是,client端的项目中的配置,目前还是之前的,它不能动态更新,必须重启才行。

解决:

  1. client端一定要有如下依赖:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jwUHlqqA-1602605634475)(images\1597649915331.png)]

  2. client 端增加 yml 配置如下,即在 bootstrap.yml 文件中:

# 暴露监控端点
management:
  endpoints:
    web:
      exposure:
        include: "*"
  1. 在controller 上添加如下注解:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lNLqCQxE-1602605634475)(images\1597650236798.png)]

到此为止,配置已经完成,但是测试仍然不能动态刷新,需要下一步。

  1. 向 client 端发送一个 POST 请求

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

两个必须:1.必须是 POST 请求,2.请求地址:http://localhost:3355/actuator/refresh

成功!

但是又有一个问题,就是要向每个微服务发送一次POST请求,当微服务数量庞大,又是一个新的问题。

就有下面的消息总线!

消息总线

Bus

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xvAk9QNt-1602605634477)(images\1597651086179.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JjdzMfYx-1602605634480)(images\1597651183100.png)]

安装RabbitMQ

在windows 上安装RabbitMQ

  1. 安装RabbitMQ的依赖环境 Erlang 下载地址: http://erlang.org/download/otp_win64_21.3.exe
  2. 安装RabbitMQ 下载地址: http://dl.bintray.com/rabbitmq/all/rabbitmq-server/3.7.14/rabbitmq-server-3.7.14.exe
  3. 进入 rabbitMQ安装目录的sbin目录下,打开cmd窗口,执行 【rabbitmq-plugins enable rabbitmq_management】
  4. 访问【http://localhost:15672/】,输入密码和账号:默认都为 guest

广播式刷新配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0Wxuhyip-1602605634483)(images\1597723463350.png)]

还是按照之前的 3344(config Server)和 3355(config client)两个项目来增进。

首先给 config Server 和 config client 都添加如下依赖:

	<!-- 添加rabbitMQ的消息总线支持包 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>

config Server 的yml文件增加如下配置:

# rabbitMq的相关配置
rabbitmq:
  host: localhost
  port: 5672  # 这里没错,虽然rabbitMQ网页是 15672
  username: guest
  password: guest
# rabbitmq 的相关配置2 暴露bus刷新配置的端点
management:
  endpoints:
    web:
      exposure:
        include: 'bus-refresh'

config Client 的yml文件修改成如下配置:(注意对齐方式,和config Server不一样)

spring:
  application:
    name: config-client
  cloud:
    # config 客户端配置
    config:
      label: master         # 分支名称
      name: client-config       # 配置文件名称
      profile: test      # 使用配置环境
      uri: http://config-3344.com:3344  # config Server 地址
  # 综合上面四个 即读取配置文件地址为: http://config-3344.com:3344/master/config-dev.yml
  # rabbitMq的相关配置
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

可在github上修改yml文件进行测试,修改完文件,向 config server 发送 请求:

【curl -X POST “http://localhost:3344/actuator/bus-refresh”】

注意,之前是向config client 一个个发送请求,但是这次是向 config Server 发送请求,而所有的config client 的配置也都全部更新。

定点通知

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s1XEgLNs-1602605634484)(images\1597725204568.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KzgcAIgF-1602605634487)(images\1597725365978.png)]

消息驱动

Stream

概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pdU8jD0L-1602605634488)(images\1597725567239.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1mYtU5k-1602605634492)(images\1597725597884.png)]

就像 JDBC 形成一种规范,统一不同数据库的接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1uN2cuHW-1602605634494)(images\1597725703365.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlntUr64-1602605634495)(images\1597726446212.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YEOdO1ur-1602605634498)(images\1597730581088.png)]

消息生产者

新建模块 cloud-stream-rabbitmq-provider8801

pom依赖:

<!-- stream-rabbit -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <!--eureka-client 目前,这个不是必须的-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <groupId>com.dkf.cloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>

yml 配置:

server:
  port: 8801
spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: # 在次配置要绑定的rabbitMQ的服务信息
        defaultRabbit: # 表示定义的名称,用于和binding整合
          type: rabbit  # 消息组件类型
          environment:  # 设置rabbitmq的相关环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings:  # 服务的整合处理
        output:   # 表示是生产者,向rabbitMQ发送消息
          destination: studyExchange  # 表示要使用的Exchange名称
          content-type: application/json  # 设置消息类型,本次是json,文本是 "text/plain"
          binder: defaultRabbit  # 设置要绑定的消息服务的具体配置
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳时间,默认是30秒
    lease-expiration-duration-in-seconds: 5 # 最大心跳间隔不能超过5秒,默认90秒
    instance-id: send-8801.com # 在信息列表显示主机名称
    prefer-ip-address: true # 访问路径变为ip地址

主启动类没什么特殊的注解。

业务类:(此业务类不是以前的service,而实负责推送消息的服务类)

package com.dkf.springcloud.service;

import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.MessageBuilder;

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

@EnableBinding(Source.class)  // 不是和controller打交道的service,而是发送消息的推送服务类
public class IMessageProviderImpl implements IMessageProvider {
    
    
									     //上面是自定义的接口
    @Resource
    private MessageChannel output;

    @Override
    public String send() {
    
    
        String serial = UUID.randomUUID().toString();
        output.send(MessageBuilder.withPayload(serial).build());
        System.out.println("******serial: " + serial);
        return null;
    }
}

controller:

@RestController
public class SendMessageController {
    
    

    @Resource
    private IMessageProvider messageProvider;

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

启动Eureka Server 7001,再启动8801,进行测试,看是否rabbitMQ中有我们发送的消息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xyb4EeOG-1602605634501)(images\1597730112088.png)]

消息消费者

新建模块 cloud-stream-rabbitmq-consumer8802

pom依赖和生产者一样。

yml配置: 在 stream的配置上,和生产者只有一处不同的地方,output 改成 input

server:
  port: 8802
spring:
  application:
    name: cloud-stream-provider
  cloud:
    stream:
      binders: # 在次配置要绑定的rabbitMQ的服务信息
        defaultRabbit: # 表示定义的名称,用于和binding整合
          type: rabbit  # 消息组件类型
          environment:  # 设置rabbitmq的相关环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings:  # 服务的整合处理
        input:   # 表示是消费者,这里是唯一和生产者不同的地方,向rabbitMQ发送消息
          destination: studyExchange  # 表示要使用的Exchange名称
          content-type: application/json  # 设置消息类型,本次是json,文本是 "text/plain"
          binder: defaultRabbit  # 设置要绑定的消息服务的具体配置
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳时间,默认是30秒
    lease-expiration-duration-in-seconds: 5 # 最大心跳间隔不能超过5秒,默认90秒
    instance-id: receive-8802.com # 在信息列表显示主机名称
    prefer-ip-address: true # 访问路径变为ip地址

接收消息的业务类:

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

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

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

    @StreamListener(Sink.INPUT)
    public void input(Message<String> message){
    
    
        System.out.println("消费者1号,serverport: " + serverPort + ",接受到的消息:" + message.getPayload());
    }
}

配置分组消费

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

8803 就是 8802 clone出来的。

当运行时,会有两个问题。

第一个问题,两个消费者都接收到了消息,这属于重复消费。例如,消费者进行订单创建,这样就创建了两份订单,会造成系统错误。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1oBY5GgB-1602605634503)(images\1597731525531.png)]

Stream默认不同的微服务是不同的组

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tpEcPiZI-1602605634507)(images\1597731630685.png)]

对于重复消费这种问题,导致的原因是默认每个微服务是不同的group,组流水号不一样,所以被认为是不同组,两个都可以消费。

解决的办法就是自定义配置分组:

消费者 yml 文件配置:

	# 8802 的消费者
	bindings:
        input:   
          destination: studyExchange  
          content-type: application/json  
          binder: defaultRabbit  
          group: dkfA  # 自定义分组配置
    # 8803 的消费者
	bindings:
        input:   
          destination: studyExchange  
          content-type: application/json  
          binder: defaultRabbit  
          group: dkfB  # 自定义分组配置

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YLYA3GGu-1602605634509)(images\1597732035990.png)]

当两个消费者配置的 group 都为 dkfA 时,就属于同一组,就不会被重复消费。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VnJjGMZn-1602605634510)(images\1597732238270.png)]

消息持久化

加上group配置,就已经实现了消息的持久化。

Sleuth

分布式请求链路跟踪,超大型系统。需要在微服务模块极其多的情况下,比如80调用8001的,8001调用8002的,这样就形成了一个链路,如果链路中某环节出现了故障,我们可以使用Sleuth进行链路跟踪,从而找到出现故障的环节。

概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IucfN4kz-1602605634512)(images\1597732562780.png)]

sleuth 负责跟踪,而zipkin负责展示。

zipkin 下载地址: http://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/2.12.9/zipkin-server-2.12.9-exec.jar

使用 【java -jar】 命令运行下载的jar包,访问地址:【 http://localhost:9411/zipkin/ 】

案例

使用之前的 提供者8001 和 消费者80

分别给他们引入依赖:

	<!-- 引入sleuth + zipkin -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

yml增加配置:

spring:
  zipkin:
    base-url: http://localhost:9411  # zipkin 地址
  sleuth:
    sampler:
      # 采样率值 介于0-1之间 ,1表示全部采集
      probability: 1

高级部分

SpringCloud Alibaba

alibaba 的 github上有中文文档

大简介

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QID2y2Fv-1602605634517)(images\1597735215211.png)]

Nacos

Nacos = Eureka + Config + Bus

github地址: https://github.com/alibaba/Nacos

Nacos 地址: https://nacos.io/zh-cn/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mqnUVTx4-1602605634520)(images\1597755893534.png)]

nacos可以切换 AP 和 CP ,可使用如下命令切换成CP模式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vNGLyEqi-1602605634522)(images\1597756097369.png)]

下载

下载地址: https://github.com/alibaba/nacos/releases/tag/1.1.4

直接下载网址: https://github.com/alibaba/nacos/releases/download/1.1.4/nacos-server-1.1.4.zip

下载压缩包以后解压,进入bin目录,打开dos窗口,执行startup命令启动它。

可访问 : 【 http://192.168.101.105:8848/nacos/index.html】地址,默认账号密码都是nacos

服务中心

提供者

新建模块 cloudalibaba-provider-payment9001

pom依赖:

	<dependencies>
        <!-- springcloud alibaba nacos 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- springboot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 日常通用jar包 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml 配置:

server:
  port: 9001
spring:
  application:
    name: nacos-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: '*'

主启动类没有特殊的注解。

Nacos 自带负载均衡机制,下面创建第二个提供者。

新建 cloudalibaba-provider-payment9003 提供者模块,clone 9001 就可以

消费者

新建消费者 模块: cloudalibaba-customer-order80

pom依赖和主启动类没有好说的,和提供者一致,yml依赖也是类似配置,作为消费者注册进nacos服务中心。

nacos底层也是ribbon,注入ReatTemplate

@Configuration
public class ApplicationContextConfig {
    
    

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

controller :

@RestController
public class OrderController {
    
    

    //在yml里面写的提供者服务路径,  值为:http://nacos-provider
    @Value("${service-url.nacos-user-service}")
    private String nacos_user_service;

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("customer/nacos/{id}")
    public String orderId(@PathVariable("id")Integer id){
    
    
        return restTemplate.getForObject(nacos_user_service + "/payment/nacos/" + id, String.class);
    }
}

配置中心

nacos 还可以作为服务配置中心,下面是案例,创建一个模块,从nacos上读取配置信息。

nacos 作为配置中心,不需要像springcloud config 一样做一个Server端模块。

新建模块 cloudalibaba-nacos-config3377

pom依赖:

 <dependencies>
        <!-- 以 nacos 做服务配置中心的依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!-- springcloud alibaba nacos 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- springboot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 日常通用jar包 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

主启动类也是极其普通:

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

***bootstrap.yml 配置:

server:
  port: 3377
spring:
  application:
    name: nacos-config-client
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848  # nacos作为服务注册中心
      config:
        server-addr: localhost:8848 # nacos作为服务配置中心
        file-extension: yaml # 指定yaml 格式的配置

controller 层进行读取配置测试:

@RestController
@RefreshScope  //支持Nacos的动态刷新
public class ConfigClientController {
    
    

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

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

下面在 Nacos 中添加配置文件,需要遵循如下规则:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hb9gcZBP-1602605634523)(images\1597757341383.png)]

从上面可以看到重要的一点,配置文件的名称第二项,spring.profiles.active 是依据当前环境的profile属性值的,也就是这个值如果是 dev,即开发环境,它就会读取 dev 的配置信息,如果是test,测试环境,它就会读取test的配置信息,就是从 spring.profile.active 值获取当前应该读取哪个环境下的配置信息。

所以要配置spring.profiles.active,新建application.yml文件,添加如下配置:

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

综合以上说明,和下面的截图,Nacos 的dataid(类似文件名)应为: nacos-config-client-dev.yaml (必须是yaml)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AUB2YDIO-1602605634525)(images\1597757775084.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sgRNbeHf-1602605634529)(images\1597757747480.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ddr77Hy4-1602605634530)(images\1597758227367.png)]

当修改配置值,会发现 3377 上也已经修改,Nacos自带自动刷新功能!

其它说明:

Nacos 的 Group ,默认创建的配置文件,都是在DEFAULT_GROUP中,可以在创建配置文件时,给文件指定分组。

yml 配置如下,当修改开发环境时,只会从同一group中进行切换。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9K5Umy1V-1602605634531)(images\1597807821441.png)]

Nacos 的namespace ,默认的命名空间是public ,这个是不允许删除的,可以创建一个新的命名空间,会自动给创建的命名空间一个流水号。

在yml配置中,指定命名空间:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LLp0NLVh-1602605634533)(images\1597808181104.png)]

最后,dataid、group、namespace 三者关系如下:(不同的dataid,是相互独立的,不同的group是相互隔离的,不同的namespace也是相互独立的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I8Bb1JyG-1602605634535)(images\1597808385154.png)]

Nacos持久化

上面只是小打小闹,下面才是真正的高级操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDNiwcOF-1602605634538)(images\1597808678658.png)]

搭建集群必须持久化,不然多台机器上的nacos的配置信息不同,造成系统错乱。它不同于单个springcloud config,没有集群一说,而且数据保存在github上,也不同于eureka,配置集群就完事了,没有需要保存的配置信息。

Nacos默认使用它自带的嵌入式数据库derby:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u8zno8tc-1602605634542)(images\1597809107487.png)]

Nacos持久化配置:

在 nacos的 conf目录下,有个nacos-mysql.sql 的sql文件,创建一个名为【nacos_config】的数据库,执行里面内容,在nacos_config数据库里面创建数据表。

找到conf/application.properties 文件,尾部追加如下内容:

spring.datasource.platform=mysql

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

重启nacos,即完成持久化配置。

集群架构

现在进行企业中真正需要的nacos集群配置,而不是上面的单机模式,需要准备如下:

一台linux虚拟机:nginx服务器,3个nacos服务,一个mysql数据库。

nginx的安装参考之前学,使用 ContOs7 至少需要安装gcc库,不然无法编译安装【yum install gcc】

nacos下载linux版本的 tar.gz 包:https://github.com/alibaba/nacos/releases/download/1.1.4/nacos-server-1.1.4.tar.gz

mysql root用户密码为 Dkf!!2020

Nacos集群配置

  1. 首先对 nacos 进行持久化操作,操作如上面一致。

  2. 修改 nacos/conf 下的cluster文件,最好先复制一份,添加如下内容:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nV6PgwvB-1602605634543)(images\1597812518508.png)]

  3. 模拟三台nacos服务,编辑nacos的startup启动脚本,使他能够支持不同的端口启动多次。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IjKxZeP3-1602605634545)(images\1597812716080.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AwF0tFkb-1602605634550)(images\1597812799242.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8FP7JCzF-1602605634553)(images\1597813020494.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hhnnjQHC-1602605634556)(images\1597815675706.png)]

  4. nginx配置负载均衡:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pFI68atZ-1602605634571)(images\1597813917440.png)]

  5. 测试完成!

使用 9003 模块注册进Nacos Server 并获取它上面配置文件的信息,进行测试。

Sentinel

sentinel在 springcloud Alibaba 中的作用是实现熔断和限流

下载

下载地址: https://github.com/alibaba/Sentinel/releases/download/1.7.1/sentinel-dashboard-1.7.1.jar

下载jar包以后,使用【java -jar】命令启动即可。

它使用 8080 端口,用户名和密码都为 : sentinel

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmGHPc4i-1602605634586)(images\1597817544663.png)]

Demo

新建模块 cloudalibaba-sentinel-service8401 ,使用nacos作为服务注册中心,来测试Sentinel的功能。

pom依赖:

	<dependencies>
        <!-- 后续做Sentinel的持久化会用到的依赖 -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <!-- sentinel  -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!-- springcloud alibaba nacos 依赖,Nacos Server 服务注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- springboot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 日常通用jar包 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml 配置:

server:
  port: 8401
spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        # 服务注册中心
        server-addr: localhost:8848
    sentinel:
      transport:
        # 配置 Sentinel Dashboard 的地址
        dashboard: localhost:8080
        # 默认8719 ,如果端口被占用,端口号会自动 +1,提供给 sentinel 的监控端口
        port: 8719
management:
  endpoints:
    web:
      exposure:
        include: '*'

写一个简单的主启动类,再写一个简单的controller测试sentinel的监控。

流控规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x5tzZVnw-1602605634589)(images\1597819388174.png)]

流控模式–直接:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QeqwzZgm-1602605634592)(images\1597819546992.png)]

限流表现:当超过阀值,就会被降级。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LlboWlwy-1602605634595)(images\1597819613273.png)]

流控模式–关联:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uaF8Zz8q-1602605634596)(images\1597819976569.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7w8WdSsC-1602605634599)(images\1597820015308.png)]

流控效果–预热:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VyEIQkXM-1602605634607)(images\1597820393038.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-42Z2u3MH-1602605634608)(images\1597820534168.png)]

流控效果–排队等待:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SoQH1QoM-1602605634609)(images\1597820662461.png)]

熔断降级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A1vzy2Bi-1602605634612)(images\1597820926895.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L6KmNoHf-1602605634621)(images\1597820943934.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8mvTCf4-1602605634623)(images\1597820987861.png)]

降级策略–RT:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o5RpVZdE-1602605634625)(images\1597821156100.png)]

降级策略–异常比例:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-exCylZyj-1602605634629)(images\1597821313361.png)]

降级测录–异常数:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RuPik2u-1602605634633)(images\1597821525073.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lsVtHckW-1602605634636)(images\1597821548056.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zweU8Oxs-1602605634638)(images\1597821618735.png)]

热点Key限流

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MQbpwmVE-1602605634641)(images\1597821753416.png)]

controller层写一个demo:

	@GetMapping("/testhotkey")
    @SentinelResource(value = "testhotkey", blockHandler = "deal_testhotkey")
    //这个value是随意的值,并不和请求路径必须一致
    //在填写热点限流的 资源名 这一项时,可以填 /testhotkey 或者是 @SentinelResource的value的值
    public String testHotKey(
            @RequestParam(value="p1", required = false) String p1,
            @RequestParam(value = "p2", required = false) String p2
    ){
    
    
        return "testHotKey__success";
    }

	//类似Hystrix 的兜底方法
    public String deal_testhotkey(String p1, String p2, BlockException e){
    
    
        return "testhotkey__fail"; 
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-01KJBNfk-1602605634645)(images\1597822501876.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMi0HoR8-1602605634646)(images\1597822772165.png)]

说明:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XLAbC1Sf-1602605634648)(images\1597822972448.png)]

系统规则

一般配置在网关或者入口应用中,但是这个东西有点危险,不但值不合适,就相当于系统瘫痪。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FShhs1zB-1602605634650)(images\1597900777242.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RSLcMMs-1602605634652)(images\1597900843477.png)]

@SentinelResource配置

@SentinelResource 注解,主要是指定资源名(也可以用请求路径作为资源名),和指定降级处理方法的。

例如:

package com.dkf.springcloud.controller;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RateLimitController {
    
    

    @GetMapping("/byResource")						//处理降级的方法名
    @SentinelResource(value = "byResource", blockHandler = "handleException")
    public CommonResult byResource(){
    
    
        return new CommonResult(200, "按照资源名限流测试0K", new Payment(2020L,"serial001"));
    }

    //降级方法
    public CommonResult handleException(BlockException e){
    
    
        return new CommonResult(444, e.getClass().getCanonicalName() + "\t 服务不可用");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r7tooa6i-1602605634657)(images\1597901945492.png)]

很明显,上面虽然自定义了兜底方法,但是耦合度太高,下面要解决这个问题。

自定义全局BlockHandler处理类

写一个 CustomerBlockHandler 自定义限流处理类:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLgYZIya-1602605634658)(images\1597903188558.png)]

整合 openfeign 服务降级

前奏

之前有 open-feign 和 hystrix 的整合,现在来实现sentinel 整合 ribbon + open-feign + fallback 进行服务熔断。

新建三个模块,两个提供者 9004、9005,和一个消费者 84

目的:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMS5NFkg-1602605634660)(images\1597905761214.png)]

上面使用sentinel有一个很明显的问题,就是sentinel,对程序内部异常(各种异常,包括超时)这种捕捉,显得很乏力,它主要是针对流量控制,系统吞吐量,或者是异常比例这种,会发生降级或熔断,但是当程序内部发生异常,直接返回给用户错误页面,根本不会触发异常比例这种降级。所以才需要整合open-feign 来解决程序内部异常时,配置相应的兜底方法

-----------------------------------------------------------两个提供者模块一致,如下:

pom依赖:

	<dependencies>
        <!-- springcloud alibaba nacos 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        
        <!-- springboot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 日常通用jar包 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml配置:

server:
  port: 9005  # / 9004
spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: '*'

主启动类只是启动,没有其它注解。

controller :

package com.dkf.sprIngcloud.controller;

import com.dkf.springcloud.entities.CommonResult;
import com.dkf.springcloud.entities.Payment;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
public class PaymentController {
    
    

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

    //模拟sql查询
    public static HashMap<Long, Payment> hashMap = new HashMap<>();
    static {
    
    
        hashMap.put(1L, new Payment(1L, "xcxcxcxcxcxcxcxcxcxcxcxcxc11111111"));
        hashMap.put(2L, new Payment(2L, "xcxcxcxcggggggggg2222222222222222"));
        hashMap.put(3L, new Payment(3L, "xcxcxcxccxxcxcfafdgdgdsgdsgds33333"));
    }


    @GetMapping("/payment/get/{id}")
    public CommonResult paymentSql(@PathVariable("id")Long id){
    
    
        Payment payment = hashMap.get(id);
        CommonResult result = new CommonResult(200, "from mysql, server port : " + serverPort + " ,查询成功", payment);
        return result;
    }
}

---------------------------------------------------------------------------------消费者:

pom依赖:

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

        <!-- springboot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 日常通用jar包 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml配置:

server:
  port: 84
spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
  application:
    name: nacos-order-consumer

主启动类不用说了。

config类里面注入 Resttemplate:

@Configuration
public class ApplicationContextConfig {
    
    
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate(){
    
    
        return new RestTemplate();
    }
}

controller 层:

@RestController
public class OrderController {
    
    

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

    @Resource
    private RestTemplate restTemplate;

    @GetMapping("/consutomer/payment/get/{id}")
    public CommonResult getPayment(@PathVariable("id")Long id){
    
    
        if(id >= 4){
    
    
            throw new IllegalArgumentException("非法参数异常...");
        }else {
    
    
            return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        }
    }
}

上面只实现了 以nacos 作为服务注册中心,消费者使用ribbon 实现负载均衡调用提供者的效果。

正式

只配置 fallback:

	@GetMapping("/consutomer/payment/get/{id}")
    @SentinelResource(value = "fallback", fallback = "handleFallback") //fallback只处理业务异常
    public CommonResult getPayment(@PathVariable("id")Long id){
    
    
        if(id >= 4){
    
    
            throw new IllegalArgumentException("非法参数异常...");
        }else {
    
    
            return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        }
    }

    //兜底方法
    public CommonResult handleFallback(@PathVariable("id")Long id, Throwable e){
    
    
        return new CommonResult(414, "---非法参数异常--", e);
    }

业务异常会被 fallback 处理,返回我们自定义的提示信息,而如果给它加上流控,并触发阈值,只能返回sentinel默认的提示信息。

只配置blockHandler:

	//@SentinelResource(value = "fallback", fallback = "handleFallback") //fallback只处理业务异常
    @GetMapping("/consutomer/payment/get/{id}")
    @SentinelResource(value = "fallback", blockHandler = "handleblockHandler")
    public CommonResult getPayment(@PathVariable("id")Long id){
    
    
        if(id >= 4){
    
    
            throw new IllegalArgumentException("非法参数异常...");
        }else {
    
    
            return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        }
    }

//    //====fallback
//    public CommonResult handleFallback(@PathVariable("id")Long id, Throwable e){
    
    
//        return new CommonResult(414, "---非法参数异常--", e);
//    }

    //====blockHandler                                       blockHandler的方法必须有这个参数
    public CommonResult handleblockHandler(@PathVariable("id")Long id, BlockException e){
    
    
        return new CommonResult(414, "---非法参数异常--", e);
    }

这时候的效果就是,运行异常直接报错错误页面。在sentinel上添加一个降级规则,设置2s内触发异常2次,触发阈值以后,返回的是我们自定义的 blockhanlder 方法返回的内容。

两者都配置:

  //@SentinelResource(value = "fallback", fallback = "handleFallback") //fallback只处理业务异常
    @GetMapping("/consutomer/payment/get/{id}")
    @SentinelResource(value = "fallback", blockHandler = "handleblockHandler", fallback = "handleFallback")
    public CommonResult getPayment(@PathVariable("id")Long id){
    
    
        if(id >= 4){
    
    
            throw new IllegalArgumentException("非法参数异常...");
        }else {
    
    
            return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
        }
    }
    //====fallback
    public CommonResult handleFallback(@PathVariable("id")Long id, Throwable e){
    
    
        return new CommonResult(414, "---非法参数异常--form fallback的提示", e);
    }

    //====blockHandler                                       blockHandler的方法必须有这个参数
    public CommonResult handleblockHandler(@PathVariable("id")Long id, BlockException e){
    
    
        return new CommonResult(414, "---非法参数异常--", e);
    }

明显两者都是有效的,可以同时配置。

全局降级

上面是单个进行 fallback 和 blockhandler 的测试,下面是整合 openfeign 实现把降级方法解耦。和Hystrix 几乎一摸一样!

还是使用上面 84 这个消费者做测试:

  1. 先添加open-feign依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. yml 追加如下配置:
# 激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true
  1. 主启动类添加注解 : @EnableFeignClients 激活open-feign
  2. service :
@FeignClient(value = "nacos-payment-provider", fallback = PaymentServiceImpl.class)
public interface PaymentService {
    
    

    @GetMapping("/payment/get/{id}")
    public CommonResult paymentSql(@PathVariable("id")Long id);
}
  1. service 实现类:
@Component
public class PaymentServiceImpl implements PaymentService {
    
    

    @Override
    public CommonResult paymentSql(Long id) {
    
    
        return new CommonResult(414, "open-feign 整合 sentinel 实现的全局服务降级策略",null);
    }
}
  1. controller 层代码没什么特殊的,和普通调用service 一样即可。
  2. 测试,关闭提供者的项目,会触发 service 实现类的方法。
  3. 总结: 这种全局熔断,是针对 “访问提供者” 这个过程的,只有访问提供者过程中发生异常才会触发降级,也就是这些降级,是给service接口上这些提供者的方法加的,以保证在远程调用时能顺利进行。而且这明显是 fallback ,而不是 blockHandler,注意区分。

fallback 和 blockHandler 肤浅的区别:

F : 不需要指定规则,程序内部异常均可触发(超时异常需要配置超时时间)

B : 配上也没用,必须去 Sentinel 上指定规则才会被触发。

异常忽略

这是 @SentinelResource 注解的一个值:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WFx9qlT6-1602605634665)(images\1597909285814.png)]

持久化

目前的sentinel 当重启以后,数据都会丢失,和 nacos 类似原理。需要持久化。它可以被持久化到 nacos 的数据库中。

  1. pom依赖:
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
  1. yml配置:
spring:
  cloud:
    sentinel:
      datasource:
        ds1:  
          nacos:
            server-addr: localhost:8848
            dataId: ${
    
    spring.application.name}
            group: DEFAULT_GROUP
            data-type: json
            rule-type: flow
  1. 去nacos上创建一个dataid ,名字和yml配置的一致,json格式,内容如下:
[
    {
    
    
        "resource": "/testA",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YTprHOyH-1602605634667)(images\1597913169207.png)]

  1. 启动应用,发现存在 关于 /testA 请求路径的流控规则。
  2. 总结: 就是在 sentinel 启动的时候,去 nacos 上读取相关规则配置信息,实际上它规则的持久化,就是第三步,粘贴到nacos上保存下来,就算以后在 sentinel 上面修改了,重启应用以后也是无效的。

Seata

Seate 处理分布式事务。

微服务模块,连接多个数据库,多个数据源,而数据库之间的数据一致性需要被保证。

官网: http://seata.io/zh-cn/

Seata术语: 一 + 三

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h40hED1Q-1602605634668)(images\1597982738615.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p7Ld2ME0-1602605634670)(images\1597982788659.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Q3eOl3s-1602605634673)(images\1597982858545.png)]

下载安装

下载地址 : https://github.com/seata/seata/releases/download/v1.0.0/seata-server-1.0.0.zip

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mnEZhrCr-1602605634674)(images\1597984908755.png)]

初始化操作

  1. 修改 conf/file.conf 文件:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWFhB75u-1602605634676)(images\1597990431346.png)]

  1. 创建名和 file.conf 指定一致的数据库。

  2. 在新建的数据库里面创建数据表,db_store.sql文件在 conf 目录下(1.0.0有坑,没有sql文件,下载0.9.0的,使用它的sql文件即可)

  3. 修改 conf/registry.conf 文件内容:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oREBm9DE-1602605634679)(images\1597986227251.png)]

  4. 先启动 nacos Server 服务,再启动seata Server 。

  5. 启动 Seata Server 报错,在bin目录创建 /logs/seata_gc.log 文件。再次双击 bat文件启动。

案例

数据库准备

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tQjz5wY3-1602605634680)(images\1597987564813.png)]

创建三个数据库: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QQYkTDbU-1602605634683)(images\1597987722338.png)]

每个数据库创建数据表:

order 库:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XzKF3f3L-1602605634685)(images\1597988061545.png)]

account 库:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DhRRzakl-1602605634686)(images\1597988768251.png)]

storage 库:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2jiIz0sK-1602605634690)(images\1597988262441.png)]

三个数据库都创建一个回滚日志表,seata/conf/ 有相应的sql文件(1.0.0没有,依然使用0.9.0中的)。

最终效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lfct2q9Y-1602605634695)(images\1597989003349.png)]

开发

实现 下订单-> 减库存 -> 扣余额 -> 改(订单)状态

需要注意的是,下面做了 seata 与 mybatis 的整合,所以注意一下,和以往的mybatis的使用不太一样。

新建模块 cloudalibaba-seata-order2001 :

pom依赖:

	<dependencies>
        <!-- seata -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.0.0</version>
        </dependency>
        <!-- springcloud alibaba nacos 依赖,Nacos Server 服务注册中心 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- open feign 服务调用 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- springboot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- 持久层支持 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <!--mysql-connector-java-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <!-- 日常通用jar包 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
            <groupId>com.dkf.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

yml配置:

server:
  port: 2001
spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        # 自定义事务组,需要和当时在 seata/conf/file.conf 中的一致
        tx-service-group: dkf_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 123456


# 注意,这是自定义的,原来的是mapper_locations
mybatis:
  mapperLocations: classpath:mapper/*.xml

logging:
  level:
    io:
      seata: info

将 seata/conf/ 下的 file.conf 和 registry.cong 两个文件拷贝到 resource 目录下。

创建 domain 实体类 : Order 和 CommonResult 两个实体类。

dao :

package com.dkf.springcloud.dao;

import org.apache.ibatis.annotations.Mapper;
import com.dkf.springcloud.domain.Order;
import org.apache.ibatis.annotations.Param;

@Mapper
public class OrderDao {
    
    

    //创建订单
    public void create(Order order);

    //修改订单状态
    public void update(@Param("userId") Long userId, @Param("status") Integer status);

}

Mapper文件:

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

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

    <!-- 以备后面会用到 -->
    <resultMap id="BaseResultMap" type="com.dkf.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>

创建service :

注意,红框标记的是通过 open-feign 远程调用微服务的service

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ms1fGdF9-1602605634700)(images\1597992817318.png)]

serviceImpl :

@Service
@Slf4j
public class OrderServiceImpl  implements OrderService {
    
    

    @Resource
    private OrderDao orderDao;

    @Resource
    private StorageService storageService;

    @Resource
    private AccountService accountService;

    @Override
    public void create(Order order) {
    
    
        log.info("--------》 开始创建订单");
        orderDao.create(order);
        log.info("--------》 订单微服务开始调用库存,做扣减---Count-");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("--------》 订单微服务开始调用库存,库存扣减完成!!");
        log.info("--------》 订单微服务开始调用账户,账户扣减---money-");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("--------》 订单微服务开始调用账户,账户扣减完成!!");
        //修改订单状态,从0到1
        log.info("--------》 订单微服务修改订单状态,start");
        orderDao.update(order.getUserId(),0);
        log.info("--------》 订单微服务修改订单状态,end");

        log.info("--订单结束--");
    }

    @Override
    public void update(Long userId, Integer status) {
    
    

    }
}

config (特殊点):

//下面是两个配置类,这个是和mybatis整合需要的配置
@Configuration
@MapperScan({
    
    "com.dkf.springcloud.alibaba.dao"})
public class MybatisConfig {
    
    
}


//这个是配置使用 seata 管理数据源,所以必须配置
package com.dkf.springcloud.config;

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

import javax.sql.DataSource;

@Configuration
public class DataSourceProxyConfig {
    
    

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

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

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

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

主启动类:

//这里必须排除数据源自动配置,因为写了配置类,让 seata 管理数据源
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableFeignClients
@EnableDiscoveryClient
public class SeataOrderMain2001 {
    
    

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

controller 层调用 orderService 方法即可。

先启动 nacos --》 再启动 seata --> 再启动此order服务,测试,可以启动。

仿照上面 创建 cloudalibaba-seata-storage2002 和 cloudalibaba-seata-account2003 两个模块,唯一大的区别就是这两个不需要导入 open-feign 远程调用其它模块。

操,累死老子啦,测试可以正常使用!

Seata使用

	@Override
	//只需要在业务类的方法上加上该注解,name值自定义唯一即可。
    @GlobalTransactional(name = "dkf-create-order", rollbackFor = Exception.class)
    public void create(Order order) {
    
    
        log.info("--------》 开始创建订单");
        orderDao.create(order);
        log.info("--------》 订单微服务开始调用库存,做扣减---Count-");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("--------》 订单微服务开始调用库存,库存扣减完成!!");
        log.info("--------》 订单微服务开始调用账户,账户扣减---money-");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("--------》 订单微服务开始调用账户,账户扣减完成!!");
        //修改订单状态,从0到1
        log.info("--------》 订单微服务修改订单状态,start");
        orderDao.update(order.getUserId(),0);
        log.info("--------》 订单微服务修改订单状态,end");

        log.info("--订单结束--");
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-134sSDLT-1602605634703)(images\1597998982271.png)]

原理三个阶段:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fKNQR0Tq-1602605634704)(images\1597999156669.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y790OOPN-1602605634705)(images\1597999227092.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UJhfb9mH-1602605634705)(G:\学习课件\大学\java框架\springCloud\images\1597999292492.png)]

猜你喜欢

转载自blog.csdn.net/hancoder/article/details/109063671