杨柳依 于2020年3月17日创建,最近更新于 2020年3月30日。 参考资料:尚硅谷周阳SpringCloud教程

基础知识

1. 包含内容

服务注册与发现,配置中心管理,服务调用,服务网关,服务熔断,服务监控,负载均衡,全链路追踪,服务降级,自动化构建部署,服务消息队列,服务定时任务调度操作

2. 版本选型

SpringCloud: Hoxton.SR1

SpringBoot: 2.2.2.RELEASE

SpringCloud alibaba: 2.1.0.RELEASE

Java: Java8

Maven: 3.5+

MySQL: 5.7+

3. Cloud升级后所推荐使用的技术

服务注册中心:[×]Eureka,[√]Zookeeper,[√]Consul,[√]Nacos

服务调用:[√]Ribbon,[√]LoadBalancer

服务调用2:[×]Feign,[√]OpenFeign

服务降级:[×]Hystrix,[√]resilience4j,[√]sentienl

服务网关:[×]Zull,[?]Zull2,[√]gateway

服务配置:[×]Config,[√]Nacos

服务总线:[×]Bus,[√]Nacos

准备工作

1. 父工程Project空间构建

新建Maven工程,GroupId为com.atguigu.springcloud,ArtifactId为cloud2020,Maven不要用IDEA自带的,选择自己的(3.5+),点击完成。

打开IDEA的设置:

  • File Encodings中将3个位置的字符编码都改为UTF-8
  • Annotation Processors中勾选Enable annotation processing激活注解
  • Java Compiler中将字节码编译版本改为8
  • File TypesIgnore files and folders中可添加不希望在IDEA编辑器中出现的文件

删除src文件夹,并修改pom.xml文件如下:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <modelVersion>4.0.0</modelVersion>
  6. <groupId>com.atguigu.springcloud</groupId>
  7. <artifactId>cloud2020</artifactId>
  8. <version>1.0-SNAPSHOT</version>
  9. <!-- 指定为父工程 -->
  10. <packaging>pom</packaging>
  11. <!-- 统一管理jar包版本 -->
  12. <properties>
  13. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  14. <maven.compiler.source>1.8</maven.compiler.source>
  15. <maven.compiler.target>1.8</maven.compiler.target>
  16. <junit.version>4.12</junit.version>
  17. <log4j.version>1.2.17</log4j.version>
  18. <lombok.version>1.16.18</lombok.version>
  19. <mysql.version>5.1.47</mysql.version>
  20. <druid.version>1.1.16</druid.version>
  21. <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
  22. </properties>
  23. <!-- 这里并不会实际import,只会在子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version -->
  24. <dependencyManagement>
  25. <dependencies>
  26. <!--spring boot 2.2.2-->
  27. <dependency>
  28. <groupId>org.springframework.boot</groupId>
  29. <artifactId>spring-boot-dependencies</artifactId>
  30. <version>2.2.2.RELEASE</version>
  31. <type>pom</type>
  32. <scope>import</scope>
  33. </dependency>
  34. <!--spring cloud Hoxton.SR1-->
  35. <dependency>
  36. <groupId>org.springframework.cloud</groupId>
  37. <artifactId>spring-cloud-dependencies</artifactId>
  38. <version>Hoxton.SR1</version>
  39. <type>pom</type>
  40. <scope>import</scope>
  41. </dependency>
  42. <!--spring cloud alibaba 2.1.0.RELEASE-->
  43. <dependency>
  44. <groupId>com.alibaba.cloud</groupId>
  45. <artifactId>spring-cloud-alibaba-dependencies</artifactId>
  46. <version>2.1.0.RELEASE</version>
  47. <type>pom</type>
  48. <scope>import</scope>
  49. </dependency>
  50. <dependency>
  51. <groupId>mysql</groupId>
  52. <artifactId>mysql-connector-java</artifactId>
  53. <version>${mysql.version}</version>
  54. </dependency>
  55. <dependency>
  56. <groupId>com.alibaba</groupId>
  57. <artifactId>druid</artifactId>
  58. <version>${druid.version}</version>
  59. </dependency>
  60. <dependency>
  61. <groupId>org.mybatis.spring.boot</groupId>
  62. <artifactId>mybatis-spring-boot-starter</artifactId>
  63. <version>${mybatis.spring.boot.version}</version>
  64. </dependency>
  65. <dependency>
  66. <groupId>junit</groupId>
  67. <artifactId>junit</artifactId>
  68. <version>${junit.version}</version>
  69. </dependency>
  70. <dependency>
  71. <groupId>log4j</groupId>
  72. <artifactId>log4j</artifactId>
  73. <version>${log4j.version}</version>
  74. </dependency>
  75. <dependency>
  76. <groupId>org.projectlombok</groupId>
  77. <artifactId>lombok</artifactId>
  78. <version>${lombok.version}</version>
  79. <optional>true</optional>
  80. </dependency>
  81. </dependencies>
  82. </dependencyManagement>
  83. <build>
  84. <finalName>cloud2020</finalName>
  85. <plugins>
  86. <!--spring-boot-maven-plugin插件在打Jar包时会引入依赖包-->
  87. <plugin>
  88. <groupId>org.springframework.boot</groupId>
  89. <artifactId>spring-boot-maven-plugin</artifactId>
  90. <configuration>
  91. <fork>true</fork>
  92. <addResources>true</addResources>
  93. </configuration>
  94. </plugin>
  95. </plugins>
  96. </build>
  97. </project>

dependencyManagement和dependencies区别? dependencyManagement用于在父项目中提供版本信息,不会产生实际引用(只声明)。子项目中可以获得父项目中的版本依赖,需要引用相关依赖时,仍需显示写出,不填版本号就继承父项目的版本号,填版本号就使用填写的版本号。

2. 服务提供者

准备工作

在父工程添加Module,选择Maven项目,ArtifactId为cloud-provider-payment8001,在POM文件中添加dependencies节点如下:

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

src\main\resources\下添加配置文件application.yml,内容如下:

  1. server:
  2. port: 8001
  3. spring:
  4. application:
  5. name: cloud-payment-service
  6. datasource:
  7. type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型
  8. driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包
  9. url: jdbc:mysql://localhost:3306/springcloud?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&useSSL=false
  10. username: root
  11. password: 123456
  12. mybatis:
  13. mapper-locations: classpath:mapper/*.xml
  14. type-aliases-package: com.atguigu.springcloud.entities # 所有Entity别名类所在包

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类PaymentMain8001

// 省略包名和import

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

springcloud包下新建包entities用于存放实体类,在entities下新建实体类Payment

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Payment implements Serializable {
    private Long id;
    private String serial;
}

在同一包下新建通用化返回前端结果类CommonResult

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

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

springcloud包下新建包dao用于持久化类,在dao下新建接口PaymentDao

@Mapper
public interface PaymentDao {
    int create(Payment payment);

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

resources文件夹下新建mapper文件夹用于存放xml文件,在mapper下新建文件PaymentMapper.xml

<?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.atguigu.springcloud.dao.PaymentDao">
    <insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
        insert into payment(serial) values (#{serial});
    </insert>

    <resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="serial" property="serial" jdbcType="VARCHAR"/>
    </resultMap>
    <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">
        select * from payment where id = #{id};
    </select>
</mapper>

springcloud包下新建包service用于存放服务层,在service下新建接口PaymentService

public interface PaymentService {
    int create(Payment payment);

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

在同一包下新建接口实现类PaymentServiceImpl

@Service
public class PaymentServiceImpl implements PaymentService {
    @Resource // 作用与@Autowired相同
    private PaymentDao paymentDao;

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

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

springcloud包下新建包controller用于存放控制层,在controller下新建类PaymentController

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

    @PostMapping(value = "/payment/create")
    public CommonResult create(@RequestBody Payment payment) { // 不要忘记@RequestBody注解
        int result = paymentService.create(payment);
        log.info("*****插入结果:" + result);
        if (result > 0) {
            return new CommonResult(200, "插入成功", result);
        } else {
            return new CommonResult(444, "插入失败", null);
        }
    }

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

接下来即可进行测试,IDEA推荐使用内置的HTTP Client进行测试,在src\test\文件夹下新建resources\test-api\,在test-api\下新建文件TestPaymentController.http用于测试PaymentController类的API接口是否正常:

# 测试PaymentController API接口
POST http://localhost:8001/payment/create?serial=loveshes
###
GET http://localhost:8001/payment/get/1
###
GET http://localhost:8001/payment/get/2

3. 服务消费者

准备工作

在父工程添加Module,选择Maven项目,ArtifactId为cloud-consumer-order80,在POM文件中添加dependencies节点如下:

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

    <dependency>
        <groupId>org.springframework.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>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 80

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类OrderMain80

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

写业务类

springcloud包下新建包entities用于存放实体类,在entities下新建实体类PaymentCommonResult(与cloud-provider-payment8001\src\main\java\com\atguigu\springcloud\entities一样)。

springcloud包下新建包config用于存放设置,在config下新建类ApplicationContextConfig

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

使用restTemplate访问restful接口 使用restTemplate访问restful接口非常简单,urlrequestMapResponseBean.class这三个参数分别代表rest请求地址,请求参数、HTTP响应被转换成的对象类型。

springcloud包下新建包controller用于存放控制层,在controller下新建控制类OrderController

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

    @Resource
    private RestTemplate restTemplate;

    // 通过浏览器来模拟,浏览器一般发GET请求
    @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);
    }
}

即可开始测试。

4. 工程重构

系统中有重复部分entities,需要重构。

新建module,ArtifactId为cloud-api-commons,在POM文件中添加dependencies节点如下:

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

entities内的文件粘贴到cloud-api-commons\src\main\java\com\atguigu\springcloud\entities\下。

在Maven选项卡选中cloud-api-commons模块,依次点击cleaninstall进行发布。

删除cloud-provider-payment8001模块和cloud-consumer-order80模块下的entities,将发布的cloud-api-commons.jar添加到2个模块的pom文件中即可:

<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
    <groupId>com.atguigu.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>${project.version}</version>
</dependency>

IDEA不出现Run Dashboard/Services窗口.idea下的workspace.xml文件中找到 <component name="RunDashboard"> 标签,然后添加如下节点:

<option name="configurationTypes">
  <set>
    <option value="SpringBootApplicationConfigurationType" />
  </set>
</option>

Eureka服务注册与发现

已进入维护阶段

1. 理论知识

什么是服务治理

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

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

什么是服务注册

Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者 | 服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))。

SpringCloud基础篇 - 图1

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

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

2. 单机Eureka构建

搭建

在父工程添加Module,选择Maven项目,ArtifactId为cloud-eureka-server7001,在POM文件中添加dependencies节点如下:

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

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 7001

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

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类EurekaMain7001

@SpringBootApplication
@EnableEurekaServer // 核心
public class EurekaMain7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaMain7001.class, args);
    }
}

启动后,浏览器访问localhost:7001即可看到spring Eureka页面。

注册

修改服务提供端cloud-provider-payment8001

修改cloud-provider-payment8001的pom文件,添加依赖:

<!--eureka-client-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改cloud-provider-payment8001的配置文件application.yml,添加如下内容:

eureka:
  client:
    register-with-eureka: true # true表示会向注册中心注册,默认为true
    # true表示从注册中心检索服务,默认为true。单节点无所谓,集群必须要设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # 设置交互地址,查询和注册都需要依赖这个地址
      defaultZone: http://localhost:7001/eureka/

在主启动类PaymentMain8001上添加注解@EnableEurekaClient即可。访问localhost:7001即可看到已经注册成功。

修改服务消费端cloud-consumer-order80

修改cloud-consumer-order80的pom文件,添加依赖:

<!--eureka-client-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改cloud-consumer-order80的配置文件application.yml,添加如下内容:

spring:
  application:
    name: cloud-order-service

eureka:
  client:
    register-with-eureka: true # true表示会向注册中心注册,默认为true
    # true表示从注册中心检索服务,默认为true。单节点无所谓,集群必须要设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      # 设置交互地址,查询和注册都需要依赖这个地址
      defaultZone: http://localhost:7001/eureka/

在主启动类OrderMain80上添加注解@EnableEurekaClient即可。访问localhost:7001即可看到已经注册成功。

3. 集群Eureka构建

原理

互相注册,相互守望。

SpringCloud基础篇 - 图2

微服务RPC远程服务调用最核心的是什么?

高可用,避免一个宕机整个系统都崩了,最好是搭建Eureka注册中心集群,实现负载均衡+故障容错。

搭建

在父工程添加Module,选择Maven项目,ArtifactId为cloud-eureka-server7002,在POM文件中添加dependencies节点如下:

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

修改本机映射配置文件C:\Windows\System32\drivers\etc\hosts,添加:

127.0.0.1  eureka7001.com
127.0.0.1  eureka7002.com

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 7002

eureka:
  instance:
    hostname: eureka7002.com   # eureka服务端的实例名称
  client:
    register-with-eureka: false # false表示不会向注册中心注册自己
    fetch-registry: false       # false表示自己就是注册中心,职责是维护服务实例,不需要去检索服务
    service-url:
      # 设置交互地址,查询和注册都需要依赖这个地址
      defaultZone: http://eureka7001.com:7001/eureka/

并修改cloud-eureka-server7001的配置文件application.yml如下:

server:
  port: 7001

eureka:
  instance:
    hostname: eureka7001.com   # eureka服务端的实例名称
  client:
    register-with-eureka: false # false表示不会向注册中心注册自己
    fetch-registry: false       # false表示自己就是注册中心,职责是维护服务实例,不需要去检索服务
    service-url:
      # 设置交互地址,查询和注册都需要依赖这个地址
      defaultZone: http://eureka7002.com:7002/eureka/

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类EurekaMain7002

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

注册

修改服务提供端cloud-provider-payment8001

修改cloud-provider-payment8001的配置文件application.yml,修改eureka.client.service-url.defaultZone的值:

defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

修改服务消费端cloud-consumer-order80

修改cloud-consumer-order80的配置文件application.yml,修改eureka.client.service-url.defaultZone的值:

defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

然后就可以重启项目进行测试,启动顺序如下:①启动Eureka Server 7001/7002;②启动服务提供者;③启动消费者。

4. 服务提供者集群

在父工程添加Module,选择Maven项目,ArtifactId为cloud-provider-payment8002,添加的依赖与cloud-provider-payment8001相同。

配置文件application.yml与项目8001差不多,需要修改server.port8002

mapper文件夹和java文件与项目8001相同。复制完成之后修改主启动类名为PaymentMain8002

修改项目8001和8002的controller,在类PaymentController中添加成员变量便于打印调试(不加也不影响结果):

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

复制文件夹最好从在资源管理器复制,而不是在IDEA复制,否则IDEA可能会对涉及到类名的地方自动加上包名之类的东西。

再修改cloud-consumer-order80项目

将服务消费者访问的IP地址改成微服务名称,修改OrderController类的URL地址为微服务名称:

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

修改ApplicationContextConfig类,在方法getRestTemplate()上加上@LoadBalanced注解:

@Bean
@LoadBalanced // 赋予RestTemplate负载均衡能力,默认为轮询
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

即可。

5. 完善配置

主机名称修改

修改cloud-provider-payment8001的配置文件application.yml,在eureka下添加内容:

instance:
  instance-id: payment8001

修改cloud-provider-payment8002的配置文件application.yml,在eureka下添加内容:

instance:
  instance-id: payment8002

访问信息IP提示

instance-id下面(同级)加上:

prefer-ip-address: true

6. 服务发现Discovery

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

修改cloud-provider-payment8001cloud-provider-payment8002PaymentController类,增加成员变量和方法:

@Resource
private DiscoveryClient discoveryClient; // 引的是springframework下面的包

@GetMapping(value = "/payment/discovery")
public Object discovery() {
    List<String> services = discoveryClient.getServices();
    services.forEach(element -> log.info("****element: " + element));

    List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
    instances.forEach(instance ->
            log.info("****instance: " + instance.getInstanceId() + "\t" + instance.getHost() + "\t" +
                     instance.getPort() + "\t" + instance.getUri())
    );
    return this.discoveryClient;
}

浏览器访问localhost:8001/payment/discovery或者localhost:8002/payment/discovery即可看到信息。

7. Eureka自我保护

理论

保护模式主要用于一组客户端和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自我保护机制?

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

什么是自我保护模式?

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

禁止自我保护

先将7001和8001改成单机版,以单机版cloud-eureka-server7001cloud-provider-payment8001为例,修改7001的配置文件application.yml,在eureka节点下添加内容:

server:
  # 关闭自我保护机制,服务不可用立即移除
  enable-self-preservation: false
  eviction-interval-timer-in-ms: 2000

打开http://localhist:7001可以看到如下内容,即表示关闭成功

THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

再修改8001的配置文件application.yml,在eureka.instance节点下添加内容:

# eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认30秒)
lease-renewal-interval-in-seconds: 1
# eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2

即可进行测试。先启动7001再启动8001,打开http://localhist:7001可以看到成功注册8001;关闭8001,可以看到微服务立刻被移除。

Zookeeper服务注册与发现

1. 安装Zookeeper

使用docker安装zookeeper

docker pull zookeeper
docker run -d --privileged=true  --name=zookeeper --publish 2181:2181 zookeeper:latest

测试zookeeper是否安装成功

# 进入容器内部
docker exec -it zookeeper /bin/bash
# 运行zookeeper客户端
bin/zkCli.sh
# 显示根节点
ls /

在IDEA中安装插件Zookeeper

2. 服务提供者

在父工程添加Module,选择Maven项目,ArtifactId为cloud-provider-payment8004,在POM文件中添加dependencies节点如下:

<dependencies>
    <!-- SpringCloud整合Zookeeper客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.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>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 8004

spring:
  application:
    name: cloud-provider-payment
  cloud:
    zookeeper:
      connect-string: 49.233.88.245:2181

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类PaymentMain8004

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

以下省略Dao、Service层的编写。

springcloud包下新建包controller用于存放控制层,在controller下新建类PaymentController

@RestController
@Slf4j
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

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

启动该模块,访问localhost:8004/payment/zk能够显示信息,连接zookeeper客户端运行以下命令说明成功注册:

[zk: localhost:2181(CONNECTED) 0] ls /
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 1] ls /services
[cloud-provider-payment]
[zk: localhost:2181(CONNECTED) 2] ls /services/cloud-provider-payment
[35fe027d-6549-41d8-a41e-97fd76042264]
[zk: localhost:2181(CONNECTED) 3] ls /services/cloud-provider-payment/35fe027d-6549-41d8-a41e-97fd76042264
[]
[zk: localhost:2181(CONNECTED) 4] get /services/cloud-provider-payment/35fe027d-6549-41d8-a41e-97fd76042264
{"name":"cloud-provider-payment","id":"35fe027d-6549-41d8-a41e-97fd76042264","address":"localhost","port":8004,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"cloud-provider-payment","metadata":{}},"registrationTimeUTC":1584550450923,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}

注册的服务提供者节点是临时节点

如果启动报错,多半是因为版本冲突,可以修改pom文件如下,再重启该项目(本来就没问题就不用改了,改了可能会导致日志框架报错):

<!-- SpringCloud整合Zookeeper客户端 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    <!-- 解决版本冲突:排除自带的zookeeper-->
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 添加服务器使用的zookeeper版本 -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.5.7</version>
</dependency>

3. 服务消费者

在父工程添加Module,选择Maven项目,ArtifactId为cloud-consumerzk-order80,在POM文件中添加dependencies节点如下:

<dependencies>
    <!-- SpringCloud整合Zookeeper客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.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>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 80

spring:
  application:
    name: cloud-consumer-order
  cloud:
    zookeeper:
      connect-string: 49.233.88.245:2181

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类OrderZKMain80

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

以下省略Dao、Service层的编写。

springcloud包下新建包config用于存放设置,在config下新建类ApplicationContextConfig

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

springcloud包下新建包controller用于存放控制层,在controller下新建类PaymentController

@RestController
@Slf4j
public class PaymentController {
    public static final String INVOKE_URL = "http://cloud-provider-payment";

    @Resource
    private RestTemplate restTemplate;

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

启动模块,即可开始测试。

Consul服务注册与发现

1. 安装

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

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

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

以win为例,官网下载windows 64版本,解压之后只有一个exe文件,在当前路径命令行直接运行consul agent -dev,访问http://localhost:8500即可。

2. 服务提供者

在父工程添加Module,选择Maven项目,ArtifactId为cloud-providerconsul-payment8006,在POM文件中添加dependencies节点如下:

<dependencies>
    <!--SpringCloud consul-server -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--日常通用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>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>RELEASE</version>
        <scope>test</scope>
    </dependency>
</dependencies>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 8006

spring:
  application:
    name: consul-provider-payment
  cloud:
    # consul注册中心地址
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类PaymentMain8006

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

以下省略Dao、Service层的编写。

springcloud包下新建包controller用于存放控制层,在controller下新建类PaymentController

@RestController
@Slf4j
public class PaymentController {
    @Value("${server.port}")
    private String serverPort;

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

启动该模块,访问http://localhost:8500能看到注册进去的节点,访问localhost:8006/payment/consul能够显示信息,说明配置成功。

3. 服务消费者

在父工程添加Module,选择Maven项目,ArtifactId为cloud-consumerconsul-order80,在POM文件中添加dependencies节点如下:

<dependencies>
    <!--SpringCloud consul-server -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--日常通用jar包配置-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 80

spring:
  application:
    name: cloud-consumer-order
  cloud:
    # consul注册中心地址
    consul:
      host: localhost
      port: 8500
      discovery:
        service-name: ${spring.application.name}

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类OrderConsulMain80

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

以下省略Dao、Service层的编写。

springcloud包下新建包config用于存放设置,在config下新建类ApplicationContextConfig

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

springcloud包下新建包controller用于存放控制层,在controller下新建类OrderConsulController

@RestController
@Slf4j
public class OrderConsulController {
    public static final String INVOKE_URL = "http://consul-provider-payment";

    @Resource
    private RestTemplate restTemplate;

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

启动该模块,访问http://localhost:8500能看到注册进去的节点,访问localhost/consumer/payment/consul能够显示信息,说明配置成功。

三个注册中心异同点

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

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。对应分布式系统而言,P一定要保证。

  • AP架构:当网络分区出现之后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。但是违背了一致性C的要求,只满足可用性和分区容错,即AP。
  • CP架构:当网络分区出现之后,为了保证一致性,就必须拒绝请求,否则无法保证一致性。但是违背了可用性A的要求,只满足一致性和分区容错,即CP。

Ribbon负载均衡服务调用

已进入维护阶段,但是生命力依旧顽强

1. 简介

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

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

LB负载均衡(Load Balance)是什么 简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件F5等。 Ribbon本地负载均衡客户端 vs Nginx服务端负载均衡区别

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

集中式LB与进程内LB

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

Ribbon = 负载均衡 + RestTemplate调用

引入Eureka的时候,自动引入了Ribbon。也可以手动添加Ribbon,打开cloud-consumer-order80项目的pom文件,添加依赖:

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

2. RestTemplate的使用

RestTemplate中的getForObject()和getForEntity()

  • getForObject()
    返回对象为响应体中数据转化成的对象,基本可以理解为JSON
  • getForEntity()
    返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等

OrderController类添加方法,之前使用的是getForObject和postForObject,下面使用使用getForEntity()和postForEntity:

@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id) {
    ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
    if (entity.getStatusCode().is2xxSuccessful()) {
        return entity.getBody();
    } else {
        return new CommonResult(444, "操作失败");
    }
}

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

3. IRule接口

Ribbon自带的7种LB算法

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

3. 修改规则

注意,由于启动类加了@SpringBootApplication,这个注解又加上了@ComponentScan注解,会扫描当前包和子包。Ribbon的自定义配置类不能放在加了@ComponentScan注解的当前包和子包下,否则配置就会被所有Ribbon共享,达不到特殊化定制的目的了。

src\main\java\下新建包com.atguigu.myrule,在包下新建配置类MySelfRule

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

然后在启动类OrderMain80上加上注解@RibbonClient

// ...
// name为需要调用的生产者服务名称
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80 {
    // ...
}

即可替换负载均衡规则为随机

4. 轮询算法解析

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

手写轮询算法:原理 + JUC(CAP+自旋锁)

修改cloud-provider-payment8001cloud-provider-payment8002项目的PaymentController,增加方法:

@GetMapping(value = "/payment/lb")
public String getPaymentLB() {
    return serverPort;
}

修改cloud-consumer-order80项目如下:

  1. 去掉ApplicationContextConfig类的@LoadBalanced注解
  2. 新建包com.atguigu.springcloud.lb,在lb内新建LoadBalancer接口```java public interface LoadBalancer { ServiceInstance instance(List serviceInstances); }

    <br />新建实现类`MyLB`:```java
    @Component
    public class MyLB implements LoadBalancer {
     private AtomicInteger atomicInteger = new AtomicInteger(0);
    
     // 自旋锁自增
     private int getAndIncrement() {
         int current;
         int next;
         do {
             current = this.atomicInteger.get();
             // Integer.MAX_VALUE 为 2147483647
             next = current >= 2147483647 ? 0 : current + 1;
         } while (!this.atomicInteger.compareAndSet(current, next));
         System.out.println("****next: " + next);
         return next;
     }
    
     @Override
     public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
         int index = getAndIncrement() % serviceInstances.size();
         return serviceInstances.get(index);
     }
    }
    
  3. 修改OrderController类,添加如下代码:```java @Resource // 自定义轮询算法 private LoadBalancer loadBalancer;

@Resource private DiscoveryClient discoveryClient;

@GetMapping(value = “/consumer/payment/lb”) public String getPaymentLB() { List instances = discoveryClient.getInstances(“CLOUD-PAYMENT-SERVICE”); if (instances == null || instances.size() <= 0) return null;

ServiceInstance serviceInstance = loadBalancer.instance(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);

}


4. 启动项目,浏览器访问`localhost/consumer/payment/lb`即可看到效果。

<a name="efe0302d"></a>
## OpenFeign服务调用

<a name="25970172-1"></a>
### 1. 简介

**Feign是什么**

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

**Feign集成了Ribbon**

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

**Feign和OpenFeign区别**

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

2. 使用步骤

在父工程添加Module,选择Maven项目,ArtifactId为cloud-consumer-feign-order80,在POM文件中添加dependencies节点如下:

<dependencies>
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--eureka-client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.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>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 80

eureka:
  client:
    register-with-eureka: false # true表示会向注册中心注册,默认为true
    # true表示从注册中心检索服务,默认为true。单节点无所谓,集群必须要设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka    # 集群

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类OrderFeignMain80

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

springcloud包下新建包service用于存放服务接口,在service下新建接口PaymentFeignService

@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 寻找对应服务
public interface PaymentFeignService {
    @GetMapping(value = "/payment/get/{id}") // 寻找该服务下的对应路径的方法
    CommonResult<Payment> getPaymentById(@PathVariable("id") Long id); // 方法名称可以不一样
}

springcloud包下新建包controller用于存放控制层,在controller下新建类PaymentFeignController

@RestController
@Slf4j
public class PaymentFeignController {
    @Resource
    private PaymentFeignService paymentFeignService;

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

启动项目(注意关掉原来的80项目),访问localhost/consumer/payment/get/1即可进行测试。

3. 超时控制

演示出错情况。

服务提供方8001故意写暂停程序

修改cloud-provider-payment8001项目的PaymentController类,增加方法:

@GetMapping(value = "/payment/feign/timeout")
public String paymentFeignTimeout() {
    // 暂停3秒钟线程
    try {
        TimeUnit.SECONDS.sleep(3);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return serverPort;
}

修改cloud-consumer-feign-order80项目的PaymentFeignService接口,增加方法:

@GetMapping(value = "/payment/feign/timeout")
String paymentFeignTimeout();

修改PaymentFeignController类,增加方法:

@GetMapping(value = "/consumer/payment/feign/timeout")
public String paymentFeignTimeout() {
    // openfeign默认等1s拿到结果
    return paymentFeignService.paymentFeignTimeout();
}

启动项目(注意不要启动8002项目),访问localhost:8001/payment/feign/timeout正常,访问localhost/consumer/payment/feign/timeout报错,说明成功。

修改超时时间

在配置文件application.yml中进行配置,增加:

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

再访问localhost/consumer/payment/feign/timeout正常,说明成功。

4. 日志打印

Feign提供了日志打印功能,可以对Feign接口的调用情况进行监控和输出。可以通过修改配置来调整日志级别。

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

springcloud包下新建包config用于存放配置,在config下新建类FeignConfig

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

修改application.yml配置文件。增加:

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

Hystrix断路器

已进入维护阶段

1. 简介

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

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

Hystrix可以用来进行服务降级服务熔断、接近实时的监控。

服务雪崩 多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的扇出。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。 对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。 所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩

2. 重要概念

  • 服务降级
    服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback。
    以下情况会触发服务降级:
    • 程序运行异常
    • 超时
    • 服务熔断触发服务降级
    • 线程池/信号量打满导致服务降级
  • 服务熔断
    类比于保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级并返回友好提示。
  • 服务限流
    秒杀等高并发操作,严禁一窝蜂的过来拥挤,排队,一秒钟N个,有序进行

3. 服务提供者

在父工程添加Module,选择Maven项目,ArtifactId为cloud-provider-hystrix-payment8001,在POM文件中添加dependencies节点如下:

<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>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.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>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 8001

spring:
  application:
    name: cloud-provider-hystrix-payment

eureka:
  client:
    register-with-eureka: true # true表示会向注册中心注册,默认为true
    # true表示从注册中心检索服务,默认为true。单节点无所谓,集群必须要设置为true才能配合ribbon使用负载均衡
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka    # 单机

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类PaymentHystrixMain8001

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

springcloud包下新建包service,在service下新建类PaymentService

@Service
public class PaymentService {
    public String paymentInfo_OK(Integer id) {
        return "线程池: " + Thread.currentThread().getName() + " paymentInfo_OK, id: " + id + "\tO(^_^)O";
    }

    public String paymentInfo_Timeout(Integer id) {
        int timeNumber = 3;
        try {
            TimeUnit.SECONDS.sleep(timeNumber);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "线程池: " + Thread.currentThread().getName() + " paymentInfo_Timeout, id: " + id +
                "\t耗时" + timeNumber + "秒";
    }
}

springcloud包下新建包controller,在controller下新建类PaymentController

@RestController
@Slf4j
public class PaymentController {
    @Resource
    private PaymentService paymentService;

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

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

    @GetMapping("/payment/hystrix/timeout/{id}")
    public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
        String result = paymentService.paymentInfo_Timeout(id);
        log.info("****result: " + result);
        return result;
    }
}

先启动项目cloud-eureka-server7001,再启动当前项目。访问http://localhost:8001/payment/hystrix/ok/1http://localhost:8001/payment/hystrix/timeout/1都正常即成功。

通过高并发压力测试可以发现paymentInfo_Timeout()拖慢了paymentInfo_OK()的正常访问。

4. 服务消费者

在父工程添加Module,选择Maven项目,ArtifactId为cloud-consumer-feign-hystrix-order80,在POM文件中添加dependencies节点如下:

<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>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--一般基础通用配置-->
    <dependency>
        <groupId>org.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>

src\main\resources\下添加配置文件application.yml,内容如下:

server:
  port: 80

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

feign:
  hystrix:
    enabled: true

src\main\java\下新建包com.atguigu.springcloud,在包下新建启动类OrderHystrixMain80

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

springcloud包下新建包service,在service下新建接口PaymentHystrixService

@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
    @GetMapping("/payment/hystrix/ok/{id}")
    String paymentInfo_OK(@PathVariable("id") Integer id);

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

springcloud包下新建包controller,在controller下新建接口OrderHystrixController

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

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

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

5. 服务降级

超时导致服务器变慢(转圈):超时不再等待

出错(宕机或程序运行出错):出错要有兜底

解决思路:

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

降级配置:@HystrixCommand

解决措施:

  • 服务端8001自身找问题,设置自身调用超时时间的峰值,峰值内可以正常运行,超过峰值需要有兜底方法进行处理,作服务降级fallbac。在cloud-provider-hystrix-payment8001项目进行以下操作
    • 首先主启动类激活,在主启动类PaymentHystrixMain8001上加上注解@EnableCircuitBreaker
    • 业务类目标方法激活,PaymentService类的paymentInfo_Timeout方法上加上注解,并在类中添加兜底方法:```java @HystrixCommand(fallbackMethod = “paymentInfo_TimeoutHandler”, commandProperties = { @HystrixProperty(name = “execution.isolation.thread.timeoutInMilliseconds”, value = “3000”) }) public String paymentInfo_Timeout(Integer id) { // int age = 10/0; // 运行异常也会触发服务降级 // … }

public String paymentInfo_TimeoutHandler(Integer id) { return “调用接口超时或异常:\t” + “当前线程池名字” + Thread.currentThread().getName(); }




访问`http://localhost:8001/payment/hystrix/timeout/1`进行测试

- 客户端80自己进行服务降级。在`cloud-consumer-feign-hystrix-order80`项目进行以下操作
   - 首先主启动类激活,在主启动类`OrderHystrixMain80`上加上注解`@EnableCircuitBreaker`
   - 业务类目标方法激活,`OrderHystrixController`类的`paymentInfo_Timeout`方法上加上注解,并在类中添加兜底方法:```java
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
    @HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = {
        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="1500")
})
public String paymentInfo_Timeout(@PathVariable("id") Integer id){
    // int age = 10 / 0; // 自己运行出错也会触发服务降级
    return paymentHystrixService.paymentInfo_Timeout(id);
}

public String paymentInfo_TimeoutHandler(@PathVariable("id") Integer id) {
    return "我是消费者80,对方支付系统繁忙或者自己运行出错,请稍后再试";
}

访问http://localhost/comsumer/payment/hystrix/timeout/1进行测试

6. 全局通用兜底方法

在需要设置默认兜底方法的类上增加注解@DefaultProperties,该注解会将没有配置过fallbackMethod的方法执行默认兜底方法,例如在类OrderHystrixController上增加注解,并添加全局fallback方法:

// ...
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystrixController {
    // ...

    // 测试全局fallback方法
    @GetMapping("/consumer/payment/hystrix/timeout2/{id}")
    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
    })
    public String paymentInfo_Timeout2(@PathVariable("id") Integer id) {
        return paymentHystrixService.paymentInfo_Timeout(id);
    }

    // 全局fallback方法
    public String payment_Global_FallbackMethod() {
        return "Global异常处理信息,请稍后再试";
    }
}

访问http://localhost/comsumer/payment/hystrix/timeout2/1进行测试

7. 解决fallback方法耦合

由于消费者调用的方法都是调用的Feign接口PaymentHystrixService中的方法,故可以对这些接口中的方法进行fallback设置。

以下操作都在项目cloud-consumer-feign-hystrix-order80中进行。

service包新建一个接口实现类PaymentFallbackService,每个Service接口都对应一个fallback方法:

@Component
public class PaymentFallbackService implements PaymentHystrixService {
    @Override
    public String paymentInfo_OK(Integer id) {
        return "-------PaymentFallbackService fall back #paymentInfo_OK------";
    }

    @Override
    public String paymentInfo_Timeout(Integer id) {
        return "-------PaymentFallbackService fall back #paymentInfo_Timeout------";
    }
}

然后修改接口PaymentHystrixService@FeignClient注解:

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

如果服务提供者宕机,客户端会触发服务降级,如果前面有设置过有fallback方法就用前面的,如果没有就用上面的。

8. 服务熔断

熔断机制概述

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

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

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

服务提供者示例

以下操作都在cloud-provider-hystrix-payment8001项目进行

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"),// 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(Integer id) {
    if (id < 0) {
        throw new RuntimeException("******id不能为负数");
    }
    String serialNumber = IdUtil.simpleUUID();
    return Thread.currentThread().getName() + "\t调用成功,流水号: " + serialNumber;
}

public String paymentCircuitBreaker_fallback(Integer id) {
    return "~~~~~~~~id不能为负数,请重试,id: " + id;
}

PaymentController类中增加对应的方法:

@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
    String result = paymentService.paymentCircuitBreaker(id);
    log.info("****result: "+result);
    return result;
}

启动项目,连续访问http://localhost:8001/payment/circuit/-1多次进行测试,然后再访问正确的id,发现也断路了。

再多次调用http://localhost:8001/payment/circuit/1,发现恢复连接。

@HystrixProperty中参数的解释

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

熔断类型

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

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

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

9. 服务限流

见Alibaba Sentinel章节

10. Hystrix Dashboard

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

新建项目cloud-consumer-hystrix-dashboard9001,添加依赖:

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

    <dependency>
        <groupId>org.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>

增加配置文件application.yml

server:
  port: 9001

新建包com.atguigu.springcloud,在springcloud包下新建主启动类HystrixDashboardMain9001

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

注意,监控信息依赖于SpringBoot Actuator,被监控的项目一定要引入Actuator

新版本Hystrix需要在被监控的的主启动类中指定监控路径,例如在启动类PaymentHystrixMain8001中添加方法:

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

访问http://localhost:9001/hystrix,输入监控地址http://localhost:8001/hystrix.stream和title访问监控面板,当被监控的目标有流量时会有图表显示。

Spring Cloud Gateway 新一代网关

1. 简介

SpringCloud Gateway使用的是Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。Gateway是基于异步非阻塞模型上进行开发的。

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

Webflux是非阻塞异步框架,核心是基于Reactor的相关API实现的。

微服务架构层级如下: | 外部请求 | 手持终端 Html5 Open接口 | 负载均衡 | 负载均衡 | 网关 | 网关1 网关2 | 微服务 | 微服务A 服务费B 服务费C 服务费D

Spring Cloud Gateway具有如下特性

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

三大核心概念

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

Predicate(断言):参考Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有路由(例如请求头或请求参数),如果请求与断言相匹配则进行路由

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

2. 使用

需求:不暴露服务提供者的8001端口,希望在8001端口外面套一层9527

新建项目cloud-gateway-gateway9527,添加依赖(网关也要注册进服务注册中心):

<dependencies>
    <!--gateway-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--eureka-client-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
    <dependency>
        <groupId>com.atguigu.springcloud</groupId>
        <artifactId>cloud-api-commons</artifactId>
        <version>${project.version}</version>
    </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>

注意,gateway不需要spring-boot-starter-web,引入会报错

新建配置文件application.yml

server:
  port: 9527

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

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

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

yaml文件中使用列表:

list: 
  - a
  - b

properties文件使用列表:

list=a,b

新建包com.atguigu.springcloud,在springcloud包下新建主启动类GatewayMain9527

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

按顺序启动单机版cloud-eureka-server7001cloud-provider-payment8001cloud-gateway-gateway9527,此时可以直接访问8001可以,也可以访问9527端口的服务http://localhost:9527/payment/get/1

配置路由

gateway中有2种配置方法,一种是上面的yaml文件中配置,另一种是在代码中注入RouteLocatorBean

下面在代码中配置,实现访问http://localhost:9527/news自动跳转到https://news.baidu.com/guonei。

新建配置类config.GatewayConfig

@Configuration
public class GatewayConfig {
    // 多个可以配在同一个方法,也可以配置在不同的方法
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("path_route_atguigu",
                r -> r.path("/guonei").uri("https://news.baidu.com/guonei")).build();
        routes.route("path_route_atguigu2",
                r -> r.path("/guoji").uri("https://news.baidu.com/guoji")).build();
        return routes.build();
    }
}

启动,访问http://localhost/guonei,看是否跳转到百度

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

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

修改配置文件的cloud节点:

cloud:
  gateway:
    discovery:
      locator:
        enabled: true # 开启中注册中心动态创建路由的功能,利用微服务名进行路由
    routes:
      - id: payment_routh           # 路由ID,要求唯一,建议配合服务名
        # uri: http://localhost:8001  # 匹配后提供服务的路由地址
        uri: lb://cloud-payment-service  # 匹配后提供服务的微服务名
        predicates:
          - Path=/payment/get/**    # 断言,路径相匹配的进行路由

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

按顺序启动单机版cloud-eureka-server7001cloud-provider-payment8001cloud-provider-payment8002cloud-gateway-gateway9527,访问http://localhost:9527/payment/lb,看是否8001/8002轮流显示

4. 常用Route Predicate断言

上述配置文件的中的predicates是一个列表,可以传入多个匹配条件,常见的Route Predicate如下。

  • After Route Predicate
  • Before Route Predicate
  • Between Route Predicate```yaml predicates:
    • After=2020-03-29T03:01:03.991+08:00[Asia/Shanghai] # 指定时间之后才有匹配
    • Before=2020-03-29T04:01:03.991+08:00[Asia/Shanghai] # 指定时间之前才有匹配

      两者之间才匹配

    • Between=2020-03-29T03:01:03.991+08:00[Asia/Shanghai],2020-03-29T04:01:03.991+08:00[Asia/Shanghai] ```

      通过ZonedDateTime.now(),可以得到符合要求的时间格式

  • Cookie Route Predicate
    需要2个参数,一个是Cookie name,一个是正则表达式```yaml predicates:
  • Header Route Predicate
    需要2个参数,一个是Cookie name,一个是正则表达式```yaml predicates:
    • Header=X-Request-Id,\d+ # 请求头要有X-Request-Id属性,并且值为纯数字的正则表达式 `` <br />使用curl访问curl http://localhost:9527/payment/lb -H “X-Request-Id:1234”`,成功。
  • Host Route Predicate```yaml predicates:
    • Host=.atguigu.com # 来源为.atguigu.com的访问才能进行 `` <br />使用curl访问curl http://localhost:9527/payment/lb -H “Host:www.atguigu.com”`,成功。
  • Method Route Predicate
    判断方法是GET、PUT等```yaml predicates:

    • Method=GET # GET请求才允许访问 ```
  • Path Route Predicate

  • Query Route Predicate
    查询条件满足正则表达式才允许通过```yaml predicates:

5. Filter过滤器

官方文档

Filter可以进入HTTP请求和HTTP响应,对其中的内容进行修改,Gateway内置了多种路由过滤器,都由GatewayFilter的工厂类产生。

  • 生命周期:pre,post
  • 种类:GatewayFilter(31种),GlobalFilter(10种)

自定义全局过滤器

新建类MyLogGatewayFilter,实现2个接口GlobalFilterOrdered

@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,非法用户");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange); // 过滤链传递下去
    }

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

按顺序启动单机版cloud-eureka-server7001cloud-provider-payment8001cloud-provider-payment8002cloud-gateway-gateway9527,访问http://localhost:9527/payment/lb?uname=marry,看是否正确显示。访问不带uname参数的地址就不能正常显示。

Spring Cloud Config 分布式配置中心

1. 简介

微服务面临着配置问题,每一个微服务都有一个配置文件,Config能够统一管理、动态配置。

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

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

  • 服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
  • 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息,配置服务器默认采用Git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过Git客户端I具来方便的管理和访问配置内容。

优点:

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

2. Config服务端配置与测试

在码云上新建一个仓库springcloud-config,在其它目录下执行命令:

git clone git@gitee.com:loveshes/springcloud-config.git

进入当前路径,添加3个文件,文件内容分别如下:

# config-dev.yml
config:
  info: "config-dev.yml version=1"

# config-prod.yml
config:
  info: "config-prod.yml version=1"

# config-test.yml
config:
  info: "config-test.yml version=1"

修改后push到远程

新建模块cloud-config-center-3344,添加依赖:

<dependencies>
    <!-- config-server 服务端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.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>

添加配置文件application.yml

server:
  port: 3344

spring:
  application:
    name: cloud-config-center
  cloud:
    config:
      server:
        git:
          uri: git@gitee.com:loveshes/springcloud-config.git  # 仓库名称
          search-paths:
            - springcloud-config                              # 搜索路径
      label: master                                           # 默认读取分支

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

新建包com.atguigu.springcloud,在springcloud包下新建主启动类ConfigCenterMain3344

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

修改hosts文件,增加映射:

127.0.0.1 config-3344.com

即可开始测试,先启动7001,再启动3344,访问http://config-3344.com:3344/master/config-dev.yml即可看到配置文件的内容(不含注释)。

配置读取规则

/{label}/{application}-{profile}.yml ,读出来的是内容,推荐

`/{application}-{profile}.yml,读出来的是内容

  • 省略label标签,默认去找配置文件中的label,本项目为master分支

/{application}/{profile}/{label}.yml,读出来是json串

3. Config客户端配置与测试

新建模块cloud-config-client-3355,添加依赖:

<dependencies>
    <!-- config 客户端 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>org.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>

新建配置文件bootstrap.yml

server:
  port: 3355

spring:
  application:
    name: config-client
  cloud:
    # config客户端设置
    config:
      label: master               # 读取分支名称
      name: config                # 配置文件名称
      profile: dev                # 读取后缀名称
      uri: http://localhost:3344  # 配置中心地址
      # 以上读取的是文件 http://localhost:3344/master/config-dev.yml

eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

applicaiton.yml是用户级的资源配置项,bootstrap.yml是系统级的,优先级更加高。 Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context 负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。 Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。Bootstrap context和Application Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap Context和Application Context配置的分离。 要将Client模块下的application.ym|文件改为bootstrap.yml,这是很关键的,因为bootstrap.yml是比application.yml先加载的。 bootstrap.yml优先级高于application.yml

新建包com.atguigu.springcloud,在springcloud包下新建主启动类ConfigClientMain3355

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

springcloud包下新建业务类读取配置文件信息controller.ConfigClientController

// 项目在启动的时候就连接config服务端获取所有配置信息并存在内存中了,跟在本地写yaml文件一样
@RestController
public class ConfigClientController {
    @Value("${server.port}")
    private String serverPort;

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

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

按顺序启动7001、3344、3355,访问http://localhost:3355/configInfo,说明正确读到了配置信息

4. 客户端动态刷新

修改cloud-config-client-3355模块,POM中引入actuator依赖(前面已经引入)。

修改配置文件,暴露监控端口,添加以下内容:

management:
  endpoints:
    web:
      exposure:
        include: "*"

在需要用到动态刷新的地方加上注解@RefreshScope,本例中为ConfigClientController

手动修改远程仓库配置文件,push。访问http://localhost:3355/configInfo,此时客户端仍然没有更新。

需要对方发送POST请求刷新3355:

curl -X POST "http://localhost:3355/actuator/refresh"

客户端就会更新内容了。

这里就引出来一个问题,如果有大量微服务需要通知,每个微服务都要发送POST请求刷新,十分繁琐。 如何一次广播,一次通知,处处生效。可以通过下面的消息总线来解决。

Spring Cloud Bus 消息总线

1. 简介

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

Spring Cloud Bus整合了Java的事件处理机制和消息中间件(主题订阅)的功能,只支持两种消息代理:RabbitMQ和Kafka。

什么是总线

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

基本原理

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

2. 准备

安装RabbitMQ

先安装Erlang,再安装RabbitMQ,在安装路径的sbin目录下面执行命令安装插件:

rabbitmq-plugins enable rabbitmq_management

访问http://localhost:15672即可访问,默认用户名和密码是`guest`

或者使用docker安装。

# 下载
sudo docker pull rabbitmq:3.8-rc-management
# 运行(5672是通信端口,15672是管理页面端口)
sudo docker run -d -p 5672:5672 -p 15672:15672 --name=rabbitmq rabbitmq:3.8-rc-management
# (关闭后)启动
sudo docker start rabbitmq
# 停止
sudo docker stop rabbitmq

再建一个Config客户端

以3355为模板再建一个3366客户端,新建模块cloud-config-client-3366,添加依赖,与3355相同。

配置文件bootstrap.yml类似3355,再将server.port改为3366

新建包com.atguigu.springcloud,在springcloud包下新建主启动类ConfigClientMain3366,内容与3355相同。

springcloud包下新建业务类读取配置文件信息controller.ConfigClientController,与3355相同。

3. 设计思想

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

方法2更优。方法1不适合的原因如下:

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

4. 广播通知

给Config服务端添加消息总线支持

给模块cloud-config-center-3344添加依赖:

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

修改配置文件,添加内容:

# rabbitmq相关配置,暴露bus刷新配置的端点
management:
  endpoints: # 暴露bus刷新配置的端点
    web:
      exposure:
        include: 'bus-refresh'

给Config客户端添加消息总线支持

给模块cloud-config-center-3355添加依赖:

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

修改配置文件,在spring节点下添加内容:

#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest

模块cloud-config-center-3366操作同3355。

依次启动项目7001、3344、3355、3366,修改配置文件后只需向Config服务端发送请求:

curl -X POST "http://localhost:3344/actuator/bus-refresh“

发现3355和3366显示的信息都更新了。

5. 定点通知

只通知3355,不通知其它机器,格式为http://localhost:3344/actuator/bus-refresh/{destination}

例如:

curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355“

config-client为微服务名称,即spring.application.name。

Spring Cloud Stream 消息驱动

1. 简介

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

什么是Spring Cloud Stream

官方定义:Spring Cloud Stream是一个构建消息驱动微服务的框架。

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

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

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

目前仅支持RabbitMQ、Kafka.

2. 设计思想

标准MQ:

  • 生产者/消费者之间靠消息媒介传递信息内容:Message
  • 消息必须走特定的通道:消息通道MessageChannel
  • 消息通道里的消息如何被消费,谁负责收发处理:消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅。

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

Binder:INPUT对应消费者,OUTPUT对应生产者

Stream中的消息通信方式遵循了发布-订阅模式,通过Topic主题进行广播(在RabbitMQ中就是Exchange,在Kakfa中就是Topic)。

3. 理论知识

标准化流程

  • Binder:连接消息中间件,屏蔽差异
  • Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
  • Source和Sink:简单来说就是输入与输出。

编码API和常用注解

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

4. 消息驱动之生产者

新建模块cloud-stream-rabbitmq-provider8801,添加依赖:

<dependencies>
    <!-- stream -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!--基础配置-->
    <dependency>
        <groupId>org.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>

新建配置文件application.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: # 这个名字是一个通道的名称
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain"
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8801.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

新建包com.atguigu.springcloud,在springcloud包下新建主启动类StreamMQMain8801

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

下面开始写业务类。

新建接口service.IMessageProvider

public interface IMessageProvider {
    String send();
}

service包下新建接口实现类impl.MessageProviderImpl

@EnableBinding(Source.class) // 定义消息的推测管道(源)
public class MessageProviderImpl implements IMessageProvider {
    @Resource
    private MessageChannel output; // 消息发送管道

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

新建类controller.SendMessageController

@RestController
public class SendMessageController {
    @Resource
    private IMessageProvider messageProvider;

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

按顺序启动RabbitMQ,7001,8801,访问http://localhost:8801/sendMessage,看看后台是否打印日志和RabbitMQ界面流量变动。

5. 消息驱动之消费者

新建模块cloud-stream-rabbitmq-consumer8802,添加依赖与8801相同。

添加配置文件application.yml

server:
  port: 8802

spring:
  application:
    name: cloud-stream-consumer
  cloud:
    stream:
      binders: # 在此处配置要绑定的rabbitmq的服务信息;
        defaultRabbit: # 表示定义的名称,用于于binding整合
          type: rabbit # 消息组件类型
          environment: # 设置rabbitmq的相关的环境配置
            spring:
              rabbitmq:
                host: localhost
                port: 5672
                username: guest
                password: guest
      bindings: # 服务的整合处理
        ## 这里变成了input ##
        input:
          destination: studyExchange # 表示要使用的Exchange名称定义
          content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain"
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置

eureka:
  client: # 客户端进行Eureka注册的配置
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
    lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    instance-id: send-8802.com  # 在信息列表时显示主机名称
    prefer-ip-address: true     # 访问的路径变为IP地址

新建包com.atguigu.springcloud,在springcloud包下新建主启动类StreamMQMain8802

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

新建类controller.ReceiveMessageListener

@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListener {
    @Value("${server.port}")
    private String serverPort;

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

按顺序启动RabbitMQ,7001,8801,8802,访问http://localhost:8801/sendMessage,看8802是否能够接收到消息。

6. 分组消费与持久化

新建模块cloud-stream-rabbitmq-consumer8803,内容与8802一致。需要修改配置文件的server.portinstance-id和主启动类名称。

在前面的基础上启动8803,再访问http://localhost:8801/sendMessage,可以看到8802和8803都可以收到消息,存在重复消费。

通过Stream中的消息分组,同一个组中是竞争关系,只有其中一个可以消费,不同组可以全面消费(重复消费)。

如果没做分组的话,默认每一个消费者都在不同的组中。

分组

cloud-stream-rabbitmq-consumer8802修改配置文件,在input节点下新增:

input:
  group: atguiguA        # 分组名称

cloud-stream-rabbitmq-consumer8803修改配置文件,在input节点下新增:

input:
  group: atguiguB        # 分组名称

等待项目重启后看RabbitMQ管理界面,可以发现分组名变了。

将组名修改为相同的组名,就属于同一group了,不会出现重复消费(一般是轮询消费)。

持久化

先关掉8802和8803,去掉8802的group字段,然后8801先发送4条消息。

先启动8802,没有分组属性,拿不到消息。

启动8803,有分组属性,能拿到消息。

一定要配置分组信息group。

Spring Cloud Sleuth 分布式请求链路跟踪

1. 简介

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

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

zipkin用于可视化展现依赖关系图。

2. 使用

下载zipkin-server-2.12.9-exec.jar,启动java -jar zipkin-server-2.12.9-exec.jar,出现箭头图标即成功启动。

访问http://localhost:9411/zipkin可以看到可视化界面。

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

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

修改cloud-provider-payment8001模块的POM文件,添加依赖:

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

修改配置文件,在spring节点下添加内容:

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

PaymentController添加方法用于测试:

@GetMapping(value = "/payment/zipkin")
public String paymentZipkin() {
    return "------------------zipkin------------------";
}

修改cloud-consumer-order80模块的POM文件,添加依赖:

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

修改配置文件,在spring节点下添加内容:

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

OrderController添加方法用于测试:

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

依次启动7001、8001、80,80多调几次8001,访问http://localhost:9411/zipkin可以看到可视化界面。