Spring Cloud学习1

学习视频地址:https://www.bilibili.com/video/BV18E411x7eT?p=3

版本选型

Spring Boot版本号为数字,如Spring Boot 2.2.4,Spring Cloud版本号为英文,如Spring Cloud Hoxton
视频里使用的版本:

  • Spring Cloud : Hoxton SR1
  • Spring Boot : 2.2.2.RELEASE
  • Spring Cloud Alibaba : 2.1.0.RELEASE
  • Java : 8
  • Maven : 3.5及以上
  • MySQL : 5.7及以上

各组件的升级/替换图

创建父工程

这里好像不勾Create from archetype也行。。的



字符编码设置

注解生效激活
Java编译版本选择8

父工程的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.atguigu.springcloud</groupId><artifactId>cloud2020</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><!-- 这里添加,注意不是jar或war --><name>Maven</name><!-- FIXME change it to the project's website --><url>http://maven.apache.org/</url><inceptionYear>2001</inceptionYear><distributionManagement><site><id>website</id><url>scp://webhost.company.com/www/website</url></site></distributionManagement><!--统一管理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>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><!--Spring Boot--><dependency><groupId>org.springfranework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.2.2.RELEASE</version><type>pom</type><scope>import</scope></dependency><!--Spring Cloud--><dependency><groupId>org.springfranework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Hoxton.SR1</version><type>pom</type><scope>import</scope></dependency><!--Spring Cloud Alibaba--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2.1.0.RELEASE</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>${druid.version}</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.spring.boot.version}</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>${junit.version}</version></dependency><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>${log4j.version}</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version><optional>true</optional></dependency></dependencies></dependencyManagement><build><pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) --><plugins><plugin><artifactId>maven-clean-plugin</artifactId><version>3.1.0</version></plugin><plugin><artifactId>maven-site-plugin</artifactId><version>3.7.1</version></plugin><plugin><artifactId>maven-project-info-reports-plugin</artifactId><version>3.0.0</version></plugin></plugins></pluginManagement><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-site-plugin</artifactId><configuration><locales>en,fr</locales></configuration></plugin></plugins></build><reporting><plugins><plugin><artifactId>maven-project-info-reports-plugin</artifactId></plugin></plugins></reporting>
</project>

DependencyManagement和Dependencies

Maven使用dependencyManagement元素来提供了一种管理依赖版本号的方式。

通常会在一个组织或者项目的最顶层的父POM中看到dependencyManagement元素。
使用pom.xml中的dependencyManagement元素能让所有在子项目中引用依赖而不用显式的列出版本号。
Maven会沿着父子层次向上走,直到找到一个拥有dependencyManagement元素的项目,然后它就会使用这个dependencyManagement元素中指定的版本号。

这样做的好处就是:如果有多个子项目都引用同一样依赖,则可以避免在每个使用的子项目里都声明一个版本号,这样当想升级或切换到另一个版本时,只需要在顶层父容器里更新,而不需要一个一个子项目的修改;另外如果某个子项目需要另外的一个版本,只需要声明version就可。

  • dependencyManagement里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖。
  • 如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom。
  • 如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。

构建支付模块

创建微服务模块套路:

  • 建Module
  • 改POM
  • 写YML
  • 主启动
  • 业务类

    order订单模块能够调用payment支付模块

1.建Module



2.修改模块的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.atguigu.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cloud-provider-payment8001</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><!--别删,因为父工程里并没有指定这个的版本号--><version>1.1.10</version></dependency><!--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>

3.在模块里写YML

server:port: 8001spring:application:name: cloud-payment-servicedatasource:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/db2019?useUnicode=true&characterEncoding=utf-8&useSSL=falseusername: rootpassword: rootmybatis:mapper-locations: classpath:mapper/*.xmltype-aliases-package: com.atguigu.springcloud.entity

4.在模块里写启动类

package com.atguigu.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/**
* @author tony
* @date 2021/8/29 17:54
*/
@SpringBootApplication
public class PaymentMain8001 {public static void main(String[] args) {SpringApplication.run(PaymentMain8001.class,args);}
}

5.业务类
Java后端一般要写的东西:
数据库—实体类—dao—service—controller
数据库

CREATE DATABASE db2019
CREATE TABLE `payment`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`serial` VARCHAR(200) DEFAULT '',
PRIMARY KEY(`id`)
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

创建entities文件夹用来放实体类
实体类Payment

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

统一返回结果实体类CommonResult

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> implements Serializable {private Integer code;private String message;private T data;public CommonResult(Integer code,String message){this(code,message,null);}
}

创建dao文件夹用来放dao相关的接口文件
PaymentDao

@Mapper
public interface PaymentDao {int create(Payment payment);Payment getPaymentById(@Param("id") Long id);
}

resources文件夹下创建mapper文件夹,用来放xxxMapper.xml文件
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"><resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment"><id column="id" property="id" jdbcType="BIGINT"/><result column="serial" property="serial" jdbcType="VARCHAR"/></resultMap><insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">insert into payment(serial) values(#{serial})</insert><select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap">select * from payment where id = #{id}</select>
</mapper>

创建service文件夹用来存放service层文件
PaymentService

public interface PaymentService {int create(Payment payment);Payment getPaymentById(Long id);
}

service文件夹里创建impl文件夹存放service的实现类
PaymentServiceImpl

@Service
public class PaymentServiceImpl implements PaymentService {@Autowiredprivate PaymentDao paymentDao;@Overridepublic int create(Payment payment) {return paymentDao.create(payment);}@Overridepublic Payment getPaymentById(Long id) {return paymentDao.getPaymentById(id);}
}

创建controller文件夹
PaymentController

@RestController
@Slf4j
public class PaymentController {@AutowiredPaymentService paymentService;@PostMapping("/payment/create")public CommonResult create(@RequestBody Payment payment){int result = paymentService.create(payment);log.info("插入结果:"+result);if (result >0 ){return new CommonResult(200,"插入数据库成功",result);}else {return new CommonResult(444,"插入数据库失败");}}@GetMapping("/payment/get/{id}")public CommonResult getPaymentById(@PathVariable("id") Long id){Payment payment = paymentService.getPaymentById(id);log.info("查询结果:"+payment);if (payment != null){return new CommonResult(200,"查询数据库成功",payment);}else {return new CommonResult(444,"查询数据库失败,id为:"+id);}}
}

构建订单模块

创建模块,名字cloud-consumer-order80
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>cloud2020</artifactId><groupId>com.atguigu.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cloud-consumer-order80</artifactId><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></project>

创建application.yml

server:port: 80

启动类OrderMain80

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

把上面cloud-provider-payment8001模块里的整个entities文件夹(里面有两个类Payment类和CommonResult类)都复制过来。
创建config文件夹,创建ApplicationContextConfig配置类

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

创建controller文件夹,创建OrderController类

@RestController
@Slf4j
public class OrderController {public static final String PAYMENT_URL = "http://localhost:8001";@Autowiredprivate RestTemplate restTemplate;@GetMapping("/consumer/payment/create")public CommonResult<Payment> create(Payment payment){return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);}@GetMapping("/consumer/payment/get/{id}")public CommonResult<Payment> getPayment(@PathVariable("id") Long id){return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);}
}

关于RestTemplate
RestTemplate提供了多种便捷访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
使用:
使用restTemplate访问restful接口非常的简单粗暴无脑。
(url, requestMap, ResponseBean.class)这三个参数分别代表REST请求地址、请求参数、HTTP响应转换被转换成的对象类型。
启动cloud-provider-payment8001模块和cloud-consumer-order80模块,浏览器访问http://localhost/consumer/payment/get/1
看看能不能查到数据
访问http://localhost/consumer/payment/create?serial=6868
看看能不能插入数据

工程重构

上面已经创建了两个模块,cloud-provider-payment8001模块和cloud-consumer-order80模块,两个模块中的entities包里的代码都是一样的,应该抽取出来重复利用而不是没创建1个模块就复制1份代码。
1.创建模块,名字cloud-api-commons
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>cloud2020</artifactId><groupId>com.atguigu.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cloud-api-commons</artifactId><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.1.0</version></dependency></dependencies>
</project>

2.将cloud-consumer-order80与cloud-provider-payment8001两个模块都有的entities包复制到cloud-api-commons工程下。
3.在cloud-api-commons模块执行maven的clean、install操作
4.将cloud-consumer-order80与cloud-provider-payment8001两个模块的entities包删除,两个模块都在各自的pom.xml里引入自定义的cloud-api-commons包

<dependency><groupId>com.atguigu.springcloud</groupId><artifactId>cloud-api-commons</artifactId><version>${project.version}</version>
</dependency>

5.再重新启动项目测试看看能不能查询和插入数据

Eureka服务注册中心

什么是服务治理
Spring Cloud封装了Netflix 公司开发的Eureka模块来实现服务治理
在传统的RPC远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。

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

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

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

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

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

Eureka服务端安装

1.创建模块cloud-eureka-server7001
2.修改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.atguigu.springcloud</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cloud-eureka-server7001</artifactId><dependencies><!--eureka-server--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency><!-- 引入自己定义的api通用包 --><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></project>

3.创建application.yml

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

4.创建启动类

package com.atguigu.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;/**
* @author tony
* @date 2021/8/30 18:10
*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7001 {public static void main(String[] args) {SpringApplication.run(EurekaMain7001.class,args);}
}

启动这个模块,浏览器访问http://localhost:7001/

支付模块入驻Eureka Server

选择cloud-provider-payment8001
1.添加依赖

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

2.application.yml添加配置

eureka:client:#是否将字节注册进Eureka Server,默认trueregister-with-eureka: true#是否从Eureka Server抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetch-registry: trueservice-url:defaultZone: http://localhost:7001/eureka

3.修改启动类

package com.atguigu.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;/**
* @author tony
* @date 2021/8/29 17:54
*/
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain8001 {public static void main(String[] args) {SpringApplication.run(PaymentMain8001.class,args);}
}

启动看看这个服务有没有注册到注册中心

成功
注意:

spring.application.name的值对应Eureka Server里的内容

订单模块入驻Eureka Server

选择cloud-consumer-order80模块
1.添加依赖

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

2.修改application.yml

server:port: 80spring:application:name: cloud-order-serviceeureka:client:register-with-eureka: truefetch-registry: trueservice-url:defaultZone: http://localhost:7001/eureka

3.修改启动类

package com.atguigu.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;/*** @author tony* @date 2021/8/30 16:03*/
@SpringBootApplication
@EnableEurekaClient
public class OrderMain80 {public static void main(String[] args) {SpringApplication.run(OrderMain80.class,args);}
}

启动这个模块,看看是否成功

Eureka集群说明


图中问题的解决方法:搭建Eureka注册中心集群,实现负载均衡+故障容错。“互相注册,相互守望”

Eureka集群环境构建


创建cloud-eureka-server7002模块,pom引入的依赖与cloud-eureka-server7001模块的一样。
找到C:\Windows\System32\drivers\etc路径下的hosts文件,修改映射配置添加进hosts文件

127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com

修改cloud-eureka-server7001模块的application.yml:

server:port: 7001eureka:instance:#eureka服务端的实例名称hostname: eureka7001.comclient:#false表示不想注册中心注册自己register-with-eureka: false#false表示自己就是注册中心,我的任务是维护服务实例,不需要去检索服务fetch-registry: falseservice-url:#设置与Eureka Server交互的地址,查询服务和注册服务都需要这个地址defaultZone: http://eureka7002.com:7002/eureka/

在cloud-eureka-server7002模块里创建application.yml文件:

server:port: 7002eureka:instance:#eureka服务端的实例名称hostname: eureka7002.comclient:#false表示不想注册中心注册自己register-with-eureka: false#false表示自己就是注册中心,我的任务是维护服务实例,不需要去检索服务fetch-registry: falseservice-url:#设置与Eureka Server交互的地址,查询服务和注册服务都需要这个地址defaultZone: http://eureka7001.com:7001/eureka/

在cloud-eureka-server7002模块里创建启动类

package com.atguigu.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;/*** @author tony* @date 2021/8/31 12:00*/
@SpringBootApplication
@EnableEurekaServer
public class EurekaMain7002 {public static void main(String[] args) {SpringApplication.run(EurekaMain7002.class,args);}
}

启动cloud-eureka-server7001模块和cloud-eureka-server7002模块
浏览器访问


我第一次浏览器访问时是没有红字的,刷新了一下就有了,先不理,只看红线的地方。

订单微服务、支付微服务注册进Eureka集群

修改cloud-consumer-order80和cloud-provider-payment8001的application.yml文件:

eureka:client:register-with-eureka: truefetch-registry: trueservice-url:#defaultZone: http://localhost:7001/eurekadefaultZone: http://eureka7002.com:7002/eureka,http://eureka7001.com:7001/eureka

测试,先启动Eureka集群,再启动微服务,浏览器访问

支付微服务集群

参考cloud-provicer-payment8001,新建cloud-provider-payment8002
1.复制pom添加依赖
2.复制application.yml,记得修改端口号为8002
3.复制启动类,修改名字
4.复制业务类
cloud-provicer-payment8001和cloud-provider-payment8002的PaymentController都要添加一个变量,值为application.yml里的端口号

@RestController
@Slf4j
public class PaymentController {@AutowiredPaymentService paymentService;//新增变量@Value("${server.port}")private String serverPort;@PostMapping("/payment/create")public CommonResult create(@RequestBody Payment payment){int result = paymentService.create(payment);log.info("插入结果:"+result);if (result >0 ){//添加打印输出的内容return new CommonResult(200,"插入数据库成功,端口号:"+serverPort,result);}else {return new CommonResult(444,"插入数据库失败");}}@GetMapping("/payment/get/{id}")public CommonResult getPaymentById(@PathVariable("id") Long id){Payment payment = paymentService.getPaymentById(id);log.info("查询结果:"+payment);if (payment != null){//添加打印输出的内容return new CommonResult(200,"查询数据库成功,端口号:"+serverPort,payment);}else {return new CommonResult(444,"查询数据库失败,id为:"+id);}}
}

cloud-consumer-order80订单服务访问地址不能写死

public class OrderController {//public static final String PAYMENT_URL = "http://localhost:8001";public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";...
}

在cloud-consumer-order80模块里ApplicationContextConfig配置类里使用@LoadBalanced注解赋予RestTemplate负载均衡的能力

@Configuration
public class ApplicationContextConfig {@Bean@LoadBalanced//赋予RestTemplate负载均衡的能力public RestTemplate getRestTemplate(){return new RestTemplate();}
}

测试,先启动Eureka集群,再启动Pyament微服务集群,再启动Order微服务


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


多次访问端口号会有不同,说明负载均衡起作用了。

actuator微服务信息完善

之前微服务status栏显示的内容是这样的:

为了信息更完善一些,可以做以下改动:
在cloud-provider-payment8001和cloud-provider-payment8001的application.yml文件里添加内容:

eureka:...instance:instance-id: payment8002prefer-ip-address: true

再重新启动微服务

status栏的信息就会变这样,当鼠标指着payment8002时右下角还会显示ip地址

服务发现Discovery

对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息
修改cloud-provider-payment8001的Controller

import org.springframework.cloud.client.discovery.DiscoveryClient;
...
...@Autowiredprivate DiscoveryClient discoveryClient;
...@GetMapping(value = "/payment/discovery")public Object discovery(){List<String> services = discoveryClient.getServices();for (String element : services) {log.info("*****element: "+element);}List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");for (ServiceInstance instance : instances) {log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());}return this.discoveryClient;}

修改cloud-provider-payment8001的启动类,添加@EnableDiscoveryClient

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

先要启动Eureka Server

再启动cloud-provider-payment8001

浏览器访问http://localhost:8001/payment/discovery
页面显示这个东西

{"discoveryClients":[{"services":["cloud-payment-service"],"order":0},{"services":[],"order":0}],"services":["cloud-payment-service"],"order":0}

idea控制台打印:

2021-08-31 16:40:40.384  INFO 13744 --- [nio-8001-exec-7] c.a.s.controller.PaymentController       : *****element: cloud-payment-service
2021-08-31 16:40:40.385  INFO 13744 --- [nio-8001-exec-7] c.a.s.controller.PaymentController       : CLOUD-PAYMENT-SERVICE  192.168.241.1   8001    http://192.168.241.1:8001

Eureka自我保护

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

如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THANTHRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUSTTO BE SAFE

导致原因
一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存。

属于CAP里面的AP分支。

为什么会产生Eureka自我保护机制?
为了EurekaClient可以正常运行,防止与EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除

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

自我保护机制∶默认情况下EurekaClient定时向EurekaServer端发送心跳包

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

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

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

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

关闭自我保护

在cloud-eureka-server7001的application.yml文件中修改:

...
eureka:instance:#eureka服务端的实例名称hostname: eureka7001.comclient:#false表示不想注册中心注册自己register-with-eureka: false#false表示自己就是注册中心,我的任务是维护服务实例,不需要去检索服务fetch-registry: falseservice-url:#设置与Eureka Server交互的地址,查询服务和注册服务都需要这个地址#集群版#defaultZone: http://eureka7002.com:7002/eureka#单机版就是自己defaultZone: http://eureka7001.com:7001/eurekaserver:#false表示禁用自我保护模式,爆炸不可用服务被几时剔除enable-self-preservation: false#清理无效节点的时间间隔eviction-interval-timer-in-ms: 2000

修改cloud-provider-payment8001的application.yml文件:

...
eureka:client:#是否将字节注册进Eureka Server,默认trueregister-with-eureka: true#是否从Eureka Server抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡fetch-registry: trueservice-url:#单机版defaultZone: http://localhost:7001/eureka#集群版#defaultZone: http://eureka7002.com:7002/eureka,http://eureka7001.com:7001/eurekainstance:instance-id: payment8001prefer-ip-address: true#Eureka客户端向服务端发送心跳的时间间隔,单位秒lease-renewal-interval-in-seconds: 1#Eureka服务端收到最后一次心跳后等待时间上限,单位秒,超时将剔除服务lease-expiration-duration-in-seconds: 2

先启动cloud-eureka-server7001,再启动cloud-provider-payment8001

红字提示自我保护已关闭。停掉cloud-provider-payment8001,刷新浏览器可以看到cloud-provider-payment8001很快就没了。但是对于yml里关于时间的那几个属性的逻辑实际演示不太一样:)

根据官方消息Eureka2.0已停止更新。。。

ZooKeeper服务注册中心

支付服务注册进ZooKeeper

创建cloud-provider-payment8004模块
pom.xml添加依赖

<dependencies><!-- SpringBoot整合Web组件 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.atguigu.springcloud</groupId><artifactId>cloud-api-commons</artifactId><version>${project.version}</version></dependency><!-- SpringBoot整合zookeeper客户端 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-zookeeper-discovery</artifactId><!--先排除自带的zookeeper3.5.3 防止与3.5.7起冲突--><exclusions><exclusion><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId></exclusion></exclusions></dependency><!--添加zookeeper3.5.7版本,与虚拟机里的zookeeper版本保持一致--><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.5.7</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>

创建application.yml

server:port: 8004#服务别名,注册到zookeeper注册中心的名称
spring:application:name: cloud-provider-paymentcloud:zookeeper:#zookeeper中1个服务器的ip和端口,有多个的话就用,逗号分隔connect-string: 192.168.88.129:2181

启动类

package com.atguigu.springcloud;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;/*** @author tony* @date 2021/9/6 14:43*/
@SpringBootApplication
@EnableDiscoveryClient//用于向consul或zookeeper注册中心注册服务
public class PaymentMain8004 {public static void main(String[] args) {SpringApplication.run(PaymentMain8004.class,args);}
}

controller

@RestController
public class PaymentController {@RequestMapping("/payment/zk")public String paymentzk(){return UUID.randomUUID().toString();}
}

zookeeper之前已经配好,可以参考我博客里的文章zookeeper学习1
启动zookeeper,启动PaymentMain8004

控制台些红字内容应该是日志类有点问题,可能是spring-cloud-starter-zookeeper-discovery依赖里自带了slf4j依赖,想彻底解决红字可以参照上面pom.xml里面的exclusion排除依赖,这里不影响演示就不理了。
在虚拟机那边开启一个zookeeper客户端查看zookeeper服务端里的节点

[zk: localhost:2181(CONNECTED) 9] ls /
[services, zookeeper]

发现/节点下多了个services节点,进入services节点

[zk: localhost:2181(CONNECTED) 10] ls /services
[cloud-provider-payment]

发现了java那边的cloud-provider-payment微服务名称,再进入cloud-provider-payment这个节点看看

[zk: localhost:2181(CONNECTED) 11] ls /services/cloud-provider-payment
[d3551e8a-8982-4cf1-b03e-32669c6785b0]

获取这一串字符的节点的值

[zk: localhost:2181(CONNECTED) 12] get /services/cloud-provider-payment/d3551e8a-8982-4cf1-b03e-32669c6785b0
{"name":"cloud-provider-payment","id":"d3551e8a-8982-4cf1-b03e-32669c6785b0","address":"localhost","port":8004,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"cloud-provider-payment","metadata":{}},"registrationTimeUTC":1630913313012,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}

看起来这个值是JSON字符串,去百度格式化这个JSON好看点

{"name": "cloud-provider-payment", "id": "d3551e8a-8982-4cf1-b03e-32669c6785b0", "address": "localhost", "port": 8004, "sslPort": null, "payload": {"@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance", "id": "application-1", "name": "cloud-provider-payment", "metadata": { }}, "registrationTimeUTC": 1630913313012, "serviceType": "DYNAMIC", "uriSpec": {"parts": [{"value": "scheme", "variable": true}, {"value": "://", "variable": false}, {"value": "address", "variable": true}, {"value": ":", "variable": false}, {"value": "port", "variable": true}]}
}

zookeeper作为注册中心,把服务注册到zookeeper就是在zookeeper里创建1个临时节点,现在在idea里把cloud-provider-payment服务停止,虚拟机那边的客户端过一会就会查询不到这个节点了;在idea里把cloud-provider-payment服务启动,虚拟机那边的客户端过一会就又会查询到这个节点了。

订单服务注册进zookeeper

创建cloud-consumerzk-order80模块,pom.xml添加依赖和cloud-provider-payment模块一样
创建application.yml

server:port: 80#服务别名,注册到zookeeper注册中心的名称
spring:application:name: cloud-consumer-ordercloud:zookeeper:#zookeeper中1个服务器的ip和端口,多个用逗号隔开connect-string: 192.168.88.129:2181

启动类

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

配置类

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

controller

@RestController
public class OrderZKController {public static final String INVOKE_URL = "http://cloud-provider-payment";@Autowiredprivate RestTemplate restTemplate;@GetMapping("/consumer/payment/zk")public String paymentInfo(){String result = restTemplate.getForObject(INVOKE_URL+"/payment/zk",String.class);return result;}
}

启动这个模块和cloud-provider-payment8004模块
虚拟机那边的客户端:

[zk: localhost:2181(CONNECTED) 13] ls /services
[cloud-consumer-order, cloud-provider-payment]

可以看到2个服务已经注册进来
浏览器访问http://localhost/consumer/payment/zk

可以看到成功调用。

Consul作为注册中心(待补充)

三个注册中心异同点

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

CAP:

  • C:Consistency (强一致性)
  • A:Availability (可用性)
  • P:Partition tolerance (分区容错性)
    最多只能同时较好的满足两个。

CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求。
因此,根据CAP原理将NoSQL数据库分成了满足CA原则、满足CP原则和满足AP原则三大类:
CA - 单点集群,满足—致性,可用性的系统,通常在可扩展性上不太强大。
CP - 满足一致性,分区容忍性的系统,通常性能不是特别高。
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些。

AP架构(Eureka)

当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。违背了一致性C的要求,只满足可用性和分区容错,即AP

CP架构(ZooKeeper/Consul)
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性。违背了可用性A的要求,只满足一致性和分区容错,即CP。

Ribbon服务调用

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本地,从而在本地实现RPC远程服务调用技术。

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

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

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

一句话,负载均衡 + RestTemplate调用。
再找了一些Ribbon和Nginx的区别:

  • nginx的负载均衡是针对服务器的,但是ribbon负载均衡是针对微服务的

Ribbon的负载均衡和Rest调用


Ribbon在工作时分成两步:

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

之前模块里没有引入spring-cloud-starter-ribbon也可以使用ribbon是因为spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon依赖。

自己再次添加ribbon依赖也可以

<dependency><groupld>org.springframework.cloud</groupld><artifactld>spring-cloud-starter-netflix-ribbon</artifactid>
</dependency>

RestTemplate的使用

常用方法:
getForObject() / getForEntity()
postForObject() / postForEntity()
get读,post写

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

@GetMapping("/consumer/payment/getEntity/{id}")public ResponseEntity<CommonResult> getPayment2(@PathVariable("id") Long id){ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);return entity;}

Ribbon默认自带的负载规则

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

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

Ribbon负载规则替换

官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,
否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。也就是说不要将Ribbon配置类与主启动类同包

修改cloud-consumer-order80
新建MySelfRule规则类,注意文件存放位置:

package com.atguigu.myrule;import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** @author tony* @date 2021/9/6 20:54*/
@Configuration
public class MySelfRule {@Beanpublic IRule myRule(){return new RandomRule();//使用随机规则}
}

修改启动类

@SpringBootApplication
@EnableEurekaClient
//想要访问的是CLOUD-PAYMENT-SERVICE服务,规则用自定义的MySelfRule
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {public static void main(String[] args) {SpringApplication.run(OrderMain80.class,args);}
}

启动这些服务

浏览器访问http://localhost/consumer/payment/get/1,刷新几次端口号并不会轮流变化,是随机的

Ribbon默认负载轮询算法原理

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

List instances = discoveryClient.getInstances(“CLOUD-PAYMENT-SERVICE”);
如:
List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
8001+ 8002组合成为集群,它们共计2台机器,集群总数为2,按照轮询算法原理:
当总请求数为1时:1%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数位2时:2%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002
当总请求数位3时:3%2=1对应下标位置为1,则获得服务地址为127.0.0.1:8001
当总请求数位4时:4%2=О对应下标位置为0,则获得服务地址为127.0.0.1:8002
如此类推…

OpenFeign服务调用

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

Feign集成了Ribbon

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

Feign和OpenFeign两者区别

  • Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。

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

使用案例

创建cloud-consumer-feign-order80模块
pom添加依赖

<dependencies><!--openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--eureka client--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- 引入自己定义的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.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: 80eureka:client:register-with-eureka: falseservice-url:defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

创建启动类

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

创建PaymentFeignService接口

package com.atguigu.springcloud.service;import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;/*** @author tony* @date 2021/9/7 11:20*/
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")   //表示它寻找的是CLOUD-PAYMENT-SERVICE微服务
public interface PaymentFeignService {//对应cloud-provider-payment8001模块里的controller方法@GetMapping(value = "/payment/get/{id}")CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}

创建controller

package com.atguigu.springcloud.controller;import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentFeignService;
import org.springframework.beans.factory.annotation.Autowired;
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.RestController;/*** @author tony* @date 2021/9/7 11:24*/
@RestController
public class OrderFeignController
{@Autowiredprivate PaymentFeignService paymentFeignService;@GetMapping(value = "/consumer/payment/get/{id}")public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id){return paymentFeignService.getPaymentById(id);}
}

启动服务,先启动Eureka集群,再启动其他的

浏览器访问http://localhost/consumer/payment/get/1

刷新几次会发现端口号1,2,1交替变换,说明自带负载均衡

OpenFeign超时控制

在微服务中,服务A调用服务B,可能因为网络等各种原因服务B需要较长时间才能返回结果给服务A(比如5秒),但是服务A默认只等1秒,1秒后没结果就报错,那么就可以同配置服务A的等待时间长一点。
OpenFeign默认等待1秒钟,超过后报错
修改配置OpenFeign客户端的yml:

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

OpenFeign日志增强

日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日恙级别,从而了解Feign 中 Http请求的细节。
就是对Feign接口的调用情况进行监控和输出

日志级别:

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

创建一个日志配置类

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

YML文件里指定需要开启日志的Feign接口

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

Hystrix服务降级

概述

分布式系统面临的问题

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

服务雪崩

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

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

Hystrix是什么

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

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

英文hystrix : n. 豪猪属;猬草属;豪猪;豪猪亚属

能干嘛

  • 服务降级
  • 服务熔断
  • 接近实时的监控

根据官网Hystrix已经停止更新,进入维护阶段

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

哪些情况会出发降级

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

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

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

基础演示环境

创建cloud-provider-hystrix-payment8001模块
添加依赖

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

创建application.yml

server:port: 8001spring:application:name: cloud-provider-hystrix-paymenteureka:client:register-with-eureka: truefetch-registry: trueservice-url:#defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eurekadefaultZone: http://eureka7001.com:7001/eureka

启动类

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

业务类

package com.atguigu.springcloud.service;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;/*** @author tony* @date 2021/9/7 15:24*/
@Service
public class PaymentService {/***/public String paymentInfo_OK(Integer id){return "线程池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"O(∩_∩)O哈哈~";}public String paymentInfo_TimeOut(Integer id){//假设处理业务需要3秒钟int TimeNumber = 3;try { TimeUnit.SECONDS.sleep(TimeNumber); } catch (InterruptedException e) { e.printStackTrace(); }return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(秒): "+TimeNumber;}
}

controller

@RestController
@Slf4j
public class PaymentController {@Autowiredprivate PaymentService paymentService;@Value("${server.port}")private String serverPort;@GetMapping("/payment/hystrix/ok/{id}")public String paymentInfo_OK(@PathVariable("id") Integer id){String result = paymentService.paymentInfo_OK(id);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,启动cloud-provider-hystrix-payment8001
浏览器访问http://localhost:8001/payment/hystrix/timeout/1http://localhost:8001/payment/hystrix/ok/1正确显示内容即可。

订单微服务调用支付微服务出现卡顿

创建cloud-consumer-feign-hystrix-order80模块
pom

<dependencies><!--openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--hystrix--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId></dependency><!--eureka client--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- 引入自己定义的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.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>

yml

server:port: 80eureka:client:register-with-eureka: falseservice-url:defaultZone: http://eureka7001.com:7001/eureka/

业务类

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

controller

@RestController
public class OrderHystrixController {@Autowiredprivate PaymentHystrixService paymentHystrixService;@GetMapping("/consumer/payment/hystrix/ok/{id}")public String paymentInfo_OK(@PathVariable("id") Integer id)  {String result = paymentHystrixService.paymentInfo_OK(id);return result;}@GetMapping("/consumer/payment/hystrix/timeout/{id}")public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {String result = paymentHystrixService.paymentInfo_TimeOut(id);return result;}
}

测试
浏览器访问http://localhost/consumer/payment/hystrix/ok/1
无报错能显示内容即可。
在这里80微服务调用8001微服务,响应很快,但如果这时有大量请求调用8001微服务,这时80微服务再次调用8001微服务,响应就会有些慢了。

解决:

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

服务降级支付侧fallback

降级配置 - @HystrixCommand

8001先从自身找问题
设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处埋,作服务降级fallback。
修改cloud-provider-hystrix-payment8001的PaymentService类:

@Service
public class PaymentService {/***/public String paymentInfo_OK(Integer id){return "线程池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"O(∩_∩)O哈哈~";}//fallbackMethod属性指定兜底的方法名@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="3000")/*这里指定超过3秒就执行兜底的方法*/})public String paymentInfo_TimeOut(Integer id){//假设处理业务需要的时间int TimeNumber = 5;//int age = 10/0try { TimeUnit.SECONDS.sleep(TimeNumber); } catch (InterruptedException e) { e.printStackTrace(); }return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(秒): "+TimeNumber;}//用来兜底的方法public String paymentInfo_TimeOutHandler(Integer id){return "线程池:  "+Thread.currentThread().getName()+"  8001系统繁忙或者运行报错,请稍后再试,id:  "+id+"\t"+"o(╥﹏╥)o";}
}

上面故意制造两种异常:

int age = 10/0,计算异常
我们能接受3秒钟,它运行5秒钟,超时异常。
当前服务不可用了,做服务降级,兜底的方案都是paymentInfo_TimeOutHandler
在启动类上添加注解

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

浏览器访问http://localhost:8001/payment/hystrix/timeout/1
显示线程池: HystrixTimer-1 8001系统繁忙或者运行报错,请稍后再试,id: 1 o(╥﹏╥)o这样的内容即可。

服务降级订单侧fallback

80订单微服务,也可以更好的保护自己,自己也依样画葫芦进行客户端降级保护
修改cloud-consumer-feign-hystrix-order80模块
yml

server:port: 80eureka:client:register-with-eureka: falseservice-url:defaultZone: http://eureka7001.com:7001/eureka/feign:hystrix:enabled: trueribbon:#指的是建立连接后从服务器读取到可用资源所用的时间ReadTimeout: 10000hystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 5000

修改controller

@RestController
public class OrderHystrixController {@Autowiredprivate PaymentHystrixService paymentHystrixService;@GetMapping("/consumer/payment/hystrix/ok/{id}")public String paymentInfo_OK(@PathVariable("id") Integer id)  {String result = paymentHystrixService.paymentInfo_OK(id);return result;}@GetMapping("/consumer/payment/hystrix/timeout/{id}")@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="4000")})public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {String result = paymentHystrixService.paymentInfo_TimeOut(id);return result;}//兜底方法public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){return "我是消费者80,对方支付系统繁忙请稍后再试或者自己运行出错请检查自己,o(╥﹏╥)o";}
}

这里设置了如果paymentInfo_TimeOut()方法处理超过4秒就进入到自己的兜底方法paymentTimeOutFallbackMethod()。
修改启动类

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

cloud-provider-hystrix-payment8001那边我也修改了一下:
PaymentService

@Service
public class PaymentService {public String paymentInfo_OK(Integer id){return "线程池:  "+Thread.currentThread().getName()+"  paymentInfo_OK,id:  "+id+"\t"+"O(∩_∩)O哈哈~";}//fallbackMethod属性指定兜底的方法名@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="8000")/*这里指定超过8秒就执行兜底的方法*/})public String paymentInfo_TimeOut(Integer id){//假设处理业务需要的时间int TimeNumber = 3000;//int age = 10/0;try { TimeUnit.MILLISECONDS.sleep(TimeNumber); } catch (InterruptedException e) { e.printStackTrace(); }return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(毫秒): "+TimeNumber;}//用来兜底的方法public String paymentInfo_TimeOutHandler(Integer id){return "线程池:  "+Thread.currentThread().getName()+"  8001系统繁忙或者运行报错,请稍后再试,id:  "+id+"\t"+"o(╥﹏╥)o";}
}

这里我设置了paymentInfo_TimeOut()执行时间是3秒(就sleep了3秒),服务降级的限制是8秒,也就是说在支付服务8001这边是肯定不会进入自己的兜底方法paymentInfo_TimeOutHandler()的
启动测试

没问题,把cloud-provider-hystrix-payment8001里PaymentService里的sleep时间设置为4.5秒

public String paymentInfo_TimeOut(Integer id){//假设处理业务需要的时间int TimeNumber = 4500;//int age = 10/0;try { TimeUnit.MILLISECONDS.sleep(TimeNumber); } catch (InterruptedException e) { e.printStackTrace(); }return "线程池:  "+Thread.currentThread().getName()+" id:  "+id+"\t"+"O(∩_∩)O哈哈~"+"  耗时(秒): "+TimeNumber;}


这里因为运行时间4.5秒,已经超过了订单侧的4秒钟限制了,所以进入到订单侧的兜底方法。

下面是yml里面关于时间配置的解释(复制了B站下方的评论)
controller中超时时间配置不生效原因:
关键在于feign:hystrix:enabled: true的作用,官网解释“Feign将使用断路器包装所有方法”,也就是将@FeignClient标记的那个service接口下所有的方法进行了hystrix包装(类似于在这些方法上加了一个@HystrixCommand),这些方法会应用一个默认的超时时间为1s,所以你的service方法也有一个1s的超时时间,service 1s就会报异常,controller立马进入备用方法,controller上那个3秒那超时时间就没有效果了。
改变这个默认超时时间方法:

hystrix:command:default:execution:isolation:thread:timeoutInMilliseconds: 3000#然后ribbon的超时时间也需加上
ribbon:ReadTimeout: 5000ConnectTimeout: 5000

全局服务降级DefaultProperties

目前问题:每个业务方法对应一个兜底的方法,代码量膨胀

解决方法

  • 每个方法配置一个服务降级方法,技术上可以,但是不聪明

  • 除了个别重要核心业务有专属兜底方法,其它普通的可以通过@DefaultProperties(defaultFallback = “”)使用统一的兜底方法

package com.atguigu.springcloud.controller;import com.atguigu.springcloud.service.PaymentHystrixService;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.beans.factory.annotation.Autowired;
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.RestController;/*** @author tony* @date 2021/9/7 16:21*/
@RestController
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")//用全局兜底方法
public class OrderHystrixController {@Autowiredprivate PaymentHystrixService paymentHystrixService;@GetMapping("/consumer/payment/hystrix/ok/{id}")public String paymentInfo_OK(@PathVariable("id") Integer id)  {String result = paymentHystrixService.paymentInfo_OK(id);return result;}@GetMapping("/consumer/payment/hystrix/timeout/{id}")/*@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod",commandProperties = {@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="4000")})*/@HystrixCommand//用全局的兜底方法public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {String result = paymentHystrixService.paymentInfo_TimeOut(id);return result;}//兜底方法public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id){return "我是消费者80,对方支付系统繁忙请稍后再试或者自己运行出错请检查自己,o(╥﹏╥)o";}// 下面是全局兜底方法public String payment_Global_FallbackMethod(){return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";}
}

调整好时间参数让他故意超时就能看到效果。

通配服务降级FeignFallback

问题:目前controller层的代码比较混乱,耦合度较高,controller层的方法和兜底方法混在同一文件里比较混乱。
服务降级,客户端去调用服务端,碰上服务端宕机或关闭

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

未来我们要面对的异常

  • 运行
  • 超时
  • 宕机
    修改cloud-consumer-feign-hystrix-order80
    根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理
    PaymentFallbackService类
package com.atguigu.springcloud.service;import org.springframework.stereotype.Component;/*** @author tony* @date 2021/9/7 21:47*/
@Component
public class PaymentFallbackService implements PaymentHystrixService {/** PaymentHystrixService里的方法发生异常时,这里就是那些方法对应的兜底方法* */@Overridepublic String paymentInfo_OK(Integer id){return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";}@Overridepublic String paymentInfo_TimeOut(Integer id){return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";}
}

修改接口

@Component
//fallback指定了有兜底方法的实现类是谁
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackService.class)
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);
}

yml

server:port: 80eureka:client:register-with-eureka: falseservice-url:defaultZone: http://eureka7001.com:7001/eureka/feign:hystrix:enabled: true #在Feign中开启Hystrix

测试
单个eureka7001先启动
PaymentHystrixMain8001启动
OrderHystrixMain80启动
正常访问测试 - http://localhost/consumer/payment/hystrix/ok/1

故意关闭微服务8001
此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器。

服务熔断

熔断机制概述

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

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

视频弹幕里关于熔断的解释
1.调用失败会触发降级,而降级会调用fallback方法
2.但无论如何,降级的流程一定会先调用正常方法再调用fallback方法
3假如单位时间内调用失败次数过多,也就是降级次数过多,则触发熔断
4熔断以后就会跳过正常方法直接调用fallback方法
5所谓“熔断后服务不可用”就是因为跳过了正常方法直接执行fallback

断路器,相当于保险丝。

修改cloud-provider-hystrix-payment8001

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;import java.util.concurrent.TimeUnit;@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"),// 失败率达到多少后跳闸})public String paymentCircuitBreaker(@PathVariable("id") Integer id) {if(id < 0) {throw new RuntimeException("******id 不能负数");}String serialNumber = IdUtil.simpleUUID();return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;}public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " +id;}}

关于@HystrixProperty里面的几个参数解释:
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, as defined above exceeds the error percentage defined in : 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 request is let through. If it fails, the command stays OPEN for the sleep window. If it succeeds, it transitions to CLOSED and the logic in 1. takes over again.

修改controller

@RestController
@Slf4j
public class PaymentController
{@Autowiredprivate PaymentService paymentService;...//====服务熔断@GetMapping("/payment/circuit/{id}")public String paymentCircuitBreaker(@PathVariable("id") Integer id){String result = paymentService.paymentCircuitBreaker(id);log.info("****result: "+result);return result;}
}

测试

自测cloud-provider-hystrix-payment8001

正确 - http://localhost:8001/payment/circuit/1

错误 - http://localhost:8001/payment/circuit/-1

1.访问错误地址5次,再访问正确地址1次,马上就能显示成功和流水号。
2.访问错误地址10次,再访问正确地址1次,显示稍后再试。

现在我们再访问多次访问正确地址,就又可以正常访问了。这里大概可以体会到@HystrixProperty里设置的几个参数的含义是什么。再次提示,官方明确指出,窗口期指的是open后的时间

服务熔断总结


熔断类型

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

    断路器在什么情况下开始起作用
//=====服务熔断
@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(@PathVariable("id") Integer id) {...
}

涉及到断路器的三个重要参数:

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

断路器开启或者关闭的条件
到达以下阀值,断路器将会开启:

  • 当满足一定的阀值的时候(默认10秒内超过20个请求次数)
  • 当失败率达到一定的时候(默认10秒内超过50%的请求失败)

当开启的时候,所有请求都不会进行转发
一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启。

断路器打开之后

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

2:原来的主逻辑要如何恢复呢?

对于这一问题,hystrix也为我们实现了自动恢复功能。

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

所有配置

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

官网工作流程图

图形化Dashboard搭建

概述

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。

Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。

新建cloud-consumer-hystrix-dashboard9001
.POM添加依赖

<dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

yml

server:port: 9001

创建启动类

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

所有Provider微服务提供类(8001/8002/8003)都需要监控依赖配置

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

启动cloud-consumer-hystrix-dashboard9001该微服务后续将监控微服务8001

浏览器输入http://localhost:9001/hystrix

图形化Dashboard监控实战

修改cloud-provider-hystrix-payment8001

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

@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001
{public static void main(String[] args) {SpringApplication.run(PaymentHystrixMain8001.class, args);}/***此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑*ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",*只要在自己的项目里配置上下面的servlet就可以了*否则,Unable to connect to Command Metric Stream 404*/@Beanpublic ServletRegistrationBean getServlet() {HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);registrationBean.setLoadOnStartup(1);registrationBean.addUrlMappings("/hystrix.stream");registrationBean.setName("HystrixMetricsStreamServlet");return registrationBean;}
}

监控测试

启动1个eureka,启动8001,9001

观察监控窗口
打开http://localhost:9001/hystrix,填写监控地址 - http://localhost:8001/hystrix.stream


点击下方按钮进入

测试地址

多次访问http://localhost:8001/payment/circuit/1
多次访问http://localhost:8001/payment/circuit/-1

说明

GateWay网关

介绍

Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和Project Reactor等技术。

Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。

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

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

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

作用

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

微服务架构中网关的位置

GateWay非阻塞异步模型

有Zuull了怎么又出来Gateway?我们为什么选择Gateway?

netflix不太靠谱,zuul2.0一直跳票,迟迟不发布。

一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。
Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的Zuul 2.x,但Spring Cloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
多方面综合考虑Gateway是很理想的网关选择。
SpringCloud Gateway具有如下特性

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

SpringCloud Gateway与Zuul的区别

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

Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Serviet IO处理模型。

Servlet的生命周期?servlet由servlet container进行生命周期管理。

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

上述模式的缺点:

Servlet是一个简单的网络IO模型,当请求进入Servlet container时,Servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(如抽风用Jmeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。

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

Gateway模型

WebFlux是什么?

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

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

Spring WebFlux是Spring 5.0 引入的新的响应式框架,区别于Spring MVC,它不需要依赖Servlet APl,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。

工作流程

三大核心概念

1.Route(路由) - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由;
2.Predicate(断言) - 参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由;
3.Filter(过滤) - 指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

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

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

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

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

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

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

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

创建模块cloud-gateway-gateway9527
pom添加依赖

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

cloud-provider-payment8001看看controller的访问地址

public class PaymentController {@AutowiredPaymentService paymentService;@Value("${server.port}")private String serverPort;......@GetMapping("/payment/get/{id}")public CommonResult getPaymentById(@PathVariable("id") Long id){Payment payment = paymentService.getPaymentById(id);log.info("查询结果:"+payment);if (payment != null){return new CommonResult(200,"查询数据库成功,端口号:"+serverPort,payment);}else {return new CommonResult(444,"查询数据库失败,id为:"+id);}}@GetMapping(value = "/payment/lb")public String getPaymentLB() {return serverPort;//返回服务接口}

这里选/payment/get/{id}和/payment/lb,我们目前不想暴露8001端口,希望在8001外面套一层9527
yml

server:port: 9527spring:application:name: cloud-gateway#############################新增网关配置###########################cloud:gateway:routes:- id: payment_routh #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名uri: http://localhost:8001          #匹配后提供服务的路由地址predicates:- Path=/payment/get/**         # 断言,路径相匹配的进行路由- id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名uri: http://localhost:8001          #匹配后提供服务的路由地址predicates:- Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################eureka:instance:hostname: cloud-gateway-serviceclient: #服务提供者provider注册进eureka服务列表内register-with-eureka: truefetch-registry: trueservice-url:defaultZone: http://eureka7001.com:7001/eureka

测试

启动7001
启动8001-cloud-provider-payment8001
启动9527网关

访问说明
添加网关前 - http://localhost:8001/payment/get/1
添加网关后 - http://localhost:9527/payment/get/1
两者访问成功,返回相同结果

Gateway配置路由的两种方式

在配置文件yml中配置,见上一章节

代码中注入RouteLocator的Bean

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class GateWayConfig
{@Beanpublic RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();routes.route("path_route_atguigu",r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();return routes.build();}
}

测试

浏览器输入http://localhost:9527/guonei,返回http://news.baidu.com/guonei相同的页面。

配置动态路由

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能(不写死一个地址)。
修改9527网关的yml

server:port: 9527spring: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 #匹配后提供服务的路由地址     lb是load balance,负载均衡的意思predicates:- Path=/payment/get/**         # 断言,路径相匹配的进行路由- id: payment_routh2 #payment_route    #路由的ID,没有固定规则但要求唯一,建议配合服务名#uri: http://localhost:8001          #匹配后提供服务的路由地址uri: lb://cloud-payment-service #匹配后提供服务的路由地址     lb是load balance,负载均衡的意思predicates:- Path=/payment/lb/**         # 断言,路径相匹配的进行路由
####################################################################eureka:instance:hostname: cloud-gateway-serviceclient: #服务提供者provider注册进eureka服务列表内register-with-eureka: truefetch-registry: trueservice-url:defaultZone: http://eureka7001.com:7001/eureka

启动cloud-eureka-server7001,cloud-provider-payment8001,cloud-provider-payment8002,cloud-gateway-gateway9527

测试

浏览器输入 - http://localhost:9527/payment/lb

结果

不停刷新页面,8001/8002两个端口切换。

常用的Predicate断言

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

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

Spring Cloud Gateway创建Route 对象时,使用RoutePredicateFactory 创建 Predicate对象,Predicate 对象可以赋值给Route。Spring Cloud Gateway包含许多内置的Route Predicate Factories。
所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。
常用的Route Predicate Factory

The After Route Predicate Factory
The Before Route Predicate Factory
The Between Route Predicate Factory
The Cookie Route Predicate Factory
The Header Route Predicate Factory
The Host Route Predicate Factory
The Method Route Predicate Factory
The Path Route Predicate Factory
The Query Route Predicate Factory
The RemoteAddr Route Predicate Factory
The weight Route Predicate Factory

说白了,上面就是多种匹配规则

GateWay的Filter

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

Spring Cloud Gateway的Filter:

生命周期:

  • pre
  • post

种类(具体看官方文档):

GatewayFilter - 有31种
GlobalFilter - 有10种
常用的GatewayFilter:AddRequestParameter GatewayFilter

自定义全局GlobalFilter:

两个主要接口介绍:GlobalFilter和Ordered
能干什么:

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

GateWay9527项目添加MyLogGateWayFilter类:

@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){log.info("***********come in MyLogGateWayFilter:  "+new Date());String uname = exchange.getRequest().getQueryParams().getFirst("uname");//如果请求url里没有uname参数if(uname == null){log.info("*******用户名为null,非法用户,o(╥﹏╥)o");exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);return exchange.getResponse().setComplete();    //发送响应}return chain.filter(exchange);}@Overridepublic int getOrder(){return 0;   //这个是过滤器的顺序,数字越小优先级越高}
}

测试:

启动:
EurekaMain7001
PaymentMain8001
GateWayMain9527
PaymentMain8002

浏览器输入:

http://localhost:9527/payment/lb 无法访问
http://localhost:9527/payment/lb?uname=abc 正常访问

Config分布式配置中心(待补充)

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

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

SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理.……

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

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

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

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

能干嘛

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

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

Bus服务总线(待补充)

SpringCloud Alibaba

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

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

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

具体组件

  • Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
  • Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
  • Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
  • Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

Nacos简介和下载

为什么叫Nacos,前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service。
是什么

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

Nacos: Dynamic Naming and Configuration Service
Nacos就是注册中心+配置中心的组合 -> Nacos = Eureka+Config+Bus

能干嘛

  • 替代Eureka做服务注册中心
  • 替代Config做服务配置中心

去哪下
https://github.com/alibaba/nacos/releases
各中注册中心比较

服务注册与发现框架 CAP模型 控制台管理 社区活跃度
Eureka AP 支持 低(2.x版本闭源)
Zookeeper CP 不支持
consul CP 支持
Nacos AP 支持

Nacos安装

本地要有Java8+Maven环境
从官网下载Nacos,本次使用的是1.1.4版本
解压安装包,直接运行bin目录下的startup.cmd
命令运行成功后直接访问http://localhost:8848/nacos,默认账号密码都是nacos

服务提供者注册

创建cloudalibaba-provider-payment9001模块
记得在父工程的pom添加spring cloud alibaba的依赖

<dependencyManagement><dependencies><!--spring cloud alibaba 2.1.0.RELEASE--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>2.1.0.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies>
</dependencyManagement>

本模块POM添加依赖

<dependencies><!--SpringCloud ailibaba nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!-- SpringBoot整合Web组件 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--日常通用jar包配置--><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: 9001spring:application:name: nacos-payment-providercloud:nacos:discovery:server-addr: localhost:8848 #配置Nacos地址management:endpoints:web:exposure:include: '*'

创建启动类

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

创建controller

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

测试
启动nacos,启动cloudalibaba-provider-payment9001,访问http://localhost:8848/nacos

为了下一章节演示nacos的负载均衡,参照9001新建9002
新建cloudalibaba-provider-payment9002
9002其它步骤你懂的
或者取巧不想新建重复体力劳动,可以利用IDEA功能,直接拷贝虚拟端口映射。

服务消费者注册

新建模块 cloudalibaba-consumer-nacos-order83
pom添加依赖

<dependencies><!--SpringCloud ailibaba nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><dependency><groupId>com.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.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: 83spring:application:name: nacos-order-consumercloud:nacos:discovery:server-addr: localhost:8848#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:nacos-user-service: http://nacos-payment-provider

启动类

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

配置类

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

coontroller

@RestController
@Slf4j
public class OrderNacosController {@Resourceprivate RestTemplate restTemplate;@Value("${service-url.nacos-user-service}")private String serverURL;@GetMapping(value = "/consumer/payment/nacos/{id}")public String paymentInfo(@PathVariable("id") Long id){return restTemplate.getForObject(serverURL+"/payment/nacos/"+id,String.class);}}

测试
多次访问http://localhost:83/consumer/payment/nacos/1
端口号9001和9002交替变换,负载均衡有效

服务注册中心对比

nacos全景图
对比


Nacos支持AP和CP模式的切换

C是所有节点在同一时间看到的数据是一致的;而A的定义是所有的请求都会收到响应。

何时选择使用何种模式?

  • —般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring cloud和Dubbo服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。

  • 如果需要在服务级别编辑或者存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。

服务配置中心

创建模块cloudalibaba-config-nacos-client3377
pom添加依赖

 <dependencies><!--nacos-config--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--nacos-discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--web + actuator--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--一般基础配置--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

项目初始化时,要保证先从配置中心拉取配置,然后才能保证项目正常启动,springboot的配置文件是有优先顺序的,bootstrap优先级高于application
在resources文件夹下创建bootstrap.yml和application.yml
bootstrap.yml

# nacos配置
server:port: 3377spring:application:name: nacos-config-clientcloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址config:server-addr: localhost:8848 #Nacos作为配置中心地址file-extension: yaml #指定yaml格式的配置# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml# nacos-config-client-test.yaml   ----> config.info

application.yml

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

启动类

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

controller

@RestController
@RefreshScope //支持Nacos的动态刷新功能。
public class ConfigClientController
{@Value("${config.info}")private String configInfo;@GetMapping("/config/info")public String getConfigInfo() {return configInfo;}
}

在Nacos配置中心中添加配置信息

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

说明:之所以需要配置spring.application.name,是因为它是构成Nacos配置管理dataId 字段的一部分。

在 Nacos Spring Cloud中,dataId的完整格式如下:

${prefix}-${spring-profile.active}.${file-extension}

prefix默认为spring.application.name的值,也可以通过配置项spring.cloud.nacos.config.prefix来配置。
spring.profile.active即为当前环境对应的 profile,详情可以参考 Spring Boot文档。注意:当spring.profile.active为空时,对应的连接符 - 也将不存在,datald 的拼接格式变成${prefix}.${file-extension}
file-exetension为配置内容的数据格式,可以通过配置项spring .cloud.nacos.config.file-extension来配置。目前只支持properties和yaml类型。
通过Spring Cloud 原生注解@RefreshScope实现配置自动更新。
最后公式:

${spring.application.name)}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

套用这个公式,对应我们这个模块就是nacos-config-client-dev.yaml

添加配置

测试

运行cloud-config-nacos-client3377的主启动类
浏览器访问http://localhost:3377/config/info

修改下Nacos配置中心中的yaml配置文件的内容,再次访问上面的地址,就会发现配置已经自动刷新。

命名空间分组和DataID三者关系

问题1:

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

  • dev开发环境
  • test测试环境
  • prod生产环境。

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

问题2:

一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…那怎么对这些微服务配置进行管理呢?
Namespace+Group+Data lD三者关系?为什么这么设计?

1是什么

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


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

  • Nacos默认的Namespace是public,Namespace主要用来实现隔离。
    比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的。
  • Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
  • Service就是微服务:一个Service可以包含多个Cluster (集群),Nacos默认Cluster是DEFAULT
  • Cluster是对指定微服务的一个虚拟划分。
    比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ) ,给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能。
  • 最后是Instance,就是微服务的实例。

DataID配置多环境

1.在nacos配置中心新建一个nacos-config-client-test.yaml代表测试环境使用
2.在cloudalibaba-config-nacos-client3377模块的application.yml设置使用测试环境配置:

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

启动cloudalibaba-config-nacos-client3377模块,这样就能加载测试环境的配置
浏览器访问http://localhost:3377/config/info

Group分组方案

在nacos配置中心新建2个配置文件,相同的文件名但是不同组名


修改bootstrap.yml和application.yml

在config下增加一条group的配置即可。可配置为DEV_GROUP或TEST GROUP

Namespace空间方案


这里我创建了一个GZ命名空间,他自动生成了命名空间ID,把这个ID粘贴到模块里bootstrap.yml的namespace配置项:

# nacos配置
server:port: 3377spring:application:name: nacos-config-clientcloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址config:server-addr: localhost:8848 #Nacos作为配置中心地址file-extension: yaml #指定yaml格式的配置group: DEV_GROUP#指定命名空间namespace: 46659e95-1de1-4fcb-a22b-f16ee40e745f

Nacos集群_架构说明

推荐用户把所有服务列表放到一个vip下面,然后挂到一个域名下面

http://ip1:port/openAPI直连ip模式,机器挂则需要修改ip才可以使用。

http://VIP:port/openAPI挂载VIP模式,直连vip即可,下面挂server真实ip,可读性不好。

http://nacos.com:port/openAPI域名+VIP模式,可读性好,而且换ip方便,推荐模式
集群部署架构图

真实情况

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

Nacos支持三种部署模式

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

Windows
cmd startup.cmd或者双击startup.cmd文件

单机模式支持mysql

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

安装数据库,版本要求:5.6.5+
初始化mysq数据库,数据库初始化文件: nacos-mysql.sql
修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。

spring.datasource.platform=mysqldb.num=1
db.url.0=jdbc:mysql://11.162.196.16:3306/nacos_devtest?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=nacos_devtest
db.password=youdontknow

再以单机模式启动nacos,nacos所有写嵌入式数据库的数据都写到了mysql。

Nacos持久化切换配置

找到电脑里安装nacos的位置
nacos\conf录下找到nacos-mysql.sql文件,执行脚本。
nacos\conf目录下找到application.properties,添加以下配置(按需修改对应值)。

spring.datasource.platform=mysqldb.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=root

注意填好自己的数据库登录用户名和密码
启动Nacos,可以看到是个全新的空记录界面。

Linux版本安装

预计需要,1个Nginx+3个nacos注册中心+1个mysql
请确保是在环境中安装使用:
1.64 bit OS Linux/Unix/Mac,推荐使用Linux系统。
2.64 bit JDK 1.8+;下载.配置。
3.Maven 3.2.x+;下载.配置。
4.3个或3个以上Nacos节点才能构成集群。
下载Linux版
https://github.com/alibaba/nacos/releases/tag/1.1.4

nacos-server-1.1.4.tar.gz 解压后安装

Nacos集群配置

1.Linux服务器上mysql数据库配置
SQL脚本在哪里 - 目录nacos/conf/nacos-mysql.sql

在自己Linux机器上的Mysql数据库上运行

2.application.properties配置

添加一下内容

spring.datasource.platform=mysqldb.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=root

3.Linux服务器上nacos的集群配置cluster.conf

梳理出3台nacos集器的不同服务端口号,设置3个端口:3333,444,5555
复制出cluster.conf

添加一下内容

192.168.111.144:3333
192.168.111.144:4444
192.168.111.144:5555

ip要根据实际ip填写

4.编辑Nacos的启动脚本startup.sh,使它能够接受不同的启动端口

平时单机版的启动,都是./startup.sh即可

但是,集群启动,我们希望可以类似其它软件的shell命令,传递不同的端口号启动不同的nacos实例。
如命令: ./startup.sh -p 3333表示启动端口号为3333的nacos服务器实例,和上一步的cluster.conf配置的一致。

修改内容


执行方式 ./startup.sh - p 端口号

5.Nginx的配置,由它作为负载均衡器
修改nginx的配置文件 - nginx.conf

修改内容

按照指定配置文件启动

6.截止到此处,1个Nginx+3个nacos注册中心+1个mysql

测试

启动3个nacos注册中心

  • startup.sh - p 3333
  • startup.sh - p 4444
  • startup.sh - p 5555
  • 查看nacos进程启动数ps -ef | grep nacos | grep -v grep | wc -l

启动nginx

  • ./nginx -c /usr/local/nginx/conf/nginx.conf
  • 查看nginx进程ps - ef| grep nginx

测试通过nginx,访问nacos - http://192.168.111.144:1111/nacos/#/login
新建一个配置测试

新建后,自动在linux服务器的mysql新插入一条记录

让微服务cloudalibaba-provider-payment9002启动注册进nacos集群 - 修改配置文件

server:port: 9002spring:application:name: nacos-payment-providerc1oud:nacos:discovery:#配置Nacos地址#server-addr: Localhost:8848#换成nginx的1111端口,做集群server-addr: 192.168.111.144:1111management:endpoints:web:exposure:inc1ude: '*'

启动微服务cloudalibaba-provider-payment9002

访问nacos,查看注册结果

Sentinel

sentinel 英语读音 [ˈsentɪnl] 美 [ˈsentɪnl] n. 哨兵
Sentinel 是什么?

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

Sentinel 具有以下特征:

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

    —句话解释,之前我们讲解过的Hystrix。

Hystrix与Sentinel比较:

Hystrix
1.需要我们程序员自己手工搭建监控平台
2.没有一套web界面可以给我们进行更加细粒度化得配置流控、速率控制、服务熔断、服务降级

Sentinel
1.单独一个组件,可以独立出来。
2.直接界面化的细粒度统一配置。

约定 > 配置 > 编码
都可以写在代码里面,但是我们本次还是大规模的学习使用配置和注解的方式,尽量少写代码

安装

服务使用中的各种问题:
服务雪崩
服务降级
服务熔断
服务限流

Sentinel 分为两个部分:

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

下载

https://github.com/alibaba/Sentinel/releases/tag/1.7.0
下载到本地sentinel-dashboard-1.7.0.jar

运行命令

前提

  • Java 8 环境
  • 8080端口不能被占用

命令

  • java -jar sentinel-dashboard-1.7.0.jar
  • 访问Sentinel管理界面localhost:8080
    登录账号密码均为sentinel

初始化监控

创建模块cloudalibaba-sentinel-service8401
pom添加依赖

 <dependencies><dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><groupId>com.atguigu.springcloud</groupId><artifactId>cloud-api-commons</artifactId><version>${project.version}</version></dependency><!--SpringCloud ailibaba nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到--><dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId></dependency><!--SpringCloud ailibaba sentinel --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><!--openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!-- SpringBoot整合Web组件+actuator --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--日常通用jar包配置--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>4.6.3</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

application.yml

server:port: 8401spring:application:name: cloudalibaba-sentinel-servicecloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址sentinel:transport:dashboard: localhost:8080 #配置Sentinel dashboard地址#指定应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServerport: 8719management:endpoints:web:exposure:include: '*'

启动类

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

controller

@RestController
@Slf4j
public class FlowLimitController {@GetMapping("/testA")public String testA(){return "------testA";}@GetMapping("/testB")public String testB(){log.info(Thread.currentThread().getName()+"\t"+"...testB");return "------testB";}
}

启动Sentinel8080 - java -jar sentinel-dashboard-1.7.0.jar

启动微服务8401

启动8401微服务后查看sentienl控制台,刚刚启动控制台什么都没有的,因为Sentinel采用的懒加载,要随便先访问一下被监控的微服务8401,浏览器访问http://localhost:8401/testA,多次访问后:

流控规则简介

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

流控-QPS直接失败


表示1秒钟内查询1次就是OK,若超过次数1,就直接->快速失败,报默认错误(在页面上显示Blocked by Sentinel (flow limiting))

思考
直接调用默认报错信息,技术方面OK,但是,是否应该有我们自己的后续处理?类似有个fallback的兜底方法?给用户显示Blocked by Sentinel (flow limiting)用户肯定是看不懂的嘛,应该要有进一步处理

流控-线程数直接失败

线程数:当调用该API的线程数达到阈值的时候,进行限流。
参考上一小节。

流控-关联

当自己关联的资源达到阈值时,就限流自己
当与A资源关联的资源B达到阀值后,就限流A自己(B惹事,A挂了)

假设资源A是下单服务,资源B是支付服务,资源A和资源B关联,流程是先下单,再支付,现在支付服务那边负载太猛了顶不住了,就限制下单服务。减少调用下单服务自然就能减少调用支付服务。

设置/testA

当关联资源/testB的QPS阀值超过1时,就限流/testA的Rest访问地址,当关联资源到阈值后限制配置好的资源名。

流控-预热
Warm Up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

默认coldFactor为3,即请求QPS 从 threshold / 3开始,经预热时长逐渐升至设定的QPS阈值

WarmUp配置
案例,阀值为10+预热时长设置5秒。
系统初始化的阀值为10/ 3约等于3,即阀值刚开始为3;然后过了5秒后阀值才慢慢升高恢复到10

测试

多次快速点击http://localhost:8401/testB - 刚开始不行,后续慢慢OK

应用场景

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

流控-排队等待

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

设置:/testA每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒。

匀速排队

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,

该方式的作用如下图所示:

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

熔断降级

熔断降级概述

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。

现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。

  • RT(平均响应时间,秒级)
    平均响应时间 超出阈值 且 在时间窗口内通过的请求>=5,两个条件同时满足后触发降级。
    窗口期过后关闭断路器。
    RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)。
  • 异常比列(秒级)
    QPS >= 5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 。
  • 异常数(分钟级)
    异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级

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

当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

Sentinei的断路器是没有类似Hystrix半开状态的。(Sentinei 1.8.0 已有半开状态)

半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用。

降级-RT

什么意思?
平均响应时间(DEGRADE_GRADE_RT):当1s内持续进入5个请求,对应时刻的平均响应时间(秒级)均超过阈值( count,以ms为单位),那么在接下的时间窗口(DegradeRule中的timeWindow,以s为单位)之内,对这个方法的调用都会自动地熔断(抛出DegradeException )。注意Sentinel 默认统计的RT上限是4900 ms,超出此阈值的都会算作4900ms,若需要变更此上限可以通过启动配置项-Dcsp.sentinel.statistic.max.rt=xxx来配置。

注意:Sentinel 1.7.0才有平均响应时间(DEGRADE_GRADE_RT),Sentinel 1.8.0的没有这项,取而代之的是慢调用比例 (SLOW_REQUEST_RATIO)。

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

这里使用的是1.7.0版本

降级-异常比例

是什么?

异常比例(DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值( DegradeRule中的 count)之后,资源进入降级状态,即在接下的时间窗口( DegradeRule中的timeWindow,以s为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是[0.0, 1.0],代表0% -100%。

注意,与Sentinel 1.8.0相比,有些不同(Sentinel 1.8.0才有的半开状态),Sentinel 1.8.0的如下:

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

Sentinel 1.7.0

降级-异常数

是什么?

异常数( DEGRADE_GRADF_EXCEPTION_COUNT ):当资源近1分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若timeWindow小于60s,则结束熔断状态后码可能再进入熔断状态。

注意,与Sentinel 1.8.0相比,有些不同(Sentinel 1.8.0才有的半开状态),Sentinel 1.8.0的如下:

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

下图是Sentinel 1.7.0的。

异常数是按照分钟统计的,时间窗口一定要大于等于60秒。

热点key

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

商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

兜底方法,分为系统默认和客户自定义,两种

之前的case,限流出问题后,都是用sentinel系统默认的提示: Blocked by Sentinel (flow limiting)

我们能不能自定?类似hystrix,某个方法出问题了,就找对应的兜底降级方法?

结论 - 从HystrixCommand到@SentinelResource

@RestController
@Slf4j
public class FlowLimitController
{...@GetMapping("/testHotKey")@SentinelResource(value = "testHotKey",blockHandler = "deal_testHotKey")    //value填在sentinel的资源名那里,blockHandler指定兜底方法public String testHotKey(@RequestParam(value = "p1",required = false) String p1,@RequestParam(value = "p2",required = false) String p2) {//int age = 10/0;return "------testHotKey";}/*兜底方法*/public String deal_testHotKey (String p1, String p2, BlockException exception) {return "------deal_testHotKey,o(╥﹏╥)o";  //sentinel系统默认的提示:Blocked by Sentinel (flow limiting)}}

配置

参数索引:
如localhost:8080/testA?p1=2&p2=2,这里参数p1的索引位置就是0,参数p2的索引位置就是1
@SentinelResource两种使用情况
1.@SentinelResource(value = “testHotKey”)
异常打到了前台用户界面看到,不友好

2.@SentinelResource(value = “testHotKey”, blockHandler = “dealHandler_testHotKey”)
方法testHotKey里面第一个参数只要QPS超过每秒1次,马上降级处理,用了我们自己定义的兜底方法

参数例外项

普通 - 超过1秒钟一个后,达到阈值1后马上被限流
我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样
特例 - 假如当p1的值等于5时,它的阈值可以达到200


测试

当p1等于5的时候,阈值变为200
当p1不等于5的时候,阈值就是平常的1

前提条件 - 热点参数的注意点,参数必须是基本类型或者String

其它

在方法体抛异常

@RestController
@Slf4j
public class FlowLimitController
{...@GetMapping("/testHotKey")@SentinelResource(value = "testHotKey",blockHandler/*兜底方法*/ = "deal_testHotKey")public String testHotKey(@RequestParam(value = "p1",required = false) String p1,@RequestParam(value = "p2",required = false) String p2) {int age = 10/0;//<----------------------------会抛异常的地方return "------testHotKey";}/*兜底方法*/public String deal_testHotKey (String p1, String p2, BlockException exception) {return "------deal_testHotKey,o(╥﹏╥)o";  //sentinel系统默认的提示:Blocked by Sentinel (flow limiting)}}

将会抛出Spring Boot 2的默认异常页面,而不是兜底方法。

@SentinelResource - 处理的是sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;

RuntimeException int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管

总结 - @SentinelResource主管配置出错,运行出错该走异常走异常

系统规则

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

系统规则

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

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

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

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

SentinelResource配置(上)

按资源名称限流 + 后续处理

启动Nacos成功

启动Sentinel成功

cloudalibaba-sentinel-service8401
添加controller

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

配置流控

表示1秒钟内访问次数大于1,就跑到我们自定义的限流
测试
1秒钟点击1下,OK
超过上述,疯狂点击,返回了自己定义的限流处理信息,限流发生

额外问题
此时关闭问服务8401 -> Sentinel控制台,流控规则消失了


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

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

RateLimitController

@RestController
public class RateLimitController
{...@GetMapping("/rateLimit/byUrl")@SentinelResource(value = "byUrl")public CommonResult byUrl(){return new CommonResult(200,"按url限流测试OK",new Payment(2020L,"serial002"));}
}


测试

快速点击http://localhost:8401/rateLimit/byUrl
结果 - 会返回Sentinel自带的限流处理结果 Blocked by Sentinel (flow limiting)

上面兜底方案面临的问题
1.系统默认的,没有体现我们自己的业务要求。
2.依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观。
3.每个业务方法都添加—个兜底的,那代码膨胀加剧。
4.全局统—的处理方法没有体现。

SentinelResource配置(中)

客户自定义限流处理逻辑

自定义限流处理类 - 创建CustomerBlockHandler类用于自定义限流处理逻辑

public class CustomerBlockHandler {public static CommonResult handlerException(BlockException exception) {return new CommonResult(4444,"按客戶自定义,global handlerException----1");}public static CommonResult handlerException2(BlockException exception) {return new CommonResult(4444,"按客戶自定义,global handlerException----2");}
}

RateLimitController添加内容

@RestController
public class RateLimitController {...@GetMapping("/rateLimit/customerBlockHandler")@SentinelResource(value = "customerBlockHandler",blockHandlerClass = CustomerBlockHandler.class,//<-------- 自定义限流处理类blockHandler = "handlerException2")//<-----------public CommonResult customerBlockHandler(){return new CommonResult(200,"按客戶自定义",new Payment(2020L,"serial003"));}
}


启动微服务后先调用一次 - http://localhost:8401/rateLimit/customerBlockHandler。然后,多次快速刷新http://localhost:8401/rateLimit/customerBlockHandler。刷新后,我们自定义兜底方法的字符串信息就返回到前端。

SentinelResource配置(下)

@SentinelResource 注解

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

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

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

Sentinel主要有三个核心Api:
SphU定义资源
Tracer定义统计
ContextUtil定义了上下文

服务熔断Ribbon环境预说

sentinel整合ribbon+openFeign+fallback

Ribbon系列

启动nacos和sentinel
提供者9003/9004
消费者84

提供者9003/9004

新建cloudalibaba-provider-payment9003/9004,两个一样的做法
pom

<dependencies><!--SpringCloud ailibaba nacos --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><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.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>

yml

server:port: 9003spring:application:name: nacos-payment-providercloud:nacos:discovery:server-addr: localhost:8848 #配置Nacos地址management:endpoints:web:exposure:include: '*'

启动类

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

controller

@RestController
public class PaymentController {@Value("${server.port}")private String serverPort;//模拟数据库public static HashMap<Long,Payment> hashMap = new HashMap<>();static{hashMap.put(1L,new Payment(1L,"28a8c1e3bc2742d8848569891fb42181"));hashMap.put(2L,new Payment(2L,"bba8c1e3bc2742d8848569891ac32182"));hashMap.put(3L,new Payment(3L,"6ua8c1e3bc2742d8848569891xt92183"));}@GetMapping(value = "/paymentSQL/{id}")public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){Payment payment = hashMap.get(id);CommonResult<Payment> result = new CommonResult(200,"from mysql,serverPort:  "+serverPort,payment);return result;}}

测试地址 http://localhost:9003/paymentSQL/1


新建cloudalibaba-consumer-nacos-order84
pom

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

yml

server:port: 84spring:application:name: nacos-order-consumercloud:nacos:discovery:server-addr: localhost:8848sentinel:transport:#配置Sentinel dashboard地址dashboard: localhost:8080#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口port: 8719#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:nacos-user-service: http://nacos-payment-provider# 激活Sentinel对Feign的支持
feign:sentinel:enabled: false

启动类

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

配置类

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

controller

@RestController
@Slf4j
public class CircleBreakerController {public static final String SERVICE_URL = "http://nacos-payment-provider";@Resourceprivate RestTemplate restTemplate;@RequestMapping("/consumer/fallback/{id}")@SentinelResource(value = "fallback")//没有配置public CommonResult<Payment> fallback(@PathVariable Long id){CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);if (id == 4) {throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");}else if (result.getData() == null) {throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");}return result;}}

目的

fallback管运行异常
blockHandler管配置违规
测试地址 http://localhost:84/consumer/fallback/1,看到端口号9003和9004交替变换说明负载均衡也有了。

服务熔断无配置

没有任何配置 - 给用户error页面,不友好

服务熔断只配置fallback

修改cloudalibaba-consumer-nacos-order84的controller

@RestController
@Slf4j
public class CircleBreakerController {public static final String SERVICE_URL = "http://nacos-payment-provider";@Autowiredprivate RestTemplate restTemplate;@RequestMapping("/consumer/fallback/{id}")@SentinelResource(value = "fallback", fallback = "handlerFallback")public CommonResult<Payment> fallback(@PathVariable Long id){CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);if (id == 4) {throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");}else if (result.getData() == null) {throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");}return result;}//兜底方法public CommonResult handlerFallback(@PathVariable  Long id,Throwable e) {Payment payment = new Payment(id,"null");return new CommonResult<>(444,"兜底异常handlerFallback,exception内容  "+e.getMessage(),payment);}
}

测试

服务熔断只配置blockHandler

blockHandler只负责sentinel控制台配置违规
修改controller

@RestController
@Slf4j
public class CircleBreakerController
{public static final String SERVICE_URL = "http://nacos-payment-provider";@Resourceprivate RestTemplate restTemplate;@RequestMapping("/consumer/fallback/{id}")//@SentinelResource(value = "fallback") //没有配置//@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规public CommonResult<Payment> fallback(@PathVariable Long id){CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);if (id == 4) {throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");}else if (result.getData() == null) {throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");}return result;}//本例是fallback
/*    public CommonResult handlerFallback(@PathVariable  Long id,Throwable e) {Payment payment = new Payment(id,"null");return new CommonResult<>(444,"兜底异常handlerFallback,exception内容  "+e.getMessage(),payment);}*///本例是blockHandlerpublic CommonResult blockHandler(@PathVariable  Long id,BlockException blockException) {Payment payment = new Payment(id,"null");return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException  "+blockException.getMessage(),payment);}
}

服务熔断fallback和blockHandler都配置

1.不满足流控规则会进入到blockHandler的方法
2.程序发生异常进入到fallback兜底方法
3.如果程序发生了异常同时又不满足流控规则会进入到blockHandler的方法

@RestController
@Slf4j
public class CircleBreakerController
{public static final String SERVICE_URL = "http://nacos-payment-provider";@Resourceprivate RestTemplate restTemplate;@RequestMapping("/consumer/fallback/{id}")//@SentinelResource(value = "fallback") //没有配置//@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常//@SentinelResource(value = "fallback",blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler")public CommonResult<Payment> fallback(@PathVariable Long id){CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);if (id == 4) {throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");}else if (result.getData() == null) {throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");}return result;}//本例是fallbackpublic CommonResult handlerFallback(@PathVariable  Long id,Throwable e) {Payment payment = new Payment(id,"null");return new CommonResult<>(444,"兜底异常handlerFallback,exception内容  "+e.getMessage(),payment);}//本例是blockHandlerpublic CommonResult blockHandler(@PathVariable  Long id,BlockException blockException) {Payment payment = new Payment(id,"null");return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException  "+blockException.getMessage(),payment);}
}

服务熔断exceptionsToIgnore

exceptionsToIgnore,忽略指定异常,即这些异常不用兜底方法处理。

@RestController
@Slf4j
public class CircleBreakerController    ...@RequestMapping("/consumer/fallback/{id}")@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler",exceptionsToIgnore = {IllegalArgumentException.class})//<-------------public CommonResult<Payment> fallback(@PathVariable Long id){CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/"+id,CommonResult.class,id);if (id == 4) {//exceptionsToIgnore属性有IllegalArgumentException.class,//所以IllegalArgumentException不会跳入指定的兜底程序。throw new IllegalArgumentException ("IllegalArgumentException,非法参数异常....");}else if (result.getData() == null) {throw new NullPointerException ("NullPointerException,该ID没有对应记录,空指针异常");}return result;}...
}

服务熔断OpenFeign

修改84模块

84消费者调用提供者9003

Feign组件一般是消费侧
pom添加依赖

<!--SpringCloud openfeign --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

yml

# 激活Sentinel对Feign的支持
feign:sentinel:enabled: true

业务类


@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService
{@GetMapping(value = "/paymentSQL/{id}")public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}

@Component
public class PaymentFallbackService implements PaymentService {@Overridepublic CommonResult<Payment> paymentSQL(Long id){return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService",new Payment(id,"errorSerial"));}
}

controller

@RestController
@Slf4j
public class CircleBreakerController {...//==================OpenFeign@Resourceprivate PaymentService paymentService;@GetMapping(value = "/consumer/paymentSQL/{id}")public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){return paymentService.paymentSQL(id);}
}

启动类

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients//<------------------------
public class OrderNacosMain84 {public static void main(String[] args) {SpringApplication.run(OrderNacosMain84.class, args);}
}

测试 - http://localhost:84/consumer/paymentSQL/1

测试84调用9003,此时故意关闭9003微服务提供者,84消费侧自动降级,不会被耗死。

持久化规则

是什么

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

怎么玩

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

步骤

修改cloudalibaba-sentinel-service8401
pom添加依赖

<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

yml

server:port: 8401spring:application:name: cloudalibaba-sentinel-servicecloud:nacos:discovery:server-addr: localhost:8848 #Nacos服务注册中心地址sentinel:transport:dashboard: localhost:8080 #配置Sentinel dashboard地址port: 8719datasource: #<---------------------------关注点,添加Nacos数据源配置ds1:nacos:server-addr: localhost:8848dataId: cloudalibaba-sentinel-servicegroupId: DEFAULT_GROUPdata-type: jsonrule-type: flowmanagement:endpoints:web:exposure:include: '*'feign:sentinel:enabled: true # 激活Sentinel对Feign的支持

添加Nacos配置

配置内容:

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

启动8401后刷新sentinel发现业务规则有了

快速访问测试接口 - http://localhost:8401/rateLimit/byUrl - 页面返回Blocked by Sentinel (flow limiting)

停止8401再看sentinel - 停机后发现流控规则没有了

重新启动8401再看sentinel

多次调用 - http://localhost:8401/rateLimit/byUrl
配置出现

分布式事务问题由来

分布式前

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


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

Seata

是什么

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

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

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

  • Transaction ID XID 全局唯一的事务ID

  • 三组件概念
    1.TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
    2.TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
    3.RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

处理过程:

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

Seata-Server安装

本次使用0.9.0版本
下载地址
https://github.com/seata/seata/releases/tag/v0.9.0
下载seata-server-0.9.0.zip

SEATA 的分布式交易解决案例

解压后,修改conf目录下的file.conf配置文件,主要修改:自定义事务组名称+事务日志存储模式为db +数据库连接信息



mysql5.7数据库新建库seata,在seata库里建表
建表db_store.sql在seata\conf目录里面

修改seata\conf目录下的registry.conf配置文件

目的是:指明注册中心为nacos,及指定nacos连接信息

先启动Nacos nacos\bin\startup.cmd

再启动seata-server seata\bin\seata-server.bat

业务数据库准备

以下演示都需要先启动Nacos后启动Seata,保证两个都OK。

分布式事务业务说明

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

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

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

一言蔽之,下订单—>扣库存—>减账户(余额)。

创建业务数据库
seata_ order:存储订单的数据库;
seata_ storage:存储库存的数据库;
seata_ account:存储账户信息的数据库。

CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;

seata_order库下建t_order表

CREATE TABLE t_order(`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',`count` INT(11) DEFAULT NULL COMMENT '数量',`money` DECIMAL(11,0) DEFAULT NULL COMMENT'金额',`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

seata_storage库下建t_storage表

CREATE TABLE t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');

seata_account库下建t_account表

CREATE TABLE t_account(`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额', `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');

在上面3个库分别建对应的回滚日志表
订单-库存-账户3个库下都需要建立回滚日志表
SQL文件在seata\conf目录下的db_ undo_ log.sql
最终:

Order-Module搭建

下订单 -> 减库存 -> 扣余额 -> 改(订单)状态

创建seata-order-service2001模块

POM

<dependencies><!--nacos--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><exclusions><exclusion><artifactId>seata-all</artifactId><groupId>io.seata</groupId></exclusion></exclusions></dependency><dependency><groupId>io.seata</groupId><artifactId>seata-all</artifactId><version>0.9.0</version></dependency><!--feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--web-actuator--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency><!--mysql-druid--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.37</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.10</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies>

resources文件夹下创建file.conf和registry.conf

transport {# tcp udt unix-domain-sockettype = "TCP"#NIO NATIVEserver = "NIO"#enable heartbeatheartbeat = true#thread factory for nettythread-factory {boss-thread-prefix = "NettyBoss"worker-thread-prefix = "NettyServerNIOWorker"server-executor-thread-prefix = "NettyServerBizHandler"share-boss-worker = falseclient-selector-thread-prefix = "NettyClientSelector"client-selector-thread-size = 1client-worker-thread-prefix = "NettyClientWorkerThread"# netty boss thread size,will not be used for UDTboss-thread-size = 1#auto default pin or 8worker-thread-size = 8}shutdown {# when destroy server, wait secondswait = 3}serialization = "seata"compressor = "none"
}service {vgroup_mapping.fsp_tx_group = "default" #修改自定义事务组名称default.grouplist = "127.0.0.1:8091"enableDegrade = falsedisable = falsemax.commit.retry.timeout = "-1"max.rollback.retry.timeout = "-1"disableGlobalTransaction = false
}client {async.commit.buffer.limit = 10000lock {retry.internal = 10retry.times = 30}report.retry.count = 5tm.commit.retry.count = 1tm.rollback.retry.count = 1
}## transaction log store
store {## store mode: file、dbmode = "db"## file storefile {dir = "sessionStore"# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptionsmax-branch-session-size = 16384# globe session size , if exceeded throws exceptionsmax-global-session-size = 512# file buffer size , if exceeded allocate new bufferfile-write-buffer-cache-size = 16384# when recover batch read sizesession.reload.read_size = 100# async, syncflush-disk-mode = async}## database storedb {## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.datasource = "dbcp"## mysql/oracle/h2/oceanbase etc.db-type = "mysql"driver-class-name = "com.mysql.jdbc.Driver"url = "jdbc:mysql://127.0.0.1:3306/seata"user = "root"password = "root"min-conn = 1max-conn = 3global.table = "global_table"branch.table = "branch_table"lock-table = "lock_table"query-limit = 100}
}
lock {## the lock store mode: local、remotemode = "remote"local {## store locks in user's database}remote {## store locks in the seata's server}
}
recovery {#schedule committing retry period in millisecondscommitting-retry-period = 1000#schedule asyn committing retry period in millisecondsasyn-committing-retry-period = 1000#schedule rollbacking retry period in millisecondsrollbacking-retry-period = 1000#schedule timeout retry period in millisecondstimeout-retry-period = 1000
}transaction {undo.data.validation = trueundo.log.serialization = "jackson"undo.log.save.days = 7#schedule delete expired undo_log in millisecondsundo.log.delete.period = 86400000undo.log.table = "undo_log"
}## metrics settings
metrics {enabled = falseregistry-type = "compact"# multi exporters use comma dividedexporter-list = "prometheus"exporter-prometheus-port = 9898
}support {## springspring {# auto proxy the DataSource beandatasource.autoproxy = false}
}

registry.conf

registry {# file 、nacos 、eureka、redis、zk、consul、etcd3、sofatype = "nacos"nacos {serverAddr = "localhost:8848"namespace = ""cluster = "default"}eureka {serviceUrl = "http://localhost:8761/eureka"application = "default"weight = "1"}redis {serverAddr = "localhost:6379"db = "0"}zk {cluster = "default"serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}consul {cluster = "default"serverAddr = "127.0.0.1:8500"}etcd3 {cluster = "default"serverAddr = "http://localhost:2379"}sofa {serverAddr = "127.0.0.1:9603"application = "default"region = "DEFAULT_ZONE"datacenter = "DefaultDataCenter"cluster = "default"group = "SEATA_GROUP"addressWaitTime = "3000"}file {name = "file.conf"}
}config {# file、nacos 、apollo、zk、consul、etcd3type = "file"nacos {serverAddr = "localhost"namespace = ""}consul {serverAddr = "127.0.0.1:8500"}apollo {app.id = "seata-server"apollo.meta = "http://192.168.1.204:8801"}zk {serverAddr = "127.0.0.1:2181"session.timeout = 6000connect.timeout = 2000}etcd3 {serverAddr = "http://localhost:2379"}file {name = "file.conf"}
}

创建domain文件夹存放实体

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>
{private Integer code;private String  message;private T       data;public CommonResult(Integer code, String message){this(code,message,null);}
}
package com.atguigu.springcloud.alibaba.domain;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.math.BigDecimal;@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{private Long id;private Long userId;private Long productId;private Integer count;private BigDecimal money;private Integer status; //订单状态:0:创建中;1:已完结
}

Dao接口及实现

import com.atguigu.springcloud.alibaba.domain.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;@Mapper
public interface OrderDao
{//1 新建订单void create(Order order);//2 修改订单状态,从零改为1void update(@Param("userId") Long userId,@Param("status") Integer status);
}

OrderMapper.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.alibaba.dao.OrderDao"><resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Order"><id column="id" property="id" jdbcType="BIGINT"/><result column="user_id" property="userId" jdbcType="BIGINT"/><result column="product_id" property="productId" jdbcType="BIGINT"/><result column="count" property="count" jdbcType="INTEGER"/><result column="money" property="money" jdbcType="DECIMAL"/><result column="status" property="status" jdbcType="INTEGER"/></resultMap><insert id="create">insert into t_order (id,user_id,product_id,count,money,status)values (null,#{userId},#{productId},#{count},#{money},0);</insert><update id="update">update t_order set status = 1where user_id=#{userId} and status = #{status};</update></mapper>

Service接口
OrderService
StorageService
AccountService

Service接口实现类
OrderServiceImpl

public interface OrderService
{void create(Order order);
}
@FeignClient(value = "seata-storage-service")
public interface StorageService
{@PostMapping(value = "/storage/decrease")CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
@FeignClient(value = "seata-account-service")
public interface AccountService
{@PostMapping(value = "/account/decrease")CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
import com.atguigu.springcloud.alibaba.dao.OrderDao;
import com.atguigu.springcloud.alibaba.domain.Order;
import com.atguigu.springcloud.alibaba.service.AccountService;
import com.atguigu.springcloud.alibaba.service.OrderService;
import com.atguigu.springcloud.alibaba.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
@Slf4j
public class OrderServiceImpl implements OrderService
{@Autowiredprivate OrderDao orderDao;@Autowiredprivate StorageService storageService;@Autowiredprivate AccountService accountService;/*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态* 简单说:下订单->扣库存->减余额->改状态*/@Override//@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)public void create(Order order){log.info("----->开始新建订单");//1 新建订单orderDao.create(order);//2 扣减库存log.info("----->订单微服务开始调用库存,做扣减Count");storageService.decrease(order.getProductId(),order.getCount());log.info("----->订单微服务开始调用库存,做扣减end");//3 扣减账户log.info("----->订单微服务开始调用账户,做扣减Money");accountService.decrease(order.getUserId(),order.getMoney());log.info("----->订单微服务开始调用账户,做扣减end");//4 修改订单状态,从零到1,1代表已经完成log.info("----->修改订单状态开始");orderDao.update(order.getUserId(),0);log.info("----->修改订单状态结束");log.info("----->下订单结束了,O(∩_∩)O哈哈~");}
}

Controller

import com.atguigu.springcloud.alibaba.domain.CommonResult;
import com.atguigu.springcloud.alibaba.domain.Order;
import com.atguigu.springcloud.alibaba.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController
public class OrderController
{@Autowiredprivate OrderService orderService;@GetMapping("/order/create")public CommonResult create(Order order){orderService.create(order);return new CommonResult(200,"订单创建成功");}
}

创建Config文件夹
MyBatisConfig
DataSourceProxyConfig

@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {}
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import javax.sql.DataSource;/*** 使用Seata对数据源进行代理*/
@Configuration
public class DataSourceProxyConfig {@Value("${mybatis.mapperLocations}")private String mapperLocations;@Bean@ConfigurationProperties(prefix = "spring.datasource")public DataSource druidDataSource(){return new DruidDataSource();}@Beanpublic DataSourceProxy dataSourceProxy(DataSource dataSource) {return new DataSourceProxy(dataSource);}@Beanpublic 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();}}

启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@EnableDiscoveryClient
@EnableFeignClients
//取消数据源的自动创建,而是使用自己定义的
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderMainApp2001
{public static void main(String[] args){SpringApplication.run(SeataOrderMainApp2001.class, args);}
}

Storage-Module

创建seata-storage-service2002,与seata-order-service2001模块大致相同
POM(与seata-order-service2001模块相同)
YML

server:port: 2002spring:application:name: seata-storage-servicecloud:alibaba:seata:tx-service-group: fsp_tx_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_storageusername: rootpassword: rootlogging:level:io:seata: infomybatis:mapperLocations: classpath:mapper/*.xml

file.conf(与seata-order-service2001模块相同)

registry.conf(与seata-order-service2001模块相同)
创建domain文件夹

@Data
public class Storage {private Long id;/*** 产品id*/private Long productId;/*** 总库存*/private Integer total;/*** 已用库存*/private Integer used;/*** 剩余库存*/private Integer residue;
}

CommonResult(与seata-order-service2001模块相同)

Dao接口及实现

@Mapper
public interface StorageDao {//扣减库存void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

StorageMapper.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.alibaba.dao.StorageDao"><resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Storage"><id column="id" property="id" jdbcType="BIGINT"/><result column="product_id" property="productId" jdbcType="BIGINT"/><result column="total" property="total" jdbcType="INTEGER"/><result column="used" property="used" jdbcType="INTEGER"/><result column="residue" property="residue" jdbcType="INTEGER"/></resultMap><update id="decrease">UPDATEt_storageSETused = used + #{count},residue = residue - #{count}WHEREproduct_id = #{productId}</update></mapper>

Service接口及实现

public interface StorageService {/*** 扣减库存*/void decrease(Long productId, Integer count);
}
package com.atguigu.springcloud.alibaba.service.impl;import com.atguigu.springcloud.alibaba.domain.StorageDao;
import com.atguigu.springcloud.alibaba.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;/*** @author tony* @date 2021/9/11 11:14*/
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {//private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);@Autowiredprivate StorageDao storageDao;/*** 扣减库存*/@Overridepublic void decrease(Long productId, Integer count) {//LOGGER.info("------->storage-service中扣减库存开始");log.info("------->storage-service中扣减库存开始");storageDao.decrease(productId,count);//LOGGER.info("------->storage-service中扣减库存结束");log.info("------->storage-service中扣减库存结束");}
}

controller

@RestController
public class StorageController {@Autowiredprivate StorageService storageService;/*** 扣减库存*/@RequestMapping("/storage/decrease")public CommonResult decrease(Long productId, Integer count) {storageService.decrease(productId, count);return new CommonResult(200,"扣减库存成功!");}
}

Config配置(与seata-order-service2001模块相同)

主启动(与seata-order-service2001模块相同)

Account-Module

与seata-order-service2001模块大致相同

seata-account-service2003

POM(与seata-order-service2001模块相同)

YML

server:port: 2003spring:application:name: seata-account-servicecloud:alibaba:seata:tx-service-group: fsp_tx_groupnacos:discovery:server-addr: localhost:8848datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/seata_accountusername: rootpassword: 123456feign:hystrix:enabled: falselogging:level:io:seata: infomybatis:mapperLocations: classpath:mapper/*.xml

file.conf(与seata-order-service2001模块相同)

registry.conf(与seata-order-service2001模块相同)

domain

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {private Long id;/*** 用户id*/private Long userId;/*** 总额度*/private BigDecimal total;/*** 已用额度*/private BigDecimal used;/*** 剩余额度*/private BigDecimal residue;
}

CommonResult与seata-order-service2001模块相同
Dao接口及实现

@Mapper
public interface AccountDao {/*** 扣减账户余额*/void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}
<?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.alibaba.dao.AccountDao"><resultMap id="BaseResultMap" type="com.atguigu.springcloud.alibaba.domain.Account"><id column="id" property="id" jdbcType="BIGINT"/><result column="user_id" property="userId" jdbcType="BIGINT"/><result column="total" property="total" jdbcType="DECIMAL"/><result column="used" property="used" jdbcType="DECIMAL"/><result column="residue" property="residue" jdbcType="DECIMAL"/></resultMap><update id="decrease">UPDATE t_accountSETresidue = residue - #{money},used = used + #{money}WHEREuser_id = #{userId};</update></mapper>

Service接口及实现

public interface AccountService {/*** 扣减账户余额* @param userId 用户id* @param money 金额*/void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {@Autowiredprivate AccountDao accountDao;/*** 扣减账户余额*/@Overridepublic void decrease(Long userId, BigDecimal money) {log.info("------->account-service中扣减账户余额开始");accountDao.decrease(userId,money);log.info("------->account-service中扣减账户余额结束");}
}

Controller

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

Config配置(与seata-order-service2001模块相同)

主启动(与seata-order-service2001模块相同)

@GlobalTransactional验证

下订单 -> 减库存 -> 扣余额 -> 改(订单)状态

数据库初始情况:


正常下单 - http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

数据库正常下单后状况:

超时异常,没加@GlobalTransactional

模拟AccountServiceImpl添加超时

@Service
public class AccountServiceImpl implements AccountService {private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);@ResourceAccountDao accountDao;/*** 扣减账户余额*/@Overridepublic void decrease(Long userId, BigDecimal money) {LOGGER.info("------->account-service中扣减账户余额开始");//模拟超时异常,全局事务回滚//暂停20秒钟线程,OpenFeign调用默认时间1秒钟,所以这里肯定会报错try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }accountDao.decrease(userId,money);LOGGER.info("------->account-service中扣减账户余额结束");}
}

数据库情况

故障情况

1.当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1
2.而且由于feign的重试机制,账户余额还有可能被多次扣减

超时异常,加了@GlobalTransactional
用@GlobalTransactional标注OrderServiceImpl的create()方法。

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {.../*** 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态* 简单说:下订单->扣库存->减余额->改状态*/@Override//rollbackFor = Exception.class表示对任意异常都进行回滚@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)public void create(Order order){...}
}

还是模拟AccountServiceImpl添加超时,下单后数据库数据并没有任何改变,记录都添加不进来,达到出异常,数据库回滚的效果。

原理简介

上面用0.9.0版,2020起始,用1.0以后的版本。

分布式事务的执行流程

  • TM开启分布式事务(TM向TC注册全局事务记录) ;
  • 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态) ;
  • TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务) ;
  • TC汇总事务信息,决定分布式事务是提交还是回滚;
  • TC通知所有RM提交/回滚资源,事务二阶段结束。

AT模式如何做到对业务的无侵入
是什么
前提

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

整体机制

两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • 二阶段:
    提交异步化,非常快速地完成。
    回滚通过一阶段的回滚日志进行反向补偿。

一阶段加载
在一阶段,Seata会拦截“业务SQL”,解析SQL语义,找到“业务SQL" 要更新的业务数据,在业务数据被更新前,将其保存成"before image”
执行“业务SQL" 更新业务数据,在业务数据更新之后,其保存成"after image”,最后生成行锁。

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

一阶段

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

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

回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和"after image"。

如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。

补充

Spring Cloud学习1相关推荐

  1. Spring Cloud 学习资料收集

    导读 关于Spring Cloud 去年开始逐渐多的出现在我的视线中,随着微服务这个词越来越热,我们或多或少的都听说过这个词,我们可以将Spring Cloud 看做是java 中Spring 为我们 ...

  2. Spring Cloud 学习笔记(四)-Spring Cloud Hystrix

    Spring Cloud 学习笔记(四)-Spring Cloud Hystrix 由于前一阵子项目的原因,今天才继续弄上,今天想学习一下Hystrix组件 这个组件还挺抽象的,最开始我一直没太明白, ...

  3. Spring Cloud 学习笔记(2 / 3)

    Spring Cloud 学习笔记(1 / 3) Spring Cloud 学习笔记(3 / 3) - - - 56_Hystrix之全局服务降级DefaultProperties 57_Hystri ...

  4. Spring Cloud 学习笔记(2 3)

    Spring Cloud 学习笔记(1 / 3) Spring Cloud 学习笔记(3 / 3) - - - 56_Hystrix之全局服务降级DefaultProperties 57_Hystri ...

  5. Spring Cloud 学习笔记(1 / 3)

    Spring Cloud 学习笔记(2 / 3) Spring Cloud 学习笔记(3 / 3) - - - 01_前言闲聊和课程说明 02_零基础微服务架构理论入门 03_第二季Boot和Clou ...

  6. Spring Cloud学习笔记—网关Spring Cloud Gateway官网教程实操练习

    Spring Cloud学习笔记-网关Spring Cloud Gateway官网教程实操练习 1.Spring Cloud Gateway介绍 2.在Spring Tool Suite4或者IDEA ...

  7. Spring Cloud 学习总结

    Spring Cloud 学习笔记 微服务架构 微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,服务之间相互协调,互相配合,为用户提供最终价值.服务之间使用轻量 ...

  8. Spring Cloud学习笔记

    Spring Cloud学习笔记 相关代码地址:https://github.com/gongxings/spring-cloud-study.git 一.工程环境搭建 spring cloud版本: ...

  9. Spring Cloud学习笔记【十二】Hystrix的使用和了解

    Spring Cloud学习笔记[十二]Hystrix的使用和了解 Hystrix [hɪst'rɪks],中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力.本文所说的Hystrix是Net ...

  10. Spring Cloud 学习笔记(3 3)

    Spring Cloud 学习笔记(1 / 3) Spring Cloud 学习笔记(2 / 3) - - - 108_Nacos之Linux版本安装 109_Nacos集群配置(上) 110_Nac ...

最新文章

  1. PHP 读写TXT与Mysql性能测试
  2. 效果提升7%、速度增加220%,OCR开源神器PaddleOCR再迎升级
  3. 前端- jquery- 总结
  4. Sqlite数据库相关
  5. 多重选择函数c语言,大佬在吗,我用C写了一个去多重括号的函数,结果。。。...
  6. Vue 深度监听和初始绑定
  7. Java并发编程面试题(2020最新版)
  8. 动态生成表格呈现还是将表格直接绑定gridview等控件呈现的开发方式选择依据...
  9. python播放本地视频_python opencv 读取本地视频文件 修改ffmpeg的方法
  10. 生产环境中,RabbitMQ 持续积压消息不进行ack ,发生什么了?
  11. idea 使用时的一些问题
  12. 181101新闻:午后阳光下集思广益,课例研修尝试与挑战并存
  13. 四川绵阳:充分利用区块链等技术,为农民工证照办理提供线上便捷服务
  14. pdf怎么解除限制?如何解密?一步解决
  15. 时间管理表 - 《待办清单列表》
  16. UBUNTU 7.04安装后的配置
  17. VS Code 呈现缩进参考线以及语法高亮改变
  18. 英文事件抽取论文整理
  19. Android深入浅出系列之Bluetooth—蓝牙操作(一)
  20. 常见系统问题及其解决方法

热门文章

  1. three.js--3D全景图开发
  2. Behance 大神推荐2019 年所有设计领域的最新趋势!
  3. 赠英语学习者的十句经典名言
  4. python+OpenCV 图片合成视频
  5. 瑞芯微-RV1109主板分析
  6. 庄子 内篇 养生主第三
  7. awakeFromNib与initWithCoder
  8. UE5 Rokoko Studio Live(动捕实时链接)
  9. PTA 转段考试 数据库myql (4篇)
  10. 一组漫画告诉你Linux 系统有什么