springCloud
父工程搭建
New Project
给父工程起名字
确认创建
点击Finish
设置字符集编码
注解激活生效
设置java编译器
删除src目录
,导入依赖
<groupId>com.biao.cloud</groupId>
<artifactId>cloud</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 这个放在最上面,但如果创建子模块会挤下去,还要挪上来。。。 -->
<packaging>pom</packaging>
<!-- 统一管理jar包版本 -->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>5.1.47</mysql.version>
<druid.version>1.1.16</druid.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
</properties>
<!-- 子模块继承之后,提供作用:锁定版本+子modlue不用写groupId和version,
子模块还是要引入依赖的,这个只是提供版本而已,如果子模块写版本号,就不使用父工程的版本 -->
<dependencyManagement>
<dependencies>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
公共工程搭建
new module
填写公共模块名称
给公共模块添加公共依赖,我要用到的有这些,这些依赖根据自己项目添加需要的
<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>
把需要作为公共的类添加到公共模块项目里
其他模块如果要使用那些公共的类,就导入公共模块的依赖
就可以正常使用
Eureka
Eureka包含两个组件:
- Eureka Server(提供服务注册):用在Eureka注册中心模块的依赖
- Eureka Client(通过注册中心进行访问):用在其他服务模块要使用注册中心的client依赖
Eureka单节点搭建
new module 然后使用maven项目,选择jdk版本后,创建Eureka模块
导入pom依赖:
<dependencies>
<!--eureka-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.biao.cloud</groupId>
<artifactId>commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--boot web actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般通用配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
application.yml
server:
port: 7001
eureka:
instance:
hostname: localhost #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
# 与Eureka server 交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
主启动类
好了以后启动主启动类,然后浏览器访问:http://localhost:7001/
eureka服务没问题后,现在支付模块 PaymentMain8001 要注入注册
pom依赖添加
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml:增加这些配置
eureka:
client:
register-with-eureka: true # 表示要注册进eureka服务
fetch-registry: true # 是否从eureka服务抓取注册信息,默认为true,如果是集群,必须设置为true才可以配合ribbon负载均衡
service-url:
defaultZone: http://localhost:7001/eureka
主启动类添加注册中心注解 @EnableEurekaClient
启动,然后刷新浏览器,发现eureka已经有了一个注入进来的节点
使用同样方法,把订单服务(OrderMain80)也给注入到eureka
导入依赖:
添加配置文件
主启动类加上eureka客户端注解
启动后,刷新eureka浏览器
eureka集群搭建
修改C:\Windows\System32\drivers\etc\
路径下的hosts文件
参考上面的eureka单节点,搭建两个一样的eureka服务,区分一下端口号和命名就行了
修改7001服务的配置文件:
erver:
port: 7001
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
# 与Eureka server 交互的地址查询服务和注册服务都需要依赖这个地址
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:
# 与Eureka server 交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7001.com:7001/eureka/
启动7001和7002两个服务,然后浏览器分别访问:http://localhost:7001/ 里面有7002
访问7002,里面有7001:
http://localhost:7002/
这个时候可以给其他要注入eureka的服务配置eureka集群的地址了
PaymentMain8001支付模块:
OrderMain80订单模块:
启动支付模块和订单模块,再分别查看7001和7002
应用服务集群
准备给支付模块做集群,安装原来的PaymentMain8001模块,在创建一个一样的模块PaymentMain8002
主要端口号的区别出来
其他配置一模一样,但有个问题,没法区分8001和8002
然后在controller层接口把服务端口打印日志区分
修改8001和8002的controller
启动eureka集群和支付模块集群
查看7001eureka
查看7002eureka
注意订单服务调用接口时候,不可以把接口地址写死
RestTemplate 加上注解@LoadBalanced
这样才可以负载均衡到支付模块集群,默认就是轮询
信息显示完善
比如把两个支付模块的集群做一下名称改变,并且鼠标悬浮有ip显示
修改cloud-provider-payment8001 模块的配置文件,payment8001是修改后的服务名称,下面一行为true是鼠标悬浮显示ip
修改cloud-provider-payment8002
刷新注册中心7001
7002也一样的
服务发现Discovery
比如获取注册中心里的服务,和服务的一些信息
拿支付模块其中一个节点来举例:注入DiscoveryClient 类后可以获取到
主启动类加上注解
重新启动后,访问接口
查看后端控制台日志
eureka自我保护
比如某一个服务不可用了,eureka不会立马清理,依旧会对服务信息进行保存,默认90秒。
但是当网络分区故障发生(延时,卡顿,拥挤)时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了- 因为微服务本身其实是健康的,此时不应该注销这个微服务,Eureka通过 自我保护模式 来解决这个问题,当EurekaServer节点在短时间丢失过多客户端,那么这个节点就会进入自我保护模式
自我保护模式:默认情况下,eureka客户端定时给eureka服务端发送心跳包,如果服务端在一定时间内(默认90秒)没有收到客户端的心跳包,就直接从服务注册列表提出该服务,但短时间内(90秒)丢失大量服务实例的心跳,这时候eureka服务端就会开启自我保护机制,不会提出该服务。
这个现象可能出现网络不同,但eureka客户端为宕机,这时候如果换别的注册中心一定时间内没有收到心跳就会提出该服务,这样就出现严重失误,因为客户端还可以发送心跳,只是网络延迟问题。
自我保护机制就是为了解决这个问题。
关闭自我保护机制
如果需要关闭eureka的自我保护机制,在eureka配置文件加上以下配置
应付服务的配置文件加上如下配置,比如支付模块服务
这样的话,如果服务挂掉或者短暂的没有注册进eureka,注册中心就会立马清理掉应用服务的信息,就看不到了
zookeeper注册中心
虚拟机或者服务器安装好zookeeper后,创建子模块,端口为8004,准备使用zookeeper作为注册中心,注意zookeeper的版本和pom依赖导入的zookeeper注册中心版本一致
导入pom依赖
<dependencies>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.biao.cloud</groupId>
<artifactId>commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自带的zookeeper 版本-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.5.9版本, 因为我服务器的zookeeper就是3.5.9版本,一定要一致,不然启动直接报错-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.9</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>
配置文件
#8004表示注册到zookeeper服务器的支付服务提供者端口号
server:
port: 8004
#服务别名----注册zookeeper到注册中心名称
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: 虚拟机或者服务器端口:2181
主启动类
写个测试接口
@RestController
@Slf4j
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "/payment/zk")
public String paymentzk() {
return "springcloud with zookeeper: " + serverPort + "\t" + UUID.randomUUID().toString();
}
}
启动后,如果没有报错,调这个测试接口
到服务器查看zookeeper
[root@localhost apache-zookeeper-3.5.9-bin]# bin/zkCli.sh # 启动zk客户端
Connecting to localhost:2181
。。。。。。省略很多日志内容
[zk: localhost:2181(CONNECTED) 0] ls / # 查看什么都没有
[zookeeper]
[zk: localhost:2181(CONNECTED) 5] ls / # 查看,已经注册进去了
[services, zookeeper]
[zk: localhost:2181(CONNECTED) 6] ls /services # 查看注册进的服务
[cloud-provider-payment]
[zk: localhost:2181(CONNECTED) 7] ls /services/cloud-provider-payment # 下面这uuid字符串才是真正注入进去的服务
[790b56ea-77af-4fb8-8ab4-f5f6a8cb8480]
# 下面一行获取服务信息,是个json字符串
[zk: localhost:2181(CONNECTED) 12] get /services/cloud-provider-payment/790b56ea-77af-4fb8-8ab4-f5f6a8cb8480
{"name":"cloud-provider-payment","id":"790b56ea-77af-4fb8-8ab4-f5f6a8cb8480","address":"LAPTOP-3I9O6EOU","port":8004,"sslPort":null,"payload":{"@class":"org.springframework.cloud.zookeeper.discovery.ZookeeperInstance","id":"application-1","name":"cloud-provider-payment","metadata":{}},"registrationTimeUTC":1645712495613,"serviceType":"DYNAMIC","uriSpec":{"parts":[{"value":"scheme","variable":true},{"value":"://","variable":false},{"value":"address","variable":true},{"value":":","variable":false},{"value":"port","variable":true}]}}
zookeeper是临时节点
消费者注册zookeeper
创建新的模块cloud-consumerzk-order80
pom文件导入依赖
<dependencies>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.biao.cloud</groupId>
<artifactId>commons</artifactId>
<version>${project.version}</version>
</dependency>
<!-- SpringBoot整合zookeeper客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<!--先排除自带的zookeeper 版本-->
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.5.9版本, 因为我服务器的zookeeper就是3.5.9版本,一定要一致,不然启动直接报错-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.9</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>
配置文件:
server:
port: 80
spring:
application:
name: cloud-consumer-order
cloud:
zookeeper:
connect-string: 虚拟机或服务器ip:2181
主启动类
配置RestTemplate
测试接口:
@RestController
@Slf4j
public class OrderZKController {
private static final String INVOKE_URL = "http://cloud-provider-payment";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/comsumer/payment/zk")
public String paymentInfo() {
return restTemplate.getForObject(INVOKE_URL + "/payment/zk", String.class);
}
}
启动测试,查看服务器zk注册中心
消费者和提供者都注册进来了
调用消费者测试接口
consul
Consul是一套开源的分布式服务发现和配置管理系统,由HashiCorp公司用Go语言开发
提供了微服务系统中的服务治理、配置中心、控制总线等功能,这些功能中的每一个都可以根据需要单独使用,也可以一起使用构建全方位的服务网路,总之Consul提供了一种完整的服务网络解决方案。
它具有很多优点,包括:基于raft协议,比较简洁;支持健康检查,同时支持HTTP和DNS协议,支持跨数据中心的WAN集群,提供图形化界面,跨平台,支持Linux,MAC,Windows
下载安装:https://www.consul.io/downloads 选择对应系统,找个windows的来玩玩算了
解压缩以后是个exe文件,直接双击允许就行了
安装好后,查看版本号consul --version
启动consul: consul agent -dev
启动成功,浏览器访问:http://服务器ip:8500
提供者注册consul
新建一个模块cloud-providerconsul-payment8006
导入依赖
<dependencies>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.biao.cloud</groupId>
<artifactId>commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--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>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
编写配置文件
###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}
主启动类
测试请求接口
@RestController
@Slf4j
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/consul")
public String paymentConsul() {
return "springcloud with consul: " + serverPort + "\t " + UUID.randomUUID().toString();
}
}
浏览器查看consul:
接口也是可以正常访问的
消费者注册consul
新建模块cloud-consumerconsul-order80
导入依赖
<dependencies>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.biao.cloud</groupId>
<artifactId>commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--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>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
配置文件
###consul服务端口号
server:
port: 80
spring:
application:
name: consul-consumer-order
####consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
主启动类
配置RestTemplate
测试请求接口
@RestController
@Slf4j
public class OrderConsulController {
private static final String INVOKE_URL = "http://consul-provider-payment";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/comsumer/payment/consul")
public String paymentInfo() {
return restTemplate.getForObject(INVOKE_URL + "/payment/consul", String.class);
}
}
启动主启动类,然后查看consul
接口也可以正常请求的
CAP理论
- Consistency:强一致性
- Availability:高可用
- Partition Tolerance:分区容错性
CAP的核心是:一个分布式系统不可能同事很好的满足一致性、可用性、分区容错性,这三个需求,所以根据CAP原理将NoSQL数据库分成了满足CA、CP、AP 三大类原则:
- CA:单点集群,满足一致性,可用性的系统,通常在可扩展性上不太满足
- CP:满足一致性,分区容忍性,通常性能不是特别高
- AP:满足可用性,分区容忍性,通常对一致性要求低一些
AP架构
Eureka就算AP架构
因为同步原因出现问题,而造成数据没有一致性
当出现网络分区后,为了保证高可用,系统B可以返回旧值,保证系统的可用性
结论:违背了一致性C的要求,只满足可用性和分区容错性,即AP
CP架构
Zookeeper和Consul是CP架构
当出现网络分区后,为了保证一致性,就必须拒绝请求,否者无法保证一致性
结论:违背了可用性A的要求,只满足一致性和分区容错性,即CP
Ribbon
Ribbon目前已经进入了维护模式,但是目前主流还是使用Ribbon
Spring Cloud想通过LoadBalancer用于替换Ribbon
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端,负载均衡的工具
简单的说,Ribbon是NetFlix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供了一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
Load Balance
Load Balance,简单来说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件F5等。
- 集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方
- 进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡
Nginx是服务器负载均衡,客户端所有的请求都会交给nginx,然后由nginx实现转发请求,即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后,缓存到JVM本地,从而在本地实现RPC远程调用的技术。
一句话就是:RIbbon = 负载均衡 + RestTemplate调用
Ribbon工作原理
Ribbon其实就是一个软负载均衡的客户端组件,它可以和其它所需请求的客户端结合使用,和Eureka结合只是其中的一个实例
Ribbon在工作时分成两步
首先先选择EurekaServer,它优先选择在同一个区域内负载较少的Server
再根据用户的指定的策略,从Server取到服务注册列表中选择一个地址
其中Ribbon提供了多种策略:比如轮询,随机和根据响应时间加权
新版的Eureka已经默认引入Ribbon了,不需要额外引入
RestTemplate
主要方法为:
-
reseTemplate.getForObject 推荐使用
返回对象为响应体中数据转化成的对象,可以理解为json
-
reseTemplate.posttForObject
返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等
@GetMapping("/consumer/payment/getForEntity/{id}")
public Result<Payment> getPayment2(@PathVariable("id") Long id) {
ResponseEntity<Result> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, Result.class);
if (entity.getStatusCode().is2xxSuccessful()) { // 如果成功
return entity.getBody();
} else {
return new Result<>(444, "操作失败");
}
}
Ribbon负载规则
Ribbon默认是使用轮询作为负载均衡算法
IRule根据特定算法从服务列表中选取一个要访问的服务,IRule是一个接口
然后对该接口,进行特定的实现
负载均衡算法
IRule的实现主要有以下七种
- RoundRobinRule:轮询
- RandomRule:随机
- RetryRUle:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用服务
- WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择的权重越大,越容易被选择
- BestAvailableRule:会先过滤掉由于多次访问故障而处于短路跳闸状态的服务,然后选择一个并发量最小的服务
- AvailabilityFilteringRule:先过滤掉故障实例,在选择并发较小的实例
- ZoneAvoidanceRule:默认规则,符合判断server所在区域的性能和server的可用性选择服务器
负载均衡算法替换
官网警告:
自定义的配置类不能放在@ComponentScanner所扫描的当前包下以及子包下,否者我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化定制的目的了
编写自定义要替换的负载算法
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule(); // 随机
}
}
在主启动类中,添加@RibbonClient
,有注册中心服务名称的参数
启动注册中心和主启动类
不停的访问controller测试接口,可以看到8001和8002两个服务是随机切换的
RoundRobinRule源码分析
查看IRule 接口的实现类RoundRobinRule
查看RandomRule的源码发现,其实内部就是利用的取余的技术,同时为了保证同步机制,还是使用了AtomicInteger原子整型类
使用请求的次数来模于服务数量,得到的余数就是第几个服务节点,不算太难,可以自己看看
OpenFeign
关于Feign的停更,目前已经使用OpenFeign进行替换
Feign是一个声明式WebService客户端。使用Feign能让编写WebService客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可插拔式的编码和解码器。Spring Cloud对feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
Feign的作用
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon + RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端来包装这些依赖,所以Feign在这个基础上做了进一步封装,由他来帮助我们定义和实现依赖服务的接口定义。在Feign的实现下,我们只需要创建一个接口,并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是微服务接口上标注一个Feign注解),即可完成服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成Ribbon
利用Ribbon维护了注册中心支付模块列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且声明式的方法,优雅而简单的实现了服务调用。
Feign = Ribbon + RestTemplate
Feign和OpenFeign的区别
Feign | OpenFeign |
---|---|
Feign是Spring Cloud组件中的一种轻量级RestFul的HTTP服务客户端,Feign内置了Ribbon,用来做客户端的负载均衡,去调用服务注册中心的服务,Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 | OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等,OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方法生产实现类,实现类中做均衡并调用其它服务 |
spring-cloud-starter-feign | spring-cloud-starter-openfeign |
OpenFeign使用步骤
新建模块cloud-consumer-feign-order80
导入pom依赖
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--一般基础通用配置-->
<dependency>
<groupId>org.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>
编写配置文件
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
主启动类加上@EnableFeignClients
注解
编写OpenFeign接口
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 是注册中心的服务名
public interface PaymentFeignService {
// 这就很想controller层的接口调用,调用到了注册中心的服务
@GetMapping("/payment/get/{id}")
public Result<Payment> getPaymentById(@PathVariable("id") Long id);
}
编写测试请求接口
@RestController
@Slf4j
public class OrderFeiginController {
@Autowired
private PaymentFeignService paymentFeignService;
@GetMapping("/payment/get/{id}")
public Result<Payment> getPaymentById(@PathVariable("id") Long id){
return paymentFeignService.getPaymentById(id); // 调用的是OpenFeign接口
}
}
启动主启动类和注册中心、支付模块
请求测试接口,可以看到请求的服务轮询切换
OpenFeign的超时控制
服务提供者需要很久才能返回数据,但是服务调用者默认只等待1秒,这就会出现超时问题。
这是因为默认Feign客户端只等待一秒钟,但是服务端处理需要超过3秒钟,导致Feign客户端不想等待了,直接返回报错,这个时候,消费方的OpenFeign就需要增大超时时间
设置超时时间:修改配置文件
ribbon:
ReadTimeout: 5000
ConnectTimeout: 5000
OpenFeign日志打印功能
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节,说白了就是对Feign接口的调用情况进行监控和输出。
日志级别
- NONE:默认的,不显示任何日志
- BASIC:仅记录请求方法、URL、相应状态码以及执行时间
- HEADERS:除了BASIC中定义的信息之外,还有请求和响应头的信息
- FULL:除了HEADERS中定义的信息之外,还有请求和相应的正文及元数据
编写一个打印日志级别配置类
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
/**
* feignClient配置日志级别
*
* @return
*/
@Bean
public Logger.Level feignLoggerLevel() {
// 请求和响应的头信息,请求和响应的正文及元数据,FULL表示最全的日志信息
return Logger.Level.FULL;
}
}
配置文件添加配置
然后重新启动项目,请求接口,查看后台日志
Hystrix
复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败(网络卡顿,网络超时)
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的 雪崩效应
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其它系统资源紧张,导致整个系统发生更多的级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩
HyStrix的诞生
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
断路器 本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似于熔断保险丝),向调用方返回一个符合预期的,可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中蔓延,乃至雪崩。
Hystrix作用
- 服务降级
- 服务熔断
- 接近实时的监控(Hystrix Dashboard)
服务降级
fallback,假设对方服务不可用了,那么至少需要返回一个兜底的解决方法,即向服务调用方返回一个符合预期的,可处理的备选响应。
例如:服务繁忙,请稍后再试,不让客户端等待并立刻返回一个友好的提示,fallback
哪些情况会触发降级
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满也会导致服务降级
服务熔断
break,类比保险丝达到了最大服务访问后,直接拒绝访问,拉闸断电,然后调用服务降级的方法并返回友好提示
一般过程:服务降级 -> 服务熔断 -> 恢复调用链路
服务限流
flowlimit,秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
hystrix案例
先搭建一个测试用的环境
由于eureka启动太慢,修改为单机注册中心就好了,修改7001配置
搭建支付模块(提供者)
构建一个模块cloud-provider-hystrix-payment8001
导入依赖:
<dependencies>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency><!-- 引入自己定c义的api通用包,可以使用Payment支付Entity -->
<groupId>com.biao.cloud</groupId>
<artifactId>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>
配置文件
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka
主启动类
service类
@Service
public class PaymentService {
// 正常访问
public String paymentInfo_ok(Integer id){
return "线程池"+Thread.currentThread().getName()+" paymentInfo_ok,id: "+id;
}
// 延迟3秒
public String paymentInfo_TimeOut(Integer id){
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池"+Thread.currentThread().getName()+" paymentInfo_TimeOut耗时3秒,id: "+id;
}
}
controller测试接口
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_ok(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_ok(id);
log.info("result: "+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_TimeOut(id);
log.info("result: "+result);
return result;
}
}
启动注册中心和主启动类
访问注册中心查看无误
访问两个测试接口:
一个是正常的,立即响应
一个是延迟3秒后响应
开始高并发测试
Jmeter高并发测试
创建线程组:线程组202002
填写配置
Ctrl + S
保存,
右键线程组,选择到HTTP请求
编写好请求信息,20000个线程去访问响应延时3秒的那个接口
这个时候如果再去请求那个响应正常的接口,发现响应也会变得很慢
结论:当一个请求遇到高并发,其他的接口也会变慢
我们会发现当线程多的时候,会直接卡死,甚至把其它正常的接口都已经拖累
这是因为我们使用20000个线程去访问那个延时的接口,这样会把该微服务的资源全部集中处理 延时接口,而导致正常的接口资源不够,出现卡顿的现象。
同时tomcat的默认工作线程数被打满,没有多余的线程来分解压力和处理。
所以就需要hustrix来熔断,限流
搭建订单模块(消费者)
创建新的模块:cloud-consumer-feign-hystrix-order80
导入依赖:
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.biao.cloud</groupId>
<artifactId>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>
配置文件:
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
主启动类
使用OpenFeign,编写一个service接口
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
编写测试的请求接口
@RestController
@Slf4j
public class OrderHystirxController {
@Autowired
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
}
启动主启动类,这个时候已经启动三个服务了
请求这个正常的接口,是使用的OpenFeign调用的提供者支付服务,是没有任何问题的
然后使用JMeter测试20000个线程去访问支付服务的那个延时3秒的接口,让那个服务阻塞
这时候在调用服务端的接口,让它去访问提供者,发现阻塞很久
设置报错超时
解决方案
原因
超时导致服务器变慢,超时不再等待
出错,宕机或者程序运行出错,出错要有兜底
解决
- 对方服务8001超时了,调用者80不能一直卡死等待,必须有服务降级
- 对方服务8001宕机了,调用者80不能一直卡死,必须有服务降级
- 对方服务8001正常,调用者自己出故障或者有自我要求(自己的等待时间小于服务提供者),自己处理降级
服务降级-提供者
使用新的注解 @HystrixCommand
同时需要在主启动类上新增:@EnableCircuiteBreaker
设置8001自身调用超时时间的峰值,峰值内可以正常运行,超过了需要有兜底的方法处理,作为服务降级fallback
修改消费者服务的测试接口:PaymentService
@Service
public class PaymentService {
// 正常访问
public String paymentInfo_ok(Integer id) {
return "线程池" + Thread.currentThread().getName() + " paymentInfo_ok,id: " + id;
}
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", // 如果异常,就执行下面对应的方法: paymentInfo_TimeOutHandler
commandProperties = {
// // 方法调用响应在3秒以内,就走这个方法正常的逻辑
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
public String paymentInfo_TimeOut(Integer id) {
try {
TimeUnit.SECONDS.sleep(5); // 延时5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池" + Thread.currentThread().getName() + " paymentInfo_TimeOut耗时3秒,id: " + id;
}
public String paymentInfo_TimeOutHandler(Integer id) {
return "线程池: " + Thread.currentThread().getName() + " 8001系统繁忙或者运行报错,请稍后再试,id: " + id + "\t" + "o(╥﹏╥)o呜呜呜~";
}
}
支付模块提供者主启动类加上注解:@EnableCircuitBreaker
重启主启动类,然后访问线程延迟的测试接口:
发现超过3秒后,就会不再等待方法返回调用,直接执行下面的方法: paymentInfo_TimeOutHandler
如果方法内报错的话,也会执行下面的兜底方法paymentInfo_TimeOutHandler
调用接口后发现也会调用下面的兜底方法
服务降级-消费者
其实和提供者的服务降级差不多的,
先来给提供者支付服务的接口调成正常的:
修改一下订单服务的测试接口:
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", // 方法出错了就调用下面的方法: paymentTimeOutFallbackMethod
commandProperties = {
// 方法调用响应在1.5秒以内,就走这个方法正常的逻辑
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}
主启动类加注解:@EnableHystrix
启动消费者订单服务,请求测试接口:
然后修改测试接口,改成进来就报错:
请求接口发现还是会掉用下面的降级方法
全局服务降级
上面的服务降级的办法,接口代码和服务降级代码很容易耦合在一起,每个接口都要配合一个降级的方法,这就造成耦合度比较高
解决方法就是使用统一的服务降级方法
方法1:
@DefaultProperties
:
这个注解表示,如果接口没有指定服务降级使用哪个具体的方法,就使用同一的降级方法,
然后使用@HystrixCommand
注解标注在接口上做服务降级的标识就行了,不用填写里面参数的具体执行服务降级使用哪个方法
修改订单服务的测试接口
修改完后,启动消费者订单服务,访问测试接口,记得主启动类别忘记加:@EnableHystrix
全局通配服务降级
因为从消费者调用到提供者服务的接口,都是通过OpenFeign调用的,所以在这里做设置,对下面的所有接口做服务降级
修改订单服务的OengFeign调用类,修改@FeignClient
的属性fallback
编写PaymentFallbackService
类,调用哪个接口报错,就会降级到哪个对应的接口兜底方法
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o哭脸";
}
@Override
public String paymentInfo_TimeOut(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o哭脸";
}
}
添加配置文件的配置
启动消费者订单服务,然后请求这个正常访问的接口,发现可以正常访问,没有任何问题
现在直接把提供者订单服务给停止运行,这下接口肯定是调用失败的
可以看到只启动了消费者订单服务和注册中心,然后访问订单服务的接口
服务降级到了对应的兜底方法
服务熔断
服务熔断也是服务降级的一个特例
熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应状态
当检测到该节点微服务调用响应正常后,恢复调用链路
在Spring Cloud框架里,熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定的阈值,缺省是5秒内20次调用失败,就会启动熔断机制,熔断机制的注解还是 @HystrixCommand
这个简单的断路器避免了在电路打开时进行保护调用,但是当情况恢复正常时需要外部干预来重置它。对于建筑物中的断路器,这是一种合理的方法,但是对于软件断路器,我们可以让断路器本身检测底层调用是否再次工作。我们可以通过在适当的间隔之后再次尝试protected调用来实现这种自重置行为,并在断路器成功时重置它
熔断器的三种状态:打开,关闭,半开
这里提出了 半开的概念,首先打开一半的,然后慢慢的进行恢复,最后在把断路器关闭
降级 -> 熔断 -> 恢复
这里我们在服务提供方 8001的PaymentService类,增加服务熔断
//=====服务熔断 表示10秒种访问10次,失败率在60% 的时候就不能用,调用下面的方法: paymentCircuitBreaker_fallback
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 是否开启断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), // 失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " + id;
}
然后在提供者支付服务添加对应的测试接口
//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result: " + result);
return result;
}
启动提供者服务,不停的输入负数,让失败率达到100%%,请求测试接口
然后在这10秒内再去输入正数去请求,结果还是不可用
等一会儿,或者请求错误低于60%就可以正常访问到了
服务熔断总结
熔断类型
- 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达所设时钟则进入半熔断状态
- 熔断关闭:熔断关闭不会对服务进行熔断
& 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则,则认为当前服务恢复正常,关闭熔断
断路器启动条件
涉及到断路器的三个重要参数:快照时间窗,请求总阈值,错误百分比阈值
- 快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近的10秒。
- 请求总数阈值:在快照时间窗口内,必须满足请求总数阈值才有资格熔断。默认为20,意味着在10秒内,如果hystrix的调用总次数不足20次,即使所有请求都超时或者其他原因失败,断路器都不会打开。
- 错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,并且有15次发生了超时异常,也就是超过了50的错误百分比,在默认设定的50%阈值情况下,这时候就会将断路器打开
开启和关闭的条件
- 当满足一定阈值的时候(默认10秒内超过20个请求)
- 当失败率达到一定的时候(默认10秒内超过50%的请求失败)
- 到达以上阈值,断路器将会开启
- 当开启的时候,所有请求都不会进行转发
- 一段时间之后(默认是5秒),这个时候断路器是半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若失败,继续开启,重复4和5
断路器开启后
- 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback,通过断路器,实现了自动的发现错误并将降级逻辑切换为主逻辑,减少相应延迟的效果。
- 原来的主逻辑如何恢复?
对于这个问题,Hystrix实现了自动恢复功能,当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑是临时的成为主逻辑,当休眠时间窗到期,断路器将进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求正常返回,断路器将继续闭合,主逻辑恢复,如果这次请求依然有问题,断路器继续保持打开状态,休眠时间窗重新计时。
Hystrix工作流程
蓝色:调用路径
红色:返回路径
完整的请求路线:
- 选择一个Hystrix注册方式
- 二选一即可
- 判断缓存中是否包含需要返回的内容(如果有直接返回)
- 断路器是否为打开状态(如果是,直接跳转到8,返回)
- 断路器为健康状态,判断是否有可用资源(没有,直接跳转8)
- 构造方法和Run方法
- 将正常,超时,异常的消息发送给断路器
- 调用getFallback方法,也就是服务降级
- 直接返回正确结果
服务监控HystrixDashboard
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形化的形式展示给用户,包括每秒执行多少请求,成功多少请求,失败多少,Netflix通过Hystrix-metrics-event-stream项目实现了对以上指标的监控,SpringCloud也提供了HystrixDashboard整合,对监控内容转化成可视化页面
搭建图形化HystrixDashboard
新建模块:cloud-consumer-hystrix-dashboard9001
导入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.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>
编写配置文件:
server:
port: 9001
编写主启动类,添加注解:@EnableHystrixDashboard
启动主启动类,访问浏览器:http://localhost:9001/hystrix
使用监控
我们需要使用当前hystrix需要监控的端口号,也就是使用 9001 去监控 cloud-provider-hystrix-payment8001,即使用hystrix dashboard去监控服务提供者的端口号
修改cloud-provider-hystrix-payment8001 主启动类
@EnableCircuitBreaker // hystrix激活
@SpringBootApplication
@EnableEurekaClient
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
启动主启动类,浏览器hystrix 监控页面输入提供者支付服务的对应路径,加上后缀.stream
进来以后,再去请求支付服务的接口,可以监控到支付服务了
如果接口访问成功
如果接口访问失败
如何看懂图:
首先是七种颜色
然后是里面的圆
实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的健康程度从:
绿色 < 黄色 < 橙色 <红色,递减
该实心圆除了颜色变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆就越大,所以通过该实心圆的展示,就可以快速在大量的实例中快速发现故障实例和高压力实例
曲线:用于记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势
服务网关Gateway
Zuul
Zuul是所有来自设备和web站点到Netflix流媒体应用程序后端的请求的前门。作为一个边缘服务应用程序,Zuul的构建是为了支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。
Gateway
Cloud全家桶有个很重要的组件就是网关,在1.X版本中都是采用Zuul网关,但在2.X版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关替代Zuul,那就是SpringCloudGateway,一句话Gateway是原来Zuul 1.X 版本的替代品
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2 和 Project Reactor等技术。Gateway旨在提供一种简单而且有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断,限流,重试等。
Spring Cloud Gateway 是Spring Cloud的一个全新项目,作为Spring Cloud生态系统中的网关,目标是替代Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.X非Reactor模式的老版本,而为了提高网关的性能,Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目标提供统一的路由方式,且基于Filter链的方式提供了网关基本的功能,例如:安全,监控、指标 和 限流。
作用
- 反向代理
- 鉴权
- 流量控制
- 熔断
- 日志监控
使用场景
网关可以想象成是所有服务的入口
为什么选用Gateway
目前已经有了Zuul了,为什么还要开发出Gateway呢?
一方面是因为Zuul 1.0已经进入了维护阶段,而且Gateway是Spring Cloud团队研发的,属于亲儿子,值得信赖,并且很多功能Zuul都没有用起来,同时Gateway也非常简单便捷
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
- 请求限流功能
- 支持路径重写
Spring Cloud Gateway 和 Zuul的区别
在Spring Cloud Gateway Finchley正式版发布之前,Spring Cloud推荐网关是NetFlix提供的Zuul
- Zuul 1.X 是一个基于阻塞IO的API Gateway
- Zuul 1.x 基于Servlet 2.5使用阻塞架构,它不支持任何场连接,Zuul的设计模式和Nginx比较像,每次IO操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能较差。
- Zuul 2.X理念更先进,想基于Netty非阻塞和支持长连接,但Spring Cloud目前还没有整合。Zuul 2.X的性能相比于1.X有较大提升,在性能方面,根据官方提供的基准测试,Spring Cloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。
- Spring Cloud Gateway建立在Spring 5,Spring Boot 2.X之上,使用非阻塞API
- Spring Cloud Gateway还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验。
Spring Cloud 中所集成的Zuul版本,采用的是Tomcat容器,使用的还是传统的Servlet IO处理模型
Servlet的生命周期中,Servlet由Servlet Container进行生命周期管理。
Container启动时构建servlet对象,并调用servlet init()进行初始化
Container运行时接收请求,并为每个请求分配一个线程,(一般从线程池中获取空闲线程),然后调用Service
container关闭时,调用servlet destory() 销毁servlet
上述模式的缺点:
servlet是一个简单的网络IO模型,当请求进入Servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下,这种网络模型是适用的,但是一旦高并发(Jmeter测试),线程数就会上涨,而线程资源代价是昂贵的(上下文切换,内存消耗大),严重影响了请求的处理时间。在一些简单业务场景下,不希望为每个Request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下Servlet模型没有优势。
所以Zuul 1.X是基于Servlet之上的一种阻塞式锤模型,即Spring实现了处理所有request请求的Servlet(DispatcherServlet)并由该Servlet阻塞式处理,因此Zuul 1.X无法摆脱Servlet模型的弊端
WebFlux框架
传统的Web框架,比如Struts2,Spring MVC等都是基于Servlet API 与Servlet容器基础之上运行的,但是在Servlet 3.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的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
Gateway工作流程
Web请求通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行了一些精细化的控制。
Predicate就是我们的匹配条件,而Filter就可以理解为一个无所不能的拦截器,有了这两个元素,在加上目标URL,就可以实现一个具体的路由了。
客户端向Spring Cloud Gateway发出请求,然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。
Handler在通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求前(pre)或之后(post)执行业务逻辑。
Filter在 Pre 类型的过滤器可以做参数校验,权限校验,流量监控,日志输出,协议转换等。
在 Post类型的过滤器中可以做响应内容,响应头的修改,日志的输出,流量监控等有着非常重要的作用。
Gateway的核心逻辑:路由转发 + 执行过滤链
Gateway搭建
创建新的模块:cloud-gateway-gateway9527
导入依赖:
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.biao.cloud</groupId>
<artifactId>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>
配置文件:
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
eureka:
instance:
hostname: cloud-gateway-service
client: #服务提供者provider注册进eureka服务列表内
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
主启动类
配置完成后,启动注册中心和一个支付服务:cloud-provider-payment8001
然后访问支付服务可用:
然后启动cloud-gateway-gateway9527 的主启动类,查看已经到了注册中心
然后把请求支付服务的ip给换成gateway服务的ip9527,发现也可以访问
路由匹配
这样就不会暴露真实的服务ip地址,而是统一的9527端口和网关服务的ip
路由配置的两种方式
- 在配置文件yml中配置(也就是上面的案例)
- 代码中注入RouteLocator的Bean,网关服务编写配置类:
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route_biao", // 路由的ID,没有固定规则但要求唯一,建议配合服务名
r -> r.path("/guonei") // 访问 http://localhost:9527/guonei 就会转发到下面的地址
.uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}
相比较来说,可能yml配置文件更好一点(根据个人喜好配置)
动态路由
通过微服务名实现动态路由
修改配置文件
然后8001和8002两个支付服务的都要有测试接口,返回出服务端口
ok,启动注册中心和两个支付服务,启动网关服务
从网关访问接口
再次请求,负载均衡到另一个服务8002
Predicate的使用
断言,路径相匹配的进行路由
查看后面控制器输出
Spring Cloud Gateway将路由匹配作为Spring WebFlux HandlerMapping基础架构的一部分
Spring Cloud Gateway包括许多内置的Route Predicate 工厂,所有这些Predicate都与Http请求的不同属性相匹配,多个Route Predicate工厂可以进行组合
Spring Cloud Gateway创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route,SpringCloudGateway包含许多内置的RoutePredicateFactores。
所有这些谓词都匹配Http请求的不同属性。多种谓词工厂可以组合,并通过逻辑 and
常用的Predicate
- After Route Predicate:在什么时间之后执行
获取时间方法:
public class T2 {
public static void main(String[] args) {
ZonedDateTime zbj = ZonedDateTime.now(); // 默认时区
System.out.println(zbj); //2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
}
}
把输出的时间配在这个位置
这样的话,只有这个时间时候去请求对应的接口服务才可以,不然会找不到这个服务
-
Before Route Predicate:在什么时间之前执行
-
Between Route Predicate:在什么时间之间执行
-
Cookie Route Predicate:Cookie级别
指定cookie:是K-V键值对,value是zzyy
访问的时候带上cookie
不然会报错
常用的测试工具: -
jmeter
-
postman
-
curl
-
Header Route Predicate:携带请求头
配置好以后,请求测试一下
-
Host Route Predicate:什么样的URL路径过来
-
Method Route Predicate:什么方法请求的,Post,Get
-
Path Route Predicate:请求什么路径 - Path=/api-web/**
-
Query Route Predicate:带有什么参数的
Filter的使用
路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由过滤器只能指定路由进行使用
Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生的
Spring Cloud Gateway Filter
生命周期:only Two:pre,Post
种类:Only Two
- GatewayFilter 参考文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
- GlobalFilter 参考文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#global-filters
这些种类太多,推荐使用自定义过滤器,通过代码实现
自定义全局过滤器
主要作用:
- 全局日志记录
- 统一网关鉴权
需要实现接口:implements GlobalFilter, Ordered
全局过滤器代码如下:
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter,Ordered
{
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
log.info("***********come in MyLogGateWayFilter: "+new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if(uname == null)
{
log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); // 响应码
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder()
{
return 0;
}
}
重新启动,访问接口
如果不带uname,会拦截掉
配置中心-SpringCloudConfig
微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务,由于每个服务都需要必要的配置信息才能运行,所以一套集中式,动态的配置管理设施是必不可少的。
SpringCloud提供了ConfigServer来解决这个问题,原来四个微服务,需要配置四个application.yml,但需要四十个微服务,那么就需要配置40份配置文件,我们需要做的就是一处配置,到处生效。
所以这个时候就需要一个统一的配置管理
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用提供了一个中心化的外部配置。
服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口。
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息,配置服务器默认采用git来存储配置信息,这样有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
能做什么
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分布式部署,比如 dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取自己的信息
- 当配置发生变动时,服务不需要重启即可感知配置的变化并应用新的配置
- 将配置信息以REST接口的形式暴露:post,curl命令刷新
与Github整合部署
由于SpringCloud Config默认使用Git来存储配置文件(也有其他方式,比如支持SVN和本地文件),但最推荐的还是Git,而且使用的是Http/https访问的形式
Config服务端配置与测试
创建模块:cloud-config-center-3344
导入依赖:
<dependencies>
<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>
编写配置文件:
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进Eureka服务器的微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/wangbiao666/java8.git #Gitee上面的git仓库名字
####搜索目录
search-paths:
- java8
username: gitee登录用户名
passphrase: 密码
####读取分支
label: master
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
主启动类
修改本地hosts文件测试
浏览器请求:http://config-3344.com:3344/master/config-dev.yml
master表示分支,也可以不写,默认就是master,如果是dev或者test分支,要写明,后面config-dev.yml
表示路径下的文件
就是对应的文件内容
config应用服务端配置与测试
创建模块:cloud-config-client-3355
导入依赖:
<dependencies>
<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-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件:bootstrap.yml
server:
port: 3355
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址k
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
主启动类:
测试接口
@RestController
public class ConfigClientController {
@Value("${config.info}") // 直接获取到配置服务端的内容
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo(){
return configInfo;
}
}
启动主启动类和注册中心、cloud-config服务端3344
请求接口,直接就把内容返回了
config动态刷新
3355config客户端添加配置:
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
如下:
请求接口controller类添加注解:@RefreshScope
启动各个服务,修改git上面的文件内容,发现访问3355的测试接口,没有刷新到修改的最新数据,
因为需要让运维人员发送一个post请求:curl -X POST "http://localhost:3355/actuator/refresh"
然后就能够生效了,成功刷新了配置,避免了服务重启
这个方案存在问题:
- 假设有多个微服务客户端 3355/ 3366 / 3377
- 每个微服务都要执行一次post请求,手动刷新?
- 可否广播,一次通知,处处生效?
目前来说,暂时做不到这个,所以才用了下面的内容,即Spring Cloud Bus
消息总线
消息总线SpringCloudBUS
消息总线一般是配合SpringCloudConfig一起使用的
分布式自动刷新配置功能,SpringCloudBus配合SpringCloudConfig使用可以实现配置的动态刷新
Bus支持两种消息代理:RabbitMQ和Kafka
说白了就是通过消息队列,把curl指令给传送执行
SpringCloudBus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。
SpringCloudBus能管理和传播分布式系统的消息,就像一个分布式执行器,可用于广播状态更改,事件推送等,也可以当做微服务的通信通道。
什么是总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以被称为消息总线。在总线上的各个实例,都可以方便的广播一些需要让其它连接在该主题上的实例都知道的消息。
基本原理
ConfigClient实例都监听MQ中同一个topic(默认是SpringCloudBus),但一个服务刷新数据的时候,它会被这个消息放到Topic中,这样其它监听同一个Topic的服务就能够得到通知,然后去更新自身的配置
安装好RabbitMQ,通过topic进行广播通知
SpringCloudBus动态刷新全局广播
创建模块:cloud-config-client-3366
导入依赖:
<dependencies>
<!--添加消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<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-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件:bootstrap.yml
server:
port: 3366
spring:
application:
name: config-client
cloud:
#Config客户端配置
config:
label: master #分支名称
name: config #配置文件名称
profile: dev #读取后缀名称 上述3个综合:master分支上config-dev.yml的配置文件被读取http://config-3344.com:3344/master/config-dev.yml
uri: http://localhost:3344 #配置中心地址
#rabbitmq相关配置 15672是Web管理界面的端口;5672是MQ访问的端口
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
#服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
主启动类:
编写测试接口:
@RestController
@RefreshScope
public class ConfigClientController {
@Value("${server.port}")
private String serverPort;
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String configInfo() {
return "serverPort: " + serverPort + "\t\n\n configInfo: " + configInfo;
}
}
设计思想
- 利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端配置
- 利用消息总线出发一个服务端ConfigServer的/bus/refresh断点,而刷新所有客户端的配置
- 图二的架构更加适合,图一不适合的原因有
- 图一打破了微服务的职责单一性,因为微服务本身是业务模块,他不应该承担配置刷新的之职责
- 打破了微服务各节点的对等性
- 有一定的局限性,例如,微服务在迁移时,它的网络地址常常发生变化,此时如果想要做到自动刷新,那就会增加更多的修改。
修改3344模块的配置文件,添加mq配置
修改3355模块的配置文件,添加依赖:
配置文件:
测试
当我们的服务端配置中心 和 客户端都增加完上述配置后,我们需要做的就是手动发送一个POST请求到服务端
curl -X POST "http://localhsot:3344/actuator/bus-refresh"
执行完成后,配置中心会通过BUS消息总线,发送到所有的客户端,并完成配置的刷新操作。
完成了一次修改,广播通知,处处生效的效果,所以只用刷新3344就好了
修改git上面的内容,然后分别去通过这个服务去访问一下,看能否得到最新修改后的内容
SpringCloudBus动态刷新定点通知
就是我想通知的目标是有差异化,有些客户端需要通过,有些不通知,也就是10个客户端,我只通知1个
简单一句话,就是指定某一个实例生效而不是全部
公式:http://localhost:配置中心端口/actuator/bus-refresh/{destination}/bus/refresh
请求不再发送到具体的服务实例上,而是发送给config server并通过destination参数类指定需要更新配置的服务或实例。
案例
以刷新运行在3355端口上的config-client为例,只通知3355,不通知3366,可以使用下面命令
SpringCloud Stream
首先看到消息驱动,我们会想到,消息中间件
- ActiveMQ
- RabbitMQ
- RocketMQ
- Kafka
存在的问题就是,中台和后台 可能存在两种MQ,那么他们之间的实现都是不一样的,这样会导致多种问题出现,而且上述我们也看到了,目前主流的MQ有四种,我们不可能每个都去学习
这个时候的痛点就是:有没有一种新的技术诞生,让我们不在关注具体MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。
这个时候,SpringCloudStream就运营而生,解决的痛点就是屏蔽了消息中间件的底层的细节差异,我们操作Stream就可以操作各种消息中间件了,从而降低开发人员的开发成本。
消息驱动概述
是什么
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
这就有点像Hibernate,它同时支持多种数据库,同时还提供了Hibernate Session的语法,也就是HQL语句,这样屏蔽了SQL具体实现细节,我们只需要操作HQL语句,就能够操作不同的数据库。
什么是SpringCloudStream
官方定义 SpringCloudStream是一个构件消息驱动微服务的框架
应用程序通过inputs或者outputs来与SpringCloudStream中binder对象(绑定器)交互。
通过我们配置来binding(绑定),而SpringCloudStream的binder对象负责与消息中间件交互
所以,我们只需要搞清楚如何与SpringCloudStream交互,就可以方便的使用消息驱动的方式。
通过使用SpringIntegration来连接消息代理中间件以实现消息事件驱动。
SpringCloudStream为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅,消费组,分区的三个核心概念
目前仅支持RabbitMQ 和 Kafka
SpringCloudStrem设计思想
- 生产者/消费者之间靠消息媒介传递消息内容:Message
- 消息必须走特定的通道:Channel
- 消息通道里的消息如何被消费呢,谁负责收发处理
- 消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅
为什么用SpringCloudStream
RabbitMQ和Kafka,由于这两个消息中间件的架构上不同
像RabbitMQ有exchange,kafka有Tpic和Partitions分区
这些中间件的差异导致我们实际项目开发给我们造成了一定的困扰,我们如果用了两个消息队列的其中一种,后面的业务需求,我们想往另外一种消息队列进行迁移,这时候无疑就是灾难性的,一大堆东西都要推到重新做,因为它根我们的系统耦合了,这时候SpringCloudStream给我们提供了一种解耦的方式
这个时候,我们就需要一个绑定器,可以想成是翻译官,用于实现两种消息之间的转换
SpringCloudStream为什么能屏蔽底层差异
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间件进行消息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性。通过定义绑定器作为中间件,完美的实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,使得应用程序不需要在考虑各种不同消息中间件的实现。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Binder
- input:对应消费者
- output:对应生产者
Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(RabbitMQ切换Kafka),使得微服务开发的高度解耦,服务可以关注更多的自己的业务流程。
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ中就是Exchange,在Kafka中就是Topic
Stream标准流程套路
我们的消息生产者和消费者只和Stream交互
- Binder:很方便的连接中间件,屏蔽差异
- Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的没接,通过Channel对队列进行配置
- Source和Sink:简单的可以理解为参照对象是SpringCloudStream自身,从Stream发布消息就是输出,接受消息就是输入。
编码中的注解
案例说明
前提是已经安装好了RabbitMQ
- oud-stream-rabbitmq-procider8801,作为消息生产者进行发消息模块
- oud-stream-rabbitmq-procider8802,消息接收模块
- oud-stream-rabbitmq-procider8803,消息接收模块
消息驱动之生产者
导入依赖
<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>
<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>
配置文件
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: send-8801.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
主启动类
发送消息接口
public interface IMessageProvider {
public String send();
}
发送消息的接口实现类
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.MessageChannel;
import javax.annotation.Resource;
import java.util.UUID;
@EnableBinding(Source.class) //定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider {
@Resource
private MessageChannel output; // 消息发送管道
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: " + serial);
return null;
}
}
测试请求接口:
@RestController
public class SendMessageController {
@Resource
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
启动注册中心,rabbitmq服务、主启动类
请求多次测试接口
查看后台日志打印
消息驱动之消费者
创建模块:cloud-stream-rabbitmq-consumer8802
导入依赖:
<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>
配置文件:
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息;
defaultRabbit: # 表示定义的名称,用于于binding整合
type: rabbit # 消息组件类型
environment: # 设置rabbitmq的相关的环境配置
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
bindings: # 服务的整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为对象json,如果是文本则设置“text/plain”
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client: # 客户端进行Eureka注册的配置
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳的时间间隔(默认是30秒)
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
instance-id: receive-8802.com # 在信息列表时显示主机名称
prefer-ip-address: true # 访问的路径变为IP地址
主启动类
测试请求接口:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("消费者1号,----->接受到的消息: " + message.getPayload() + "\t port: " + serverPort);
}
}
启动主启动类,请求8801发送消息请求
查看控制台提供者发送消息日志打印
查看消费者后台日志打印
重复消费问题解决
创建新的模块,和cloud-stream-rabbitmq-consumer8802 一模一样,端口改成8803
启动8803服务,和8802,以及消息提供者和注册中心
请求消息提供者服务,刷新两次,也就是发送两个消息
查看消费者8802 和8803,
发现8802 消费了消息
8803 也消费了同样的两条消息
如何解决:使用分组和持久化属性 group来解决
比如在如下场景中,订单系统我们做集群部署,都会从RabbitMQ中获取订单信息,那如果一个订单同时被两个服务获取到,那么就会造成数据错误,我们得避免这种情况,这时我们就可以使用Stream中的消息分组来解决。
注意:在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只能被其中一个消费一次
不同组是可以全面消费的(重复消费)
同一组会发生竞争关系,只能其中一个可以消费
分布式微服务应用为了实现高可用和负载均衡,实际上都会部署多个实例,这里部署了8802 8803
多数情况下,生产者发送消息给某个具体微服务时,只希望被消费一次,按照上面我们启动两个应用的例子,虽然它们同属一个应用,但是这个消息出现了被重复消费两次的情况,为了解决这个情况,在SpringCloudStream中,就提供了 消费组 的概念
分组
微服务应用放置于同一个group中,就能够保证消息只会被其中一个应用消费一次,不同的组是可以消费的,同一组内会发生竞争关系,只有其中一个可以被消费。
我们将8802和8803划分为同一组,修改它们的配置文件:
启动消费者,发送2条消息,刷新2次
查看消息提供者的日志,发送了2条消息
查看8802消费者服务,消费了1条消息
8803也是只消费了1条消息
消息持久化
案例
- 停止8802和8803,并移除8802的group,保留8803的group
- 8801先发送4条消息到RabbitMQ
- 先启动8802,无分组属性,后台没有打出来消息
- 在启动8803,有分组属性,后台打出来MQ上的消息
先发送4条消息
8802配置上去掉分组的那个配置,重启8802服务,发现8802没有去消费消息
8803的配置不变,还是保留分组的配置,启动8803服务后,发现消费了消息
这就说明消息已经被持久化了,等消费者登录后,会自动从消息队列中获取消息进行消费
SpringCloudSleuth请求链路跟踪
在微服务框架中,一个由客户端发起的请求在后端系统中会经过多个不同的服务节点调用来协同产生最后的请求结果,每一个前端请求都会形成一条复杂的分布式服务调用链路,链路中的任何一环出现高延时或错误都会引起整个请求最后的失败。
当链路特别多的时候
就需要有一个用于调用链路的监控和服务跟踪的解决方案
SpringCloudSleuth提供了一套完整的服务跟踪解决方案,在分布式系统中,提供了追踪解决方案,并且兼容支持了zipkin。
搭建
SpringCloud从F版起,已经不需要自己构建Zipkin Server了,只需要调用jar包即可
下载地址:https://repo1.maven.org/maven2/io/zipkin/zipkin-server/
下载得到一个jar包
运行:java -jar 对应的jar包
打开:http://localhost:9411/zipkin
名词解释
- Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识
- Span:表示调用链路来源,通俗的理解span就是一次请求信息
完整的调用链路
表示一请求链路, 一条链路通过Trace ID唯一标识,Span标识发起请求信息,各span通过parent id关联起来。
一条链路通过Trace Id唯一标识,Span表示发起的请求信息,各span通过parent id关联起来
整个链路的依赖关系如下:
测试使用
新增cloud-provider-payment8001 模块的pom依赖:
<!--包含了sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
新增cloud-provider-payment8001 模块的配置文件
新增请求接口:
@GetMapping("/payment/zipkin")
public String paymentZipkin() {
return "hi ,i'am paymentzipkin server fall back,welcome to atguigu,O(∩_∩)O哈哈~";
}
修改cloud-consumer-order80 模块的配置文件和新增的依赖,配置内容是一模一样的
新增请求接口:
// ====================> zipkin+sleuth
@GetMapping("/consumer/payment/zipkin")
public String paymentZipkin() {
String result = restTemplate.getForObject("http://localhost:8001" + "/payment/zipkin/", String.class);
return result;
}
订单服务和支付服务通过注册中心得到的服务请求,分别启动注册中心、支付服务、订单服务
订单服务多调用几次接口:
到zipkin页面刷新一下,可以看到请求的一个流程走向具体信息
Spring Cloud Alibaba
作用
- 服务限流降级:默认支持servlet,Feign,RestTemplate,Dubbo和RocketMQ限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级Metrics监控
- 服务注册与发现:适配Spring Cloud服务注册与发现标准,默认集成了Ribbon的支持
- 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新
- 消息驱动能力:基于Spring Cloud Stream (内部用RocketMQ)为微服务应用构建消息驱动能力
- 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务,支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
- 分布式任务调度:提供秒级,精准、高可靠、高可用的定时(基于Cron表达式)任务调度服务,同时提供分布式的任务执行模型,如网格任务,网格任务支持海量子任务均匀分配到所有Worker
依赖版本控制
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Nacos
Nacos服务注册和配置中心,兼顾两种
为什么叫Nacos
前四个字母分别为:Naming(服务注册) 和 Configuration(配置中心) 的前两个字母,后面的s 是 Service
Nacos就是注册中心 + 配置中心的组合
等价于:Nacos = Eureka + Config
下载
官网:https://github.com/alibaba/nacos
nacos文档:https://nacos.io/zh-cn/docs/what-is-nacos.html
比较
Nacos在阿里巴巴内部有超过10万的实例运行,已经过了类似双十一等各种大型流量的考验
安装并运行
本地需要 java8 + Maven环境
下载:https://github.com/alibaba/nacos/releases
使用windows 的玩一玩就行了
github经常抽风,可以使用:https://blog.csdn.net/buyaopa/article/details/104582141
解压后:运行bin目录下的:startup.cmd -m standalone
,单机命令启动,nacos版本不一样,可能存在差异
打开:http://localhost:8848/nacos
结果页面
用户名和密码都是:nacos
Nacos作为服务注册中心
服务提供者注册Nacos
创建模块:
引入依赖
<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>
配置文件
server:
port: 9001
spring:
application:
name: nacos-payment-provider # 服务名称
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
主启动类
测试请求接口
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id) {
return "nacos registry, serverPort: " + serverPort + "\t id" + id;
}
}
启动主启动类
启动后查看注册到了nacos
消费者注册和负载
新建模块:cloudalibaba-provider-payment9002
和cloudalibaba-provider-payment9001 一模一样,只有端口配置成9002就好了,
启动后查看nacos,统一服务有2个实例
测试负载均衡
创建消费者:cloudalibaba-consumer-nacos-order84
依赖
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.biao.cloud</groupId>
<artifactId>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>
配置文件
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
主启动类
配置负载均衡请求
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
测试请求接口
@RestController
@Slf4j
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;
@Value("${service-url.nacos-user-service}") // 配置文件里的是注册中心的服务
private String serverURL;
@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
return restTemplate.getForObject(serverURL + "/payment/nacos/" + id, String.class);
}
}
启动主启动类,还有2个提供者服务9001、9002
启动后查看nacos服务列表
从消费者服务访问接口:http://localhost:84/consumer/payment/nacos/1
再请求一次
nacos底层整合了ribbon
服务中心对比
之前我们提到的注册中心对比图
但是其实Nacos不仅支持AP,而且还支持CP,它的支持模式是可以切换的,我们首先看看Spring Cloud Alibaba的全景图,
Nacos和CAP
CAP:分别是一致性,可用性,分区容错性
我们从下图能够看到,nacos不仅能够和Dubbo整合,还能和K8s,也就是偏运维的方向
Nacos支持AP和CP切换
C是指所有的节点同一时间看到的数据是一致的,而A的定义是所有的请求都会收到响应
合适选择何种模式?
一般来说,如果不需要存储服务级别的信息且服务实例是通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring Cloud 和 Dubbo服务,都是适合AP模式,AP模式为了服务的可用性而减弱了一致性,因此AP模式下只支持注册临时实例。
如果需要在服务级别编辑或存储配置信息,那么CP是必须,K8S服务和DNS服务则适用于CP模式。
CP模式下则支持注册持久化实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误。
Nacos服务配置中心
将我们的配置写入Nacos,然后以Spring Cloud Config的方式,用于抓取配置
引入依赖
<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>
YML配置文件
Nacos同SpringCloud Config一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常运行。
SpringBoot中配置文件的加载是存在优先级顺序的:bootstrap优先级 高于 application
application.yml配置
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info
bootstrap.yml配置
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
主启动类
测试请求接口
@RestController
@RefreshScope //支持Nacos的动态刷新功能。
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
在Nacos中添加配置信息
Nacos中匹配规则:
Nacos中的dataid的组成格式及与SpringBoot配置文件中的匹配规则
准备添加配置
填写配置信息,发布
发布成功后,启动3377服务,可以正常启动后,访问测试接口
修改nacos配置种的数据,version改成2,发布后再次访问接口
分类配置
在实际开发中,通常一个系统会准备:
- dev开发环境
- test测试环境
- prod生产环境
如何保证指定环境启动时,服务能正确读取到Nacos上相应环境的配置文件呢?
同时,一个大型分布式微服务系统会有很多微服务子项目,每个微服务子项目又都会有相应的开发环境,测试环境,预发环境,正式环境,那怎么对这些微服务配置进行管理呢?
Namespace + Group + Data ID 三者关系
默认情况:
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微服务起一个集群名称,还可以尽量让同一个机房的微服务相互调用,以提升性能,最后Instance,就是微服务的实例。
三种方案加载配置
DataID方案
- 指定spring.profile.active 和 配置文件的DataID来使不同环境下读取不同的配置
- 默认空间 + 默认分组 + 新建dev 和 test两个DataID
在nacos配置中心再创建一个配置:nacos-config-client-test.yaml
修改项目中的配置,dev
改成test
重启项目后,请求接口,读到的就是test版本的配置文件了,
这样项目上到对应的环境,修改一下配置就是对应的不同环境了
Group方案
在创建的时候,添加分组信息
再创建一个,添加另一个分组信息
两个不同分组的配置文件配置好以后,
修改配置文件使用环境为info
添加项目中的分组配置
重启项目,请求测试接口,读取到的就是DEV_GROUP
分组的配置文件
修改分组配置,info环境配置文件不用改
重启项目,请求测试接口,读取到的就是TEST_GROUP
分组的配置文件
Namspace方案
首先我们需要新建一个命名空间
新建完成后,能够看到有命名空间id
再创建一个test
命名空间
创建完成后,回到配置列表,我们会发现,多出了几个命名空间切换
同时,我们到服务列表,发现也多了命名空间的切换
可以通过引入namespaceI,来创建到指定的命名空间下:
再dev 命名空间创建3个配置文件:
第1个:默认分组
第2个:DEV分组
第3个:TEST分组
创建完成:
修改项目配置中,切换成dev环境
添加命名空间id的配置,修改对应的分组
启动项目后,请求接口读取到的就是dev命名空间下的对应分组的配置文件内容
如果切换成TEST分组
再请求接口:
Nacos集群和持久化配置
默认Nacos使用嵌入数据库实现数据的存储,所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
Nacos支持三种部署模式
- 单机模式:用于测试和单机使用
- 集群模式:用于生产环境,确保高可用
- 多集群模式:用于多数据中心场景
单机模式支持mysql
在0.7版本之前,在单机模式下nacos使用嵌入式数据库实现数据的存储,不方便观察数据存储的基本情况。0.7版本增加了支持mysql数据源能力,具体的操作流程:
- 安装数据库,版本要求:5.6.5 +
- 初始化数据库,数据库初始化文件:nacos-mysql.sql
- 修改conf/application.properties文件,增加mysql数据源配置,目前仅支持mysql,添加mysql数据源的url,用户名和密码
找到nacos安装目录的conf 目录下nacos-mysql.sql
复制里面所有内容,不要做任何修改,直接粘贴到mysql执行这些sql脚本,下面有一些对应的表,来做持久化配置信息
选择或创建一个新的数据库,执行这些sql
好了以后同目录下修改application.properties
文件,把数据库连接配置添加到下面
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://对应的ip:3306/对应的数据库名?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=用户名
db.password=自己的密码
修改完以后,重新打开一次nacos,发现之前的配置文件都没了,变成空的了,
添加一个配置文件
然后找到对应的mysql里面的表
已经持久化了
Linux安装Nacos
下载地址:https://github.com/alibaba/nacos/releases
下载好的压缩包上传到服务器,解压缩
解压好以后的目录和windows 版本的是一样的,一样使用就行
集群配置
如果是一个nacos:启动 8848即可
如果是多个nacos:3333,4444,5555
那么就需要修改startup.sh
里面的,传入端口号
步骤:
- Linux服务器上mysql数据库配置
找到conf 目录下的nacos-mysql.sql
文件,拷贝下来在服务器的mysql中执行这些sql脚本
- application.properties配置
在conf 目录找到这个文件,粘贴这些连接数据库配置到application.properties
文件的最下面,保存
为了安全起见,备份一下之前初始的
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://对应的ip:3306/对应的数据库名?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=用户名
db.password=自己的密码
- Linux服务器上nacos的集群配置cluster.conf.example
- 梳理出3台nacos集群的不同服务端口号
- 复制出cluster.conf(备份)
- 修改
为了安全起见,备份一下之前初始的
注释掉里面所有内容,添加新的内容:不可以写127.0.0.1 或者localhost
本机ip地址:3333
本机ip地址:4444
本机ip地址:5555
- 编辑Nacos的启动脚本startup.sh,使它能够接受不同的启动端口
- /nacos/bin 目录下有
startup.sh
- /nacos/bin 目录下有
- 平时单机版的启动,直接
./startup.sh
- 但是集群启动时,我们希望可以类似其它软件的shell命令,传递不同的端口号启动不同的nacos实例,命令:
./startup.sh -p 3333
表示启动端口号为3333的nacos服务器实例,和上一步的cluster.conf配置一样。
修改bin 目录下启动脚本startup.sh
,添加P,这样能够明确nacos启动的什么脚本
现在比较高级的版本已经有了p的参数设置,如果是之前nacos低版本的话,自行添加
下面还有个修改的地方,加上-Dserver.port=${PORT}
修改完成后,就能够使用下列命令启动集群了
./startup.sh -p 3333
./startup.sh -p 4444
./startup.sh -p 5555
- Nginx的配置,由它作为负载均衡器
nginx安装参考:https://www.cnblogs.com/abiu/p/15055877.html
修改nginx的配置文件
修改:
保存后,
启动nacos,使用-p 带上端口参数
查看nacos数量:
[root@localhost bin]# ps -ef | grep nacos|grep -v grep | wc -l
3
[root@localhost bin]#
启动nginx,指定启动时候执行哪个配置文件
通过nginx访问1111端口,访问到nacos
测试
在nacos添加一个新的配置文件
在数据库查到对应信息已经持久化
在项目中配置nacos,修改模块:cloudalibaba-provider-payment9002 里面的nacos连接配置
这下就可以通过nginx访问到nacos集群
启动项目,查看nacos 已经成功注册
Sentinel实现熔断和限流
Github:https://github.com/alibaba/Sentinel
Sentinel:分布式系统的流量防卫兵,相当于Hystrix
Hystrix存在的问题
- 需要我们程序员自己手工搭建监控平台
- 没有一套web界面可以给我们进行更加细粒度化的配置,流量控制,速率控制,服务熔断,服务降级。。
这个时候Sentinel运营而生
- 单独一个组件,可以独立出来
- 直接界面化的细粒度统一配置
约定 > 配置 >编码,都可以写在代码里,但是尽量使用注解和配置代替编码
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
主要特征
生态圈
下载
Github:https://github.com/alibaba/Sentinel/releases
安装Sentinel控制台
sentinel组件由两部分组成,后台和前台8080
Sentinel分为两部分
- 核心库(Java客户端)不依赖任何框架/库,能够运行在所有Java运行时环境,同时对Dubbo、SpringCloud等框架也有较好的支持。
- 控制台(Dashboard)基于SpringBoot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器
使用 java -jar 启动,同时Sentinel默认的端口号是8080,因此不能被占用
启动后访问:http://localhost:8080/#/login
用户名和密码都是sentinel
初始化演示工程
创建模块:cloudalibaba-sentinel-service8401
导入依赖:
<dependencies>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.biao.cloud</groupId>
<artifactId>commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- SpringBoot整合Web组件+actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--日常通用jar包配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.6.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件:
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
主启动类
测试请求接口:
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}
@GetMapping("/testB")
public String testB() {
return "------testB";
}
}
启动sentinel 和nacos,启动主启动类
因为它是懒加载,需要有请求后才有响应
请求testB接口
这个时候再查看sentinel
流控规则
推荐从这里加流控
当然了,也可以从这里加
字段说明
- 资源名:唯一名称,默认请求路径
- 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
- 阈值类型 / 单机阈值
- QPS:(每秒钟的请求数量):但调用该API的QPS达到阈值的时候,进行限流
- 线程数:当调用该API的线程数达到阈值的时候,进行限流
- 是否集群:不需要集群
- 流控模式
- 直接:api都达到限流条件时,直接限流
- 关联:当关联的资源达到阈值,就限流自己
- 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】
- 流控效果
- 快速失败:直接失败,抛异常
- Warm UP:根据codeFactory(冷加载因子,默认3),从阈值/CodeFactor,经过预热时长,才达到设置的QPS阈值
- 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置QPS,否则无效
直接
直接-快速失败默认的
给testA 新增流程控制,设置直接,快速失败
QPS设置成1了,1秒只能访问1次,所以不能的请求testA 接口时,会提示限流
思考:
直接调用的是默认报错信息,能否有我们的后续处理,比如更加友好的提示,类似有hystrix的fallback方法
线程数
这里的线程数表示一次只有一个线程进行业务请求,允许多个请求访问进来,但当前出现请求无法响应的时候,会直接报错,
例如,在方法的内部增加一个睡眠,那么后面来的就会失败
修改tesetA 接口:
@GetMapping("/testA")
public String testA() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "------testA";
}
这时候如果开启多个请求接口去请求testA 接口,也是会限流的
关联
比如testA 接口达到阈值后,testB 接口就限流,这样有什么意义呢?
比如支付接口达到阈值后,就限流订单接口,可以防止连锁反应的阻塞。
当testA 接口遇到并发情况下,去请求testB 的话,testB 是不可用
链路
多个请求调用了同一个微服务
流控效果
预热
系统最怕的就是出现,平时访问是0,然后突然一瞬间来了10W的QPS
公式:阈值 除以 clodFactor
(默认值为3),经过预热时长后,才会达到阈值
Warm Up
方式,即预热/冷启动方式,当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能会瞬间把系统压垮。通过冷启动,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值,给冷系统一个预热的时间,避免冷系统被压垮。通常冷启动的过程系统允许的QPS曲线如下图所示
默认clodFactor为3,即请求QPS从threshold / 3开始,经预热时长逐渐提升至设定的QPS阈值
假设这个系统的QPS是10,那么最开始系统能够接受的 QPS = 10 / 3 = 3,然后从3逐渐在5秒内提升到10
测试:
刚开始请求会出现直接报错
但是经过5秒以后,就可以经得住QPS10以内的请求了
应用场景:
秒杀系统在开启的瞬间,会有很多流量上来,很可能把系统打死,预热的方式就是为了保护系统,可能慢慢的把流量放进来,慢慢的把阈值增长到设置的阈值。
排队等待
大家均速排队,让请求以均匀的速度通过,阈值类型必须设置成QPS,否则无效
均速排队方式必须严格控制请求通过的间隔时间,也即让请求以匀速的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列,想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒处于空闲状态,我们系统系统能够接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
设置含义:/testA 每秒1次请求,超过的话,就排队等待,等待时间超过20000毫秒
降级规则
- RT(平均响应时间,秒级)
- 平均响应时间,超过阈值 且 时间窗口内通过的请求 >= 5,两个条件同时满足后出发降级
- 窗口期过后,关闭断路器
- RT最大4900(更大的需要通过 -Dcsp.sentinel.staticstic.max.rt=XXXXX才能生效)
- 异常比例(秒级)
- QPA >= 5 且异常比例(秒级)超过阈值时,触发降级;时间窗口结束后,关闭降级
- 异常数(分钟级)
- 异常数(分钟统计)超过阈值时,触发降级,时间窗口结束后,关闭降级
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。
当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都进行自动熔断(默认行为是抛出DegradeException)
Sentinel的断路器是没有半开状态
半开的状态,系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,有异常则继续打开断路器不可用,具体可以参考hystrix
降级-RT
平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
在cloudalibaba-sentinel-service8401 模块新增测试接口:
@GetMapping("/testD")
public String testD() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testD 测试RT");
return "------testD";
}
启动后,然后使用Jmeter压力测试工具进行测试
按照上述操作,永远1秒种打进来10个线程,大于5个了,调用tesetD,我们希望200毫秒内处理完本次任务,如果200毫秒没有处理完,在未来的1秒的时间窗口内,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电
后续我们停止使用jmeter,没有那么大的访问量了,断路器关闭(保险丝恢复),微服务恢复OK
异常比例
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
单独访问一次,必然来一次报错一次,开启jmeter后,直接高并发发送请求,多次调用达到我们的配置条件了,断路器开启(保险丝跳闸),微服务不可用,不在报错,而是服务降级了
设置3秒内,如果请求百分50出错,那么就会熔断
修改testD 接口
请求接口,肯定会报异常
我们用jmeter每秒发送10次请求,3秒后,再次调用 localhost:8401/testD 出现服务降级
Blocked by Sentinel(flow limiting)
异常数
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态
时间窗口一定要大于等于60秒
异常数是按分钟来统计的
下面设置是,一分钟内出现5次,则熔断
新增测试接口:
@GetMapping("/testE")
public String testE() {
log.info("testE 测试异常数");
int age = 10 / 0;
return "------testE 测试异常数";
}
首先我们再次访问 http://localhost:8401/testE
第一次访问绝对报错,因为除数不能为0,我们看到error窗口,
但是达到5次报错后,进入熔断后的降级
热点规则
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。
兜底的方法
分为系统默认的和客户自定义的,两种,之前的case中,限流出现问题了,都用sentinel系统默认的提示:Blocked By Sentinel,我们能不能自定义,类似于hystrix,某个方法出现问题了,就找到对应的兜底降级方法。
从 @HystrixCommand
到 @SentinelResource
配置
@SentinelResource
的value
,就是我们的资源名,也就是对哪个方法配置热点规则,名称只要唯一就行
blockHandler属性:表示如果方法有异常,就执行这个兜底方法
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
return "------testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockException exception) {
return "------deal_testHotKey,o(╥﹏╥)o"; //sentinel系统默认的提示:Blocked by Sentinel (flow limiting)
}
配置热点
请求http://localhost:8401/testHotKey?p1=a 接口,如果参数索引0,也就是p1参数超过QPS,就会执行兜底方法
根据第一个参数来判定的,如果没有p1参数请求接口,是不会有降级的
正常访问
狂点,触发兜底方法
参数例外项
上面案例演示了第一个参数p1,当QPS超过1秒1次点击狗,马上被限流
- 普通:超过一秒1个后,达到阈值1后马上被限流
- 我们期望p1参数当它达到某个特殊值时,它的限流值和平时不一样
- 特例:假设当p1的值等于5时,它的阈值可以达到200
- 一句话说:当key为特殊值的时候,不被限制
平时的时候,参数1的QPS是1,超过的时候被限流,但是有特殊值,比如5,那么它的阈值就是200
我们通过 http://localhost:8401/testHotKey?p1=5 一直刷新,发现不会触发兜底的方法,这就是参数例外项
热点参数的注意点,参数必须是基本类型或者String
请求接口,当参数p1的值是5 的时候,不会触发触发兜底方法,因为它的QPS是200
@SentinelResource 处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理
RuntimeException,如 int a = 10/0 ;
这个是java运行时抛出的异常,RuntimeException,@RentinelResource不管
也就是说:**@SentinelResource **主管配置出错,运行出错不管。
如果想要有配置出错,和运行出错的话,那么可以设置 fallback
系统配置
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式:
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
- CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
以人口QPS为例测试:
设置好QPS为1,随便访问哪个接口,开始狂点,直接降级
虽然方便,但也危险。
@SentinelResource注解
- 按资源名称限流 + 后续处理
- 按URL地址限流 + 后续处理
问题
- 系统默认的,没有体现我们自己的业务要求
- 依照现有条件,我们自定义的处理方法又和业务代码耦合在一块,不直观
- 每个业务方法都添加一个兜底方法,那代码膨胀加剧
- 全局统一的处理方法没有体现
- 关闭8401,发现流控规则已经消失,说明这个是没有持久化
自定义限流处理逻辑
创建CustomerBlockHandler类用于自定义限流处理逻辑
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public Result byResource() {
return new Result(200, "按资源名称限流测试OK", new Payment(2020L, "serial001"));
}
public Result handleException(BlockException exception) {
return new Result(444, exception.getClass().getCanonicalName() + "\t 服务不可用");
}
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public Result byUrl() {
return new Result(200, "按url限流测试OK", new Payment(2020L, "serial002"));
}
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, // 哪个类
blockHandler = "handlerException2") // 里面的哪个方法做限流降级
public Result customerBlockHandler() {
return new Result(200, "按客戶自定义", new Payment(2020L, "serial003"));
}
}
创建一个类:CustomerBlockHandler
public class CustomerBlockHandler {
public static Result handlerException(BlockException exception) {
return new Result(4444, "按客戶自定义,global handlerException----1");
}
public static Result handlerException2(BlockException exception) {
return new Result(4444, "按客戶自定义,global handlerException----2");
}
}
在使用的时候,就可以首先指定是哪个类,哪个方法
测试
然后请求http://localhost:8401//rateLimit/customerBlockHandler
当请求多的时候,调用了自定义的降级方法
服务熔断
创建模块:cloudalibaba-provider-payment9003 和 cloudalibaba-provider-payment9004
两个提供者
分别导入依赖:
<dependencies>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.biao.cloud</groupId>
<artifactId>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>
配置文件:区分一下端口就行了,两个服务一样的
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
主启动类,都加上注解@EnableDiscoveryClient
都创建测试请求接口,只有1、2、3 三个id,其他的id没有
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "28a8c1e3bc2742d8848569891fb42181"));
hashMap.put(2L, new Payment(2L, "bba8c1e3bc2742d8848569891ac32182"));
hashMap.put(3L, new Payment(3L, "6ua8c1e3bc2742d8848569891xt92183"));
}
@GetMapping(value = "/paymentSQL/{id}")
public Result<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
Result<Payment> result = new Result(200, "from mysql,serverPort: " + serverPort, payment);
return result;
}
}
创建消费者:cloudalibaba-consumer-nacos-order85
导入依赖:
<dependencies>
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud ailibaba nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.biao.cloud</groupId>
<artifactId>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>
配置文件
server:
port: 85
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
主启动类
配置类:
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
测试请求接口
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") //没有配置
public Result<Payment> fallback(@PathVariable Long id) {
Result<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, Result.class, id);
if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}
return result;
}
}
启动消费者和两个提供者,访问提供者接口测试
参数id如果是1、2、3 都可以,
4 会报参数异常,
其他的都是空指针异常
设置fallback
修改接口类代码
如果有异常,就会执行下面方法handlerFallback 方法
比如访问是id参数为4 或5:
设置blockHandler
修改代码:
blockHandler 主要管的sentinel 的违规请求会触发
blockHandler和fallback都配置
若blockHandler 和 fallback都进行了配置,则被限流降级而抛出 BlockException时,只会进入blockHandler处理逻辑
blockHandler 高于fallback
异常忽略
修改属性配置
如果请求id为4,就不走兜底方法了,忽略这种IllegalArgumentException
异常
降级熔断——Feign系列
确定导入依赖:
<!--SpringCloud openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
修改配置
确认启动类添加激活注解
编写Feign接口
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
public Result<Payment> paymentSQL(@PathVariable("id") Long id);
}
加入fallback兜底方法实现
@Component
public class PaymentFallbackService implements PaymentService {
@Override
public Result<Payment> paymentSQL(Long id) {
return new Result<>(44444, "服务降级返回,---PaymentFallbackService", new Payment(id, "errorSerial"));
}
}
添加测试请求的接口
测试
请求接口:http://localhost:84/consumer/paymentSQL/1
测试85调用9003,此时故意关闭9003微服务提供者,看84消费侧自动降级
我们发现过了一段时间后,会触发服务降级,返回失败的方法
Sentinel规则持久化
一旦我们重启应用,sentinel规则将会消失,生产环境需要将规则进行持久化
将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上的流控规则持续有效
解决方法
使用nacos持久化保存
修改cloudalibaba-sentinel-service8401 模块
确认导入依赖
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
修改配置文件
nacos 上面配置
准备配置
- resource:资源名称
- limitApp:来源应用
- grade:阈值类型,0表示线程数,1表示QPS
- count:单机阈值
- strategy:流控模式,0表示直接,1表示关联,2表示链路
- controlBehavior:流控效果,0表示快速失败,1表示Warm,2表示排队等待
- clusterMode:是否集群
这样启动的时候,调用一下接口,我们的限流规则就会重新出现,并且配置也是生效的
Seata处理分布式事务
分布式事务
跨数据库,多数据源的统一调度,就会遇到分布式事务问题
如下图,单体应用被拆分成微服务应用,原来的三个模板被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证。
Seata简介
官方文档:https://seata.io/zh-cn/
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
分布式事务处理过程的一致性ID + 三组件模型
- Transaction ID XID:全局唯一的事务ID
- 三组件的概念
- Transaction Coordinator(TC):事务协调器,维护全局事务,驱动全局事务提交或者回滚
- Transaction Manager(TM):事务管理器,控制全局事务的范围,开始全局事务提交或回滚全局事务
- Resource Manager(RM):资源管理器,控制分支事务,负责分支注册分支事务和报告
处理过程
TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
XID在微服务调用链路的上下文中传播
RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
TM向TC发起针对XID的全局提交或回滚决议
TM调度XID下管辖的全部分支事务完成提交或回滚请求
下载
地址:https://github.com/seata/seata/releases
修改conf目录下的file.conf 配置文件:
首先我们需要备份原始的file.conf文件
主要修改,自定义事务组名称 + 事务日志存储模式为db + 数据库连接信息,也就是修改存储的数据库
修改registry.conf
创建一个seata数据库
里面创建3个表
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
启动nacos 和 seata-server
测试demo
- 本地:@Transaction
- 全局:@GlobalTransaction
Spring自带的是 @Transaction
控制本地事务
而 @GlobalTransaction
控制的是全局事务
我们只需要在需要支持分布式事务的业务类上,使用该注解即可
订单/库存/账户业务微服务准备
在这之前首先需要先启动Nacos,然后启动Seata,保证两个都OK
分布式事务的业务说明
这里我们会创建三个微服务,一个订单服务,一个库存服务,一个账户服务。
当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,在通过远程调用账户服务来扣减用户账户里面的金额,最后在订单服务修改订单状态为已完成
该操作跨越了三个数据库,有两次远程调用,很明显会有分布式事务的问题。
意思是:下订单 -> 扣库存 -> 减余额
创建订单库
里面建表
CREATE TABLE `t_order` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) 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:已完结',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;
创建库存库
里面建表
CREATE TABLE `t_storage` (
`int` bigint(11) NOT NULL AUTO_INCREMENT,
`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 '剩余库存',
PRIMARY KEY (`int`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '库存' ROW_FORMAT = Dynamic;
INSERT INTO `t_storage` VALUES (1, 1, 100, 0, 100);
创建账户信息库
里面建表
CREATE TABLE `t_account` (
`id` bigint(11) NOT NULL 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 NULL COMMENT '剩余可用额度',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账户表' ROW_FORMAT = Dynamic;
INSERT INTO `t_account` VALUES (1, 1, 1000, 0, 1000);
订单库、库存库、账户信息库 分别添加一个回滚日志表:undo_log
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
业务需求
下订单 -> 减库存 -> 扣余额 -> 改(订单)状态
创建订单模块:seata-order-service2001
注意点
pom依赖
配置文件
创建库存模块:seata-storage-service2002
创建账户模块:seata-account-service2003
开始测试
数据初始化
表t_order,目前没有数据
表t_storage,1号产品,库存100个,卖出0个,剩余100个
表t_account,1号账户有1000元,用了0元,剩余1000元
启动三个服务:订单、库存、账户
正常下单试试
请求:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
订单库添加了一条信息,1号用户买了1号产品,买了10个,花了100块钱,1表示已支付/0表示未完成
库存表数据发生改变,1号产品库存100个,卖了10个,剩余90个
账户信息改变,1号用户有1000块钱,用了100,剩余900
如果有异常,还没加注解@GlobalTransactional
,
模拟异常,在扣减账户余额时候,线程阻塞一段时间
再次请求接口下订单:http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100
出现了数据不一致的问题
订单表,状态为0表示失败
库存信息还说发生改变
用户金额也发生改变
故障情况
- 当库存和账户金额扣减后,订单状态并没有设置成已经完成,没有从零改成1
- 而且由于Feign的重试机制,账户余额还有可能被多次扣除
超时异常,添加注解@GlobalTransaction
在下订单时候,这个入口调用的方法上添加
rollbackFor 属性表示,什么什么错误就会回滚
添加这个后,发现下单后的数据库并没有改变,记录都添加不进来
数据还是刚才的数据,没有发生改变
补充原理说明
TC/TM/RM三大组件
什么是TC,TM,RM
- TC:seata服务器
- TM:带有@GlobalTransaction注解的方法
- RM:数据库,也就是事务参与方
分布式事务的执行流程
- TM开启分布式事务(TM向TC注册全局事务记录),相当于注解 @GlobelTransaction注解
- 按业务场景,编排数据库,服务等事务内部资源(RM向TC汇报资源准备状态)
- TM结束分布式事务,事务一阶段结束(TM通知TC提交、回滚分布式事务)
- TC汇总事务信息,决定分布式事务是提交还是回滚
- TC通知所有RM提交、回滚资源,事务二阶段结束
AT模式
前提
- 基于支持本地ACID事务的关系型数据库
- Java应用,通过JDBC访问数据库
也就是:
整体机制
两阶段提交协议的演变
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源
- 二阶段
- 提交异步化,非常快速的完成
- 回滚通过一阶段的回滚日志进行反向补偿
一阶段加载
在一阶段,Seata会拦截 业务SQL
- 解析SQL语义,找到业务SQL,要更新的业务数据,在业务数据被更新前,将其保存成 before image(前置镜像)
- 执行业务SQL更新业务数据,在业务数据更新之后
- 将其保存成 after image,最后生成行锁
以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
每个服务对于的数据库表有个undo_log
表
二阶段提交
二阶段如果顺利提交的话,因为业务SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照和行锁删除掉,完成数据清理即可
二阶段回滚
二阶段如果回滚的话,Seata就需要回滚到一阶段已经执行的 业务SQL,还原业务数据
回滚方式便是用 before image 还原业务数据,但是在还原前要首先校验脏写,对比数据库当前业务数据 和after image,如果两份数据完全一致,没有脏写,可以还原业务数据,如果不一致说明有脏读,出现脏读就需要转人工处理
总体流程