SpringCloud 2.0
SpringCloud第二季
前言
SpringCloud介绍及微服务架构编码构建
SpringCloud介绍
SpringCloud 是微服务一站式服务解决方案,微服务全家桶。它是微服务开发的主流技术栈。它采用了名称,而非数字版本号。
SpringCloud 和 springCloud Alibaba 目前是最主流的微服务框架组合。
SpringCloud版本选择
选用 springboot 和 springCloud 版本有约束,不按照它的约束会有冲突。
Cloud Release Train | Boot Version |
---|---|
Hoxton | 2.2.x, 2.3.x (Starting with SR5) |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgwj | 1.5.x |
Dalston | 1.5.x |
查看版本对应关系:https://start.spring.io/actuator/info
所用软件版本
软件 | 版本 |
---|---|
cloud | Hoxton.SR1 |
boot | 2.2.2.RELEASE |
cloud alibaba | 2.1.0.RELEASE |
java | java8 |
maven | 3.5及以上 |
mysql | 5.7及以上 |
题外话:boot版已经到2.2.4为最新,为什么选2.2.2?
- 如果项目中只用到 boot,直接用最新,由cloud决定boot版本
- 同时用boot和cloud,需要照顾cloud,由cloud决定boot版本(可官网查看)
Cloud各种组件的停更/升级/替换
2020年:
- Eureka停用,可以使用Zookeeper+Dubbo作为服务注册中心
- 服务调用,Ribbon准备停更,代替为LoadBalance
- Feign改为OpenFeign
- Hystrix停更,改为resilence4j,或者阿里巴巴的sentienl
- Zuul改为gateway
- 服务配置Config改为 Nacos
- 服务总线Bus改为Nacos
资料
-
SpringCloud
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/
Spring Cloud中文文档:
https://www.bookstack.cn/read/spring-cloud-docs/docs-index.md
-
SpringBoot
https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/
微服务架构编码构建
约定 > 配置 > 编码
构建父工程
-
微服务cloud整体聚合父工程Project
IDEA创建Maven工程,修改IDEA项目iava版本,文件编码等
-
父工程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.wang</groupId> <artifactId>spring-cloud2020</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <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.18.20</lombok.version> <mysql.version>5.1.47</mysql.version> <druid.version>1.1.16</druid.version> <druid-spring-boot-starter.version>1.1.10</druid-spring-boot-starter.version> <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version> <spring-boot.version>2.2.2.RELEASE</spring-boot.version> <spring-cloud.version>Hoxton.SR1</spring-cloud.version> <spring-cloud-alibaba.version>2.1.0.RELEASE</spring-cloud-alibaba.version> <spring-boot-maven-plugin.version>2.3.5.RELEASE</spring-boot-maven-plugin.version> </properties> <!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version --> <dependencyManagement> <dependencies> <!--spring boot 2.2.2--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud Hoxton.SR1--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud alibaba 2.1.0.RELEASE--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</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>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid-spring-boot-starter.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> <finalName>mscloud</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot-maven-plugin.version}</version> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build> </project>
-
Maven工程落地细节复习
-
Maven中的DependencyManagement和Dependencies
-
dependencyManagement
a. Maven 使用dependencyManagement 元素来提供了一种管理依赖版本号的方式。通常会在一个组织或者项目的最顶层的父POM 中看到dependencyManagement 元素。
b. 使用pom.xml 中的dependencyManagement 元素能让所有在子项目中引用一个依赖而不用显式的列出版本号。
c. Maven 会沿着父子层次向上走,直到找到一个拥有dependencyManagement 元素的项目,然后它就会使用这个
dependencyManagement 元素中指定的版本号。d. 这样做的好处就是:如果有多个子项目都引用同一样依赖,则可以避免在每个使用的子项目里都声明一个版本号,这样当想升级或切换到另一个版本时,只需要在顶层父容器里更新,而不需要一个一个子项目的修改;另外如果某个子项目需要另外的一个版本,只需要声明version就可。
dependencyManagement 里只是声明依赖,并不实现引入,因此子项目需要显示的声明需要用的依赖 如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且version和scope都读取自父pom 如果子项目中指定了版本号,那么会使用子项目中指定的jar版本
-
maven中跳过单元测试
<!-- properties属性配置 --> <properties> <maven.test.skip>true</maven.test.skip> </properties> <!-- 或 --> <properties> <skipTests>true</skipTests> </properties>
-
-
Rest微服务工程构建
支付模块8001
cloud-provider-payment8001 微服务提供者支付Module模块, 建 cloud-provider-payment8001,创建完成后请回到父工程查看pom文件变化
-
改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>mscloud</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-provider-payment8001</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <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>
-
写YML
application.yml
server: port: 8001 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: com.mysql.jdbc.Driver # mysql驱动包 com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/springcloud2021?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root mybatis: mapperLocations: classpath:mapper/*.xml type-aliases-package: com.atguigu.springcloud.entities # 所有Entity别名类所在包
-
主启动
@SpringBootApplication public class CloudProviderPayment8001Application { public static void main(String[] args) { SpringApplication.run(CloudProviderPayment8001Application.class, args); } }
-
建库建表
CREATE TABLE `payment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `serial` varchar(200) DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
-
业务代码
-
主实体 Payment
@Data @AllArgsConstructor @NoArgsConstructor public class Payment implements Serializable { private Long id; private String serial; }
-
Json封装体CommonResult
这个类是传递给前端的,前端不管什么 payment,它只要响应状态码、message…
@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); } }
-
controller
@Slf4j @RestController @RequestMapping("/payment") public class PaymentController { @Resource private PaymentService paymentService; @PostMapping("/create") public CommonResult<Object> create(Payment payment) { return paymentService.create(payment); } @GetMapping("/get/{id}") public CommonResult<Object> getPaymentById(@PathVariable("id") Long id) { return paymentService.getPaymentById(id); } }
-
service
public interface PaymentService { /** * 创建一个 payment * @param payment * @return */ CommonResult create(Payment payment); /** * 根据 id 查询 payment * @param id * @return */ CommonResult getPaymentById(Long id); }
-
serviceImpl
@Slf4j @Service public class PaymentServiceImpl implements PaymentService { @Resource private PaymentDao paymentDao; @Override public CommonResult<Object> create(Payment payment) { int result = paymentDao.create(payment); log.info("*****插入操作返回结果:{}", result); if (result > 0) { return new CommonResult<>(200, "插入数据库成功", result); } else { return new CommonResult<>(444, "插入数据库失败"); } } @Override public CommonResult<Object> getPaymentById(Long id) { Payment payment = paymentDao.getPaymentById(id); log.info("*****查询结果:{}", payment); if (payment != null) { return new CommonResult<>(200, "查询成功", payment); } else { return new CommonResult<>(444, "没有对应记录,查询ID: " + id); } } }
-
dao
@Mapper public interface PaymentDao { /** * 创建一个 payment * * @param payment * @return */ int create(Payment payment); /** * 根据 id 查询 payment * * @param id * @return */ Payment getPaymentById(@Param("id") Long id); }
-
mapper.xml
src\main\resources\mapper\PaymentMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.atguigu.springcloud.dao.PaymentDao"> <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" keyColumn="id"> insert into payment(serial) values (#{serial}) </insert> <select id="getPaymentById" parameterType="Long" resultMap="BaseResultMap"> select id, serial from payment where id = #{id} </select> </mapper>
-
测试(调用接口查看结果)
http://localhost:8001/payment/create?serial=‘111’ http://localhost:8001/payment/get/1
-
IDEA设置
View --> Tool Windows --> Services
-
热部署Devtools
仅开发环境可使用
-
8001的pom添加
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>
-
父工程的pom
<build> <finalName>mscloud</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.5.RELEASE</version> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build>
-
备注
个人不喜欢自动构建,可百度
IDEA使用spring-boot-devtools
等关键字了解设置手动热启动即更改代码后点击IDEA上方Build,选择 Recompile 'xxxx',快捷键
Ctrl + Shift + F9
订单模块80
cloud-consumer-order80 微服务消费者订单Module模块
-
改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>mscloud</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-consumer-order80</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <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>
-
写YML
# application.yml server: port: 80
-
主启动类
@SpringBootApplication public class MainApp80 { public static void main(String[] args) { SpringApplication.run(MainApp80.class, args); } }
-
业务代码
-
主实体 Payment
@Data @AllArgsConstructor @NoArgsConstructor public class Payment implements Serializable { private Long id; private String serial; }
-
Json封装体CommonResult
这个类是传递给前端的,前端不管什么 payment,它只要响应状态码、message…
@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); } }
-
RestTemplate
-
RestTemplate是什么
RestTemplate提供了多种便捷访问远程Http服务的方法, 是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
官网地址:
https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html
-
使用
使用restTemplate访问restful接口非常的简单粗暴无脑
- url:REST请求地址
- requestMap:请求参数
- ResponseBean.class:HTTP响应转换被转换成的对象类型
-
Config配置类
-
ApplicationContextConfig
@Configuration public class ApplicationContextConfig { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
-
controller
@Slf4j @RestController @RequestMapping("/consumer") public class OrderController { public static final String PAYMENT_SRV_URL = "http://localhost:8001"; @Resource private RestTemplate restTemplate; /** * 客户端用浏览器是get请求,但是底层实质发送post调用服务端8001 * * @param payment * @return */ @GetMapping("/payment/create") public CommonResult<Object> create(Payment payment) { // 这种方法不支持泛型 // return restTemplate.postForObject(PAYMENT_SRV_URL + "/payment/create", payment, CommonResult.class); ParameterizedTypeReference<CommonResult<Object>> responseType = new ParameterizedTypeReference<CommonResult<Object>>() { }; return restTemplate.exchange(PAYMENT_SRV_URL + "/payment/create", HttpMethod.POST, new HttpEntity<>(payment), responseType).getBody(); } /** * @param id * @return */ @GetMapping("/payment/get/{id}") public CommonResult<Object> getPayment(@PathVariable("id") Long id) { // 这种方法不支持泛型 // return restTemplate.getForObject(PAYMENT_SRV_URL + "/payment/get/" + id, CommonResult.class, id); ParameterizedTypeReference<CommonResult<Object>> responseType = new ParameterizedTypeReference<CommonResult<Object>>() { }; return restTemplate.exchange(PAYMENT_SRV_URL + "/payment/get/" + id, HttpMethod.GET, null, responseType).getBody(); } }
注意:
/payment/get/
路径中 get 后面的 / 不要忘了加,否则会一直报错 -
测试
# 调用接口查看结果 http://localhost/consumer/payment/get/1 http://localhost/consumer/payment/create?serial=aaaaa1
发现数据库中只有主键并没有数据
// 8001中的 create 不要忘记@RequestBody注解 --》 标记为json方式接收参数,之前为表单传参形式,可百度restTemplate表单传参 @PostMapping("/create") public CommonResult create(@RequestBody Payment payment) { return paymentService.create(payment); }
-
工程重构
-
观察问题
系统中有重复部分,重构
-
cloud-api-commons
-
pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.1.0</version> </dependency> </dependencies>
-
把 8001 和 80 的 entities 移到 commons 中
-
maven命令clean
-
订单80和支付8001分别改造
-
删除各自的原先有过的entities文件夹
-
各自粘贴POM内容
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency>
-
-
SpringCloud Eureka 服务注册与发现
Eureka基础知识
什么是服务治理
Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理
在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理
管理服务与服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
什么是服务注册
Eureka采用了CS的设计架构,Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息 比如 服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何rpc远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))
下左图是Eureka系统架构,右图是Dubbo的架构,请对比:
Eureka两组件
Eureka包含两个组件:Eureka Server和Eureka Client
-
Eureka Server 提供服务,注册服务
各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到 -
EurekaClient 通过注册中心进行访问
是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)
单机Eureka构建
eurekaServer端服务注册中心
IDEA生成eurekaServer端服务注册中心类似物业公司
- 建Module
cloud-eureka-server7001
-
改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>mscloud</artifactId> <groupId>com.atguigu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-eureka-server7001</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--eureka-server--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <dependency> <groupId>com.atguigu.springcloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--boot web actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--图形监控,以后的swagger和Hystrix要用的--> <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>
1.X和2.X的对比说明:
<!-- 以前的老版本(当前使用2018)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- 现在新版本(当前使用2020.2)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
-
写YML
server: port: 7001 eureka: instance: hostname: localhost #eureka服务端的实例名称 client: #false表示不向注册中心注册自己。 register-with-eureka: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 fetch-registry: false service-url: #设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个地址。 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
-
主启动
@SpringBootApplication @EnableEurekaServer public class EurekaMain7001 { public static void main(String[] args) { SpringApplication.run(EurekaMain7001.class, args); } }
-
测试
http://localhost:7001/
打开Eureka页面No application available 没有服务被发现,因为没有注册服务进来当然不可能有服务被发现
8001注册进Eureka成为提供者
EurekaClient 端 cloud-provider-payment8001 将注册进 EurekaServer 成为服务提供者 provider,类似学校对外提供授课服务
-
改POM
<!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
写YML
# application.yml eureka: instance: hostname: localhost client: # 表示是否将自己注册进EurekaServer默认为true register-with-eureka: true # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka
-
主启动
@SpringBootApplication @EnableEurekaClient public class PaymentMain8001 { public static void main(String[] args) { SpringApplication.run(PaymentMain8001.class,args); } }
-
测试
先要启动EurekaServer,
http://localhost:7001/
微服务注册名配置说明 ->
application.yml
中spring.application.name
值即为Eureka
服务注册名
Eureka自我保护机制
Eureka
页面显示红色英文EMERGENCY ! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCESARE NOT BEING EXPIRED JUST TO BE SAFE.
即代表自我保护机制启动
使用eureka.server.enable-self-preservation = false 可以禁用自我保护模式
80注册进Eureka成为消费者
EurekaClient 端 cloud-consumer-order80 将注册进 EurekaServer 成为服务消费者 consumer,类似去消费的各位
-
改POM
<!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
写YML
# application.yml server: port: 80 spring: application: name: cloud-order-service eureka: instance: # 修改Eureka界面的Status名称 默认主机名 instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 这个是设置鼠标放在status上的时候,出现的提示,设置ip地址显示 prefer-ip-address: true hostname: localhost client: # 表示是否将自己注册进EurekaServer默认为true register-with-eureka: true # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka healthcheck: # 开启健康检查,需要引入actuator enabled: true
-
主启动
@SpringBootApplication @EnableEurekaClient public class MainApp80 { public static void main(String[] args) { SpringApplication.run(MainApp80.class, args); } }
-
测试
先要启动EurekaServer——7001服务,再要启动服务提供者provider——8001服务
查看Eureka页面并调用接口查看结果
-
如果存在
Failed to bind properties under ‘eureka.client.service-url’ to java.util.Map<java.lang.String, java.lang.String>
报错,检查yml文件层次缩进和空格
集群Eureka构建
Eureka集群原理说明
问题:微服务RPC远程服务调用最核心的是什么?
高可用,试想你的注册中心只有一个only one, 它出故障了那就呵呵( ̄▽ ̄)"了,会导致整个为服务环境不可用,所以解决办法:搭建Eureka注册中心集群 ,实现负载均衡+故障容错
EurekaServer集群环境构建步骤
新建cloud-eureka-server7002
参考cloud-eureka-server7001,新建cloud-eureka-server7002
-
改POM
<artifactId>cloud-eureka-server7002</artifactId>
父工程 mscloud 的 POM
<module>cloud-eureka-server7002</module>
-
修改映射配置
找到C:\Windows\System32\drivers\etc路径下的hosts文件,修改映射配置添加进hosts文件
- 127.0.0.1 eureka7001.com
- 127.0.0.1 eureka7002.com
或
- 127.0.0.1 eureka7001.com eureka7002.com
原因:eureka集群 hostname,defaultZone不能一样
-
写YML
- 7001
server: port: 7001 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://eureka7002.com:7002/eureka/
- 7002
server: port: 7002 eureka: instance: hostname: eureka7002.com #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://eureka7001.com:7001/eureka/
-
主启动类
@EnableEurekaServer @SpringBootApplication public class CloudEurekaServer7002Application { public static void main(String[] args) { SpringApplication.run(CloudEurekaServer7002Application.class, args); } }
-
测试
分别访问7001和7002的Eureka页面
在7001页面
DS Replicas
看到 7002,在7002页面DS Replicas
看到 7001
不构建也可以在IDEA以多例方式启动两次eureka7001,第二次启动记得修改端口号(server.port)
需注释掉eureka7001服务pom文件的spring-boot-devtools,否则每次更改都会自动占用,在代码主启动类上启动代码即创建一个新的实例
Edit Configurations...(右上角启动按钮服务框下拉) -> Modify options(Alt + M) -> Allow multiple instances(Alt + U)
8001发布到2台Eureka集群
# application.yml
# 集群版
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
80发布到2台Eureka集群
#application.yml
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版
测试
先要启动EurekaServer,7001/7002服务,再要启动服务提供者provide_8001,最后再启动消费者_80
http://localhost/consumer/payment/get/1
支付服务提供者8002集群环境构建
参考cloud-provider-payment8001
可直接 cloud-provider-payment8001 按 f5 改名 cloud-provider-payment8002 然后 修改以下内容全部修改完成后,将 cloud-provider-payment8002 导入为Module (File -> Project Structure... -> Modules -> Import Module -> 选择对应文件夹)
-
pom.xml
<artifactId>cloud-provider-payment8002</artifactId>
-
application.yml
server: port: 8002
-
主启动
@EnableEurekaClient @SpringBootApplication public class CloudProviderPayment8002Application { public static void main(String[] args) { SpringApplication.run(CloudProviderPayment8002Application.class, args); } }
修改8001/8002的PaymentServiceImpl
-
8001
@Value("${server.port}") private String serverPort;
@Slf4j @Service public class PaymentServiceImpl implements PaymentService { @Resource private PaymentDao paymentDao; @Value("${server.port}") private String serverPort; @Override public CommonResult<Object> create(Payment payment) { int result = paymentDao.create(payment); log.info("*****插入操作返回结果:{}", result); if (result > 0) { return new CommonResult<>(200, "插入数据库成功-" + serverPort, result); } else { return new CommonResult<>(444, "插入数据库失败-" + serverPort); } } @Override public CommonResult<Object> getPaymentById(Long id) { Payment payment = paymentDao.getPaymentById(id); log.info("*****查询结果:{}", payment); if (payment != null) { return new CommonResult<>(200, "查询成功-" + serverPort, payment); } else { return new CommonResult<>(444, "没有对应记录,查询ID: " + id + "-" + serverPort); } } }
-
8002
同理
@Value("${server.port}") private String serverPort;
-
测试
查看
http://eureka7001.com:7001/
和http://eureka7002.com:7001/
的Application
信息调用
http://localhost:8001/payment/get/1
和http://localhost:8002/payment/get/1
调用
http://localhost/consumer/payment/get/1
-
bug
我们发现不管怎么刷新请求,请求的端口号一直是8001,原因是我们在80的controller中写死了
OrderController
//不要把地址写死 //public static final String PaymentSrv_URL = "http://localhost:8001"; // 通过在eureka上注册过的微服务名称调用 public static final String PAYMENT_SRV = "http://CLOUD-PAYMENT-SERVICE";
此时再测试,发现报错
java.net.UnknownHostException: CLOUD-PAYMENT-SERVICE
这又是什么情况?
现在注册中心不再是暴露出具体的端口号,而是微服务名称 CLOUD-PAYMENT-SERVICE,但是这个微服务名称代表的集群中有很多个,比如8001、8002…用哪个,它并不知道
需要再配置一下负载均衡
负载均衡
-
@LoadBlanced
使用 @LoadBalanced 注解赋予 RestTemplate 负载均衡的能力
cloud-consumer-order80#ApplicationContextConfig
@LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced //使用@LoadBalanced注解赋予RestTemplate负载均衡的能力 public RestTemplate restTemplate() { return new RestTemplate(); } }
-
测试
多次调用查看所访问的服务
负载均衡效果达到,8001/8002端口交替出现
提前剧透:Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号,且该服务还有负载功能了。
actuator微服务信息完善
主机名称:服务名称修改
-
当前问题
Eureka 页面 Status 含有主机名称
-
修改8001和8002
-
8001
application.yml
instance: instance-id: payment8001
server: port: 8001 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/springcloud2021?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: #defaultZone: http://localhost:7001/eureka defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 instance: // 默认 ${主机名}:${spring.application.name}:${server.port} instance-id: payment8001 mybatis: mapperLocations: classpath:mapper/*.xml type-aliases-package: com.atguigu.springcloud.entities # 所有Entity别名类所在包
-
8002
同理
instance: # 修改Eureka界面的Status名称 instance-id: payment8002
重启服务查看Eureka 页面 Status 变化
-
访问信息有IP信息提示
-
当前问题
鼠标移动到对应服务 Status 上,没有IP提示
-
修改8001和8002
-
8001
application.yml
instance: prefer-ip-address: true #访问路径可以显示IP地址
eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: #defaultZone: http://localhost:7001/eureka defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 instance: instance-id: payment8001 prefer-ip-address: true #访问路径可以显示IP地址
-
8002
同理
instance: # 设置鼠标放在status上的时候,出现的提示,设置ip地址显示 prefer-ip-address: true
-
服务发现Discovery
对于注册进eureka里面的微服务,可以通过服务发现来获得该服务的信息
主启动类添加
8001,8002同理
@EnableDiscoveryClient //服务发现
这个注解的作用主要是将来可以把服务信息暴露给消费端,
- 服务ip、端口
- 服务名称
从Spring Cloud Edgware开始,@EnableDiscoveryClient
或@EnableEurekaClient
可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。
Controller添加
8001,8002同理
@Resource
private DiscoveryClient discoveryClient;
@Value("${spring.application.name}")
private String applicationName;
@GetMapping(value = "/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String service : services) {
log.info(">>> service:{}", service);
}
List<ServiceInstance> instances = discoveryClient.getInstances(applicationName);
for (ServiceInstance instance : instances) {
log.info(">>> " + instance.getServiceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return this.discoveryClient;
}
测试
-
访问
http://localhost:8001/payment/discovery
查看日志(控制台打印)>>> service:cloud-payment-service >>> service:cloud-order-service >>> CLOUD-PAYMENT-SERVICE localhost 8002 http://localhost:8002 >>> CLOUD-PAYMENT-SERVICE localhost 8001 http://localhost:8001
Eureka自我保护
故障现象
概述:保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
如果在Eureka Server的首页看到以下这段提示,则说明Eureka进入了保护模式
导致原因
为什么会产生Eureka自我保护机制?
为了防止EurekaClient可以正常运行,但是 与 EurekaServer网络不通情况下,EurekaServer不会立刻将EurekaClient服务剔除
什么是自我保护模式?
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。
它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。一句话讲解:好死不如赖活着
综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
一句话:某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存
属于CAP里面的AP分支
怎么禁止自我保护
现在可以不用集群版了,只用开一个,在 application.yml 中把集群配置改为单机版
-
注册中心 eureakeServer 端 7001
出厂默认,自我保护机制是开启的 eureka.server.enable-self-preservation=true,使用eureka.server.enable-self-preservation = false 可以禁用自我保护模式
application.yml
server: #关闭自我保护机制,保证不可用服务被及时踢除 enable-self-preservation: false eviction-interval-timer-in-ms: 2000
server: port: 7001 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://eureka7002.com:7002/eureka/ server: # 服务端是否开启自我保护机制 (默认true) # 关闭自我保护机制,保证不可用服务被及时踢除 enable-self-preservation: false # 扫描失效服务的间隔时间(单位毫秒,默认是60*1000)即60秒 # 让服务端每隔2秒扫描一次,是服务能尽快的剔除 eviction-interval-timer-in-ms: 2000
关闭效果
打开7001
eureake
页面,看到红色字体RENEWALS ARE LESSER THAN THE THRESHOLD. THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.
-
生产者客户端
eureakeClient
端8001默认配置
eureka.instance.lease-renewal-interval-in-seconds=30 #单位为秒(默认是30秒) eureka.instance.lease-expiration-duration-in-seconds=90 #单位为秒(默认是90秒)
配置
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒) eureka.instance.lease-renewal-interval-in-seconds: 1 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务 eureka.instance.lease-expiration-duration-in-seconds: 2
server: port: 8001 spring: application: name: cloud-payment-service datasource: type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动包 com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/springcloud2021?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root eureka: client: #表示是否将自己注册进EurekaServer默认为true。 register-with-eureka: true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetchRegistry: true service-url: defaultZone: http://localhost:7001/eureka #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka # 集群版 instance: instance-id: payment8001 #访问路径可以显示IP地址 prefer-ip-address: true #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒) lease-renewal-interval-in-seconds: 1 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务 lease-expiration-duration-in-seconds: 2 mybatis: mapperLocations: classpath:mapper/*.xml type-aliases-package: com.atguigu.springcloud.entities # 所有Entity别名类所在包
-
测试
7001和8001都配置完成,先启动7001再启动8001,打开
http://eureka7001.com:7001/
模拟8001出现了故障关闭了,以前7001还会保留,但是现在直接被剔除
Zookeeper服务注册与发现
Eureka停止更新了怎么办
SpringCloud整合Zookeeper代替Eureka
注册中心Zookeeper
-
在 Linux上部署 Zookeeper
-
如果 Zookeeper 部署在 Linux 上,先关闭防火墙
systemctl stop firewalld
-
开启 Zookeeper
./zkServer.sh start
-
开启 Zookeeper 后,查看 ip
执行
ifconfig
然后查看inet
-
ping 一下 Windows 查看是否连通
-
最后打开 Zookeeper 客户端
./zkCli.sh
一. zk服务命令 1.启动ZK服务: bin/zkServer.sh start 2.查看ZK服务状态: bin/zkServer.sh status 3.停止ZK服务: bin/zkServer.sh stop 4.重启ZK服务: bin/zkServer.sh restart 5.连接服务器: zkCli.sh -server 127.0.0.1:2181 二. zk客户端命令 1.ls – 查看某个目录包含的所有文件,例如: [zk: 127.0.0.1:2181(CONNECTED) 1] ls / ls /path 2.ls2 – 查看某个目录包含的所有文件,与ls不同的是它查看到time、version等信息,例如: [zk: 127.0.0.1:2181(CONNECTED) 1] ls2 / 3.create – 创建znode,并设置初始内容,例如: [zk: 127.0.0.1:2181(CONNECTED) 1] create /test “test” Created /test 创建一个新的 znode节点“ test ”以及与它关联的字符串 create /path data 默认创建持久节点 create -s /path data 创建顺序节点 create -e /path data 创建临时节点 create /parent/sub/path /data 4.get – 获取znode的数据,如下: [zk: 127.0.0.1:2181(CONNECTED) 1] get /test get /path get /path0000000018 访问顺序节点必须输入完整路径 5.set – 修改znode内容,例如: [zk: 127.0.0.1:2181(CONNECTED) 1] set /test “ricky” 6.delete – 删除znode,例如: [zk: 127.0.0.1:2181(CONNECTED) 1] delete /test delete /path 删除没有子节点的节点 rmr /path 移除节点并且递归移除所有子节点 7.quit – 退出客户端 8.help – 帮助命令
服务提供者 Provider
-
新建一个模块 :cloud-provider-payment-8004
-
修改POM
<dependencies> <dependency> <groupId>com.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-zookeeper-discovery --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
编写YML
server: port: 8004 spring: application: name: cloud-provider-payment # 这里和之前不同 cloud: zookeeper: # 改为自己的 zookeeper 连接地址 connect-string: 192.168.159.11:2181
-
编写主启动类
@SpringBootApplication @EnableDiscoveryClient public class CloudProviderPayment8004Application { public static void main(String[] args) { SpringApplication.run(CloudProviderPayment8004Application.class, args); } }
-
Controller
@RestController @RequestMapping("/payment") public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping(value = "/zk") public String paymentZk() { return "springCloud with zookeeper:" + serverPort + "\t" + UUID.randomUUID(); } }
-
启动zookerper
./zkServer.sh start ./zkCli.sh ls /
-
启动8004
发现报错
Caused by: org.apache.zookeeper.KeeperException$UnimplementedException: KeeperErrorCode = Unimplemented for /services/cloud-provider-payment/f0c8b27d-bcd3-43ad-b1fc-ad7a2bdd1abd at org.apache.zookeeper.KeeperException.create(KeeperException.java:103) ~[zookeeper-3.5.3-beta.jar:3.5.3-beta-8ce24f9e675cbefffb8f21a47e06b42864475a60] at org.apache.zookeeper.KeeperException.create(KeeperException.java:51) ~[zookeeper-3.5.3-beta.jar:3.5.3-beta-8ce24f9e675cbefffb8f21a47e06b42864475a60] at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:1525) ~[zookeeper-3.5.3-beta.jar:3.5.3-beta-8ce24f9e675cbefffb8f21a47e06b42864475a60] at org.apache.curator.framework.imps.CreateBuilderImpl$17.call(CreateBuilderImpl.java:1181) ~[curator-framework-4.0.1.jar:4.0.1] at org.apache.curator.framework.imps.CreateBuilderImpl$17.call(CreateBuilderImpl.java:1158) ~[curator-framework-4.0.1.jar:4.0.1] at org.apache.curator.connection.StandardConnectionHandlingPolicy.callWithRetry(StandardConnectionHandlingPolicy.java:64) ~[curator-client-4.0.1.jar:na] at org.apache.curator.RetryLoop.callWithRetry(RetryLoop.java:100) ~[curator-client-4.0.1.jar:na] at org.apache.curator.framework.imps.CreateBuilderImpl.pathInForeground(CreateBuilderImpl.java:1155) ~[curator-framework-4.0.1.jar:4.0.1] at org.apache.curator.framework.imps.CreateBuilderImpl.protectedPathInForeground(CreateBuilderImpl.java:605) ~[curator-framework-4.0.1.jar:4.0.1] at org.apache.curator.framework.imps.CreateBuilderImpl.forPath(CreateBuilderImpl.java:595) ~[curator-framework-4.0.1.jar:4.0.1] at org.apache.curator.framework.imps.CreateBuilderImpl.forPath(CreateBuilderImpl.java:49) ~[curator-framework-4.0.1.jar:4.0.1] at org.apache.curator.x.discovery.details.ServiceDiscoveryImpl.internalRegisterService(ServiceDiscoveryImpl.java:236) ~[curator-x-discovery-4.0.1.jar:na] at org.apache.curator.x.discovery.details.ServiceDiscoveryImpl.registerService(ServiceDiscoveryImpl.java:191) ~[curator-x-discovery-4.0.1.jar:na] at org.springframework.cloud.zookeeper.serviceregistry.ZookeeperServiceRegistry.register(ZookeeperServiceRegistry.java:71) ~[spring-cloud-zookeeper-discovery-2.2.0.RELEASE.jar:2.2.0.RELEASE] ... 26 common frames omitted
看到报错信息里有
zookeeper-3.5.3-beta.jar
和spring-cloud-zookeeper-discovery-2.2.0.RELEASE.jar
,报错提示JAR包冲突因为
spring-cloud-zookeeper-discovery-2.2.0.RELEASE.jar
自带zookeeper
jar包为3.5.3版本,高于自己的 Zookeeper 版本 -
解决办法
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-zookeeper-discovery --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <!--排除zk3.5.3--> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <!--添加zk 对应的版本,按照自己的 Zookeeper 版本来--> <!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper --> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.11</version> <exclusions> <!-- 这里 slf4j-log4j12 也冲突了 --> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
-
重启 8004,没有报错成功启动
-
去 Zookeeper 上查看,是否注册进去
[zk: localhost:2181(CONNECTED) 0] ls / [services, zookeeper] [zk: localhost:2181(CONNECTED) 1] ls /services [cloud-provider-payment] [zk: localhost:2181(CONNECTED) 2] ls /services/cloud-provider-payment [da546915-d188-4f8e-a3c8-c26cbbaed45e]
访问 :
http://localhost:8004/payment/zk
服务消费者 Consumer
-
新建一个模块 :cloud-consumer-order-zk-80
-
修改POM
<dependencies> <dependency> <groupId>com.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-zookeeper-discovery --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId> <!--排除zk3.5.3--> <exclusions> <exclusion> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> </exclusion> </exclusions> </dependency> <!--添加zk 对应的版本,按照自己的 Zookeeper 版本来--> <!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper --> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.11</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
编写YML
server: port: 80 spring: application: name: cloud-consumer-order-zk # 这里和之前不同 cloud: zookeeper: connect-string: 192.168.159.11:2181
-
编写主启动类
@EnableDiscoveryClient @SpringBootApplication public class CloudConsumerOrderZk80Application { public static void main(String[] args) { SpringApplication.run(CloudConsumerOrderZk80Application.class, args); } }
-
Config
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
-
Controller
@Slf4j @RestController @RequestMapping("/consumer") public class OrderController { // 和 Eureka 不一样,这里 服务名 大小写得保持一致 public static final String PAYMENT_SRV_URL = "http://cloud-provider-payment"; @Resource private RestTemplate restTemplate; @GetMapping("/payment/zk") public String payment() { return restTemplate.getForObject(PAYMENT_SRV_URL + "/payment/zk", String.class); } }
-
启动测试
# zookeper [zk: localhost:2181(CONNECTED) 10] ls /services [cloud-provider-payment, cloud-consumer-order-zk]
访问
http://localhost/consumer/payment/zk
SpringCloud Consul 服务注册与发现
Consul简介
是什么
What is Consul?
HashiCorp Consul is a service networking solution that enables teams to manage secure network connectivity between services and across on-prem and multi-cloud environments and runtimes. Consul offers service discovery, service mesh, traffic management, and automated updates to network infrastructure device. You can use these features individually or together in a single Consul deployment.
Consul 是一套开源的分布式服务发现和配置管理系统,由 HashiCorp 公司用 Go 语言开发
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案
它具有很多优点。包括: 基于 raft 协议,比较简洁; 支持健康检查, 同时支持 HTTP 和 DNS 协议 支持跨数据中心的 WAN 集群 提供图形界面 跨平台,支持 Linux、Mac、Windows
能干嘛
The key features of Consul are:
Service Discovery: Clients Of Consul can register a service, such as api or mysql,and other clients can
use Consul to discover providers Of a given service. Using either DNS or HTTR, applications can easily find
the services they depend upon.
Health Checking: Consul clients can provide any number of health checks, either associated with a given
service ("is the webserver returning 200 OK"), or with the local node ("is memory utilization below 90%")
This information can be used by an operator to monitor cluster health, and it is used by the service
discovery components to route traffic away from unhealthy hosts.
KV Store: AppIications can make use Consul's hierarchical key/value store for any number of purposes,
including dynamic configuration, feature flagging, coordination, leader election, and more. The simple
HTTP API makes it easy tO use.
Secure Service Communication: Consul can generate and distribute TLS certificates for services to
establish mutual TLS connections. lntentions can be used to define which services are allowed to
communicate. Service segmentation can be easily managed with intentions that can be changed in real
time instead of using complex network topologies and static firewall rules.
Multi Datacenter: Consul supports multiple datacenters out Of the box. This means users Of Consul do not
have to worry about building additional layers abstraction to grow to multiple regions.
Spring Cloud Consul 具有如下特性:
- 服务发现:提供HTTP和DNS两种发现方式
- 健康监测:支持多种方式,HTTP、TCP、Docker、Shell脚本定制化监控
- KV存储:Key、Value的存储方式
- 多数据中心:Consul支持多数据中心
- 可视化Web界面
去哪下
怎么玩
🔗, Spring Cloud Consul 中文文档 参考手册 中文版
安装并运行Consul
官网安装说明
下载和安装
下载完成后只有一个consul.exe文件,硬盘路径下双击运行,查看版本号信息
直接在软件目录中输入 cmd 进入命令控制台
# consul --version:查看 consul 版本号
D:\javaUtils\consul>consul --version
Consul v1.8.3
Revision a9322b9c7
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)
使用开发模式启动
consul agent -dev
通过以下地址可以访问Consul的首页:http://localhost:8500
服务提供者
-
新建Module支付服务cloud-providerconsul-payment-8006
-
改POM
<dependencies> <!--SpringCloud consul-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
改YML
###consul服务端口号 server: port: 8006 spring: application: name: consul-provider-payment ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: #hostname: 127.0.0.1 service-name: ${spring.application.name}
-
主启动类
@EnableDiscoveryClient @SpringBootApplication public class CloudProviderConsulPayment8006Application { public static void main(String[] args) { SpringApplication.run(CloudProviderConsulPayment8006Application.class, args); } }
-
Controller
@Slf4j @RestController @RequestMapping("/payment") public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping("/consul") public String paymentInfo() { return "springcloud with consul: " + serverPort + "\t" + UUID.randomUUID(); } }
-
验证测试
访问:
http://localhost:8500/
, 查看Services
和Nodes
访问:
http://localhost:8006/payment/consul
服务消费者
-
新建Module消费服务cloud-consumerconsul-order-80
-
改POM
<dependencies> <!--SpringCloud consul-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
改YML
###consul服务端口号 server: port: 80 spring: application: name: cloud-consumer-order-consul ####consul注册中心地址 cloud: consul: host: localhost port: 8500 discovery: #hostname: 127.0.0.1 service-name: ${spring.application.name}
-
主启动类
@EnableDiscoveryClient @SpringBootApplication public class CloudConsumerOrderConsul80Application { public static void main(String[] args) { SpringApplication.run(CloudConsumerOrderConsul80Application.class, args); } }
-
业务代码
@Configuration public class ApplicationContextConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } } @Slf4j @RestController @RequestMapping("/consumer") public class OrderController { // 和 Eureka 不一样,这里 服务名 大小写得保持一致 public static final String PAYMENT_SRV_URL = "http://consul_provider_payment"; @Resource private RestTemplate restTemplate; @GetMapping("/payment/consul") public String payment() { return restTemplate.getForObject(PAYMENT_SRV_URL + "/payment/consul", String.class); } }
-
验证测试
访问
http://localhost:8500
访问
http://localhost/consumer/payment/consul
三个注册中心异同点
组件名 | 语言 | CAP | 服务健康检查 | 对外暴露接口 | SpringClould集成 |
---|---|---|---|---|---|
Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |
CAP
C:Consistency(强一致性)
A:Availability(可用性)
P:Partition tolerance(分区容错性)
CAP理论关注粒度是数据,而不是整体系统设计的策略
经典CAP图
最多只能同时较好的满足两个。
CAP理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,因此,根据 CAP 原理将 NoSQL 数据库分成了满足 CA 原则、满足 CP 原则和满足 AP 原则三大类:
CA - 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大
CP - 满足一致性,分区容忍必的系统,通常性能不是特别高
AP - 满足可用性,分区容忍性的系统,通常可能对一致性要求低一些
AP(Eureka):
AP架构:
当网络分区出现后,为了保证可用性,系统B可以返回旧值,保证系统的可用性。
结论:违背了一致性C的要求,只满足可用性和分区容错,即AP
CP(Zookeeper/Consul)
CP架构:
当网络分区出现后,为了保证一致性,就必须拒接请求,否则无法保证一致性
结论:违背了可用性A的要求,只满足一致性和分区容错,即CP
SpringCloud Ribbon 负载均衡服务调用
概念
是什么
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
官网资料
Ribbon目前也进入维护模式 查看🔗README.md
存在Project Status: On Maintenance
说明
未来替换方案Spring Cloud Loadbalancer
能干嘛
LB(负载均衡)
LB负载均衡(Load Balance)是什么?
- 简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用),常见的负载均衡有软件Nginx,LVS,硬件 F5等
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别?
- Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的
- Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术
集中式LB:
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5, 也可以是软件,如nginx), 由该设施负责把访问请求通过某种策略转发至服务的提供方;
进程内LB:
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器
Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址
举例
前面我们讲解过了80通过轮询负载访问8001/8002【详情见eureka】
一句话
负载均衡+RestTemplate调用
Ribbon负载均衡演示
架构说明
Ribbon在工作时分成两步:
第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址;其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权
总结:Ribbon其实就是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。
POM
之前写样例时候没有引入spring-cloud-starter-ribbon也可以使用ribbon
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
猜测: spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用
证明: 可以看到spring-cloud-starter-netflix-eureka-client 确实引入了Ribbon
以上:POM 中既可以加 ribbon 的依赖也可以不用加,因为 eureka 已经集成了 ribbon
RestTemplate的使用
官网
getForObject方法/getForEntity方法
getForObject方法:返回对象为响应体中数据转化成的对象,基本上可以理解为Json
restTemplate.getForObject(PAYMENT_SRV_URL + "/payment/get/" + id, CommonResult.class, id); // 不支持泛型
restTemplate.getForObject(PAYMENT_SRV_URL + "/payment/zk", String.class)
getForEntity方法:返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
restTemplate.getForEntity(PAYMENT_SRV_URL + "/payment", String.class).getBody();
postForObject方法/postForEntity方法
与GET对应方法意义一致,只是请求方法为POST,其余请求方法类似
restTemplate.postForEntity(PAYMENT_SRV_URL + "/payment/create", payment, CommonResult.class).getBody();
exchange
// 支持泛型写法
ParameterizedTypeReference<CommonResult<Object>> responseType = new ParameterizedTypeReference<CommonResult<Object>>() {};
restTemplate.exchange(PAYMENT_SRV_URL + "/payment/create", HttpMethod.POST, new HttpEntity<>(payment), responseType).getBody();
GET请求方法
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables);
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables);
<T> T getForObject(URI url, Class<T> responseType);
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables);
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables);
<T> ResponseEntity<T> getForEntity(URI var1, Class<T> responseType);
POST请求方法
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);
<T> T postForObject(URI url, @Nullable Object request, Class<T> responseType);
<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);
<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);
<T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType);
Ribbon核心组件IRule
IRule
IRule
:根据特定算法中从服务列表中选取一个要访问的服务
AbstractLoadBalancerRule
——快捷键:Ctrl+Alt+Shift+U
生成类图 或者 Ctrl + H
查看所有实现类
- com.netflix.loadbalancer.RoundRobinRule:轮询
- com.netflix.loadbalancer.RandomRule:随机
- com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
- WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
- BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
- ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器
如何替换(自定义路由规则)
-
修改cloud-consumer-order80
-
注意配置细节
官方文档明确给出了警告:
这个自定义配置类不能放在@ComponentScan所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了。
官网原文
WARNING The FooConfiguration has to be @Configuration but take care that it is not in a @ComponentScan for the main application context, otherwise it will be shared by all the @RibbonClients. If you use @ComponentScan (or @SpringBootApplication) you need to take steps to avoid it being included (for instance put it in a separate, non-overlapping package, or specify the packages to scan explicitly in the @ComponentScan). 警告 FooConfiguration必须有@Configuration,但注意它并不在主应⽤上下⽂的@ComponentScan中,否则它会被所有的@RibbonClients分享(意思就是覆盖所有客户端的默认值)。如果开发⼈员使⽤@ComponentScan(或@SpringBootApplication),那就必须采取措施避免被覆盖到(例如将其放⼊⼀个独⽴的,不重叠的包中,或以@ComponentScan指明要扫描的包。
即不能放在主启动类所在的包下,因为@SpringBootApplication包含了@ComponentScan,它会扫描这个类所在的包及其子包
-
新建package
在主启动类外(非主启动类所在包)创建新的包或者利用 @ComponentScan 排除对应的包
@ComponentScan(basePackages = {"com.wang"}, excludeFilters = { @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.wang.rule.*") })
-
自定义规则类
上面创建的包下新建
@Configuration public class ConsumerRule { /** * 方法名不要与类名相同 */ @Bean public IRule consumerRuleI() { // 定义为随机 return new RandomRule(); } }
-
主启动类下添加
@RibbonClient
// name 必填,且需为大写 @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = ConsumerRule.class)
问题
这里的 CLOUD-PAYMENT-SERVICE 如果是大写就是我们定义的随机访问规则;
如果是小写 cloud-payment-service 则我们定义的不生效,它还是轮询访问
如果是小写,但是把 ConsumerRule 放在 @ComponentScan 可以扫描的包下时则又可以随机访问了
-
测试
http://localhost/consumer/payment/get/1
, 刷新后就不再是轮询了,而是随机负载均衡了
Ribbon负载均衡算法
原理
负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标 ,每次服务重启动后rest接口计数从1开始
List<ServiceInstance> 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 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
当总请求数位3时:3 % 2 =1 对应下标位置为1 ,则获得服务地址为127.0.0.1:8001
当总请求数位4时:4 % 2 =0 对应下标位置为0 ,则获得服务地址为127.0.0.1:8002
如此类推…
RoundRobinRule源码
IDEA -> 双击 Shift -> 搜索RoundRobinRule即可查看,视频链接
// com.netflix.loadbalancer.RoundRobinRule类
public Server choose(ILoadBalancer lb, Object key) {
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();//获取原子的值
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next)) //CAS
return next;
}
}
手写负载均衡器
自己试着写一个本地负载均衡器试试
-
cloud-eureka-server7001/7002集群启动
-
cloud-provider-payment8001/8002微服务改造
// Eureka配置文件改动 -> defaultZone(集群) // PaymentController // ... @Value("${spring.application.name}") private String applicationName; @Value("${server.port}") private String port; // ... @GetMapping("/get/lb") public String getPayment() { return applicationName + ":" + port + " " + Instant.now().toString(); } // ...
-
cloud-consumer-order80微服务改造
// Eureka配置文件改动 -> defaultZone(集群) // 编写自己的负载均衡算法 public interface LoadBalancer { /** * 从服务列表选出一台机器进行访问 * * @param serviceInstances 服务列表 * @return 服务器实例 */ ServiceInstance instances(List<ServiceInstance> serviceInstances); } @Slf4j @Component public class ConsumerLoadBalancer implements LoadBalancer { private final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0); @Override public ServiceInstance instances(List<ServiceInstance> serviceInstances) { int index = getAndIncrement() % serviceInstances.size(); return serviceInstances.get(index); } public final int getAndIncrement() { int current; int next; do { current = this.ATOMIC_INTEGER.get(); next = current >= Integer.MAX_VALUE ? 0 : current + 1; } while (!this.ATOMIC_INTEGER.compareAndSet(current, next)); //atomicInteger.compareAndSet(expect, update), //比较except和atomicInteger当前value,如果相等返回true并将atomicInteger当前value更新为update,否则返回fasle log.info("*********第 {} 次访问", next); return next; } } // ApplicationContextConfig @Configuration public class ApplicationContextConfig { @Bean // @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } } // CloudConsumerOrder80Application @EnableEurekaClient @SpringBootApplication // @ComponentScan(basePackages = {"com.wang"}, // excludeFilters = { // @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com.wang.rule.*") // }) // // name 必填,且需为大写 // @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = ConsumerRule.class) public class CloudConsumerOrder80Application { public static void main(String[] args) { SpringApplication.run(CloudConsumerOrder80Application.class, args); } } // OrderController // ... public static final String PAYMENT_SRV = "CLOUD-PAYMENT-SERVICE"; public static final String PAYMENT_SRV_URL = "http://" + PAYMENT_SRV; @Resource private LoadBalancer loadBalancer; @Resource private DiscoveryClient discoveryClient; @GetMapping(value = "/consumer/payment/lb") // ... public String getPayment(){ List<ServiceInstance> instances = discoveryClient.getInstances(PAYMENT_SRV); if (CollectionUtils.isEmpty(instances)) { return null; } ServiceInstance serviceInstance = loadBalancer.instances(instances); return restTemplate.getForObject(serviceInstance.getUri() + "/payment/get/lb", String.class); } // ...
SpringCloud OpenFeign 服务接口调用
概述
Feign是一个声明式WebService客户端,使用Feign能让编写Web Service客户端更加简单,它的使用方法是定义一个服务接口然后在上面添加注解,Feign也支持可拔插式的编码器和解码器,Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters,Feign可以与Eureka和Ribbon组合使用以支持负载均衡
Feign是一个声明式的Web服务客户端,让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可
能干嘛
Feign旨在使编写Java Http客户端变得更容易,前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成了Ribbon,利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用
Feign和OpenFeign两者区别
-
Feign:
- Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端
- Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务
- Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> </dependency>
-
OpenFeign:
- OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等
- OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
OpenFeign使用步骤
SpringClouldOpenFeign官网(找配置最好看官网)
接口+注解:微服务调用接口+@FeignClient
-
新建cloud-consumer-order-feign-80
Feign在消费端使用
-
POM.xml
<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.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--一般基础通用配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
application.yml
server: port: 80 eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
-
主启动类
@EnableFeignClients
@EnableFeignClients @SpringBootApplication public class CloudConsumerOrderFeignApplication { public static void main(String[] args) { SpringApplication.run(CloudConsumerOrderFeignApplication.class, args); } }
-
业务类
业务逻辑接口+@FeignClient配置调用provider服务
新建PaymentFeign 接口并新增注解@FeignClient
// @FeignClient value 的内容就是 Eureka 中要调用的微服务的名 @FeignClient("CLOUD-PROVIDER-PAYMENT") public interface PaymentFeign { @GetMapping("/payment/get/{id}") CommonResult<Object> getPaymentById(@PathVariable("id") Long id); }
@FeignClient value 的内容就是 Eureka 中要调用的微服务的名, PaymentFeign 中的内容是复制的 被调用服务 的 Controller 的方法头(与方法名无关,只与路径有关)
控制层Controller
@Slf4j @RestController @RequestMapping("/consumer") public class OrderController { @Resource private PaymentFeign paymentFeign; @GetMapping(value = "payment/get/{id}") public CommonResult<Object> getPaymentById(@PathVariable("id") Long id) { return paymentFeign.getPaymentById(id); } }
-
测试
启动eureka -> 启动 cloud-provider-payment-8001/8002 -> 启动 cloud-consumer-order-feign-80
访问
http://localhost:81/consumer/payment/get/2
OpenFeign超时控制
-
模拟超时
超时设置,故意设置超时演示出错情况,服务提供方8001和8002故意写暂停程序
@GetMapping("/get/timeOut") public String getPaymentTimeOut() { try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { log.info("发生异常 ", e); } return applicationName + ":" + port + " " + Instant.now().toString(); }
服务消费方80添加超时方法,
// PaymentFeign @GetMapping("/payment/get/timeOut") String getPaymentTimeOut(); // OrderController @GetMapping(value = "/payment/get/timeOut") public String getPaymentTimeOut() { return paymentFeign.getPaymentTimeOut(); }
-
测试
访问:
http://localhost:8001/payment/get/timeout
正常请求访问:
http://localhost:80/consumer/payment/get/timeOut
得到结果
{ "timestamp": "2022-12-15T07:15:18.609+0000", "status": 500, "error": "Internal Server Error", "message": "Read timed out executing GET http://CLOUD-PROVIDER-PAYMENT/payment/get/timeOut", "trace": "feign.RetryableException: ...", "path": "/consumer/payment/get/timeOut" }
-
分析
-
为什么自测的8001不报错,而80调用报错了呢?
OpenFeign默认等待1秒钟,超过后报错
-
OpenFeign超时控制是什么?
默认Feign客户端只等待一秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不想等待了,直接返回报错。为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制
-
OpenFeign默认支持Ribbon
-
YML文件里需要开启OpenFeign客户端超时控制
# 这种写法不做研究,因为目前很多项目都是移除 ribbon 运行 # 设置feign客户端超时时间(OpenFeign默认支持ribbon) ribbon: #指的是建立连接后从服务器读取到可用资源所用的时间 ReadTimeout: 5000 #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ConnectTimeout: 5000
-
设置feign超时时间
feign: client: config: default: # 日志级别 # loggerLevel: full # 超时设置 5 秒超时 connectTimeout: 5000 readTimeout: 5000
-
补充
方案二:
feign: client: config: default: # 日志级别 loggerLevel: full # 超时设置 connectTimeout: 1500 readTimeout: 1500 feignName: connectTimeout: 5000 readTimeout: 5000 # 断路器 circuitbreaker: enabled: true
// 为 PaymentFeign 设置上 name 属性, // 默认情况下 feignName 是不能重复的,如果有重复,可以指定 contextId,此时 在注册Feign Client Configuration的时候需要一个名称,名称是通过getClientName方法获取的,即为 contextId 名称,此时配置文件的 feignName 也要变为 contextId 的值 // FeignClient#value的默认取name的值 @FeignClient(name = "feignName", contextId="", path = "/payment") public interface PaymentFeign { @PostMapping("/create") PaymentVo create(@RequestBody @Validated PaymentDto paymentDto); }
这样做,我们就可以减少影响,减少下游的影响。
# 还有一点我门需要注意,该配置不支持热更新,如果我们修改超时时间过后,需要重启服务。 如果我们不重启的,可以修改配置, 实现 feign.client.refresh-enabled=true
-
OpenFeign日志打印功能
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。说白了就是对Feign接口的调用情况进行监控和输出。
日志级别
- NONE:默认的,不显示任何日志
- BASIC:仅记录请求方法、URL、响应状态码及执行时间
- HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息
- FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据
配置日志bean
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
// 去掉 FeignConfig 的 @Configuration 注解
// 可通过 @FeignClient(configuration = FeignConfig.class) 来指定某个feign接口使用此配置
YML文件里需要开启日志的Feign客户端
logging:
level:
# feign日志以什么级别监控哪个接口
com.atguigu.springcloud.service.PaymentFeignService: debug
配置文件配置(推荐使用)
feign:
client:
config:
default:
# 日志级别
loggerLevel: full
logging:
level:
com.wang.feign.*: DEBUG
# 如果我们同时创建 bean 和配置属性,配置属性将获胜。它将覆盖值。但是如果你想把优先级改成,可以改成feign.client.default-to-properties=false
测试
访问:http://localhost/consumer/payment/get/2
看到日志
2022-12-15 17:01:02.326 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] ---> GET http://CLOUD-PROVIDER-PAYMENT/payment/get/2 HTTP/1.1
2022-12-15 17:01:02.326 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] ---> END HTTP (0-byte body)
2022-12-15 17:01:02.636 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] <--- HTTP/1.1 200 (309ms)
2022-12-15 17:01:02.636 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] connection: keep-alive
2022-12-15 17:01:02.636 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] content-type: application/json
2022-12-15 17:01:02.636 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] date: Thu, 15 Dec 2022 09:01:02 GMT
2022-12-15 17:01:02.636 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] keep-alive: timeout=60
2022-12-15 17:01:02.636 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] transfer-encoding: chunked
2022-12-15 17:01:02.636 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById]
2022-12-15 17:01:02.637 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] {"code":200,"message":"查询成功-8002","data":{"id":2,"serial":"A"}}
2022-12-15 17:01:02.637 DEBUG 16472 --- [p-nio-81-exec-4] com.wang.feign.PaymentFeign : [PaymentFeign#getPaymentById] <--- END HTTP (71-byte body)
SpringCloud Hystrix 断路器
概述
分布式系统面临的问题
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败
服务雪崩:
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”,如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和,比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加、备份队列、线程和其他系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统,所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩
Hystrix 是什么
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。
Hystrix能干嘛
服务降级、服务熔断、接近实时的监控······
被动修复bugs、不再接受合并请求、不再发布新版本
Hystrix重要概念
服务降级
服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,fallback
哪些情况会出发降级?
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
服务熔断
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
就是保险丝:服务的降级->进而熔断->恢复调用链路
服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
Hystrix案例
-
新建
cloud-provider-payment-hystrix-8008
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-provider-payment-hystrix-8008</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <zookeeper.version>3.4.11</zookeeper.version> </properties> <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.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
application.yml
server: port: 8008 spring: application: name: cloud-provider-payment-hystrix main: allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册 eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka # 单机版 # defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka # 集群版
-
主启动
@SpringBootApplication @EnableEurekaClient //本服务启动后会自动注册进eureka服务中(也可以不写) @EnableDiscoveryClient public class CloudProviderPaymentHystrix8008Application { public static void main(String[] args) { SpringApplication.run(CloudProviderPaymentHystrix8008Application.class, args); } }
-
业务类
// service public interface PaymentService { /** * 正常访问一切 OK */ String paymentInfoOk(Integer id); /** * 超时访问,演示降级 */ String paymentInfoTimeOut(Integer id); } public class PaymentServiceImpl implements PaymentService { @Value("${server.port}") private String serverPort; @Override public String paymentInfoOk(Integer id) { return String.format("paymentInfo_OK, 线程池: %s, port: %s, id: %d \t (*^_^*)", Thread.currentThread().getName(), serverPort, id); } @Override public String paymentInfoTimeOut(Integer id) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { log.error("异常 ", e); } return String.format("paymentInfo_TimeOut, 线程池: %s, port: %s, id: %d \t ヾ(≧▽≦*)o", Thread.currentThread().getName(), serverPort, id); } } // controller @Slf4j @RestController @RequestMapping("/payment") public class PaymentController { @Resource private PaymentService paymentService; @GetMapping("/hystrix/ok/{id}") public String paymentInfoOk(@PathVariable("id") Integer id) { String result = paymentService.paymentInfoOk(id); log.info("****result: " + result); return result; } @GetMapping("/hystrix/timeout/{id}") public String paymentInfoTimeOut(@PathVariable("id") Integer id) { String result = paymentService.paymentInfoTimeOut(id); log.info("****result: " + result); return result; } }
-
测试
启动
eureka7001
,hystrix8008
访问:
http://localhost:8001/payment/hystrix/ok/1
,http://localhost:8001/payment/hystrix/timeout/1
上述module均OK,以上述为根基平台,从正确->错误->降级熔断->恢复
-
高并发测试
上述在非高并发情形下,还能勉强满足,利用Jmeter压测测试:
开启Jmeter,来20000个并发压死8008,20000个请求都去访问
/payment/hystrix/timeout/2
再来一个访问:
http://localhost:8008/payment/hystrix/ok/1
看演示结果:
/payment/hystrix/ok/1
在转圈圈,为什么会被卡死?-
tomcat的默认的工作线程数被打满了,没有多余的线程来分解压力和处理
-
Jmeter压测结论:
上面还是服务提供者8008自己测试,假如此时外部的消费者80也来访问,那消费者只能干等,最终导致消费端80不满意,服务端8008直接被拖死
-
-
构建 cloud-consumer-order-hystrix-80
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-consumer-order-hystrix-80</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>com.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--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> <!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--一般基础通用配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
application.yml
server: port: 80 spring: application: name: cloud-consumer-order-hystrix eureka: instance: # 修改Eureka界面的Status名称 instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} # 设置鼠标放在status上的时候,出现的提示,设置ip地址显示 prefer-ip-address: true hostname: localhost client: # 表示是否将自己注册进EurekaServer默认为true register-with-eureka: true # 是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka # 单机版 # defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka # 集群版
-
主启动类
@EnableFeignClients @SpringBootApplication public class CloudConsumerOrderHystrix80Application { public static void main(String[] args) { SpringApplication.run(CloudConsumerOrderHystrix80Application.class, args); } }
-
业务代码
// @FeignClient value 的内容就是 Eureka 中要调用的微服务的名 @FeignClient(value = "CLOUD-PROVIDER-PAYMENT-HYSTRIX", path = "/payment") public interface PaymentFeign { @GetMapping("/hystrix/ok/{id}") String paymentInfoOk(@PathVariable("id") Integer id); @GetMapping("/hystrix/timeout/{id}") String paymentInfoTimeOut(@PathVariable("id") Integer id); } @Slf4j @RestController @RequestMapping("/consumer") @RequiredArgsConstructor public class OrderController { private final PaymentFeign paymentFeign; @GetMapping("/payment/hystrix/ok/{id}") public String paymentInfoOk(@PathVariable("id") Integer id) { return paymentFeign.paymentInfoOk(id); } @GetMapping("/payment/hystrix/timeOut/{id}") public String paymentInfoTimeOut(@PathVariable("id") Integer id) { return paymentFeign.paymentInfoTimeOut(id); } }
-
测试
正常访问:
http://localhost/consumer/payment/hystrix/ok/1
高并发测试
-
2W个线程压服务提供方
cloud-provider-payment-hystrix-8008
-
消费端80微服务再去访问正常的Ok微服务8001地址
-
http://localhost/consumer/payment/hystrix/ok/1
-
消费者80,o(╥﹏╥)o
-
要么转圈圈等待
-
要么消费端报超时错误
There was an unexpected error (type=Internal Server Error, status=500). Read timed out executing GET http://CLOUD-PROVIDER-PAYMENT-HYSTRIX/payment/hystrix/ok/1 feign.RetryableException: Read timed out executing GET http://CLOUD-PROVIDER-PAYMENT-HYSTRIX/payment/hystrix/ok/1
-
-
故障现象和导致原因
- 8008同一层次的其它接口服务被困死,因为tomcat线程池里面的工作线程已经被挤占完毕
- 80此时调用8008,客户端访问响应缓慢,转圈圈
上诉结论:正因为有上述故障或不佳表现,才有我们的降级/容错/限流等技术诞生
如何解决?解决的要求
超时导致服务器变慢(转圈):超时不再等待
出错(宕机或程序运行出错):出错要有兜底
解决:
- 对方服务(8008)超时了,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8008)宕机了,调用者(80)不能一直卡死等待,必须有服务降级
- 对方服务(8008)OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者)自己处理降级
服务降级
降级配置:@HystrixCommand
8008先从自身找问题:设置自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作服务降级fallback
8008fallback
-
业务类添加降级
... public class PaymentController { ... @GetMapping("/hystrix/abnormal/{id}") public String paymentInfoAbnormal(@PathVariable("id") Integer id) { String result = paymentService.paymentInfoAbnormal(id); log.info("****result: " + result); return result; } } ... public class PaymentServiceImpl implements PaymentService { ... @Override @HystrixCommand(fallbackMethod = "paymentInfoFallback", commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) public String paymentInfoTimeOut(Integer id) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { log.error("异常 ", e); } return String.format("paymentInfo_TimeOut, 线程池: %s, port: %s, id: %d \t ヾ(≧▽≦*)o", Thread.currentThread().getName(), serverPort, id); } @Override @HystrixCommand(fallbackMethod = "paymentInfoFallback") public String paymentInfoAbnormal(Integer id) { throw new RuntimeException("自定义异常"); } public String paymentInfoFallback(Integer id, Throwable throwable) { // https://blog.csdn.net/varyall/article/details/113749263 // package com.netflix.hystrix.exception // 可根据异常类型来自定义输出信息 if (Objects.nonNull(throwable)) { if (throwable instanceof HystrixTimeoutException) { return String.format("paymentInfo_Fallback, 线程池: %s, port: %s, id: %d, 执行超时\t /(ㄒoㄒ)/~~", Thread.currentThread().getName(), serverPort, id); } else { return String.format("paymentInfo_Fallback, 线程池: %s, port: %s, id: %d, 异常:%s\t /(ㄒoㄒ)/~~", Thread.currentThread().getName(), serverPort, id, throwable.getMessage()); } } return String.format("paymentInfo_Fallback, 线程池: %s, port: %s, id: %d, /(ㄒoㄒ)/~~", Thread.currentThread().getName(), serverPort, id); } }
@HystrixCommand报异常后如何处理?
一旦调用服务方法失败并抛出了错误信息后,会自动调用@HystrixCommand标注好的fallbackMethod调用类中的指定方法(写在当前类外面测试失败,还是就写在当前类里面吧)
paymentInfoAbnormal
方法制造异常paymentInfoTimeOut
方法接受运行3秒,实际休眠5秒- 降级方法都为
paymentInfoFallback
当前服务不可用了,做服务降级,兜底的方案都是
paymentInfoFallback
-
主启动类激活
添加新的注解
@EnableCircuitBreaker
,也可以添加@EnableHystrix
这两个注解都是激活hystrix的功能,根据@EnableHystrix源码可得出来结论,只需要在服务启动类加入@EnableHystrix注解即可,无须增加@EnableCircuitBreaker注解,因为@EnableHystrix注解已经涵盖了EnableCircuitBreaker的功能。
-
测试
访问:
http://localhost:8008/payment/hystrix/timeout/1
得到结果paymentInfo_Fallback, 线程池: HystrixTimer-1, port: 8008, id: 1, 执行超时 /(ㄒoㄒ)/~~
访问:
http://localhost:8008/payment/hystrix/abnormal/1
得到结果paymentInfo_Fallback, 线程池: hystrix-PaymentServiceImpl-3, port: 8008, id: 1, 异常:自定义异常 /(ㄒoㄒ)/~~
80fallback
80订单微服务,也可以更好的保护自己,自己也依样画葫芦进行客户端降级保护
题外话,切记:我们自己配置过的热部署方式对java代码的改动明显,但对@HystrixCommand内属性的修改建议重启微服务
-
主启动类添加
@EnableHystrix
-
application.yml
feign: hystrix: # 设置为 false, 否则 hystrix 对 feign 默认超时时间为 1s, 影响下面测试 enabled: false client: config: default: # 日志级别 # 超时设置 5 秒超时 # 防止由于服务器处理时间过长而阻止调用方。 connectTimeout: 5000 # 从建立连接时开始应用,并在返回响应时间过长时触发。 readTimeout: 5000 #如下配置建议在低版本使用 #feign: # hystrix: # enabled: true # #hystrix: # command: # default: # execution: # isolation: # thread: # timeoutInMilliseconds: 5000 ##设置feign客户端超时时间(OpenFeign默认支持ribbon) #ribbon: # #指的是建立连接后从服务器读取到可用资源所用的时间 # ReadTimeout: 5000 # #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 # ConnectTimeout: 5000
在于feign:hystrix:enabled: true的作用,官网解释“Feign将使用断路器包装所有方法”,也就是将@FeignClient标记的service接口下所有的方法进行了hystrix包装(类似于在这些方法上加了一个@HystrixCommand) 配置文件这里的 timeoutInMilliseconds 并不是覆盖注解中的设置,而是两者取较低值,同时也会算上 ribbon: ReadTimeout 的值,也就是三者取最低值。
-
OrderController
... public class OrderController { ... @GetMapping("/payment/hystrix/timeout/{id}") @HystrixCommand(fallbackMethod = "paymentFallback", commandProperties = { @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "1500") }) public String paymentInfoTimeOut(@PathVariable("id") Integer id) { return paymentFeign.paymentInfoTimeOut(id); } private String paymentFallback(Integer id, Throwable throwable) { if (Objects.nonNull(throwable)) { // 如果 设置 feign.hystrix.enabled: true, 且触发 feign.hystrix 调用的超时,异常类型会改变 if (throwable instanceof HystrixTimeoutException) { return String.format("payment_Fallback, 线程池: %s, id: %d, 超时\t `(*>﹏<*)′", Thread.currentThread().getName(), id); } return String.format("payment_Fallback, 线程池: %s, id: %d, 异常:%s\t `(*>﹏<*)′", Thread.currentThread().getName(), id, throwable.getMessage()); } return String.format("payment_Fallback, 线程池: %s, id: %d, `(*>﹏<*)′", Thread.currentThread().getName(), id); } }
-
测试之前先把8008的超时时间和睡眠时间调整:
超时时间调整为5000ms,睡眠时间改为3s
-
测试
访问:
http://localhost/consumer/payment/hystrix/timeout/1
结果:payment_Fallback, 线程池: HystrixTimer-1, id: 1, 超时 ``(*>﹏<*)′
把80客户端的超时时间换成4s
@HystrixCommand(fallbackMethod = "paymentFallback", commandProperties = { @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "4000") })
再次访问
http://localhost/consumer/payment/hystrix/timeout/1
, 结果:paymentInfo_TimeOut, 线程池: hystrix-PaymentServiceImpl-6, port: 8008, id: 1 ヾ(≧▽≦*)o
说明
是否调用兜底fallback方法是取决于 @HystrixProperty
中的 value = "4000"
,只要这个value的值大于服务端8001的睡眠时间或进来就直接异常,比如说 80发生异常就会走80的fallback
方法,与application.yml
中配置的ribbon的那个feign
的超时时间以及hystrix
的时间无关
controller中超时时间配置不生效原因:
关键在于feign:hystrix:enabled: true的作用,官网解释“Feign将使用断路器包装所有方法”,也就是将@FeignClient标记的那个service接口下所有的方法进行了hystrix包装(类似于在这些方法上加了一个@HystrixCommand),这些方法会应用一个默认的超时时间为1s,所以你的service方法也有一个1s的超时时间,service1s就会报异常,controller立马进入备用方法,controller上那个3秒那超时时间就没有效果了。
改变这个默认超时时间方法:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
然后ribbon的超时时间也需加上
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
问题与解决方案
-
目前问题
- 每个业务方法对应一个兜底的方法,代码膨胀
- 统一和自定义的分开
-
解决方案
- 每个方法配置一个 >>>>> 膨胀
@DefaultProperties(defaultFallback = “”)
-
说明:
@DefaultProperties(defaultFallback = “”)
- 每个方法配置一个服务降级方法,技术上可以,实际上不太实用
- N 除了个别重要核心业务有专属,其它普通的可以通过
@DefaultProperties(defaultFallback = “”)
统一跳转到统一处理结果页面
通用的和独享的各自分开,避免了代码膨胀,合理减少了代码量,O(∩_∩)O哈哈~
... @DefaultProperties(defaultFallback = "defeatFallback") public class OrderController { ... @GetMapping("/payment/hystrix/timeout/{id}") // @HystrixCommand(fallbackMethod = "paymentFallback", commandProperties = { // @HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "1500") // }) @HystrixCommand //加了@DefaultProperties属性注解,并且没有写具体方法名字,就用统一全局的 public String paymentInfoTimeOut(@PathVariable("id") Integer id) { return paymentFeign.paymentInfoTimeOut(id); } ... private String defeatFallback(Throwable throwable) { if (Objects.nonNull(throwable)) { // 如果 设置 feign.hystrix.enabled: true, 且触发 feign.hystrix 调用的超时,异常类型会改变 if (throwable instanceof HystrixTimeoutException) { return String.format("defeat_Fallback, 线程池: %s, 超时\t `(*>﹏<*)′", Thread.currentThread().getName()); } return String.format("defeat_Fallback, 线程池: %s, 异常:%s\t `(*>﹏<*)′", Thread.currentThread().getName(), throwable.getMessage()); } return String.format("defeat_Fallback, 线程池: %s, `(*>﹏<*)′", Thread.currentThread().getName()); } }
访问:
http://localhost:81/consumer/payment/hystrix/timeout/1
结果:defeat_Fallback, 线程池: HystrixTimer-1, 超时
(>﹏<)′` -
解决方法二
和业务逻辑混一起>>>混乱
-
服务降级,客户端去调用服务端,碰上服务端宕机或关闭
-
本次案例服务降级处理是在客户端80实现完成的,与服务端8008没有关系,只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦
-
未来我们要面对的异常
- 运行
- 超时
- 宕机
再看我们的业务类PaymentController【混合在一块 ,每个业务方法都要提供一个】
-
-
修改
cloud-consumer-order-hystrix-80
根据
cloud-consumer-order-hystrix-80
已经有的PaymentFeign
接口,重新新建一个类(PaymentFeignImpl
)实现该接口,统一为接口里面的方法进行异常处理@Component // 一定要加,不然启动就报错 public class PaymentFeignImpl implements PaymentFeign { @Override public String paymentInfoOk(Integer id) { return String.format("====PaymentFeign fallback paymentInfoOk, id: %d, o(╥﹏╥)o====", id); } @Override public String paymentInfoTimeOut(Integer id) { return String.format("====PaymentFeign fallback paymentInfoTimeOut, id: %d, o(╥﹏╥)o====", id); } @Override public String paymentInfoAbnormal(Integer id) { return String.format("====PaymentFeign fallback paymentInfoAbnormal, id: %d, o(╥﹏╥)o====", id); } } // 若需获取报错的异常信息,需使用 fallbackFactory, 使用方法可参考 http://www.ay1.cc/article/11484.html @FeignClient(value = "CLOUD-PROVIDER-PAYMENT-HYSTRIX", path = "/payment", fallback = PaymentFeignImpl.class) public interface PaymentFeign {...} // 配置文件 feign.hystrix.enabled=true // feign调用服务宕机还是需要借助 hystrix 来完成降级
-
测试
访问:
http://localhost:81/consumer/payment/hystrix/ok/1
, 结果:paymentInfo_OK, 线程池: http-nio-8008-exec-4, port: 8008, id: 1 (*^_^*)
故意关闭微服务8008,客户端自己调用提示,此时服务端provider已经down了,但是我们做了服务降级处理,让客户端在服务端不可用时也会获得提示信息而不会挂起耗死服务器
再次访问:
http://localhost:81/consumer/payment/hystrix/ok/1
, 结果:====PaymentFeign fallback paymentInfoOk, id: 1, o(╥﹏╥)o====
tips:服务端宕机/超时会调用这个兜底的实现类中的方法,但是客户端中的方法出错还是会调用方法头上那个注解
修改代码和配置文件测试 feign 不基于 hystrix 的超时
... public class OrderController { ... @HystrixCommand(commandProperties = {@HystrixProperty(name = HystrixPropertiesManager.EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "4000")}) public String paymentInfoTimeOut(@PathVariable("id") Integer id) {...} ... } // 配置文件 // feign.hystrix.enabled=false // feign.client.config.default.connectTimeout=100 // feign.client.config.default.readTimeout=1000 // 可使用全局异常处理对宕机异常进行统一说明
访问:
http://localhost:81/consumer/payment/hystrix/timeout/1
, 结果:defeat_Fallback, 线程池: hystrix-OrderController-1, 异常:Read timed out executing GET http://CLOUD-PROVIDER-PAYMENT-HYSTRIX/payment/hystrix/timeout/1
(>﹏<)′`
服务熔断
断路器:一句话就是家里的保险丝
熔断是什么
-
熔断机制概述:
熔断机制是应对雪崩效应的一种微服务链路保护机制,当链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息,当检测到该节点微服务调用响应正常后,恢复调用链路
在Spring Cloud
框架里,熔断机制通过Hystrix
实现。Hystrix
会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand
大神论文:https://martinfowler.com/bliki/CircuitBreaker.html
-
总结
降解是思想,熔断是对降解的具体实现,但是降解的实现并不止熔断这一种
- 调用失败会触发降级,而降级会调用
fallback
方法 - 但无论如何降级的流程一定会先调用正常方法再调用
fallback
方法 - 假如单位时间内调用失败次数过多,也就是降级次数过多,则触发熔断
- 熔断以后就会跳过正常方法直接调用
fallback
方法 - 所谓“熔断后服务不可用”就是因为跳过了正常方法直接执行
fallback
- 调用失败会触发降级,而降级会调用
代码演练
-
cloud-provider-payment-hystrix-8008
public interface PaymentService { ... /** * 演示熔断 */ String paymentInfoCircuitBreaker(@PathVariable("id") Integer id); } ... public class PaymentServiceImpl implements PaymentService { ... // =====服务熔断===== // 这个时间窗口期是打开短路器之后到尝试恢复,期间拒绝请求的时间 // 时间窗口期是指保险丝开启后经过的一段时间再转换为半开状态的时间 @Override @HystrixCommand(fallbackMethod = "paymentInfoFallback", commandProperties = { // 开启熔断 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ENABLED, value = "true"),// 是否开启断路器 // 熔断触发的最小个数,即在一定的时间窗口内请求达到一定的次数,默认20 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"),// 请求次数 // 熔断多长时间后,尝试放一次请求进来,默认5秒 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "10000"),// 时间窗口期 // 失败率达到多少百分比后熔断 默认值:50 @HystrixProperty(name = HystrixPropertiesManager.CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "60"),// 失败率达到多少后跳闸 }) public String paymentInfoCircuitBreaker(@PathVariable("id") Integer id) { if (id < 0) { throw new RuntimeException("**** id 不能负数"); } return String.format("paymentInfo_CircuitBreaker, 线程池: %s, port: %s, id: %d, 流水号:%s \t ヾ(≧▽≦*)o", Thread.currentThread().getName(), serverPort, id, IdUtil.simpleUUID()); } ... } ... public class PaymentController { ... // ====服务熔断===== @GetMapping("/hystrix/circuit/{id}") String paymentInfoCircuitBreaker(@PathVariable("id") Integer id) { String result = paymentService.paymentInfoCircuitBreaker(id); log.info("****result: " + result); return result; } }
-
配置解读
以上配置表示,10秒内,如果请求次数达到10次后,若60%都失败了,则开启熔断功能,并在熔断功能开启10秒后,允许一次请求(即此时熔断为半开状态),如果请求访问成功则关闭熔断,恢复正常调用,否则继续熔断10秒,以此循环。
-
这个时间窗口期是打开短路器之后到尝试恢复,期间拒绝请求的时间
-
时间窗口期是指保险丝开启后经过的一段时间再转换为半开状态的时间
需配置参数可前往官网查找
-
-
测试
访问:
http://localhost:8008/payment/hystrix/circuit/1
, 结果:paymentInfo_CircuitBreaker, 线程池: hystrix-PaymentServiceImpl-3, port: 8008, id: 1, 流水号:49d401419a9b4bab98a9292664fbaf99 ヾ(≧▽≦*)o
访问:
http://localhost:8008/payment/hystrix/circuit/-1
, 结果:paymentInfo_Fallback, 线程池: hystrix-PaymentServiceImpl-4, port: 8008, id: -1, 异常:**** id 不能负数 /(ㄒoㄒ)/~~
一次正确一次错误尝试,重点测试,多次错误,然后慢慢正确,发现刚开始不满足条件,就算是正确的访问地址也不能进行
多次访问异常情况,即
id
为负数,直到出现结果paymentInfo_Fallback, 线程池: http-nio-8008-exec-1, port: 8008, id: -1, 异常:Hystrix circuit short-circuited and is OPEN /(ㄒoㄒ)/~~
(主要看异常信息)再去访问正常情况,发现依旧为异常(注意窗口期为10s,手速别太慢)
启动项目后一直访问失败,第十一次会触发
Hystrix circuit short-circuited and is OPEN
, 对应设置的请求次数为10和失败率达到60%
原理
-
熔断类型
- 熔断打开
- 请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
- 熔断关闭
- 熔断关闭不会对服务进行熔断
- 熔断半开
- 部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断
- 熔断打开
-
官网断路器流程图
-
官网步骤
-
断路器在什么情况下开始起作用
@HystrixCommand( fallbackMethod = "paymentInfoFallback", 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")} )
涉及到断路器的三个重要参数:请求总数阈值、快照时间窗、错误百分比阈值
-
请求总数阈值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开
-
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒【这个时间窗口期是打开短路器之后到尝试恢复,期间拒绝请求的时间】
-
错误百分比阈值:当请求总数在快照时间窗内超过了阀值,比如发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况下,这时候就会将断路器打开
-
-
断路器开启或关闭的条件
- 当满足一定的阀值的时候(默认10秒内超过20个请求次数)
- 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
- 到达以上阀值,断路器将会开启
- 当开启的时候,所有请求都不会进行转发
- 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发;如果成功,断路器会关闭;若失败,继续开启。重复4和5
-
断路器打开之后
再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback,通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应延迟的效果
-
原来的主逻辑如何恢复
对于这一问题,hystrix也为我们实现了自动恢复功能。当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,那么断路器将闭合,主逻辑恢复;如果这次请求依然有问题,断路器继续进入打开状态,休眠时间窗重新计时
所有配置
//========================All
@HystrixCommand(fallbackMethod = "str_fallbackMethod",
groupKey = "strGroupCommand",
commandKey = "strCommand",
threadPoolKey = "strThreadPool",
commandProperties = {
// 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
// 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
@HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
// 配置命令执行的超时时间
@HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
// 是否启用超时时间
@HystrixProperty(name = "execution.timeout.enabled", value = "true"),
// 执行超时的时候是否中断
@HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
// 执行被取消的时候是否中断
@HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
// 允许回调方法执行的最大并发数
@HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
// 服务降级是否启用,是否执行回调函数
@HystrixProperty(name = "fallback.enabled", value = "true"),
// 是否启用断路器
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
// 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
// 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
// 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过
// circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50,
// 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
// 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,
// 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,
// 如果成功就设置为 "关闭" 状态。
@HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
// 断路器强制打开
@HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
// 断路器强制关闭
@HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
// 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
@HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
// 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据
// 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
// 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
// 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
@HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
// 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
@HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
// 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
@HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
// 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
// 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
// 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
@HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
// 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
@HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
// 是否开启请求缓存
@HystrixProperty(name = "requestCache.enabled", value = "true"),
// HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
@HystrixProperty(name = "requestLog.enabled", value = "true"),
},
threadPoolProperties = {
// 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
@HystrixProperty(name = "coreSize", value = "10"),
// 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,
// 否则将使用 LinkedBlockingQueue 实现的队列。
@HystrixProperty(name = "maxQueueSize", value = "-1"),
// 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
// 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue
// 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
}
)
public String strConsumer() {
return "hello 2020";
}
public String str_fallbackMethod()
{
return "*****fall back str_fallbackMethod";
}
服务限流
与AlibabaSentinel
一起说明
hystrix工作流程
Hystrix工作流程:https://github.com/Netflix/Hystrix/wiki/How-it-Works
步骤说明:
- 创建 HystrixCommand(用在依赖的服务返回单个操作结果的时候) 或 HystrixObserableCommand(用在依赖的服务返回多个操作结果的时候) 对象
- 命令执行。其中 HystrixComand 实现了下面前两种执行方式;而 HystrixObservableCommand 实现了后两种执行方式:execute():同步执行,从依赖的服务返回一个单一的结果对象, 或是在发生错误的时候抛出异常。queue():异步执行, 直接返回 一个Future对象, 其中包含了服务执行结束时要返回的单一结果对象。observe():返回 Observable 对象,它代表了操作的多个结果,它是一个 Hot Obserable(不论 “事件源” 是否有 “订阅者”,都会在创建后对事件进行发布,所以对于 Hot Observable 的每一个 “订阅者” 都有可能是从 “事件源” 的中途开始的,并可能只是看到了整个操作的局部过程)。toObservable(): 同样会返回 Observable 对象,也代表了操作的多个结果,但它返回的是一个Cold Observable(没有 “订阅者” 的时候并不会发布事件,而是进行等待,直到有 “订阅者” 之后才发布事件,所以对于 Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程)
- 若当前命令的请求缓存功能是被启用的, 并且该命令缓存命中, 那么缓存的结果会立即以 Observable 对象的形式返回
- 检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到 fallback 处理逻辑(第 8 步);如果断路器是关闭的,检查是否有可用资源来执行命令(第 5 步)
- 线程池/请求队列/信号量是否占满。如果命令依赖服务的专有线程池和请求队列,或者信号量(不使用线程池的时候)已经被占满, 那么 Hystrix 也不会执行命令, 而是转接到 fallback 处理逻辑(第8步)
- Hystrix 会根据我们编写的方法来决定采取什么样的方式去请求依赖服务。HystrixCommand.run() :返回一个单一的结果,或者抛出异常。HystrixObservableCommand.construct(): 返回一个Observable 对象来发射多个结果,或通过 onError 发送错误通知
- Hystrix会将 “成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器, 而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行 “熔断/短路”
- 当命令执行失败的时候, Hystrix 会进入 fallback 尝试回退处理, 我们通常也称该操作为 “服务降级”。而能够引起服务降级处理的情况有下面几种:第4步: 当前命令处于"熔断/短路"状态,断路器是打开的时候。第5步: 当前命令的线程池、 请求队列或 者信号量被占满的时候。第6步:HystrixObservableCommand.construct() 或 HystrixCommand.run() 抛出异常的时候
- 当Hystrix命令执行成功之后, 它会将处理结果直接返回或是以Observable 的形式返回
如果我们没有为命令实现降级逻辑或者在降级处理逻辑中抛出了异常, Hystrix 依然会返回一个 Observable 对象, 但是它不会发射任何结果数据, 而是过 onError 方法通知命令立即中断请求,并通过onError()方法将引起命令失败的异常发送给调用者
服务监控hystrixDashboard
概述
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
仪表盘9001
-
新建
cloud-consumer-hystrix-dashboard-9001
-
application.yml
server: port: 9001 spring: application: name: cloud-consumer-hystrix-dashboard
-
主启动类
@SpringBootApplication @EnableHystrixDashboard // 新注解 public class CloudConsumerHystrixDashboard9001Application { public static void main(String[] args) { SpringApplication.run(CloudConsumerHystrixDashboard9001Application.class, args); } }
-
需监控的服务需包含依赖,此处指
cloud-provider-payment-hystrix-8008
<!-- actuator监控信息完善 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
启动
cloud-consumer-hystrix-dashboard-9001
该微服务后续将监控微服务8008Hystrix Dashboard
页面地址http://localhost:9001/hystrix
断路器演示
-
修改
cloud-provider-payment-hystrix-8008
注意:被监控端需要添加一个配置(非必须,可采用其余办法)
/** * 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑 * ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream", * 只要在自己的项目里配置上下面的servlet就可以了 */ @Bean public ServletRegistrationBean getServlet() { HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet(); ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet); registrationBean.setLoadOnStartup(1); registrationBean.addUrlMappings("/hystrix.stream"); registrationBean.setName("HystrixMetricsStreamServlet"); return registrationBean; } // 若使用此方法,则监控地址(即Hystrix Dashboard输入地址)为 http://localhost:8001/{自己填写的urlMappings} // 即为http://localhost:8001/hystrix.stream
若不添加上述配置(个人采用此方法)
因为Hystrix是通过监控服务调用监控信息的,并且需要访问被监控服务的“/hystrix.stream”接口,而这个接口也是Actuator监控的一个端点,所以需要在服务调用者的pom.xml文件中添加Actuator依赖,并开放监控的端点信息
在
application.yml
添加# 暴露监控服务 management: endpoints: web: exposure: # include: "*" # 开放全部端口 include: hystrix.stream # 指定暴露 hystrix.stream
监控测试
启动对应服务
打开页面http://localhost:9001/hystrix
, 填写监控地址http://localhost:8008/actuator/hystrix.stream
(采用配置文件方式为此地址), 点击Monitor Stream
(监控流),若显示Loading ...
, 则发送一次后刷新页面
从上面页面内容可发现,有几个配置项
Hystrix Dashboard支持三种不同的监控方式
默认集群监控:通过URL(https://turbine-hostname:port/turbine.stream)开启,实现对默认集群的监控
指定的集群监控:通过URL( https://turbine-hostname:port/turbine.stream?cluster=[clusterName])开启,实现对clusterName集群的监控。
单体应用的监控:通过URL(https://hystrix-app:port/actuator/hystrix.stream)开启,实现对具体某个服务实例的监控Delay:默认是2000ms,主要用来控制服务器上轮询监控信息的延迟时间,通过该配置可以降低客户端的网络和CPU消耗。
Title:标题。默认使用具体监控实例的URL。
分别访问http://localhost:8008/payment/hystrix/circuit/1
和http://localhost:8008/payment/hystrix/circuit/-1
,上述测试通过
先访问正确地址,再访问错误地址,再正确地址,会发现图示断路器都是慢慢放开的
监控结果,成功
-
如何看
7色、1圈、1线
-
1圈
实心圆:共有两种含义,它通过颜色的变化代表了实例的健康程度,它的健康度从绿色<黄色<橙色<红色递减。该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大。所以通过该实心圆的展示,就可以在大量的实例中快速的发现故障实例和高压力实例
-
1线
曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势。
-
七色
成功|短路|错误请求|超时|被拒绝|失败|错误
整体说明
-
SpringCloud Gateway 网关
概述简介
-
官网
上一代Zuul 1.X
当前Gateway
-
是什么
Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关;但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代
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以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目标提供统一的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
源码框架:可在IDEA查看GatewayMaven依赖结构
-
能干嘛
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
微服务架构中网关在哪里?
有Zuul了怎么又出来了Gateway
neflix不太靠谱,Zuul2.0一直跳票,迟迟不发布,一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来也非常的简单便捷。Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的 Zuul 2.x,但 Spring Cloud 貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?多方面综合考虑Gateway是很理想的网关选择。
Gateway特征
- 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建
- 动态路由:能够匹配任何请求属性
- 可以对路由指定 Predicate(断言)和 Filter(过滤器)
- 集成Hystrix的断路器功能
- 集成 Spring Cloud 服务发现功能
- 易于编写的 Predicate(断言)和 Filter(过滤器)
- 请求限流功能
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/ O 操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx 用C++ 实现,Zuul 用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得Zuul 的性能相对较差。
-
Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。 Zuul 2.x的性能较 Zuul 1.x 有较大提升。在性能方面,根据官方提供的基准测试, Spring Cloud Gateway 的 RPS(每秒请求数)是Zuul 的 1. 6 倍。
-
Spring Cloud Gateway 建立 在 Spring Framework 5、 Project Reactor 和 Spring Boot 2 之上, 使用非阻塞 API。
-
Spring Cloud Gateway 还 支持 WebSocket, 并且与Spring紧密集成拥有更好的开发体验
Zuul1.x模型
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型
-
JavaWeb中Servlet的生命周期
servlet由servlet container进行生命周期管理,container启动时构造servlet对象并调用servlet init()进行初始化;container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()
container关闭时调用servlet destory()销毁servlet -
上述模式的缺点:
servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势
所以Zuul 1.X是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端
GateWay模型
传统的Web框架,比如说:struts2,springmvc等都是基于Servlet API与Servlet容器基础之上运行的。但是
在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring5必须让你使用java8)
Spring WebFlux 是 Spring 5.0 引入的新的响应式框架,区别于 Spring MVC,它不需要依赖Servlet API,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。
三大核心概念
Route(路由)
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
Predicate(断言)
参考的是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
Filter(过滤)
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们的匹配条件;而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
Gateway工作流程
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler;Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回;过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑;Filter在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用
核心逻辑:路由转发+执行过滤器链
基础配置
-
新建Module:
cloud-service-gateway-9527
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-service-gateway-9527</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <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.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--一般基础配置类--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
application.yml
server: port: 9527 spring: application: name: cloud-service-gateway cloud: gateway: routes: - id: payment_get #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 - id: payment_lb #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 predicates: - Path=/payment/lb/** # 断言,路径相匹配的进行路由 eureka: instance: instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} prefer-ip-address: true hostname: localhost client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://localhost:7001/eureka # 单机版 # defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka # 集群版
-
业务类
// 将匹配到的路由信息打印出来,方便测试 @Slf4j @Component public class RouteFilter implements GlobalFilter, Ordered { @Override @SneakyThrows public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取匹配到的路由信息 Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); // 打印 log.info(">>> route: {}", route); // 请求地址 URI requestUrl = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); log.info(">>>> requestUrl: {}", requestUrl); return chain.filter(exchange); } @Override public int getOrder() { // 打断点发现 // RouteToRequestUrlFilter的order为10000 // LoadBalancerClientFilter的order为10100 // 故这里设置为 10200,避免获取 请求地址 为 null // 值越小越先执行 return 10200; } }
-
主启动类
@SpringBootApplication public class CloudServiceGateway9527Application { public static void main(String[] args) { SpringApplication.run(CloudServiceGateway9527Application.class, args); } }
-
cloud-provider-payment-8001
添加... public class PaymentController { ... @GetMapping(value = "/lb/{content}") public String lb(@PathVariable("content") String content) { return String.format("content: %s, threadName: %s, port: %s", content, Thread.currentThread().getName(), port); } } // 8002也加一下,后面要用
-
测试
启动对应服务
访问:
http://localhost:9527/payment/get/2
, 控制台输出:>>> route:{"id":"payment_get","uri":"http://localhost:8001","order":0,"predicate":{},"metadata":{},"filters":[]}
,并得到http://localhost:8001/payment/get/2
相同结果访问:
http://localhost:9527/payment/lb/测试
(实际请求http://localhost:9527/payment/lb/%E6%B5%8B%E8%AF%95
), 控制台输出:>>> route:{"id":"payment_lb","uri":"http://localhost:8001","order":0,"predicate":{},"metadata":{},"filters":[]}
, 并得到http://localhost:8001/payment/lb/测试
相同结构若使用Apifix作为Api测试工具,当链接中出现中文时会无法发送请求,是因为中文需要转码,可在接口设置种将URL自动编码设置为遵循WHATWG即可
解析
当发送
http://localhost:9527/payment/get/2
时,网关断言Path=/payment/get/**
匹配到路由payment_get
,拼接uri
信息得到http://localhost:8001/payment/get/2
并进行转发,payment_lb
同理 -
配置说明
Gateway网关路由有两种配置方式:
- 在配置文件yml中配置:见前面的步骤
- 代码中注入RouteLocator的Bean
// 官网案例 RemoteAddressResolver resolver = XForwardedRemoteAddressResolver .maxTrustedIndex(1); ... .route("direct-route", r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24") .uri("https://downstream1") .route("proxied-route", r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24") .uri("https://downstream2") )
-
写个案例
通过9527网关访问到外网的百度新闻网址:
http://news.baidu.com/internet
可以上
http://news.baidu.com/
网站查看哪些大标题是可以点击的,不知道是不是访问时间不对,发现好多标题都无法点击@Configuration public class GateWayConfig { /** * 配置了一个id为route-name的路由规则, * 当访问地址 http://localhost:9527/internet 时会自动转发到地址:http://news.baidu.com/internet */ @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { RouteLocatorBuilder.Builder routes = builder.routes(); // routes.route("baidu_internet" // , r -> r.path("/internet") // .uri("http://news.baidu.com")) // .route("baidu_sh" // , r -> r.path("/sh") // .uri("http://news.baidu.com")) // .route("baidu_finance" // , r -> r.path("/finance") // .uri("http://news.baidu.com")); // 可整体写为 访问:http://localhost:9527/news.baidu/internet routes.route("baidu_news" , r -> r.path("/news.baidu/**") // 转发时去掉第一个路径,即 /news.baidu .filters(f -> f.stripPrefix(1)) .uri("http://news.baidu.com")); return routes.build(); } }
浏览器访问:
http://localhost:9527/internet
,发现显示http://news.baidu.com/internet
内容
通过微服务名实现动态路由
-
以前的配置
uri: http://localhost:8001
写死了uri
信息,集群情况下会不适用默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能
启动:一个eureka7001 + 两个服务提供者8001/8002
访问:
http://localhost:9527/payment/lb/测试
,结果:8001和8002交替返回
Predicate的使用
是什么
启动gateway时看到控制台打印
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [After]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Before]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Between]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Cookie]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Header]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Host]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Method]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Path]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Query]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [ReadBodyPredicateFactory]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [RemoteAddr]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [Weight]
INFO 12136 --- [ restartedMain] o.s.c.g.r.RouteDefinitionRouteLocator : Loaded RoutePredicateFactory [CloudFoundryRouteService]
-
Route Predicate Factories
这个是什么?configuring-route-predicate-factories-and-gateway-filter-factories
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分,Spring Cloud Gateway包括许多内置的Route Predicate工厂,所有这些Predicate都与HTTP请求的不同属性匹配,多个Route Predicate工厂可以进行组合
Spring Cloud Gateway 创建 Route 对象时,使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。 Spring Cloud Gateway 包含许多内置的Route Predicate Factories。所有这些谓词都匹配HTTP请求的不同属性。多种谓词工厂可以组合,并通过逻辑and。
常用的Route Predicate
configuring-route-predicate-factories-and-gateway-filter-factories
-
After Route Predicate
// 获取配置文件要填写的时间内容 public class ZonedDateTimeDemo { public static void main(String[] args) { ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区 System.out.println("zbj = " + zbj); ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York")); // 用指定时区获取当前时间 System.out.println("zny = " + zny); } } // zbj = 2022-03-08T23:04:51.718+08:00[Asia/Shanghai] // zny = 2022-03-08T10:04:51.723-05:00[America/New_York]
predicates: - Path=/payment/lb/** # 断言,路径相匹配的进行路由 - After=2022-03-08T23:14:51.718+08:00[Asia/Shanghai] # 指定时间后可访问,用途:项目上线定时开启访问时间可以用、秒杀
-
Before Route Predicate
predicates: ... - Before=2023-03-08T24:21:51.718+08:00[Asia/Shanghai] # 指定时间前可访问
-
Between Route Predicate
predicates:
...
- Between=2022-02-02T17:45:06.206+08:00[Asia/Shanghai],2023-03-25T18:59:06.206+08:00[Asia/Shanghai] # 指定时间之间可以访问
-
Cookie Route Predicate
Cookie Route Predicate需要两个参数,一个是 Cookie name,一个是正则表达式
路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由;如果没有匹配上则不执行
predicates: ... - Cookie=username, yfstart # 请求Cookie要有username属性并且值为yfstart
不带cookie访问:
curl http://localhost:9527/payment/lb
/1 发现404
带上cookies访问:
curl http://localhost:9527/payment/lb/1 --cookie “username=yfstart”
,正常返回如果加入curl返回中文乱码:
https://blog.csdn.net/leedee/article/details/82685636
windows下使用curl请使用cmd,PowerShell的curl会调用Invoke-WebRequest,命令有差别
-
Header Route Predicate
两个参数:一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行
predicates: ... - Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
访问:
curl http://localhost:9527/payment/lb/1 -H “X-Request-Id:123”
,正常返回访问:
curl http://localhost:9527/payment/lb/1 -H “X-Request-Id:-123”
, 404 -
Host Route Predicate
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符
它通过参数中的主机地址作为匹配规则
predicates: ... - Host=**.atguigu.com
正常访问:
curl http://localhost:9527/payment/lb/1 -H “Host: www.atguigu.com”
curl http://localhost:9527/payment/lb/1 -H “Host: java.atguigu.com”
404:
curl http://localhost:9527/payment/lb/1 -H “Host: java.atguigu.net”
-
Method Route Predicate
predicates: ... - Method=GET,POST # 如果请求方法是 GET 或 POST,则此路由匹配
-
Path Route Predicate
predicates: - Path=/red/{segment},/blue/{segment} # 如果请求路径是,则此路由匹配,例如:/red/1 或 /red/1/ 或 /red/blue 或 /blue/green。 # 如果 matchTrailingSlash (匹配尾部斜线, 默认为true)设置为 false,则不会匹配请求路径 /red/1/。
-
Query Route Predicate
支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式
predicates: ... - Query=username, \d+ # 要有参数名username并且值还要是整数才能路由
正确:
http://localhost:9527/payment/lb?username=1
错误:
http://localhost:9527/payment/lb?username=-1
, -
RemoteAddr Route Predicate
predicates: ... - RemoteAddr=192.168.1.1/24 # 如果请求的远程地址是 192.168.1.10,则此路由匹配。
-
Weight Route Predicate
spring: cloud: gateway: routes: - id: weight_high uri: https://weighthigh.org predicates: - Weight=group1, 8 - id: weight_low uri: https://weightlow.org predicates: - Weight=group1, 2 # 这条路线将约80%的流量转发到weighthigh.org,约20%的流量转发至weightlow.org
-
XForwardedRemoteAddr Route Predicate
predicates: ... - XForwardedRemoteAddr=192.168.1.1/24 # 如果X-Forwarded-For标头包含例如192.168.1.10,则此路由匹配。
小总结:说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理
Filter的使用
是什么
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用
Spring Cloud Gateway 内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生
Spring Cloud Gateway的Filter
-
Spring Cloud Gateway 提供了以下两种类型的过滤器,可以对请求和响应进行精细化控制。
过滤器类型 说明 Pre 类型 这种过滤器在请求被转发到微服务之前可以对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作 Post 类型 这种过滤器在微服务对请求做出响应后可以对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等 -
按照作用范围划分,Spring Cloud gateway 的 Filter 可以分为 2 类:
- GatewayFilter:应用在单个路由或者一组路由上的过滤器。
- GlobalFilter:应用在所有的路由上的过滤器。
-
官网
-
目前版本有37个小节,共32种
-
目前版本有10个小节,共9种
-
目前版本有3个小节
-
常用的GatewayFilter
官网查看
filters:
...
- StripPrefix=1 # 去掉地址中的第一部分
spring:
gateway:
default-filters: # 默认过滤项
- StripPrefix=1 #转发之前去掉path中第一层路径 /service1
自定义过滤器
自定义全局GlobalFilter
两个主要接口介绍:implements GlobalFilter,Ordered
可用于,全局日志记录,统一网关鉴权等
案例代码
@Slf4j
@Component
public class LogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info(">>> time:{} 执行了自定义的全局过滤器: MyLogGateWayFilter", new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
log.info(">>> uname: {}", uname);
if (StringUtils.isBlank(uname)) {
log.info(">>> 无法登录");
ServerHttpResponse response = exchange.getResponse();
// 设置响应头信息Content-Type类型
// response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
// 设置http响应状态码
response.setStatusCode(HttpStatus.NOT_ACCEPTABLE);
// 设置返回数据
DataBuffer buffer = response.bufferFactory().wrap("请输入用户名".getBytes(StandardCharsets.UTF_8));
// DataBufferUtils.release(buffer); 释放 DataBuffer,此处需要返回,所以不能释放
// writeWith
return response.writeWith(Mono.just(buffer));
// Flux.just 与 ByteBufFlux.just 源码一致
// return response.writeWith(Flux.just(buffer));
// return response.writeWith(ByteBufFlux.just(buffer));
// writeAndFlushWith
// return response.writeAndFlushWith(Flux.just(ByteBufFlux.just(buffer)));
// return response.writeAndFlushWith(Flux.just(buffer).map(Flux::just));
// return response.writeAndFlushWith(ByteBufFlux.just(buffer).map(Flux::just));
// 直接返回
// return response.setComplete();
}
log.info(">>> 登录成功");
return chain.filter(exchange);
}
/**
* 这个返回的数值越小,上面的filter优先级就越高
*/
@Override
public int getOrder() {
return 0;
}
}
/**
* 1.如果不需要返回数据则直接response.setComplete()
* 2.如果返回的不是json格式的数据则response.writeWith(Flux.just(response.bufferFactory().wrap(data)))或response.writeWith(Mono.just(response.bufferFactory().wrap(data)))
* 注意,此处依旧可以返回JSON格式,writeWith 和 writeAndFlushWith 的区别应该不在返回格式
* 3.如果返回的是application/json格式的数据则response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap(getWrapData()))))或
* response.writeAndFlushWith(Mono.just(ByteBufMono.just(response.bufferFactory().wrap(getWrapData()))))
* 注意,非JSON格式也可使用此方法,writeWith 和 writeAndFlushWith 的区别应该不在返回格式
* 4.Flux与Mono 的区别参考 https://www.jianshu.com/p/611f3667c4d2
* Flux:发射0到N个元素的异步"发射器,Flux<T>是一个标准Publisher<T>
* Mono:发射0到1个元素的异步"发射器,Mono<T>是一个专门的Publisher<T>,它只提供了可用于Flux的操作符的子集
* Flux和Mono之间可以进行转换。对一个 Flux 序列进行计数操作,得到的结果是一个Mono对象。把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。
* 5.注意writeWith与writeAndFlushWith的参数的泛型区别,所以在writeAndFlushWith需要使用Flux包装两次
* 原文链接:https://blog.csdn.net/name_is_wl/article/details/89460817
* <pr/>
* https://www.cnblogs.com/freedom-only/p/14214891.html
* 看到一段代码
* org.springframework.cloud.gateway.filter.NettyWriteResponseFilter#filter 源码
* ...
* final Flux<DataBuffer> body = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR).inbound().receive().retain().map(byteBuf -> wrap(byteBuf, response));
* ...
* return (isStreamingMediaType(contentType) ? response.writeAndFlushWith(body.map(Flux::just)) : response.writeWith(body));
* 因为 http 响应都是基于流的,所以源码中走了 response.writeAndFlushWith 方法
* (/(ㄒoㄒ)/~~ 看源码能力有限,可能不对哈)
* 通过看 writeAndFlushWith 和 writeWith 源码发现 最后 走了相同逻辑, 但 writeWith中看到了 DataBufferUtils.release 操作, writeAndFlushWith 则没有
* 猜测 若 DataBuffer 无需再用,则使用 writeWith,若需要再用 则 调用 writeAndFlushWith
*/
Ordered 的源码
package org.springframework.core;
public interface Ordered {
int HIGHEST_PRECEDENCE = -2147483648;
int LOWEST_PRECEDENCE = 2147483647;
int getOrder();
}
-
测试
请求:
http://localhost:9527/payment/lb/%E6%B5%8B%E8%AF%95?uname=
, 结果:请输入用户名
# 控制台 INFO 23196 --- [ctor-http-nio-3] com.wang.filter.LogGateWayFilter : >>> time:Thu Dec 22 16:29:55 CST 2022 执行了自定义的全局过滤器: MyLogGateWayFilter INFO 23196 --- [ctor-http-nio-3] com.wang.filter.LogGateWayFilter : >>> uname: INFO 23196 --- [ctor-http-nio-3] com.wang.filter.LogGateWayFilter : >>> 无法登录 INFO 23196 --- [ctor-http-nio-3] com.wang.filter.LogGateWayFilter : >>> time:Thu Dec 22 16:30:51 CST 2022 执行了自定义的全局过滤器: MyLogGateWayFilter
请求:
http://localhost:9527/payment/lb/%E6%B5%8B%E8%AF%95?uname=%E6%97%A0%E5%90%8D
, 结果:content: 测试, threadName: http-nio-8001-exec-10, port: 8001
# 控制台 INFO 23196 --- [ctor-http-nio-3] com.wang.filter.LogGateWayFilter : >>> uname: 无名 INFO 23196 --- [ctor-http-nio-3] com.wang.filter.LogGateWayFilter : >>> 登录成功 INFO 23196 --- [ctor-http-nio-3] com.wang.filter.RouteFilter : >>> route: Route{id='payment_lb', uri=lb://CLOUD-PROVIDER-PAYMENT, order=0, predicate=Paths: [/payment/lb/**], match trailing slash: true, gatewayFilters=[], metadata={}} INFO 23196 --- [ctor-http-nio-3] com.wang.filter.RouteFilter : >>>> requestUrl: http://2.0.1.28:8001/payment/lb/%E6%B5%8B%E8%AF%95?uname=%E6%97%A0%E5%90%8D
SpringCloud Config 分布式配置中心
概述
-
分布式系统面临的配置问题
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,我们每一个微服务自己带着一个application.yml,上百个配置文件的管理…
是什么
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
怎么玩
SpringCloud Config分为服务端和客户端两部分。
-
服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口
-
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
能干嘛
-
集中管理配置文件
-
不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod/beta/release
-
运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
-
当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
-
将配置信息以REST接口的形式暴露:post、curl访问刷新均可…
与Gitee整合配置
由于SpringCloud Config默认使用Git来存储配置文件(也有其它方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是http/https访问的形式
Config服务端配置与测试
服务端配置
-
用你自己的账号在Gitee上新建一个名为springcloud-config的新Repository(公开)
-
由上一步获得刚新建的git地址
-
https://gitee.com/jun_jian/springCloudConfig
-
git@gitee.com:jun_jian/springCloudConfig.git
(可根据官网查看ssh配置方法)
-
-
本地硬盘目录上新建git仓库并clone
- 找个文件目录直接克隆即可
git clone https://gitee.com/jun_jian/springCloudConfig.git
-
IDEA 打开项目
- 创建
spring-cloud2020
文件夹,文件夹内添加config-test.yml
、config-dev.yml
、config-master.yml
文件 - 保存格式必须为
UTF-8
- 创建
-
修改文件内容
# config-dev.yml spring: application: name: springcloud-config-dev profiles: active: dev # config-master.yml spring: application: name: springcloud-config-master profiles: active: master # config-test.yml spring: application: name: springcloud-config-test profiles: active: test
> git add . > git commit -m "message" > git push
配置模块 cloud-config-center-3344
新建Module模块cloud-config-center-3344
,它即为Cloud的配置中心模块cloudConfig Center
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-config-center-3344</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--spring-cloud-config--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
application.yml
server: port: 3344 spring: application: name: cloud-config-center # 注册进Eureka服务器的微服务名 cloud: config: server: git: # 若仓库为私有,需填写 username 和 password,还有其它认证方式,如SSL,方法参考官网 # username: username # password: password uri: https://gitee.com/jun_jian/springCloudConfig.git #Gitee上面的git仓库名字 # 搜索目录 search-paths: - spring-cloud2020 # 读取分支 (测试发现这里没啥用,默认读取 master,改了 链接中 不指明 label 时 还是读 master) label: master #服务注册到eureka地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka
-
主启动类
@SpringBootApplication @EnableConfigServer // 配置中心服务器端 public class CloudConfigCenter3344Application { public static void main(String[] args) { SpringApplication.run(CloudConfigCenter3344Application.class, args); } }
-
映射(可选)
windows下修改hosts文件,增加映射:127.0.0.1 config-3344.com
-
测试
启动对应服务
访问
http://localhost:3344/config-master.yml
或者http://localhost:3344/master/config-dev.yml
查看到配置信息访问链接不指明 {label} 时 默认为 master
配置读取规则
-
{application} 就是应用名称,对应到配置文件上来,就是配置文件的名称部分,例如我上面创建的配置文件
-
{profile} 就是配置文件的版本,我们的项目有开发版本、测试环境版本、生产环境版本,对应到配置文件上来就是以 application-{profile}.yml 加以区分,例如application-dev.yml、application-sit.yml、application-prod.yml
-
{label} 表示 git 分支,默认是 master 分支,如果项目是以分支做区分也是可以的,那就可以通过不同的 label 来控制访问不同的配置文件了
官网
# The HTTP service has resources in the following form:(HTTP服务具有以下形式的资源)
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
基于master
分支创建develop
分支,略微修改develop
分支文件内容,例如加入spring.lable: develop
-
/{application}/{profile}[/{label}]
// 访问:http://localhost:3344/config/master { "name": "config", "profiles": [ "master" ], "label": null, "version": "5295e7f855e18f18d0172058a0c08c51b081be2f", "state": null, "propertySources": [ { "name": "https://gitee.com/jun_jian/springCloudConfig.git/spring-cloud2020/config-master.yml", "source": { "spring.application.name": "springcloud-config-master", "spring.profiles.active": "master" } } ] } // 访问: http://localhost:3344/config/dev/develop { "name": "config", "profiles": [ "dev" ], "label": "develop", "version": "b0ffd79bb52b61c96c1cf353a66b82ba6d3c5be6", "state": null, "propertySources": [ { "name": "https://gitee.com/jun_jian/springCloudConfig.git/spring-cloud2020/config-dev.yml", "source": { "spring.application.name": "springcloud-config-dev", "spring.profiles.active": "dev", "spring.lable": "develop" } } ] }
-
/{application}-{profile}.yml
# 访问 http://localhost:3344/config-master.yml spring: application: name: springcloud-config-master profiles: active: master
-
/{label}/{application}-{profile}.yml
# 访问 http://localhost:3344/develop/config-dev.yml spring: application: name: springcloud-config-dev lable: develop profiles: active: dev
-
/{application}-{profile}.properties
# 访问 http://localhost:3344/config-master.properties spring.application.name: springcloud-config-master spring.profiles.active: master
-
/{label}/{application}-{profile}.properties
# http://localhost:3344/develop/config-dev.properties spring.application.name: springcloud-config-dev spring.lable: develop spring.profiles.active: dev
Config客户端配置与测试
-
新建cloud-config-client-3355
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-config-client-3355</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--动态刷新--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--spring-cloud-config--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-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> <!--@ConfigurationProperties--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies> </project>
-
bootstrap.yml
pplicaiton.yml是用户级的资源配置项;bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个“Bootstrap Context”,作为Spring应用的Application Context的父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。
Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap context和Application Context有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap Context和Application Context配置的分离。
要将Client模块下的application.yml文件改为bootstrap.yml,这是很关键的,因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
# bootstrap.yml spring: profiles: active: dev # bootstrap-dev.yml server: port: 3355 spring: application: name: cloud-config-client cloud: #Config客户端配置 config: label: master #分支名称 name: config #配置文件名称 profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://localhost:3344/master/config-dev.yml uri: http://localhost:3344 #配置中心地址 #服务注册到eureka地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka
-
修改config-dev.yml配置并提交到GitHub中,比如加个变量age或者版本号version
# 仅供参考 spring: # 这里的 application.name 会覆盖掉 bootstrap.yml 中的,所以注释掉 # application: # name: springcloud-config-dev cloud: config: label: master profiles: dev version: 1.0
-
主启动类
@SpringBootApplication public class CloudConfigClient3355Application { public static void main(String[] args) { SpringApplication.run(CloudConfigClient3355Application.class, args); } }
-
业务类
@Data @Component @ConfigurationProperties(prefix = "spring.cloud.config") public class ConfigInfo { private String label; private String profiles; private String version; } @RestController public class ConfigClientController { @Resource private ConfigInfo configInfo; @GetMapping("/configInfo") public ConfigInfo getConfigInfo() { return configInfo; } }
-
测试
启动对应微服务
访问
http://localhost:3344/master/config-dev.yml
查看结果,访问http://localhost:3355/configInfo
查看结果对比,结果一致
修改gitee 上
master/config-dev.yml
的配置:把 version 改为1.1再次访问,发现
3344 version: 1.1
,3355 "version": "1.0"
,结果不一致重启 3335,再次访问,
3355 "version": "1.1"
,结果一致发现问题:每次修改配置都需要重启客户端,期望能够动态更新
分布式配置的动态刷新问题
- Linux运维修改Gitee上的配置文件内容做调整
- 刷新3344,发现ConfigServer配置中心立刻响应
- 刷新3355,发现ConfigClient客户端没有任何响应
- 3355没有变化除非自己重启或者重新加载
- 难到每次运维修改配置文件,客户端都需要重启?
Config客户端之动态刷新
避免每次更新配置都要重启客户端微服务3355,我们需要它能动态刷新,下面来修改3355模块
-
POM 引入
actuator
监控<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
修改YML,暴露监控端口
# 暴露监控端点 management: endpoints: web: exposure: include: refresh # include: "*"
-
@RefreshScope
业务类ConfigInfo
修改@Data @Component // @RefreshScope // 实际测试时并未添加,依旧可以动态刷新 加了之后 jackson 序列化异常 解决办法太繁琐了,不加了不加了 @ConfigurationProperties(prefix = "spring.cloud.config") public class ConfigInfo { private String label; private String profiles; private String version; }
-
此时修改gitee,再测:
http://localhost:3355/configInfo
,发现3355还是没有变化需要运维人员发送Post请求刷新3355
curl -X POST "http://localhost:3355/actuator/refresh"
返回:
["config.client.version","spring.cloud.config.version"]
, 同时注意查看3355服务控制台变化再次请求:
http://localhost:3355/configInfo
,成功实现了客户端3355刷新到最新配置内容,避免了服务重启 -
思考
想想还有什么问题?
- 假如有多个微服务客户端3355/3366/3377
- 每个微服务都要执行一次post请求,手动刷新?
- 可否广播,一次通知,处处生效?
- 我们想大范围的自动刷新,求方法
SpringCloud Bus 消息总线
概述
上一讲解的加深和扩充,一言以蔽之,分布式自动刷新配置功能
Spring Cloud Bus 配合 Spring Cloud Config 使用可以实现配置的动态刷新
是什么
Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。Spring Clud Bus目前支持RabbitMQ和Kafka。
能干嘛
Spring Cloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。
总线
-
什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
-
基本原理
ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
RabbitMQ环境配置
访问http://ip:15672/
, 输入账号密码并登录:admin 123456
注意:默认的
guest guest
只能在localhost登录,如果Linux启动,Windows登录,一定要创建一个自己的远程用户
动态刷新全局广播
搭建服务
-
必须先具备良好的RabbitMQ环境先,演示广播效果,增加复杂度,再以3355为模板再制作一个3366
新建
cloud-config-client-3366
-
代码全部与
cloud-config-client-3355
一致, 只需修改著启动类名称和server.port=3366
设计思想
-
利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置
-
利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而刷新所有客户端的配置
图二的架构显然更加适合,图一不适合的原因如下:
-
打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责
-
破坏了微服务各节点的对等性
-
有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改
配置中心3344添加消息总线支持
给cloud-config-center-3344配置中心服务端添加消息总线支持
-
pom.xml
<!--添加消息总线RabbitMQ支持--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
-
application.yml
... spring: ... #rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口 rabbitmq: host: 192.168.159.11 port: 5672 username: admin password: 123456 ... # rabbitmq相关配置,暴露bus刷新配置的端点 management: endpoints: #暴露bus刷新配置的端点 web: exposure: include: bus-refresh
客户端3355添加消息总线支持
给cloud-config-client-3355客户端添加消息总线支持
-
pom.xml
<!--添加消息总线RabbitMQ支持--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
-
application.yml
... spring: ... #rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口 rabbitmq: host: 192.168.159.11 port: 5672 username: admin password: 123456 ...
客户端3366添加消息总线支持
与cloud-config-client-3355
相同修改
测试
启动对应服务, 可观察rabbitmq
变化
修改Gitee上配置文件增加版本号
依次访问http://localhost:3344/master/config-dev.yml
,http://localhost:3355/configInfo
, http://localhost:3366/configInfo
,只有3344
g改变
发送POST请求curl -X POST "http://localhost:3344/actuator/bus-refresh"
, 可观察控制台
再依次访问,发现全部改变,实现:一次发送,处处生效, 一次修改,广播通知,处处生效
动态刷新定点通知
不想全部通知,只想定点通知:只通知3355,不通知3366
简单一句话:指定具体某一个实例生效而不是全部
公式:http://localhost:配置中心的端口号/actuator/bus-refresh/{destination}
/bus/refresh
请求不再发送到具体的服务实例上,而是发给config server
并通过destination
参数类指定需要更新配置的服务或实例
destination 为 spring.application.name:server.port, 注意 spring.application.name 大小写严格一致,spring.application.name 可能被gitee配置覆盖
案例
修改Gitee上配置文件增加版本号
我们这里以刷新运行在3355端口上的config-client为例:只通知3355,不通知3366
通知前,依次访问查看结果
curl -X POST "http://localhost:3344/actuator/bus-refresh/cloud-config-client:3355"
通知后,依次访问查看结果
SpringCloud Stream 消息驱动
消息驱动概述
是什么
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
官网
Spring Cloud Stream
是用于构建与共享消息传递系统连接的高度可伸缩的事件驱动微服务框架,该框架提供了一个灵活的编程模型,它建立在已经建立和熟悉的Spring熟语和最佳实践上,包括支持持久化的发布/订阅、消费组以及消息分区这三个核心概念
设计思想
标准MQ
-
生产者/消费者之间靠消息媒介传递信息内容——
Message
-
消息必须走特定的通道——消息通道
MessageChannel
-
消息通道里的消息如何被消费呢,谁负责收发处理—消息通道MessageChannel的子接口
SubscribableChannel
,由MessageHandler
消息处理器所订阅
为什么用Cloud Stream
-
问题:为什么要引入SpringCloud Stream
-
举例:对于我们Java程序员来说,可能有时要使用ActiveMQ,有时要使用RabbitMQ,甚至还有RocketMQ以及Kafka,这之间的切换似乎很麻烦,我们很难,也没有太多时间去精通每一门技术,那有没有一种新技术的诞生,让我们不再关注具体MQ的细节,自动的给我们在各种MQ内切换。
-
简介:
Spring Cloud Stream
是一个用来为微服务应用构建消息驱动能力的框架。它可以基于Spring Boot
来创建独立的、可用于生产的Spring
应用程序。Spring Cloud Stream
为一些供应商的消息中间件产品提供了个性化的自动化配置实现,并引入了发布-订阅、消费组、分区这三个核心概念。通过使用Spring Cloud Stream
,可以有效简化开发人员对消息中间件的使用复杂度,让系统开发人员可以有更多的精力关注于核心业务逻辑的处理。但是目前Spring Cloud Stream
只支持RabbitMQ
和Kafka
的自动化配置。(截止目前,官网支持的MQ已经特别多,其中大部分标注为partner maintained(合作伙伴维护) -
一句话:屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。
Binder
在没有绑定器这个概念的情况下,我们的SpringBoot
应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。Stream
对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq
切换为kafka
),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程
通过定义绑定器Binder
作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder
可以生成Binding
,Binding
用来绑定消息容器的生产者和消费者,它有两种类型,INPUT
和OUTPUT
,INPUT
对应于消费者,OUTPUT
对应于生产者。
Stream中的消息通信方式遵循了发布-订阅模式
Topic主题进行广播:在RabbitMQ就是Exchange,在Kakfa中就是Topic
Stream标准流程套路
Binder:很方便的连接中间件,屏蔽差异
Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel
对队列进行配置
Source和Sink:简单的可理解为参照对象是Spring Cloud Stream
自身,从Stream
发布消息就是输出,接受消息就是输入
编码API和常用注解
组成 | 说明 |
---|---|
Middleware | 中间件,支持MQ见官网 |
Binder | Binder是应用于消息中间件之间的封装,目前实行了Kafka和RabbitMQ的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现 |
@Input | 注解标识输入通道,通过该输入通道接收到的消息进入应用程序 |
@Output | 注解标识输出通道,发布的消息将通过该通道离开应用程序 |
@StreamListener | 监听队列,用于消费者的队列的消息接收 |
@EnableBinding | 指信道channel和exchange绑定在一起 |
注意:注解依然是能用的,但是官方明确表示注解已经被弃用,弃用并不是不能用,而是用了会画横杠不建议用。但是功能是没有问题的,低版本的cloud是没有被弃用的。针对于注解和函数式编程两种我都会进行使用。
案例说明
RabbitMQ环境已经OK,工程中新建三个子模块
- cloud-stream-rabbitmq-provider-8801 , 作为生产者进行发消息模块
- cloud-stream-rabbitmq-consumer-8802 , 作为消息接收模块
- cloud-stream-rabbitmq-consumer-8803 , 作为消息接收模块
消息驱动之生产者
-
cloud-stream-rabbitmq-provider-8801
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-stream-rabbitmq-provider-8801</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <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.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--spring-cloud-stream-rabbit--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</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: 8801 spring: application: name: cloud-stream-rabbitmq-provider cloud: stream: binders: # 在此处配置要绑定的rabbitmq的服务信息; defaultRabbit: # 表示定义的名称,用于于binding整合,即自定义的rabbitMq实例 type: rabbit # 消息组件类型 environment: # 设置rabbitmq的相关的环境配置 spring: rabbitmq: host: 192.168.159.11 port: 5672 username: admin password: 123456 bindings: # 服务的整合处理 output: # 这个名字是一个通道的名称 destination: studyExchange # 表示要使用的Exchange名称定义 content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain” binder: defaultRabbit # 设置要绑定的消息服务的具体设置,即绑定的rabbitMq实例 eureka: client: # 客户端进行Eureka注册的配置 service-url: defaultZone: http://localhost:7001/eureka instance: lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒) lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒) instance-id: send-8801.com # 在信息列表时显示主机名称 prefer-ip-address: true # 访问的路径变为IP地址
注意:我之前按照上述中的配置来连接ribbitmq时报错了 !!(启动时会有一个报错
AmqpConnectException
, 但是不影响使用)上述的application.yml中使用了
spring.cloud.stream.binders.defaultRabbit.environment.spring.rabbitmq.xx
来配置rabbitmq的环境,如果你用的是远程服务器上的rabbitmq,按照上述配置方式的话,启动时会试图连接两次rabbitmq程序,第一次试图连接访问的就是application.yml中配置的地址,此时已经订阅成功了,但是程序还会在之后进行第二次连接,此时访问的地址就是localhost:5673,在我的环境中,我本地没有rabbitmq环境,所以直接报异常,因此,如果是使用自己的服务器来配置,则需要修改配置文件,将rabbitmq的配置信息移动到application.yml中的spring节点下!spring: application: name: cloud-stream-rabbitmq-provider rabbitmq: host: 192.168.159.11 port: 5672 username: admin password: 123456 cloud: stream: binders: # 在此处配置要绑定的rabbitmq的服务信息; defaultRabbit: # 表示定义的名称,用于于binding整合,即自定义的rabbitMq实例 type: rabbit # 消息组件类型 # environment: # 设置rabbitmq的相关的环境配置 # spring: # rabbitmq: # host: ${spring.rabbitmq.host} # port: ${spring.rabbitmq.port} # username: ${spring.rabbitmq.username} # password: ${spring.rabbitmq.password} bindings: # 服务的整合处理 output: # 这个名字是一个通道的名称 destination: studyExchange # 表示要使用的Exchange名称定义 content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain” binder: defaultRabbit # 设置要绑定的消息服务的具体设置,即绑定的rabbitMq实例
-
主启动类
@SpringBootApplication public class CloudStreamRabbitmqProvider8801Application { public static void main(String[] args) { SpringApplication.run(CloudStreamRabbitmqProvider8801Application.class, args); } }
-
业务类
// 发送消息接口 public interface IMessageProvider { String send(); } /** * * @EnableBinding(Source.class) :可以理解为是一个消息的发送管道的定义 * * @author junjian * @since 2022/12/27 15:20 星期二 */ @Slf4j @EnableBinding(Source.class) public class IMessageProviderImpl implements IMessageProvider { /** * 消息的发送管道 */ @Resource private MessageChannel output; @Override public String send() { String serial = UUID.randomUUID().toString(); // 创建并发送消息 output.send(MessageBuilder.withPayload(serial).build()); log.info(">>> 发送消息 serial:{}", serial); return serial; } } @RestController @RequiredArgsConstructor public class SendMessageController { private final IMessageProvider iMessageProvider; @GetMapping(value = "/sendMessage") public String sendMessage() { return iMessageProvider.send(); } }
-
测试
启动对应服务
访问:
http://localhost:8801/sendMessage
, 查看RabbitMQ#Exchanges
,发现studyExchange
已创建注意。服务启动时即已创建,并非发送消息之后
消息驱动之消费者
-
cloud-stream-rabbitmq-consumer-8802
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-stream-rabbitmq-consumer-8802</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</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: 8801 spring: application: name: cloud-stream-rabbitmq-provider rabbitmq: host: 192.168.159.11 port: 5672 username: admin password: 123456 cloud: stream: binders: # 在此处配置要绑定的rabbitmq的服务信息; defaultRabbit: # 表示定义的名称,用于于binding整合,即自定义的rabbitMq实例 type: rabbit # 消息组件类型 bindings: # 服务的整合处理 output: # 这个名字是一个通道的名称 destination: studyExchange # 表示要使用的Exchange名称定义 content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain” binder: defaultRabbit # 设置要绑定的消息服务的具体设置,即绑定的rabbitMq实例 eureka: client: # 客户端进行Eureka注册的配置 service-url: defaultZone: http://localhost:7001/eureka instance: lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒) lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒) instance-id: send-8801.com # 在信息列表时显示主机名称 prefer-ip-address: true # 访问的路径变为IP地址
-
主启动类
@SpringBootApplication public class CloudStreamRabbitmqProvider8801Application { public static void main(String[] args) { SpringApplication.run(CloudStreamRabbitmqProvider8801Application.class, args); } }
-
业务类
@Slf4j @Component @EnableBinding(Sink.class) public class ReceiveMessageListener { @Value("${server.port}") private String serverPort; @StreamListener(Sink.INPUT) public void input(Message<String> message) { log.info(">>> 接收到消息: message:{}, port:{}", message.getPayload(), serverPort); } }
分组消费与持久化
cloud-stream-rabbitmq-consumer-8803
依照8802,clone出来一份运行8803
分组消息
启动
RabbitMQ 7001 8801 8802 8803
消息生产者发送消息后发现两个问题
- 有重复消费问题(一次发送,两个消费者都收到消息)
- 消息持久化问题()
消费
发送消息http://localhost:8801/sendMessage
, 8802/8803同时都收到了,存在重复消费问题
-
如何解决?
分组和持久化属性group
-
生产实际案例:
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况。这时我们就可以使用Stream中的消息分组来解决
注意在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次
不同组是可以全面消费的(重复消费),同一组内会发生竞争关系,只有其中一个可以消费
分组
原理:微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次。不同的组是可以消费的,同一个组内会发生竞争关系,只有其中一个可以消费。
-
不同组
8802/8803在不同组,两者group不同
8802配置文件添加
spring.cloud.stream.bindings.input.group=groupA
8803配置文件添加
spring.cloud.stream.bindings.input.group=groupB
查看MQ页面 Queues 和 对应 Exchanges 的 Bindings
分布式微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例,本例阳哥启动了两个消费微服务(8802/8803),多数情况,生产者发送消息给某个具体微服务时只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况。为了解决这个问题,在Spring Cloud Stream中提供了消费组的概念
结论:还是重复消费
8802/8803实现了轮询分组,每次只有一个消费者,8801模块的发的消息只能被8802或8803其中一个接收到,这样避免了重复消费
-
同组
8802/8803都变成相同组,group两个相同,都变成 atguiguA
结论:同一个组的多个微服务实例,每次只会有一个拿到,且为轮询
持久化
通过上述,解决了重复消费问题,再看看持久化
-
停止8802/8803,并去除掉8802的分组group: groupA,8803的分组group: groupA没有去掉
-
8801先发送4条消息到rabbitmq
-
先启动8802,无分组属性配置,后台没有打出来消息
-
再启动8803,有分组属性配置,后台打出来了MQ上的消息(四条)
看MQ页面,可以发现没有设置 group 时,应用自动新建了一个Queues并绑在了对于Exchange下,当应用关闭后该 Queues 消失
自己设置的 group 则会永久保存
-
停止8802/8803,并将 8802的分组置为group: groupC
-
8801先发送4条消息到rabbitmq
-
启动 8802,没有消费消息
此时 应用自动新建了一个Queues:groupC并绑在了对于Exchange下
-
启动 8803,后台打出来了MQ上的消息(四条)
-
停止8802/8803
-
8801先发送4条消息到rabbitmq
-
启动 8802,后台打出来了MQ上的消息(四条)
-
启动 8803,后台没有打出来消息 (因为消息已经被 8002 消费掉了,即哪个先启动哪个先消费到消息)
得出结论
若将 group 设置为 已存在 Exchanges
的Bindings
,则即连接当前group的服务都宕机了,下次启动时也会消费到消息
SpringCloud Sleuth 分布式请求链路跟踪
概述
为什么会出现这个技术?需要解决哪些问题?
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败
是什么
Spring Cloud Sleuth官网,Github地址
Spring Cloud Sleuth
提供了一套完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并且兼容支持了zipkin
搭建链路监控步骤
zipkin
SpringCloud
从F版起已不需要自己构建Zipkin Server
了,只需调用jar包即可
io/zipkin/zipkin-server | zipkin-server
我下的io.zipkin:zipkin-server:2.19.0
, zipkin-server-2.12.9-exec.jar
应该是io.zipkin.java
包下的,看起来三年没更新了
spring-cloud-starter-zipkin
包含为2.19.0
版本
zipkin-server-2.19.3-exec.jar
启动:
java -jar zipkin-server-2.19.0-exec.jar
nohup java -jar zipkin-server-2.19.0-exec.jar > ./logs/zipkin-server.log &
nohup java -jar zipkin-server-2.19.0-exec.jar > ./logs/zipkin-server.log 2>&1 &
# 查看:ps -ef | grep zipkin,杀进程:kill PID, 强制杀死进程:kill -9 PID
运行控制台:http://192.168.159.11:9411/zipkin/
术语 - 完整的调用链路: 表示一请求链路,一条链路通过Trace Id
唯一标识,Span
标识发起的请求信息,各span
通过parent id
关联起来
一条链路通过Trace Id
唯一标识,Span
标识发起的请求信息,各span
通过parent id
关联起来
名词解释:
Trace
:类似于树结构的Span
集合,表示一条调用链路,存在唯一标识span
:表示调用链路来源,通俗的理解span
就是一次请求信息
服务提供者
cloud-provider-payment-8001
-
POM.xml
<!--添加--> <!--包含了sleuth+zipkin--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
-
application.yml
# 添加 spring: zipkin: base-url: http://192.168.159.11:9411 sleuth: sampler: # 采样率值介于 0 到 1 之间,1 则表示全部采集 probability: 1
-
业务代码
... public class PaymentController { ... @GetMapping("/zipkin") private String zipkin() { log.info("执行:{}", port); return "Zipkin-Server"; } }
服务消费者(调用方)
cloud-consumer-order-80
-
POM.xml
<!--添加--> <!--包含了sleuth+zipkin--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
-
application.yml
# 添加 spring: zipkin: base-url: http://192.168.159.11:9411 sleuth: sampler: # 采样率值介于 0 到 1 之间,1 则表示全部采集 probability: 1
-
业务代码
... public class OrderController { ... public static final String PAYMENT_SRV = "CLOUD-PROVIDER-PAYMENT"; public static final String PAYMENT_SRV_URL = "http://" + PAYMENT_SRV; ... @GetMapping("/payment/zipkin") public String paymentZipkin() { log.info("调用:{}", PAYMENT_SRV); return restTemplate.getForObject(PAYMENT_SRV_URL + "/payment/zipkin", String.class); } }
测试
-
依次启动eureka7001,8001,80
-
80调用8001几次测试下:
http://localhost:81/consumer/payment/zipkin
-
查看控制台日志
INFO [cloud-consumer-order,94891d70a60b982a,94891d70a60b982a,true] 66456 --- [p-nio-81-exec-8] com.wang.controller.OrderController : 调用:CLOUD-PROVIDER-PAYMENT --- INFO [cloud-provider-payment,94891d70a60b982a,5d605950fc6e4ccf,true] 70240 --- [nio-8001-exec-4] com.wang.controller.PaymentController : 执行:8001 [服务名, trackId, spanId,是否输出] - 服务名。即spring.application.name 的值 - Trace ID。d1e92e984eaec1ff,sleuth生成的一个ID,叫Trace ID,用来标识一条请求链路,一条请求链路中包含一个Trace ID,多个Span ID - spanID 。d1e92e984eaec1ff、spanID 基本的工作单元,获取元数据,如发送一个http - true,是否要将该信息输出到zipkin服务中来收集和展示。
-
打开浏览器访问:
http://http://192.168.159.11:9411
zipkin为懒加载,只有调用一次后才会显示服务列表
依赖关系
若显示为空就点击查找按钮
单击上方“Try Lens UI”切换成了 Lens UI,有bug谨慎点击,新版本已移除,若需从Lens UI切换回来,找到浏览器Cookie删除对应页面lens
原理
SpringCloud Alibaba 入门简介
为什么会出现SpringCloud alibaba
Spring Cloud Netflix项目进入维护模式
Spring Cloud Netflix Projects Entering Maintenance Mode
Recently, Netflix announced that Hystrix is entering maintenance mode. Ribbon has been in a similar state since 2016. Although Hystrix and Ribbon are now in maintenance mode, they are still deployed at scale at Netflix.
The Hystrix Dashboard and Turbine have been superseded by Atlas. The last commits to these project are 2 years and 4 years ago respectively. Zuul 1 and Archaius 1 have both been superseded by later versions that are not backward compatible.
The following Spring Cloud Netflix modules and corresponding starters will be placed into maintenance mode:
spring-cloud-netflix-archaius
spring-cloud-netflix-hystrix-contract
spring-cloud-netflix-hystrix-dashboard
spring-cloud-netflix-hystrix-stream
spring-cloud-netflix-hystrix
spring-cloud-netflix-ribbon
spring-cloud-netflix-turbine-stream
spring-cloud-netflix-turbine
spring-cloud-netflix-zuul
This does not include the Eureka or concurrency-limits modules.
Spring Cloud Netflix Projects Entering Maintenance Mode
什么是维护模式?
What is Maintenance Mode?
Placing a module in maintenance mode means that the Spring Cloud team will no longer be adding new features to the module. We will fix blocker bugs and security issues, and we will also consider and review small pull requests from the community.
We intend to continue to support these modules for a period of at least a year from the general availability of the Greenwich release train.
将模块置于维护模式,意味着 Spring Cloud 团队将不会再向模块添加新功能。我们将修复 block 级别的 bug 以及安全问题,我们也会考虑并审查社区的小型 pull request
进入维护模式意味着什么呢?
进入维护模式意味着 Spring Cloud Netflix 将不再开发新的组件,我们都知道Spring Cloud 版本迭代算是比较快的,因而出现了很多重大ISSUE都还来不及Fix就又推另一个Release了。进入维护模式意思就是目前一直以后一段时间Spring Cloud Netflix提供的服务和功能就这么多了,不在开发新的组件和功能了。以后将以维护和Merge分支Full Request为主
新组件功能将以其他替代平代替的方式实现
Replacements
We recommend the following as replacements for the functionality provided by these modules.
Current Replacement Hystrix Resilience4j Hystrix Dashboard / Turbine Micrometer + Monitoring System Ribbon Spring Cloud Loadbalancer Zuul 1 Spring Cloud Gateway Archaius 1 Spring Boot external config + Spring Cloud Config Look for a future blog post on Spring Cloud Loadbalancer and integration with a new Netflix project Concurrency Limits.
SpringCloud alibaba带来了什么
是什么
诞生:2018.10.31,Spring Cloud Alibaba 正式入驻了 Spring Cloud 官方孵化器,并在 Maven 中央库发布了第一个版本
能干嘛
-
服务限流降级:默认支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控
-
服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成了 Ribbon 的支持
-
分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新
-
消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力
-
阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据
-
分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行
去哪下
怎么玩
Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
更多组件请参考 Roadmap。
SpringCloud alibaba学习资料获取
Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
SpringCloud Alibaba进入了SpringCloud官方孵化器,而且毕业了
SpringCloud Alibaba Nacos服务注册和配置中心
Nacos简介
为什么叫Nacos
前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service
是什么
Nacos: Dynamic Naming and Configuration Service
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
Nacos 就是注册中心 + 配置中心的组合 Nacos = Eureka+Config +Bus
能干嘛
- 替代Eureka做服务注册中心
- 替代Config做服务配置中心
去哪下
各种注册中心比较
服务注册与发现框架 | CAP模型 | 控制台管理 | 社区活跃度 |
---|---|---|---|
Eureka | AP | 支持 | 低(2.x 版本闭源) |
Zookeeper | CP | 不支持 | 中 |
Consul | CP | 支持 | 高 |
Nacos | AP | 支持 | 高 |
据说 Nacos 在阿里巴巴内部有超过 10 万的实例运行,已经过了类似双十一等各种大型流量的考验
安装并运行Nacos
本地Java8+Maven环境已经OK
先从官网下载Nacos,Tags
寻找1.1.4
版本
解压安装包,直接运行bin目录下的startup.cmd startup.cmd -m standalone
| ./startup.sh -m standalone
nacos默认启动模式为集群模式cluste
可直接修改Nacos启动文件,将配置 set MODE="cluster" 改为 set MODE="standalone",或启动时通过
-m standalone
指定cluster:集群模式;standalone:单体模式。
命令运行成功后直接访问:http://ip:8848/nacos
默认账号密码都是nacos
Nacos作为服务注册中心
基于Nacos的服务提供者9001
新建Module:cloud-alibaba-provider-payment-9001
-
pom.xml
<!--父 pom.xml 添加--> <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>
<!--本模块 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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-alibaba-provider-payment-9001</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <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.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: 9001 spring: application: name: cloud-alibaba-provider-payment cloud: # discovery: # enabled: false # 实现禁止服务被发现的功能,该配置项的值默认为true nacos: discovery: server-addr: 192.168.191.112:8848 #配置Nacos地址
-
主启动类
@EnableDiscoveryClient @SpringBootApplication public class CloudAlibabaProviderPayment9001Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaProviderPayment9001Application.class, args); } }
-
业务代码
@RestController @RequestMapping("/payment") public class PaymentController { @Value("${server.port}") private String serverPort; @GetMapping(value = "/nacos/{id}") public String getPayment(@PathVariable("id") Integer id) { return String.format("nacos registry - serverPort: %s, id: %d", serverPort, id); } }
-
测试
访问:
http://localhost:9001/payment/nacos/1
,结果:nacos registry - serverPort: 9001, id: 1
查看nacos控制台服务管理->服务列表,看到
cloud-alibaba-provider-payment
服务nacos服务注册中心+服务提供者9001都OK了
为了下一章节演示nacos的负载均衡,参照9001新建9002
新建
cloud-alibaba-provider-payment-9002
或者直接拷贝虚拟端口映射
Services 窗口找到需要Copy的实例右键 -> 点击
Copy Configutation...
-> 输入Name
和VM options
-DServer.port=9002
基于Nacos的服务消费者83
新建Module:cloud-alibaba-consumer-order-80
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-alibaba-consumer-order-80</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <dependency> <groupId>com.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
application.yml
server: port: 80 spring: application: name: cloud-alibaba-consumer-order cloud: nacos: discovery: server-addr: 192.168.191.112:8848
-
主启动类
@EnableFeignClients @SpringBootApplication public class CloudAlibabaConsumerOrder80Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaConsumerOrder80Application.class, args); } }
-
业务代码
@FeignClient(name = "cloud-alibaba-provider-payment", path = "/payment") public interface PaymentFeign { @GetMapping("/nacos/{id}") String getPayment(@PathVariable("id") Integer id); } @RestController @RequestMapping("/consumer") public class OrderNacosController { @Resource private PaymentFeign paymentFeign; @GetMapping("/payment/nacos/{id}") public String paymentInfo(@PathVariable("id") Integer id) { return paymentFeign.getPayment(id); } }
-
测试
查看nacos控制台
看到
cloud-alibaba-consumer-order
和cloud-alibaba-provider-payment
服务,且cloud-alibaba-provider-payment
服务实例数为2多次访问:
http://localhost:81/consumer/payment/nacos/1
,发现轮询访问 -
nacos支持负载均衡的原因
查看
com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery:2.1.0.RELEASE
,在里面发现org.springframework.cloud:spring-cloud-starter-netflix-ribbon:2.2.1.RELEASE
服务注册中心对比
-
Nacos 支持AP和CP模式的切换:
C是所有节点在同一时间看到的数据是一致的;而A的定义是所有的请求都会收到响应
-
何时选择使用何种模式?
一般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如 Spring cloud 和 Dubbo 服务,都适用于AP模式,AP模式为了服务的可能性而减弱了一致性,因此AP模式下只支持注册临时实例。
如果需要在服务级别编辑或者存储配置信息,那么 CP 是必须,K8S服务和DNS服务则适用于CP模式。CP模式下则支持注册持久化实例,此时则是以 Raft 协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
-
模式切换
# 通过curl发送一个put请求即可切换 curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP
CAP原则:
- C(Consistency):一致性
- A(Availablitity):可用性
- P(Partition tolerance):分区容错性
Nacos作为服务配置中心
Nacos作为配置中心3377-基础配置
新建 cloud-alibaba-config-client-3377
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-alibaba-config-client-3377</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <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.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>
-
配置文件
两个yml
Nacos 同 springcloud-config 一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动。
springboot 中配置文件的加载是存在优先级顺序的,bootstrap 优先级高于application
<!--cloud新版本默认将bootstrap移除了,所以需要添加如下依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
# bootstrap.yml spring: profiles: active: dev
# bootstrap-dev.yml # nacos配置 # 一般来说,测试环境和生产环境会使用两个不同的nacos,所以bootstrap.yml也需要区分环境配置 server: port: 3377 spring: application: name: cloud-alibaba-config-client cloud: nacos: discovery: server-addr: 192.168.191.112:8848 #Nacos服务注册中心地址 config: server-addr: 192.168.191.112:8848 #Nacos作为配置中心地址 file-extension: yaml #指定yaml格式的配置 # 配置文件名:${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# appliction-dev.yml # 一般本地测试时使用
# 一些备注 # nacos 本地配置覆盖远程 本地配置优先(默认远端优先),一定要在 nacos上配置不然不生效(开发环境可开启) spring: cloud: config: # 如果本地配置优先级高,那么 override-none 设置为 true,包括系统环境变量、本地配置文件等配置 override-none: true # 如果想要远程配置优先级高,那么 allow-override 设置为 false,如果想要本地配置优先级高那么 allow-override 设置为 true allow-override: true # 只有系统环境变量或者系统属性才能覆盖远程配置文件的配置,本地配置文件中配置优先级低于远程配置;注意本地配置文件不是系统属性 override-system-properties: false # 若nacos注册实例IP错误可采取以下两者方案 # 根据网卡信息字段映射服务中心实例IP spring: cloud: inetutils: ignored-interfaces: eth.* # 需映射ip网卡头信息 preferred-networks: 10.100 # 需映射ip前两段地址 # 指定IP spring: cloud: nacos: # username: nacos 默认 nacos # password: nacos 默认 nacos discovery: # server-addr: 192.168.191.112:8848 # group: ${spring.profiles.active} # 注册服务分组名称,远程调用时,服务中心会调用当前分组内的其它服务 # cluster-name: local # 集群名称,优先同集群调用 ip: 10.100.23.98 # 指定注册服务的实例IP # 多配置文件加载 spring: cloud: nacos: #... config: server-addr: 192.168.191.112:8848 file-extension: yaml namespace: 6011cb49-3872-4c75-a2c3-298398e6098c # refresh-enabled: true # 新版有这个属性,默认为true # 用于共享的配置文件 shared-configs: - data-id: common-mysql.yaml group: SPRING_CLOUD_SHARED_GROUP refresh: true # - ... # ext-config # 旧版为这个,新版标记为已弃用 # 常规配置文件 # 优先级大于 shared-configs,在 shared-configs 之后加载 extension-configs: - dataId: application-database.yaml group: ${spring.profiles.active} refresh: true # 默认为 false - dataId: application-redis.yaml group: ${spring.profiles.active} refresh: true # - ... # 关于自动刷新的猜测,存在两个概念,一个是nacos配置动态刷新,一个是SpringBean动态刷新(因为Bean为单例),配置文件里的是控制nacos配置动态加载的,需配合@RefreshScope才能刷新到Bean里面去,产生感官上的动态刷新(访问链接发现参数变化)(个人猜测,猜测依据,打印nacos获取到的配置信息,改变远端配置后,控制台输出数据已经发生变化,但请求链接获取数据依旧不变) # 当设置spring.cloud.nacos.config.namespace=public时就会不停打印nacos获取到的配置信息,因为public是没有唯一标识(命名空间ID)的,可取消spring.cloud.nacos.config.namespace=public的配置来解决,另外还有https://blog.csdn.net/qq_42971035/article/details/126819956这种情况
-
主启动类
@SpringBootApplication public class CloudAlibabaConfigClient3377Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaConfigClient3377Application.class, args); } }
-
业务代码
@RequestMapping("/config") @RestController @RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能 public class ConfigClientController { @Value("${config.info}") private String configInfo; @GetMapping("/info") public String getConfigInfo() { return configInfo; } }
在Nacos中添加配置信息
理论
Nacos中的dataid的组成格式及与SpringBoot配置文件中的匹配规则
说明:之所以要配置 spring.application.name,是因为它是构成 Nacos 配置管理 dataId字段的一部分
在 Nacos Spring Cloud 中,dataId 的完整格式如下:(就是说在nacos端我们怎么命名文件的)
https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
- prefix 默认为 spring.application.name 的值,也可以通过配置项 spring.cloud.nacos.config.prefix 来配置
- spring.profiles.active 即为当前环境对应的 profile,详情可以参考 Spring Boot文档。 注意:当 spring.profiles.active 为空时,对应的连接符 - 也将不存在,dataId的拼接格式变成
${prefix}.${file-extension}
- file-exetension 为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension 来配置。目前只支持 properties 和 yaml类型(注意nacos里必须使用yaml)
- 通过 Spring Cloud 原生注解
@RefreshScope
实现配置自动刷新
最后公式:
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
综合以上说明,和下面的截图,Nacos 的 dataid(类似文件名)应为:cloud-alibaba-config-client-dev.yaml
(必须是yaml)
实操
nacos配置列表新增:cloud-alibaba-config-client-dev.yaml
tips:不要掉了 .yaml
配置列表,右侧+号
config:
info: "config info for dev,from nacos config center."
历史配置:Nacos会记录配置文件的历史版本默认保留30天,此外还有一键回滚功能,回滚操作将会触发配置更新
配置列表,对应配置点击更多历史版本 或者 直接在 历史版本列表输入 Data Id 和 Group 进行搜索
测试
-
完成对应配置并启动对应服务
-
访问:
http://localhost:3377/config/info
,查看到对应结果 -
自带动态刷新:修改下Nacos中的yaml配置文件,再次调用查看配置的接口,就会发现配置已经刷新
去除
@RefreshScope
后动态刷新失效,写在主启动类上也失效注意配置了
@RefreshScope
注解的类通过Spring注入JSON序列化可能失败,因为是个代理类,结构比较复杂
Nacos作为配置中心-分类配置
问题——多环境多项目管理
-
问题1:
实际开发中,通常一个系统会准备:dev开发环境、test测试环境、prod生产环境,如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?
-
问题2:
一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…那怎么对这些微服务配置进行管理呢?
Nacos的图形化管理界面
- 配置管理
- 命名空间
Namespace+Group+Data ID三者关系?
思考:为什么这么设计?
-
是什么?
类似Java里面的package名和类名,最外层的namespace是可以用于区分部署环境的,Group和DataID逻辑上区分两个目标对象
-
三者情况
-
默认情况
Namespace=public,Group=DEFAULT_GROUP,默认Cluster是DEFAULT
-
Nacos默认的命名空间是public;Namespace主要用来实现隔离,比方说我们现在有三个环境:开发、测试、生产环境,我们就可以创建三个Namespace,不同的Namespace之间是隔离的
-
Group默认是DEFAULT_GROUP,Group可以把不同的微服务划分到同一个分组里面去
-
Service就是微服务;一个Service可以包含多个Cluster(集群),Nacos默认Cluster是DEFAULT,Cluster是对指定微服务的一个虚拟划分。比方说为了容灾,将Service微服务分别部署在了杭州机房和广州机房,这时就可以给杭州机房的Service微服务起一个集群名称(HZ),给广州机房的Service微服务起一个集群名称(GZ),还可以尽量让同一个机房的微服务互相调用,以提升性能
-
最后是Instance,就是微服务的实例
-
三种方案加载配置
-
DataId方案
指定spring.profile.active和配置文件的DataID来使不同环境下读取不同的配置
默认空间+默认分组+新建dev和test两个DataID
新建test配置DataID:
cloud-alibaba-config-client-test.yaml
config: info: "config info for test,from nacos config center."
通过
spring.profile.active
属性就能进行多环境下配置文件的读取-
测试
修改
spring.profile.active=test
访问:
http://localhost:3377/config/info
,发现读取到test配置文件的的信息
-
-
Group方案
通过Group实现环境区分,新建Group
Data Id:
cloud-alibaba-config-client.yaml
Group:
DEV_GROUP
|TEST_GROUP
config: info: "cloud-alibaba-config-client.yaml,DEV_GROUP,version = v1.0." --- config: info: "cloud-alibaba-config-client.yaml,TEST_GROUP,version = v1.0."
新建配置时选择Group为DEV_GROUP/TEST_GROUP
# 修改 配置文件 spring: cloud: nacos: config: # ... 添加 group: DEV_GROUP # group: TEST_GROUP
访问:
http://localhost:3377/config/info
查看结果 -
Namespace方案
命名空间 - 新建dev/test的Namespace,注意命名空间列表的命名空间ID
服务管理-服务列表-查看命名空间栏
# 指定服务启动后的配置空间,命名空间ID # 注意:不同命名空间的服务是无法通过Feign调用的 # 默认为 public spring.cloud.nacos.discovery.namespace=6011cb49-3872-4c75-a2c3-298398e6098c
配置管理-配置列表-查看命名空间栏
# 指定获取配置的空间,命名空间ID # 默认为 public spring.cloud.nacos.config.namespace=6011cb49-3872-4c75-a2c3-298398e6098c
按照域名配置填写:
config: info: "dev,cloud-alibaba-config-client-dev.yaml,DEFAULT_GROUP,version = v1.0." --- config: info: "test,cloud-alibaba-config-client-test.yaml,test,DEFAULT_GROUP,version = v1.0."
# 修改 配置文件 spring: cloud: nacos: config: # ... 添加 (去掉group,因为远端写的DEFAULT_GROUP) namespace: 6011cb49-3872-4c75-a2c3-298398e6098c # 指定获取配置的空间,命名空间ID # namespace: 0295e0d2-3380-49f4-8a88-f7a5e6b97f71
访问:
http://localhost:3377/config/info
查看结果 -
常用方法
有两个大项目A和B, A下有微服务A1和A2, B下有微服务B1和B2,假设命名空间ID和命名空间名称相同
-
Namespace
主要用来区分环境,一般为项目名+环境
,因为不同命名空间下的服务说无法通过Feign
调用的spring.cloud.nacos.config.namespace=A-dev或A-test或B-dev或B-test
-
Group
主要用来代表项目,一般为当前服务(项目)名
spring.cloud.nacos.config.namespace.group=A1或A2或B1或B2
-
DataId
是一个工程的主配置文件, 一般为,当前服务(项目)名+对应的环境
A1-dev.yaml | A1-test.yaml | A2-dev.yaml | A2-test.yaml | B1-dev.yaml | B1-test.yaml | B2-dev.yaml | B2-test.yaml
-
Nacos集群和持久化配置
官网说明
集群部署架构图
因此开源的时候推荐用户把所有服务列表放到一个vip下面,然后挂到一个域名下面
http://ip1:port/openAPI 直连ip模式,机器挂则需要修改ip才可以使用。
http://SLB:port/openAPI 挂载SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),直连SLB即可,下面挂server真实ip,可读性不好。
http://nacos.com:port/openAPI 域名 + SLB模式(内网SLB,不可暴露到公网,以免带来安全风险),可读性好,而且换ip方便,推荐模式
tips:VIP = Virtual IP Address,虚拟IP地址,主要是用来进行不同主机之间的切换,主要用在服务器的主从切换。
上图官网翻译,真实情况
-
说明:
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储
按照上述,我们需要mysql数据库
-
重点说明:
Nacos支持三种部署模式
- 单机模式 - 用于测试和单机试用。
- 集群模式 - 用于生产环境,确保高可用。
- 多集群模式 - 用于多数据中心场景。
环境准备
- 安装好 JDK,需要 1.8 及其以上版本
- 建议: 2核 CPU / 4G 内存 及其以上
- 建议: 生产环境 3 个节点 及其以上
单机模式下运行Nacos
Linux/Unix/Mac
# Standalone means it is non-cluster Mode. $ sh startup.sh -m standalone
Windows
# Standalone means it is non-cluster Mode. $ cmd startup.cmd -m standalone
单机模式支持mysql
在0.7版本之前,在单机模式时nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作步骤:
- 1.安装数据库,版本要求:5.6.5+
- 2.初始化mysql数据库,数据库初始化文件:mysql-schema.sql
- 3.修改conf/application.properties文件,增加支持mysql数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码。
spring.datasource.platform=mysql db.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默认自带的是嵌入式数据库derby
derby到mysql切换配置步骤:
-
nacos-server-1.1.4\nacos\conf
目录下找到sql脚本,执行nacos-mysql.sql
# 先创建数据库 CREATE DATABASE nacos_config; USE nacos_config;
-
修改
nacos/conf/application.properties
文件(切换数据库),增加支持mysql
数据源配置(目前只支持mysql),添加mysql数据源的url、用户名和密码spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=123456
重启nacos,
shutdown.cmd
->startup.cmd
-
访问:
http://localhost:8848/nacos
,可以看到是个全新的空记录界面,以前是记录进derby添加一个配置文件
test
内容随意,查看数据库发现config_info
表和his_config_info
表存入记录说明:如果新增配置失败,可能是不同版本间数据库存在部分差异,删除数据库里面的表然后重新执行当前
nacos
的config
目录下nacos-mysql.sql
重启nacos,配置依旧存在
Linux版Nacos+MySQL生产环境配置
需求,1个Nginx+3个nacos注册中心+1个mysql
需求来源,官网
环境准备
- 安装好 JDK,需要 1.8 及其以上版本
- 建议: 2核 CPU / 4G 内存 及其以上
- 建议: 生产环境 3 个节点 及其以上
下载地址nacos-server-1.1.4.tar.gz
-
解压后安装:
[root@junjian logs]# pwd /opt/study [root@junjian study]# tar -zxvf nacos-server-1.1.4.tar.gz [root@junjian study]# ls nacos [root@junjian study]# cp -r nacos cluster-nacos [root@junjian study]# ls cluster-nacos nacos [root@junjian study]# cd cluster-nacos/conf [root@junjian conf]# ls application.properties application.properties.example cluster.conf.example nacos-logback.xml nacos-mysql.sql schema.sql [root@junjian conf]# mysql -uroot -p Enter password: mysql> CREATE DATABASE nacos_config; Query OK, 1 row affected (0.00 sec) mysql> USE nacos_config; Database changed mysql> source /opt/study/cluster-nacos/conf/nacos-mysql.sql mysql> show tables; +------------------------+ | Tables_in_nacos_config | +------------------------+ | config_info | | config_info_aggr | | config_info_beta | | config_info_tag | | config_tags_relation | | group_capacity | | his_config_info | | roles | | tenant_capacity | | tenant_info | | users | +------------------------+ 11 rows in set (0.01 sec) mysql> exit; Bye
-
修改配置
[root@junjian conf]# pwd /opt/study/cluster-nacos/conf [root@junjian conf]# vim application.properties [root@junjian conf]# ls application.properties application.properties.example cluster.conf.example nacos-logback.xml nacos-mysql.sql schema.sql [root@junjian conf]# cp cluster.conf.example cluster.conf [root@junjian conf]# hostname -i 192.168.159.11 [root@junjian conf]# vim cluster.conf [root@junjian conf]# cat cluster.conf #it is ip 192.168.159.11:3333 192.168.159.11:4444 192.168.159.11:5555
-
application.properties
添加spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=root db.password=123456
-
cluster.conf
内容#it is ip 192.168.159.11:3333 192.168.159.11:4444 192.168.159.11:5555
这个IP不能写127.0.0.1,必须是Linux命令
hostname -i
能够识别的IP
-
-
编辑Nacos的启动脚本
startup.sh
,使它能够接受不同的启动端口[root@junjian bin]# pwd /opt/study/cluster-nacos/bin [root@junjian bin]# vim startup.sh
# 更改前 57 while getopts ":m:f:s:" opt 58 do 59 case $opt in 60 m) 61 MODE=$OPTARG;; 62 f) 63 FUNCTION_MODE=$OPTARG;; 64 s) 65 SERVER=$OPTARG;; 66 ?) 67 echo "Unknown parameter" 68 exit 1;; 69 esac 70 done ... 130 # start 131 echo "$JAVA ${JAVA_OPT}" > ${BASE_DIR}/logs/start.out 2>&1 & 132 nohup $JAVA ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1 & 133 echo "nacos is starting,you can check the ${BASE_DIR}/logs/start.out"
# 更改后 57 while getopts ":m:f:s:p:" opt # 更改 58 do 59 case $opt in 60 m) 61 MODE=$OPTARG;; 62 f) 63 FUNCTION_MODE=$OPTARG;; 64 s) 65 SERVER=$OPTARG;; 66 p) # 新增 67 PORT=$OPTARG;; # 新增 68 ?) 69 echo "Unknown parameter" 70 exit 1;; 71 esac 72 done ... 134 nohup $JAVA -Dserver.port=${PORT} ${JAVA_OPT} nacos.nacos >> ${BASE_DIR}/logs/start.out 2>&1 & # 更改
-
启动
# 看到日志文件successfully之后再启动下一个 tail -f /opt/study/cluster-nacos/logs/start.out [root@junjian bin]# ./startup.sh -p 3333 [root@junjian bin]# ./startup.sh -p 4444 [root@junjian bin]# ./startup.sh -p 5555 # ... nacos is starting with cluster nacos is starting,you can check the /opt/study/cluster-nacos/logs/start.out
-
Nginx的配置,由它作为负载均衡器
修改nginx的配置文件:
[root@junjian conf]# pwd /usr/local/nginx/conf [root@junjian conf]# vim nginx.conf
# 新增 # cluster-nacos upstream cluster{ //配置机器集群 server 127.0.0.1:3333; server 127.0.0.1:4444; server 127.0.0.1:5555; } server { listen 1111; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { #root html; #index index.html index.htm; proxy_pass http://cluster; } }
启动或者重新加载nginx配置
systemctl start nginx # 启动 systemctl reload nginx # 重新加载配置
-
1个Nginx+3个nacos注册中心+1个mysql已配置完成,测试
访问:
http://192.168.159.11:1111/nacos/
, 查看节点列表新建一个配置测试:
# Data ID:cloud-alibaba-config-client-dev.yaml # Group: DEFAULT_GROUP # 配置内容 config: info: cluster-nacos
查看
config_info
表和his_config_info
表,看到对应数据修改
cloud-alibaba-config-client-3377
配置文件bootstrap-dev.yml
spring: cloud: nacos: discovery: server-addr: 192.168.191.112:1111 # 修改 config: server-addr: 192.168.191.112:1111 # 修改
启动服务,访问:
http://localhost:3377/config/info
,结果:cluster-nacos
问题总结
个人使用mysql8.0以上的,但nacos-server1.1.4默认支持的,所以无法连接,启动报错
org.springframework.jdbc.CannotGetJdbcConnectionException: Failed to obtain JDBC Connection; nested exception is org.apache.commons.dbcp.SQLNestedException: Cannot create PoolableConnectionFactory (Could not create connection to database server. Attempted reconnect 3 times. Giving up.)
解决办法
在
application.properties
的db.url.0
后加上时区信息&serverTimezone=GTM
# 使用mysql8.0驱动包 [root@junjian cluster-nacos]# pwd [root@junjian cluster-nacos]# mkdir -p plugins/mysql [root@junjian cluster-nacos]# cd plugins/mysql/ # 上传mysql8.0的驱动包 [root@junjian mysql]# ls mysql-connector-java-8.0.25.jar # 修改启动文件 [root@junjian mysql]# cd ../../bin/ [root@junjian bin]# vim startup.sh
# 修改内容 # 修改前 110 JAVA_OPT="${JAVA_OPT} -Dloader.path=${BASE_DIR}/plugins/health -jar ${BASE_DIR}/target/${SERVER}.jar" # 修改后 110 JAVA_OPT="${JAVA_OPT} -Dloader.path=${BASE_DIR}/plugins/health -jar ${BASE_DIR}/target/${SERVER}.jar ${BASE_DIR}/plugins/mysql"
重启nacos即可
集群nacos时间一长就会挂一个节点
百度搜索
nacos 集群节点无缘无故挂一个
关键字,原因有很多
若看到了JVM内存不足的报错,则修改启动时的JVM内存参数
启动后,发现仅部分节点可以访问,查看start.out日志,看到JVM内存不足的报错,需要调整JVM参数 # /opt/study/cluster-nacos/bin -> vim startup.sh 默认配置 87 JAVA_OPT="${JAVA_OPT} -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" 默认内存占用过大,导致后续的节点内存不足无法启动 可改为 -Xms512m -Xmx512m -Xmn256m -XX:MetaspaceSize=128m 等
nacos日志打印频繁,导致节点挂掉
客户端连接时心跳日志打印频繁,日志文件生成太快并且很大,多数都是nacos中com.alibaba.nacos.client 解决办法: 修改nacos\conf\nacos-logback.xml 添加 <logger name="com.alibaba.nacos.client.naming" level="ERROR"/> 第二种办法 jar -jar配置参数: java -jar -Dcom.alibaba.nacos.config.log.level=error -Dcom.alibaba.nacos.naming.log.level=error
一些配置总结
nacos 默认是需要登录, 有些麻烦,开发测试的时候,可以 修改 conf/application.properties 的相关配置 把它关闭
### turn off security spring.security.enabled=false management.security=false security.basic.enabled=false nacos.security.ignore.urls=/** #nacos.security.ignore.urls=/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/login,/v1/console/health/**,/v1/cs/**,/v1/ns/**,/v1/cmdb/**,/actuator/**,/v1/console/server/**
另外,nacos 的日志实在增长太快, 可以把tomcat.accesslog 关闭:
server.tomcat.accesslog.enabled=true – 改为false
另外,还可以把 日志级别调整一下,修改 conf/nacos-logback.xml 即可。
高可用小总结
SpringCloud Alibaba Sentinel 实现熔断与限流
概述
是什么
一句话解释,与Hystrix类似
Sentinel: 分布式系统的流量防卫兵
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。
去哪下
下载地址 本次下载1.7.1
版本
能干嘛
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
- 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 的主要特性:
怎么玩
服务使用中的各种问题:
- 服务雪崩
- 服务降级
- 服务熔断
- 服务限流
安装Sentinel控制台
sentinel组件由两部分构成:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器
安装步骤:
下载,下载到本地 sentinel-dashboard-1.7.1.jar
Sentinel默认是8080端口
# 启动示例
java -jar sentinel-dashboard-1.7.1.jar # 以默认8080端口启动
java -jar sentinel-dashboard-1.7.1.jar --server.port=9095 # 以9095端口启动,默认8080
nohup java -jar sentinel-dashboard-1.7.1.jar > ./logs/sentinel-dashboard.log 2>&1 & # Linux后台启动
--server.port=自定义端口。默认为 8080
--auth.username=自定义账号 和 --auth.password=自定义账号密码。默认为sentinel
--logging.file=自定义日志文件位置。默认为 ${user.home}/logs/csp/sentinel-dashboard.log
访问sentinel管理界面:http://localhost:8080
,登录账号密码均为sentinel
初始化演示工程
nacos保持启动
新建 cloud-alibaba-service-sentinel-8401
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-alibaba-service-sentinel-8401</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--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>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.wang</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> </project>
-
application.yml
server: port: 8401 spring: application: name: cloud-alibaba-service-sentinel cloud: nacos: discovery: #Nacos服务注册中心地址 server-addr: 192.168.191.112:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: 192.168.191.112:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719
-
主启动类
@SpringBootApplication public class CloudAlibabaServiceSentinel8401Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaServiceSentinel8401Application.class, args); } }
-
业务代码
@RestController public class FlowLimitController { @GetMapping("/testA") public String testA() { return "------testA"; } @GetMapping("/testB") public String testB() { return "------testB"; } }
-
测试
启动对应服务
访问:
http://192.168.159.11:8080/
,发现页面什么也没有,因为Sentinel
为懒加载访问:
localhost:8401/testA
http://localhost:8401/testB
,侧边出现cloud-alibaba-service-sentinel
,实时监控,簇点链路存在对应显示结论:sentinel正在监控微服务
cloud-alibaba-service-sentinel
流控规则
基本介绍
点击 cloud-alibaba-service-sentinel
-> 流控规则 -> 新增流控规则
解释说明:
- 资源名:唯一名称,默认请求路径
- 针对来源:sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型/单机值:
- QPS(每秒钟的请求数量):当调用该api就QPS达到阈值的时候,进行限流
- 线程数.当调用该api的线程数达到阈值的时候,进行限流
- 是否集群:不需要集群
- 流控模式:
- 直接:api达到限流条件时,直接限流。分为QPS和线程数
- 关联:当关联的资到阈值时,就限流自己。别人惹事,自己买单
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
- 流控效果:
- 快速失败:直接抛异常
- warm up:根据codeFactor(冷加载因子,默认3)的值,从阈值codeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排毒,让请求以匀速通过,阈值类型必须设置为QPS,否则无效
QPS和线程数的区别:
- QPS 类似于银行的保安:所有的请求到Sentinel 后,他会根据阈值放行,超过报错
- 线程数类似于银行的窗口:所有的请求会被放进来,但如果阈值设置为1,那么其他的请求就会报错也就是,银行里只有一个窗口,一个人在办理业务中,其他人跑过来则会告诉你“现在不行,没到你”
重要属性:
Field | 说明 | 默认值 |
---|---|---|
resource | 资源名,资源名是限流规则的作用对象 | |
count | 限流阈值 | |
grade | 限流阈值类型,QPS 模式(1)或并发线程数模式(0) | QPS 模式 |
limitApp | 流控针对的调用来源 | default,代表不区分调用来源 |
strategy | 调用关系限流策略:直接、链路、关联 | 根据资源本身(直接) |
controlBehavior | 流控效果(直接拒绝/WarmUp/匀速+排队等待),不支持按调用关系限流 | 直接拒绝 |
clusterMode | 是否集群限流 | 否 |
流控模式
直接(默认)
-
直接 -> 快速失败:系统默认
-
配置及说明:表示1秒钟内请求路径1次就是OK,若超过次数1,就直接-快速失败,报默认错误
资源名:/testA;单机阈值:1;其余默认
-
快速访问
http://localhost:8401/testA
, 结果:Blocked by Sentinel (flow limiting)
-
思考
调用可否返回默认自定义报错信息,是否应该有我们自己的后续处理,类似有个fallback的兜底方法?
关联
- 当关联的资源达到阈值时,就限流自己
- 当与A关联的资源B达到阀值后,就限流A自己
- B惹事,A挂了
配置:设置效果,当关联资源/testB的qps阀值超过1时,就限流/testA的访问,当关联资源到阈值后限制配置好的资源名
资源名:/testA;单机阈值:1;流控模式:关联;关联资源:/testB;其余默认
-
测试
快速访问:
http://localhost:8401/testA
和http://localhost:8401/testB
都不会触发流控启用
Jmeter
, 线程组循环次数选择永远,保证每秒有大于1个的线程请求http://localhost:8401/testB
,如设置线程数2,Ramp-Up时间(秒)1ramp-up 用于告知jmeter要在多长时间内建立所有的线程。即jmeter会立即建立所有的线程;如果ramp-up设置为T秒,全部线程数设置为N,则jmeter将每隔T/N秒建立一个线程。
页面访问
http://localhost:8401/testB
正常,访问:http://localhost:8401/testA
触发流控Blocked by Sentinel (flow limiting)
链路【尝试失败】
链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值
例如有两条请求链路:
- /test1 /common
- /test2 /common
如果只希望统计从/test2进入到/common的请求,对/test2 进行限流,则可以这样配置:
@Configuration
public class FilterContextConfig {
/**
* 在spring-cloud-alibaba v2.1.1.RELEASE及前,sentinel1.7.0及后,关闭URL PATH聚合需要通过该方式,spring-cloud-alibaba v2.1.1.RELEASE后,可以通过配置关闭:spring.cloud.sentinel.web-context-unify=false
* 手动注入Sentinel的过滤器,关闭Sentinel注入CommonFilter实例,修改配置文件中的 spring.cloud.sentinel.filter.enabled=false
* 入口资源聚合问题:https://github.com/alibaba/Sentinel/issues/1024 或 https://github.com/alibaba/Sentinel/issues/1213
* 入口资源聚合问题解决:https://github.com/alibaba/Sentinel/pull/1111
*/
@Bean
public FilterRegistrationBean sentinelFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CommonFilter());
registration.addUrlPatterns("/*");
// 入口资源关闭聚合
registration.addInitParameter(CommonFilter.WEB_CONTEXT_UNIFY, "false");
registration.setName("sentinelFilter");
registration.setOrder(1);
return registration;
}
}
public interface FlowLimitService {
String common();
}
@Service
public class FlowLimitServiceImpl implements FlowLimitService {
@Override
@SentinelResource("common")
public String common() {
return "--->common";
}
}
...
public class FlowLimitController {
@Resource
private FlowLimitService flowLimitService;
...
@GetMapping("/test1")
public String test1() {
return flowLimitService.common();
}
@GetMapping("/test2")
public String test2() {
return flowLimitService.common();
}
}
spring:
# ...
cloud:
# ...
sentinel:
# 自动过滤Http请求并注册为sentinel资源
# filter:
# enabled: false
# web-context-unify: false
# 资源名带Http方法,如 GET:/testA
# http-method-specify: true
# ...
可参考百度:Alibaba Sentinel 流控规则之链路,当前版本无法实现
流控效果
快速失败(默认的流控处理)
直接失败,抛出异常:Blocked by Sentinel (flow limiting)
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.DefaultController
预热
Warm Up
Warm Up(
RuleConstant.CONTROL_BEHAVIOR_WARM_UP
)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参见 WarmUpFlowDemo。通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
默认coldFactor
为3,即请求 QPS
从 threshold / 3
开始,经预热时长逐渐升至设定的 QPS
阈值
限流冷启动:https://github.com/alibaba/Sentinel/wiki/%E9%99%90%E6%B5%81—%E5%86%B7%E5%90%AF%E5%8A%A8
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController
WarmUp配置:
默认 coldFactor(冷加载因子) 为 3,即请求QPS从(threshold / 3) 开始,经多少预热时长才逐渐升至设定的 QPS 阈值
案例,阀值为10, 预热时长设置5秒,即 资源名:/testB
,单机阈值:10
, 流控效果:Warm Up
, 预热时长:5
,其余默认
系统初始化的阀值为10 / 3
约等于3
,即阀值刚开始为3
;然后过了5秒后阀值才慢慢升高恢复到10,效果为:
开始访问 http://localhost:8401/testB
时每秒请求别超过10/3个才能正常访问,5秒后可以接受的请求可以达到每秒10次
多次点击:http://localhost:8401/testB
, 刚开始不行,后续慢慢OK
-
应用场景:
秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值
排队等待
匀速排队,让请求以均匀的速度通过,阀值类型必须设成QPS,否则无效
-
设置:
资源名:/testA
,单机阈值:2
,流控效果:排队等待
,超时时间:20000
,其余默认 -
设置含义:/testA每秒1次请求,超过的话就排队等待,等待的超时时间为20000毫秒
代表 1 秒匀速的通过 2 个请求,也就是每个请求平均间隔恒定为
1 / 2 = 0.5s
也即500 ms
,每一个请求的最长等待时间为20s
同理,如果单机阈值为
1
时,每个请求的平均间隔恒定为1000/1 = 10000ms
-
说明:匀速排队,阈值必须设置为
QPS
匀速排队
匀速排队(
RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
注意:匀速排队模式暂时不支持 QPS > 1000 的场景。
源码:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController
JMeter
设置线程数20,Ramp-up时间(秒)为1,循环次数永远,请求/testA
, 查看控制台日志
@Slf4j
...
public class FlowLimitController {
...
@GetMapping("/testA")
public String testA() {
log.info("------testA");
return "------testA";
}
}
结果:当阈值为2时,每0.5s一条日志;当阈值为1时每1秒一条日志
降级规则
介绍
RT(平均响应时间,秒级)
- 平均响应时间超出阈值且在时间窗口内通过的请求
>=5
,两个条件同时满足后触发降级 - 窗口期过后关闭断路器
- RT最大4900(更大的需要通过-Dcsp.sentinel.statistic.max.rt=XXXX才能生效)
异常比列(秒级)
QPS >= 5
且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
异常数(分钟级)
- 异常数(分钟统计)超过阈值时,触发降级;时间窗口结束后,关闭降级
进一步说明:
-
Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误
-
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出
DegradeException
) -
Sentinel 的断路器是没有半开状态的(1.8.0版本已有半开状态)
半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用;有异常则继续打开断路器不可用。具体可以参考
Hystrix
RT
介绍
平均响应时间 (
DEGRADE_GRADE_RT
) : 当 1s 内持续进入 5 个请求 , 对应时刻的平均响应时间 ( 秒级 ) 均超过阈值 (count
, 以 ms 为单位 ) , 那么在接下的时间窗口 (DegradeRu1e
中的timeWindow
, 以 s 为单位 ) 之内 , 对这个方法的调用都会自动地熔断 ( 抛出DegradeException
〕 。 注意 sentinel 默认统计的 RT 上限是 4900 ms, 超出此阈值的都会算作4900 ms, 若需要变更此上限可以过虐动配置项-Dcsp.sentine1.statistic.max.rt=xxx
来配置 。
实操
...
public class FlowLimitController {
...
@GetMapping("/testC")
public String testC() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("------testC");
return "------testC";
}
...
}
-
配置:
cloud-alibaba-service-sentinel
->降级规则
->新增降级规则
->资源名:/testC
,降级策略:RT
,RT:200
,时间窗口
:1
JMeter
设置线程数10,Ramp-up时间(秒)为1,循环次数永远,请求/testC
-
页面访问:
http://localhost:8401/testC
,结果:Blocked by Sentinel (flow limiting)
停止
JMeter
后,结果------testC
-
结论:
按照上述配置,永远一秒钟打进来10个线程(大于5个了)调用
testC
,我们希望200毫秒处理完本次任务,如果超过200毫秒还没处理完,在未来1秒钟的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了,后续停止jmeter
,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务恢复
异常比例
异比例(
DEGRADE_GRADE-EXCEPTION_RATIO
) : 当资源的每秒清求量 > = 5 , 并且每秒异常总数占通过量的比值超过阈值(DegradeRu1e
中的count
) 之后 , 资源进入降级状态 , 在接下的时间窗口(DegradeRu1e
中的timeWindow
, 以 s 为单位 ) 之内 , 对这个方法的调用都会自动地返回 。 异常比率的阈值范围是 [ 0.0, 1.0], 代表 0% -100% 。
实操
...
public class FlowLimitController {
...
@GetMapping("/testD/{id}")
public String testD(@PathVariable("id") Integer id) {
if (id % 2 == 0) {
throw new RuntimeException("请输入奇数");
}
log.info("----testD --{}", id);
return String.format("------testD --%d", id);
}
...
}
/**
* <p>
* Sentinel中的HTTP服务的限流默认由Sentinel—Web-Servlet包中的CommonFilter来实现,这个Filter会把每个不同的URL都作为不同的资源来处理
* 在提供像携带{id}参数的REST风格API,对于每一个不同的{id},URL也都不一样,所以在默认情况下Sentinel会把所有的URL当做资源来进行流控
* 这种会导致两个问题:
* 1.限流统计不准确,实际需要是控制clean方法总的QPS,结果统计的是每个URL的QPS
* 2.导致Sentinel中资源数量过多,默认资源数量的阈值是6000,对于多出的资源规则将不会生效
* 正对这个问题可以通过URLCleaner接口来实现资源清洗,也就是对于/clean/{id}这个URL,我们可以统一归类到/clean/*资源下
* 实现UrlCleaner接口,并重写clean方法。
* </p>
*/
@Slf4j
@Configuration
public class CustomerUrlCleaner implements UrlCleaner {
private static final Map<String, String> URL_CLEANER_MAP = new HashMap<String, String>() {
{
put("/testD/", "/testD/*");
}
};
@Override
public String clean(String url) {
log.info(">>> url: {}", url);
if (Objects.isNull(url)) {
return url;
}
for (Map.Entry<String, String> urlCleanerEntry : URL_CLEANER_MAP.entrySet()) {
if (url.startsWith(urlCleanerEntry.getKey())) {
return urlCleanerEntry.getValue();
}
}
return url;
}
}
默认访问
/testD/1
,/testD/2
会在簇点链路中生成两个资源名/testD/1
和/testD/2
,添加UrlCleaner
对/testD/
进行资源清洗后,可将/testD/{id}
资源名统一为/testD/*
新增降级规则 -> 资源名:/testD/*
-> 降级策略:异常比例
-> 异常比例:0.2
-> 时间窗口:1
单独一次访问:http://localhost:8401/testD/2
, 结果:...请输入奇数...
,极快速点击http://localhost:8401/testD/2
(1秒大于5次),结果Blocked by Sentinel (flow limiting)
,再慢点http://localhost:8401/testD/2
, 结果:...请输入奇数...
,体现了 异常比例:0.2
和时间窗口:1
JMeter
设置线程数20,Ramp-up时间(秒)为1,循环次数永远,请求/testD/2
, 页面访问:http://localhost:8401/testD/1
, 结果:Blocked by Sentinel (flow limiting)
JMeter
请求/testD/1
, 页面极快速访问:http://localhost:8401/testD/2
, 结果:...请输入奇数...
JMeter
测试结果体现了异常比例:0.2
这一配置,JMeter
请求速度太快了
结论
按照上述配置,单独访问一次,必然来一次报错请输入奇数
,调一次错一次;多次调用达到配置条件后,断路器开启(保险丝跳闸),微服务不可用了,不再报错而是服务降级了
异常数
介绍
时间窗口一定要大于等于60秒、异常数是按照分钟统计的
异常数 (
DEGRADE_GRADE_EXCEPTION_COUNT
): 当资源近 1 分钟的异常数目超过阈值之后会进行熔断 。 汪意由于统计时间窗口是分钟级别的 , 若timeWindow
小于 60s , 则结束熔断状态后仍可能再进入熔断状态 。
实战
编辑降级规则/testD/*
-> 降级策略:异常数
-> 异常数:5
-> 时间窗口:61
访问:http://localhost:8401/testD/2
,第一次访问, 结果:...请输入奇数...
,访问第6次时,触发熔断降级,结果:Blocked by Sentinel (flow limiting)
,访问http://localhost:8401/testD/1
,结果:Blocked by Sentinel (flow limiting)
,等待61
秒后,结果:------testD --1
热点key限流
基本介绍
何为热点:热点即经常访问的数据,很多时候我们希望统计或者限制某个热点数据中访问频次最高的TopN数据,并对其访问进行限流或者其它操作
兜底方法:分为系统默认和客户自定义两种
-
之前的限流出问题后,都是用sentinel系统默认的提示:
Blocked by Sentinel (flow limiting)
-
我们能不能自定?类似hystrix,某个方法出问题了,就找对应的兜底降级方法?
结论:从 @HystrixCommand
到 @SentinelResource
实操
...
public class FlowLimitController {
...
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "dealHandlerTestHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
log.info("----testHotKey -{} -{}", p1, p2);
return String.format("------testHotKey -%s -%s", p1, p2);
}
public String dealHandlerTestHotKey(String p1, String p2, BlockException exception) {
return String.format("------dealHandler_testHotKey -%s -%s", p1, p2);
}
...
}
@SentinelResource
参数说明:
-
value
:资源名 -
fallback
:若本接口出现未知异常,则调用fallback指定的接口 -
blockHandler
:若本次访问被限流或服务降级,则调用blockHandler指定的接口
配置:
新增热点限流规则
-> 资源名:testHotKey
,参数索引:0
,单机阈值:1
,统计窗口时长:1
限流模式只支持QPS模式,固定写死了。(这才叫热点)@SentinelResource注解的方法参数索引,0代表第一个参数,1代表第二个参数,依此类推,单机阀值以及统计窗口时长表示在此窗口时间超过阀值就限流。即当第一个参数有值的话,1秒的QPS为1,超过就限流,限流后调用dealHandlerTestHotKey
方法。
测试:
连续点击访问:http://localhost:8401/testHotKey?p1=1&p2=2
, 结果:------dealHandler_testHotKey -1 -2
连续点击访问:http://localhost:8401/testHotKey?p1=1
, 结果:------dealHandler_testHotKey -1 -null
连续点击访问:http://localhost:8401/testHotKey?p2=2
, 结果:------testHotKey -null -2
。未触发限流
RESTFUL使用
@GetMapping("/testD/{id}") @SentinelResource(value = "testDHotKey", blockHandler = "dealHandlerTestDHotKey") // 一定要加,否则无法统计到参数 public String testD(@PathVariable("id") Integer id) { if (id % 2 == 0) { throw new RuntimeException("请输入奇数"); } log.info("----testD --{}", id); return String.format("------testD --%d", id); } public String dealHandlerTestDHotKey(Integer id, BlockException exception) { // 不要掉了 BlockException exception,方法名最好不要一样 return String.format("------dealHandler_testDHotKey --%d", id); }
新增热点限流规则
->资源名:testDHotKey
,参数索引:0
,单机阈值:1
,统计窗口时长:1
参数例外项
上述案例当第一个参数p1,当QPS超过1秒1次点击后马上被限流
特例情况:我们期望p1参数当它是某个特殊值时,它的限流值和平时不一样
特例:假如当p1的值等于5时,它的阈值可以达到200
热点参数的注意点,参数必须是基本类型或者String
配置:
热点规则testHotKey
-> 编辑
-> 高级选项
-> 参数类型:java.lang.String
,参数值:5
,限流阈值:200
-> 添加
-> 保存
测试:
连续点击访问:http://localhost:8401/testHotKey?p1=5
,结果:------testHotKey -5 -null
,无法触发限流
JMeter
设置线程数200,Ramp-up时间(秒)为1,循环次数永远,请求/testHotKey?p1=5
页面访问:http://localhost:8401/testHotKey?p1=5
, 结果:------dealHandler_testHotKey -5 -null
,触发限流
JMeter
指定 QPS 为固定值
HTTP请求
右键 -> 添加-定时器-常数吞吐量定时器 -> 设置目标吞吐量(每分钟的样本量):12000
想要QPS为200(每秒),目标吞吐量(每分钟的样本量) = 200 * 60 = 12000
其它
@SentinelResource属性介绍:
- Value:资源名称,必需项(不能为空)
- entryType:entry类型,标记流量的⽅向,取值IN/OUT,可选项(默认为EntryType.OUT)
- blockHandler:处理BlockException的函数名称(可以理解对Sentinel的配置进⾏⽅法兜底)。函数要求:
- 必须是public修饰
- 返回类型与原⽅法⼀致
- 参数类型需要和原⽅法相匹配,并在最后加BlockException类型的参数。
- 默认需和原⽅法在同⼀个类中,若希望使⽤其他类的函数,可配置blockHandlerClass,并指定blockHandlerClass⾥⾯的⽅法。
- blockHandlerClass:存放blockHandler的类。对应的处理函数必须是public static修饰,否则⽆法解析,其他要求:同
blockerHandler。 - fallback:⽤于在抛出异常的时候提供fallback处理逻辑(可以理解为对java异常情况⽅法兜底)。fallback函数可以针对所有类型的异
常(除了exceptionsToIgnore⾥⾯排除掉的异常类型)进⾏处理。函数要求:- 返回类型与原⽅法⼀致
- 参数类型需要和原⽅法相匹配,Sentinel 1.6开始,也可以在⽅法最后加Throwable类型的参数。
- 默认需和原⽅法在同⼀个类中。若希望使⽤其他类的函数,可配置fallbackClass,并制定fallbackClass⾥⾯的⽅法。
- fallbackClass:存放fallback的类。对应的处理函数必须static修饰,否则⽆法解析,其他要求:同fallback。
- defaultFallback:⽤于通⽤的fallback逻辑。默认fallback函数可以针对所有类型的异常(除了exceptionsToIgnore⾥⾯排除掉的异常类
型)进⾏处理。若同时配置了fallback和defaultFallback,以fallback为准。函数要求:- 返回类型与原⽅法⼀致。
- ⽅法参数列表为空,或者有⼀个Throwable类型的参数。
- 默认需要和原⽅法在同⼀个类中。若希望使⽤其他类的函数,可配置fallbackClass,并指定fallbackClass⾥⾯的⽅法。
- exceptionsToIgnore:指定排除掉哪些异常。排除的异常不会计⼊异常统计,也不会进⼊fallback逻辑,⽽是原样抛出。
- exceptionsToTrace:需要trace(追踪)的异常
系统规则
介绍
各项配置参数说明
系统规则
系统保护规则是从应用级别的入口流量进行控制 , 从单台机器的 load 、 CPIJ 便用率 、 平均 RT 、 入口QPS 和并发线程数等几个维度监控应用指标 , 让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性 。
系统保护规则是应用整体维度的 , 而不是资源维度的 ,并且仅对入口流量生效 。 入口流量指的是进入应用的流量 (EntryType.IN
) , 比如 Web 服务或 Dubbo 服务端接收的情求 , 都属于入口流是 。
系统规则支持以下的模式:
- Load自适应(仅对Linux/Unix-like 机器生效):系统的 load1 作为启发指标 , 进行自适应系统保护 。 当系统 loadl 超过设定的启发值 , 且系统当前的并发线程数超过估算的系统容是时才会触发系统保护 (BBR 阶段 ) 。 系统容是由系统的
maxQps * minRt
估算得出 。 设定参考值一般是CPU cores * 2.5
- CPU usage(1.5.0 + 版本):当系统 CPU 使用率超过阈值即触发系统保护 (取值范围 0.0—1.0,比较灵敏 。
- 平均 RT:当单台机器上所有入口流的平均 RT 达到值即触发系统保护 , 单位是毫秒 。
- 并发线程数:当单台机器上所有入口流的并发线程数达到阈值即触发系统保护 。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护 。
可以配置全局QPS
@SentinelResource
按资源名称限流+后续处理
cloud-alibaba-service-sentinel-8401
pom文件增加
<dependency>
<groupId>com.wang</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
// 添加Controller
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public CommonResult<Object> byResource() {
return new CommonResult<>(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
public CommonResult<Object> handleException(BlockException exception) {
return new CommonResult<>(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
}
访问:http://localhost:8401/byResource
,结果:{"code":200,"message":"按资源名称限流测试OK","data":{"id":2020,"serial":"serial001"}}
配置流控规则,簇点链路:byResource
是资源名
簇点链路页面
找到资源名byResource
点击右侧流控
-> 设置单机阈值:1
, 表示1秒钟内查询次数大于1,就跑到我们自定义的处流,限流
快速访问:http://localhost:8401/byResource
,结果:{"code":444,"message":"com.alibaba.csp.sentinel.slots.block.flow.FlowException\t 服务不可用","data":null}
-
额外问题
此时关闭问服务8401看看:Sentinel控制台,流控规则消失了,此时Sentinel所配置的规则为临时的,如何持久化。
按照Url地址限流+后续处理
通过访问的URL来限流,会返回Sentinel自带默认的限流处理信息
...
public class RateLimitController {
...
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult<Object> byUrl() {
return new CommonResult<>(200, "按url限流测试", new Payment(2020L, "serial002"));
}
}
簇点链路页面
找到资源名/rateLimit/byUrl
点击右侧流控
-> 设置单机阈值:1
快速访问http://localhost:8401/rateLimit/byUrl
,显示默认的流控处理信息Blocked by Sentinel (flow limiting)
URL地址流控优先级大于资源名称流控
- 上面兜底方案面临的问题
- 系统默认的,没有体现我们自己的业务要求
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观
- 每个业务方法都添加一个兜底的,那代码膨胀加剧
- 全局统一的处理方法没有体现
客户自定义限流处理逻辑
// 新建 CustomerBlockHandler 用于自定义限流处理逻辑
public class CustomerBlockHandler {
public static CommonResult<Object> handleException(BlockException exception) {
return new CommonResult<>(2020, "自定义的限流处理信息......CustomerBlockHandler");
}
public static CommonResult<Object> handleException2(BlockException exception) {
return new CommonResult<>(2020, "自定义的限流处理信息2......CustomerBlockHandler2");
}
}
...
public class RateLimitController {
...
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
public CommonResult<Object> customerBlockHandler() {
return new CommonResult<>(200, "按客户自定义限流处理逻辑");
}
}
测试
启动微服务后先调用一次:http://localhost:8401/rateLimit/customerBlockHandler
资源名customerBlockHandler
-> 设置`单机阈值:1
快速访问:http://localhost:8401/rateLimit/customerBlockHandler
,结果:{"code":2020,"message":"自定义的限流处理信息2......CustomerBlockHandler2","data":null}
更多注解属性说明
@SentinelResource注解最主要的两个用法:限流控制和熔断降级的具体使用案例介绍完了。另外,该注解还有一些其他更精细化的配置,比如忽略某些异常的配置、默认降级函数等等,具体可见如下说明:
- value:资源名称,必需项(不能为空)
- entryType:entry类型,可选项(默认为 EntryType.OUT)
- fallback: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 逻辑中,而是会原样抛出。
所有的代码都要用try-catch-finally方式进行处理
2 . Define Resource
Wrap your code snippet via Sentinel API:
SphU.entry(resourceName)
. ln below example, it isSys tem.out.println("hello World")
;
try (Entry entry = SphU.entry("He11oWor1d")) { // Your business logic here. System.out.print1n ("he110 world"); } catch (B1ockException e) { // Hand1e rejected request . e.printStackTrace(); } // try-with-resources auto exit
Sentinel主要有三个核心Api:
- SphU定义资源
- Tracer定义统计
- ContextUtil定义了上下文
服务熔断功能
sentinel
整合ribbon
+openFeign
+fallback
84
-> ribbon
负载均衡 -> 9003/9004
熔断Ribbon系列—提供者9003/9004
新建cloud-alibaba-provider-payment-9003/9004
, 两个一样的做法
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-alibaba-provider-payment-9003</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!-- springcloud alibaba nacos 依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- springboot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!-- 日常通用jar包 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <groupId>com.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> </dependencies> </project>
-
application.yml
server: port: 9003 spring: application: name: cloud-alibaba-provider-payment cloud: nacos: discovery: server-addr: 192.168.191.112:8848 management: endpoints: web: exposure: include: '*'
-
主启动类
@SpringBootApplication public class CloudAlibabaProviderPayment9003Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaProviderPayment9003Application.class, args); } }
-
业务代码
@RestController @RequestMapping("/payment") public class PaymentController { @Value("${server.port}") private String serverPort; /** * 模拟sql查询 */ public static HashMap<Long, Payment> hashMap = new HashMap<>(); static { hashMap.put(0L, new Payment(null, "0c1b0a057bc5e488e917d22d51d38e10a")); hashMap.put(1L, new Payment(null, "16f753cda7b4f43679e2048e6ef2abefe")); hashMap.put(2L, new Payment(null, "21c66e6a7152d44e8acd5d91cc9dd3bf9")); } @GetMapping("/get/{id}") public CommonResult<Object> paymentSql(@PathVariable("id") Long id) { Payment payment = hashMap.get(id); return new CommonResult<>(200, "from mysql, server port : " + serverPort + " ,查询成功", payment); } }
-
测试
访问:
http://localhost:9003/payment/get/1
,结果:{"code":200,"message":"from mysql, server port : 9003 ,查询成功","data":{"id":1,"serial":"0c1b0a057bc5e488e917d22d51d38e10a"}}
熔断Ribbon系列—消费者84
新建:cloud-alibaba-consumer-order-84
-
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>spring-cloud2020</artifactId> <groupId>com.wang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-alibaba-consumer-order-84</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!--SpringCloud ailibaba nacos --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--SpringCloud ailibaba sentinel --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --> <dependency> <groupId>com.wang</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!-- SpringBoot整合Web组件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <!--日常通用jar包配置--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
-
application.yml
server: port: 84 spring: application: name: cloud-alibaba-consumer-order cloud: nacos: discovery: server-addr: 192.168.191.112:8848 sentinel: transport: #配置Sentinel dashboard地址 dashboard: 192.168.191.112:8080 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口 port: 8719
-
主启动类
@EnableFeignClients @SpringBootApplication public class CloudAlibabaConsumerOrder84Application { public static void main(String[] args) { SpringApplication.run(CloudAlibabaConsumerOrder84Application.class, args); } }
-
业务代码
@FeignClient(name = "cloud-alibaba-provider-payment", path = "/payment") public interface PaymentFeign { @GetMapping("/get/{id}") CommonResult<Payment> getPayment(@PathVariable("id") Integer id); } @RestController @RequestMapping("/consumer") public class OrderController { @Resource private PaymentFeign paymentFeign; @GetMapping("/payment/fallback/{id}") @SentinelResource(value = "fallback") public CommonResult<Payment> paymentInfo(@PathVariable("id") Integer id) { CommonResult<Payment> result = paymentFeign.getPayment(id); if (id <= 0) { throw new IllegalArgumentException("IllegalArgumentException,非法参数异常...."); } else if (result.getData() == null) { throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常"); } return result; } }
①无配置
修改@SentinelResource
后需重启微服务:热部署对java代码级生效及时,对@SentinelResource
注解内属性,有时效果不好
目的:fallback
管运行异常、blockHandler
管配置违规
访问:http://localhost:84/consumer/payment/fallback/1
,结果:{"code":200,"message":"from mysql, server port : 9004 ,查询成功","data":{"id":1,"serial":"0c1b0a057bc5e488e917d22d51d38e10a"}}
访问:http://localhost:84/consumer/payment/fallback/4
,结果:...NullPointerException,该ID没有对应记录,空指针异常...
展示error页面,不友好
②只配置fallback
...
public class OrderController {
...
@SentinelResource(value = "fallback", fallback = "handlerFallback")
...
public CommonResult<Payment> handlerFallback(Integer id, Throwable e) {
Payment payment = new Payment(null, "null");
if (Objects.nonNull(id)) {
payment.setId(Long.valueOf(id));
}
return new CommonResult<>(444, "兜底异常handlerFallback,exception内容 " + e.getMessage(), payment);
}
}
访问:http://localhost:84/consumer/payment/fallback/4
,结果:{"code":444,"message":"兜底异常handlerFallback,exception内容 NullPointerException,该ID没有对应记录,空指针异常","data":{"id":4,"serial":"null"}}
③只配置blockHandler
...
public class OrderController {
...
@SentinelResource(value = "fallback", blockHandler = "blockHandler")
...
public CommonResult<Payment> blockHandler(@PathVariable Integer id, BlockException blockException) {
Payment payment = new Payment(null, "null");
if (Objects.nonNull(id)) {
payment.setId(Long.valueOf(id));
}
return new CommonResult<>(445, "blockHandler-sentinel限流,无此流水: blockException " + blockException.getMessage(), payment);
}
}
本例sentinel需配置:
资源名fallback -> 降级 -> 降级策略:异常数 -> 异常数:2 -> 时间窗口:70
访问:http://localhost:84/consumer/payment/fallback/4
,结果:前两次异常界面,第三次后{"code":445,"message":"blockHandler-sentinel限流,无此流水: blockException null","data":{"id":4,"serial":"null"}}
,70s后恢复
④fallback和blockHandler都配置
...
public class OrderController {
...
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler")
...
}
本例sentinel需配置:
资源名fallback -> 降级 -> 降级策略:异常数 -> 异常数:2 -> 时间窗口:50
访问:http://localhost:84/consumer/payment/fallback/4
,前两次触发兜底异常handlerFallback,exception内容 NullPointerException,该ID没有对应记录,空指针异常
, 之后(时间窗口期结束前)触发blockHandler-sentinel限流,无此流水: blockException null
结论:若 blockHandler
和 fallback
都进行了配置,则被限流降级而抛出 BlockException
时只会进入 blockHandler
处理逻辑
⑤忽略异常
...
public class OrderController {
...
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler", exceptionsToIgnore = {IllegalArgumentException.class})
...
}
本例sentinel需配置:
资源名fallback -> 降级 -> 降级策略:异常数 -> 异常数:2 -> 时间窗口:50
访问:http://localhost:84/consumer/payment/fallback/-1
,结果:...IllegalArgumentException,非法参数异常....
且为erro
页面,不会触发handlerFallback
和blockHandler
,因为IllegalArgumentException
已被忽略
熔断OpenFeign
上面是单个进行 fallback
和 blockhandler
的测试,下面是整合 openfeign
实现把降级方法解耦。和Hystrix
几乎一摸一样!
-
添加open-feign依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
yml
追加如下配置# 激活Sentinel对Feign的支持 feign: sentinel: enabled: true # default context 连接超时时间 # feign.client.config.default.connectTimeout = 5000 # default context 读超时时间 # feign.client.config.default.readTimeout = 10000 # 设置重试处理器,默认直接抛出异常 # feign.client.config.default.retryer = Class<Retryer> # 设置日志级别,默认NONE # feign.client.config.default.loggerLevel = FULL # 连接超时时间,默认为1秒,该值会被FeignClient配置connectTimeout覆盖 # ribbon.ConnectTimeout=5000 # 读超时时间,默认为1秒,该值会被FeignClient配置readTimeout覆盖 # ribbon.ReadTimeout=5000 # 最大重试次数 # ribbon.MaxAutoRetries=1
-
代码
主启动类添加注解 :
@EnableFeignClients
激活open-feign
// 84 @FeignClient(name = "cloud-alibaba-provider-payment", path = "/payment", ) public interface PaymentFeign { @GetMapping("/get/{id}") CommonResult<Payment> getPayment(@PathVariable("id") Integer id); } @Component // 一定要写,不然服务无法启动 public class PaymentFallbackService implements PaymentFeign { @Override public CommonResult<Payment> getPayment(Integer id) { return new CommonResult<>(44444,"服务降级返回,---PaymentFallbackService"); } } ... public class OrderController { @Resource private PaymentFeign paymentFeign; ... @GetMapping("/payment/get/{id}") public CommonResult<Payment> getPaymentInfo(@PathVariable("id") Integer id) { return paymentFeign.getPayment(id); } ... } // 9003/9004 // 为了模拟异常,在远程调用方添加延时(open-feign默认延时大于1秒才会回调),如下。 ... public class PaymentController { ... public CommonResult<Object> paymentSql(@PathVariable("id") Long id) { try { TimeUnit.SECONDS.sleep(6); } catch (InterruptedException ignore) { } ... } ... } // 测试时发现如果只给一个微服务加延时,另一个不加,会一直调用没有延时的微服务,所以要么两个都加延时或者停掉一个。
-
测试
访问:
http://localhost:84/consumer/payment/get/1
,结果:{"code":44444,"message":"服务降级返回,---PaymentFallbackService","data":null}
持久化
目前当 sentinel
重启以后,数据都会丢失,和 nacos
类似原理。需要持久化。它可以被持久化到 nacos
的数据库中。
选一个带sentinel
的服务操作,以84
为例
-
pom文件
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency>
-
application.yml
spring: cloud: sentinel: datasource: # 这个名字任意起 ds-flow: nacos: server-addr: 192.168.191.112:8848 # 和 nacos中保持对应 dataId: ${spring.application.name}-flow-rules # 和nacos中保持对应 group: DEFAULT_GROUP # 对于 nacos 数据类型 data-type: json # 规则类型:流控 rule-type: flow
-
先创建
nacos
配置去
nacos
上创建一个dataid
,名字和yml
配置的一致,json
格式,内容如下:Data ID:cloud-alibaba-consumer-order-flow-rules
,Group:DEFAULT_GROUP
,配置格式:JSON
// 配置内容 [ { "resource": "fallback", "limitApp": "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表示排队等待;cIusterMode:
是否集群
不知道配置内容该如何获取:
页面新建一个流控规则,在
流控规则
页面打开控制台,点击刷新查看接口返回结果,将不需要的参数去除,如id
,port
,ip
等 -
测试
启动应用,发现存在 关于资源
fallback
的流控规则;重启sentinel
,配置还在。修改
nacos
:cloud-alibaba-consumer-order-flow-rules
的count
为10,fallback
流控规则的阈值也变为10这种方法只支持
sentinel
拉取nacos
中的配置内容,不支持将新增的配置推送到nacos
若需推送,参考sentinel-dashboard-nacos: sentine | 在生产环境中使用 Sentinel
总结: 就是在 sentinel 启动的时候,去 nacos 上读取相关规则配置信息,实际上它规则的持久化,就是第三步,粘贴到nacos上保存下来,就算以后在 sentinel 上面修改了,重启应用以后也是无效。
SpringCloud Alibaba Seata 解决分布式事务问题
分布式事务问题由来
分布式前
- 单机单库没这个问题
- 从1:1 -> 1:N -> N:N
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证。
用例
用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持:
- 仓储服务:对给定的商品扣除仓储数量。
- 订单服务:根据采购需求创建订单。
- 帐户服务:从用户帐户中扣除余额。
架构图
一句话:一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
Seata简介
是什么
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
能干嘛
一个典型的分布式事务过程
分布式事务处理的过程一一ID+三组件模型:
Transaction ID
XID
全局唯一的事务ID
三组件概念
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
处理过程
- TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID;
- XID在微服务调用链路的上下文中传播;
- RM向TC注册分支事务,将其纳入XID对应全局事务的管辖;
- TM向TC发起针对XID的全局提交或回滚决议;
- TC调度XID下管辖的全部分支事务完成提交或回滚请求。
Seata-Server安装
去哪下
Releases · seata/seata · GitHub
怎么玩
- 本地
@Transactional
- 全局
@GlobalTransactional
SEATA 的分布式交易解决方案
我们只需要使用一个 @GlobalTransactional
注解在业务方法上:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
......
}
安装
下载v0.9.0
版本 Releases · seata/seata · GitHub
[root@junjian study]# pwd
/opt/study
[root@junjian study]# tar -zxvf seata-server-0.9.0.tar.gz
[root@junjian seata]# ls
seata
[root@junjian seata]# cd seata/conf/
修改配置信息,主要修改:自定义事务组名称+事务日志存储模式为db +数据库连接信息
-
修改
file.conf
[root@junjian conf]# cp file.conf file.conf.init [root@junjian conf]# vim file.conf
# service模块 29 service { 30 #vgroup->rgroup 31 vgroup_mapping.fsp_tx_group = "default" 32 #only support single node 33 default.grouplist = "127.0.0.1:8091" 34 #degrade current not support 35 enableDegrade = false 36 #disable 37 disable = false 38 #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent 39 max.commit.retry.timeout = "-1" 40 max.rollback.retry.timeout = "-1" 41 } # store模块 ## transaction log store 55 store { 56 ## store mode: file、db 57 mode = "db" 58 59 ## file store 60 file { 61 dir = "sessionStore" 62 63 # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions 64 max-branch-session-size = 16384 65 # globe session size , if exceeded throws exceptions 66 max-global-session-size = 512 67 # file buffer size , if exceeded allocate new buffer 68 file-write-buffer-cache-size = 16384 69 # when recover batch read size 70 session.reload.read_size = 100 71 # async, sync 72 flush-disk-mode = async 73 } 74 75 ## database store 76 db { 77 ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. 78 datasource = "dbcp" 79 ## mysql/oracle/h2/oceanbase etc. 80 db-type = "mysql" 81 driver-class-name = "com.mysql.cj.jdbc.Driver" 82 url = "jdbc:mysql://127.0.0.1:3306/seata" 83 user = "root" 84 password = "123456" 85 min-conn = 1 86 max-conn = 3 87 global.table = "global_table" 88 branch.table = "branch_table" 89 lock-table = "lock_table" 90 query-limit = 100 91 } 92 }
#vgroup->rgroup vgroup_mapping.key = "value" #only support single node value.grouplist = "127.0.0.1:8091"
-
替换默认
mysql
驱动包因为
mysql
为8.0以上版本,当前版本seata默认mysql驱动包版本较低,故需要替换8.0以上
driver-class-name = "com.mysql.cj.jdbc.Driver"
[root@junjian lib]# pwd /opt/study/seata/lib [root@junjian lib]# find -name 'mysql*' ./mysql-connector-java-5.1.30.jar [root@junjian lib]# rm -f mysql-connector-java-5.1.30.jar # 上传8.0的驱动包到当前目录下 [root@junjian lib]# find -name 'mysql*' ./mysql-connector-java-8.0.25.jar
-
数据库新建库seata,在seata库里建表(执行
seata/conf/db_store.sql
)mysql> CREATE DATABASE seata; Query OK, 1 row affected (0.07 sec) mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | nacos_config | | performance_schema | | seata | | sys | +--------------------+ 6 rows in set (0.04 sec) mysql> use seata; Database changed mysql> source /opt/study/seata/conf/db_store.sql; Query OK, 0 rows affected, 1 warning (0.00 sec) Query OK, 0 rows affected (0.05 sec) Query OK, 0 rows affected, 1 warning (0.00 sec) Query OK, 0 rows affected (0.03 sec) Query OK, 0 rows affected, 1 warning (0.01 sec) Query OK, 0 rows affected (0.04 sec) mysql> show tables; +-----------------+ | Tables_in_seata | +-----------------+ | branch_table | | global_table | | lock_table | +-----------------+ 4 rows in set (0.00 sec) mysql> exit; Bye
-
修改
registry.conf
配置文件,改用为nacos[root@junjian conf]# vim registry.conf
1 registry { 2 # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa 3 type = "nacos" 4 5 nacos { # application = "seata-server" 修改seata注册nacos服务名,默认serverAddr 6 serverAddr = "localhost" 7 namespace = "" 8 cluster = "default" 9 }
启动
nohup sh seata-server.sh -p 8091 -h 127.0.0.1
命令说明:
通过nohup
命令让seata server
在系统后台运行。
脚本参数:
-p
:指定启动seata server
的端口号。-h
:指定seata server
所绑定的主机,这里配置要注意指定的主机IP要与业务服务内的配置文件保持一致;
如:-h 192.168.1.10,业务服务配置文件内应该配置192.168.1.10,即使在同一台主机上也要保持一致。
[root@junjian bin]# pwd
/opt/study/seata/bin
[root@junjian bin]# nohup sh seata-server.sh > /opt/study/logs/seata-server.log 2>&1 &
[root@junjian bin]# tail -f /opt/study/logs/seata-server.log
[root@junjian bin]# ps -ef | grep seata
如果日记没报错且最后是[main]io.seata.core.rpc.netty.AbstractRpcRemotingServer.start:156 -Server started ...
表明seata服务启动成功,可以在nacos
的服务列表看到一个服务名为 serverAddr
的服务。
Module配置搭建
Seata业务数据库准备
这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单, 然后通过远程调用库存服务来扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后在订单服务中修改订单状态为已完成。
该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。
- 要求:具有InnoDB引擎的MySQL。
注意: 实际上,在示例用例中,这3个服务应该有3个数据库。 但是,为了简单起见,我们只创建一个数据库并配置3个数据源。
mysql> create database seata_service;
Query OK, 1 row affected (0.00 sec)
mysql> use seata_service;
Database changed
-- 创建数据库
create database seata_service;
use seata_service;
-- 存储订单的表
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=1 DEFAULT CHARSET=utf8;
-- 存储库存的表
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=1 DEFAULT CHARSET=utf8;
INSERT INTO t_storage(`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0','100');
-- 存储账户信息的表
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=1 DEFAULT CHARSET=utf8;
INSERT INTO t_account(`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000');
创建日志回滚表(如果有三个数据库,则三个库下都需要建各自的回滚日志表)
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| nacos_config |
| performance_schema |
| seata |
| seata_service |
| sys |
+--------------------+
7 rows in set (0.00 sec)
mysql> use seata_service;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> source /opt/study/seata/conf/db_undo_log.sql;
ERROR 1051 (42S02): Unknown table 'seata_service.undo_log'
Query OK, 0 rows affected, 4 warnings (0.03 sec)
mysql> show tables;
+-------------------------+
| Tables_in_seata_service |
+-------------------------+
| t_account |
| t_order |
| t_storage |
| undo_log |
+-------------------------+
4 rows in set (0.00 sec)
Seata之Order-Module配置搭建
新建cloud-alibaba-seata-order-2001
新建cloud-alibaba-seata-storage-2002
新建cloud-alibaba-seata-account-2003
注意:
-
spring
版本低 需要把file.config
和registry.config
存放到resources
并配置相关参数因为
seata
是在虚拟机,代码是在windows
上运行,所以需要修改file.config
和registry.config
的连接ip和代码对应版本的mysql驱动写法 -
spring
版本高可在yml
文件配置seata
Seata之@GlobalTransactional验证
下订单 -> 减库存 -> 扣余额 -> 改(订单)状态
正常情况演示
AccountServiceImpl
无超时代码
-
下单:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
-
订单新增,库存扣减,账户余额扣减,订单状态已完结
超时异常,没加@GlobalTransactional
AccountServiceImpl
@Override
public void decrease(Long userId, BigDecimal money) {
LOGGER.info("------->account-service中扣减账户余额开始");
try {
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException ignore) {
}
accountMapper.decrease(userId, money);
LOGGER.info("------->account-service中扣减账户余额结束");
}
-
下单:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
OpenFeign的调用默认时间是1s以内,所以最后会抛异常。
-
在账户服务休眠时间内 -> 订单新增,库存扣减,超时异常
account-service中扣减账户余额结束后 -> 账户余额扣减,订单状态依旧为创建中
故障情况
- 当库存和账户金额扣减后,订单状态并没有设置为已经完成,没有从零改为1
- 而且由于feign的重试机制,账户余额还有可能被多次扣减
超时异常,加了@GlobalTransactional
用@GlobalTransactional标注OrderServiceImpl的create()方法。
...
public class OrderServiceImpl implements OrderService {
...
@GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
public void create(Order order) {
...
}
}
-
下单:
http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
超时异常后,数据回滚,账户服务停止执行余下逻辑
可在
cloud-alibaba-seata-storage-2002#com.wang.service.impl.StorageServiceImpl#decreased
的减库存结束日志处打断点查看数据库数据情况
Seata之原理简介
2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架。
2020起始,用1.0以后的版本。Alina Gingertail
- TC (Transaction Coordinator) - 事务协调者
- TM (Transaction Manager) - 事务管理器
- RM (Resource Manager) - 资源管理器
分布式事务的执行流程
- 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"。
如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。
补充
参考
本文作者:Zzzy君不见
本文链接:https://www.cnblogs.com/Zzzyyw/p/17048845.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步