SpringCloud
简介
微服务架构是一种架构模式,倡导将单一应用程序划分成一组小的服务,服务之间相互协调、配合整体对外提供服务。每个小的服务独立运行部署,运行在独立的进程中,服务与服务之间采用轻量级的通信机制相互协作,每个服务围绕具体业务进行构建。避免统一的集中式管理,对于具体的一个服务而言,应根据上下文,选择合适的语言和工具进行构建。
分布式微服务的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。
版本选型
SpringBoot2.X版 + SpringCloud H版
版本对应关系:
Release Train | Boot Version |
Hoxton | 2.2.x |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
官网技术选型:
{
"spring-cloud":{
"Hoxton.SR12":"Spring Boot >=2.2.0.RELEASE and <2.4.0.M1",
"2020.0.6":"Spring Boot >=2.4.0.M1 and <2.6.0-M1",
"2021.0.0-M1":"Spring Boot >=2.6.0-M1 and <2.6.0-M3",
"2021.0.0-M3":"Spring Boot >=2.6.0-M3 and <2.6.0-RC1",
"2021.0.0-RC1":"Spring Boot >=2.6.0-RC1 and <2.6.1",
"2021.0.5":"Spring Boot >=2.6.1 and <3.0.0-M1",
"2022.0.0-M1":"Spring Boot >=3.0.0-M1 and <3.0.0-M2",
"2022.0.0-M2":"Spring Boot >=3.0.0-M2 and <3.0.0-M3",
"2022.0.0-M3":"Spring Boot >=3.0.0-M3 and <3.0.0-M4",
"2022.0.0-M4":"Spring Boot >=3.0.0-M4 and <3.0.0-M5",
"2022.0.0-M5":"Spring Boot >=3.0.0-M5 and <3.0.0-RC1",
"2022.0.0-RC1":"Spring Boot >=3.0.0-RC1 and <3.1.0-M1"
}
}
最终技术选型:
技术 | 版本 |
SpringCloud | Hoxton.SR1 |
SpringBoot | 2.2.2.RELEASE |
SpringCloudAlibaba | 2.1.0.RELEASE |
Java | Java8 |
Maven | 3.5及以上 |
MySQL | 5.7及以上 |
SpringCloud对应的SpringBoot推荐版本:Spring官网
当前最新版本为:
Release Train Version: 2021.0.5
Supported Boot Version: 2.6.13
Cloud组件升级
服务注册中心
Eureka
传统RPC远程调用框架中,管理每个服务与服务之间的依赖关系比较复杂,管理繁杂,所以需要服务治理,管理服务与服务之间的依赖关系,可以实现服务调用、负载均衡、容错,实现服务发现与注册。
服务注册与发现
Eureka采用了CS的架构设计模式,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监听系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息,比如服务地址通讯地址等以别名方式注册到注册中心上。另一方(消费者|服务提供者),以该别名的方式去注册中心上获取实际的服务通讯地址,然后再实现本地RPC调用RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系(服务治理概念)。在任何RPC远程框架中,都会有一个注册中心(存放服务地址相关信息(接口地址))。
Eureka架构:
Dubbo架构:
Eureka Server
Eureka Server提供服务注册服务,各个微服务节点通过配置启动后,会在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
Eureka Client
Eureka Client通过这册中心进行访问,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没收到某个节点的心跳,Eureka Server将会从服务注册中把这个服务节点移除(默认90秒)。
构建eureka server
pom依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
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/
启动类
@SpringBootApplication
@EnableEurekaServer //表明是eureka的服务注册中心
public class EurekaServerApp {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApp.class, args);
}
}
@EnableEurekaServer //表明是eureka的服务注册中心
启动访问:http://localhost:7001
Eureka Client
Eureka Client注册进Eureka Server
pom依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.yml
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer 默认为true
fetch-registry: true #是否从EurekaServer抓取已有注册信息,默认true。单节点无所谓,集群必须设置true,才能配合ribbon负载均衡
service-url:
defaultZone: http://localhost:7001/eureka
启动类:
/**
* 启动类
*
* @author zhangzengxiu
* @date 2022/11/20
*/
@SpringBootApplication
@EnableEurekaClient //表明是eureka的客户端
public class PaymentApp {
public static void main(String[] args) {
SpringApplication.run(PaymentApp.class, args);
}
}
@EnableEurekaClient //表明是eureka的客户端
启动访问
注册成功。
EMERGENCY红色文字表示Eureka的自我保护机制
注册到Eureka上的服务名与配置文件中的名称一致。
配置文件信息改为false时,则不会注册到Eureka Server
eureka:
client:
register-with-eureka: false #表示是否将自己注册进EurekaServer false表示不注册
服务注册:将服务信息注册进注册中心
服务发现:从注册中心上获取服务信息
实质:存key服务名 取value调用地址
流程:
- 先启动eureka注册中心
- 启动服务提供者payment支付服务
- 支付服务启动后会把自身信息(比如服务地址以别名方式注册进eureka)
- 消费者order服务在需要调用接口时,使用服务别名去注册中心获取实际的RPC远程调用地址
- 消费者获得调用地址后,底层实际是利用HttpClient技术实现远程调用
- 消费者获得服务地址后,会缓存到本地jvm中,默认每间隔30秒更新一次服务调用地址
Eureka集群
实现负载均衡和故障容错。
集群搭建
多台机器之间相互注册
单机版本:
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/
集群:
现在cloud-eureka-server有两台服务。
现在为区分两台机器,本地配置host
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
cloud-eureka-server:
pom.xml(每个server服务一致)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud</artifactId>
<groupId>com.test.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-eureka-server</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.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>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
application.yml
server:
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/
cloud-eureka-server2:
application.yml
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/
两台机器之间相互注册!!!
其他内容与另一台机器一致。启动测试。
cloud-eureka-server 7001:
cloud-eureka-server2 7002:
集群搭建完成!!!
将支付服务发布到集群中:
单机版:
server:
port: 8001
spring:
application:
name: cloud-payment-service
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer 默认为true
fetch-registry: true #是否从EurekaServer抓取已有注册信息,默认true。单节点无所谓,集群必须设置true,才能配合ribbon负载均衡
service-url:
defaultZone: http://localhost:7001/eureka
集群版配置:
server:
port: 8001
spring:
application:
name: cloud-payment-service
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer 默认为true
fetch-registry: true #是否从EurekaServer抓取已有注册信息,默认true。单节点无所谓,集群必须设置true,才能配合ribbon负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
依次启动:
cloud-eureka-serve、cloud-eureka-serve2、eureka-provider-payment、eureka- consumer-order
启动测试
使用postman调用服务,依旧一切正常:
现在我们的服务已经全部注册到我们的eureka集群中了。
服务提供者集群搭建
新建一个模块,基本与另一个服务保持一致。
配置文件
server:
port: 8002
spring:
application:
name: cloud-payment-service
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/dbcloud?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: root
password: 19960327xiu
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.test.springcloud.entities
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer 默认为true
fetch-registry: true #是否从EurekaServer抓取已有注册信息,默认true。单节点无所谓,集群必须设置true,才能配合ribbon负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
修改端口号为8002。
启动测试:
同一个服务下有两个机器,即服务集群注册成功。
测试访问:
但是当前存在的问题:无论访问多少次,只会去找8001这一台机器,不会去找另外一台8002机器。
原因是代码将ip和端口写死了。
/**
* @author zhangzengxiu
* @date 2022/11/21
*/
@RestController
@RequestMapping("/order")
public class OrderController {
public static final String PAYMENT_URI = "http://localhost:8001";
@Autowired
private RestTemplate restTemplate;
@PostMapping("/consumer/payment/addPayment")
public CommonResult<Payment> addPayment(@RequestBody Payment payment) {
return restTemplate.postForObject(PAYMENT_URI + "/payment/addPayment", payment, CommonResult.class);
}
@GetMapping("/consumer/payment/getPayment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URI + "/payment/getPayment/" + id, CommonResult.class);
}
}
ip和端口固定写死了。
当前固定访问8001端口,就无法访问另一台机器,无法实现负载均衡的目的。
所以现在不能再去根据ip和端口去找对应的服务了,而是需要根据在eureka中注册的服务名去调用服务。
现对调用服务进行修改:
修改前:
public static final String PAYMENT_URI = "http://localhost:8001";
修改后:
public static final String PAYMENT_URI = "http://CLOUD-PAYMENT-SERVICE";
启动测试调用:
此时访问会报错,当前我们对外暴露的不再是ip和端口,而是微服务名。
无法根据这个服务找到对应的一台机器,无法识别是哪台机器提供服务。
解决方案:
开启RestTemplate负载均衡。
RestTemplate配置类:
/**
* @author zhangzengxiu
* @date 2022/11/21
*/
@Configuration
public class ApplicationContextConfiguration {
@Bean
@LoadBalanced//负载均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
@LoadBalanced注解为RestTemplate提供了负载均衡的能力。这样才能通过微服务名称直接进行调用。
默认是轮询。
重启服务进行测试:
后续Ribbon与Eureka整合后,Consumer可以直接调用服务,而不需要再关心地址和端口,且该服务还具有负载均衡的能力。
acatuator微服务信息完善
依赖的jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
服务名称修改
按照规范不要暴露主机名,暴露服务名,去掉主机名。
application.yaml 配置
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer 默认为true
fetch-registry: true #是否从EurekaServer抓取已有注册信息,默认true。单节点无所谓,集群必须设置true,才能配合ribbon负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
instance-id: payment8002
启动测试:
ip显示修改
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer 默认为true
fetch-registry: true #是否从EurekaServer抓取已有注册信息,默认true。单节点无所谓,集群必须设置true,才能配合ribbon负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
instance-id: payment8001
prefer-ip-address: true #访问路径可以显示ip地址
重启测试:
服务发现Discovery
如果微服务自身需要向外暴露,需要成功注册到Eureka上的微服务信息,如ip、端口。
对于注册到eureka中的微服务,可以通过服务发现来获得该服务的信息。
/**
* @author zhangzengxiu
* @date 2022/11/20
*/
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String service : services) {
System.out.println(service);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
System.out.println(instance.getInstanceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return discoveryClient;
}
}
主启动类
/**
* 启动类
*
* @author zhangzengxiu
* @date 2022/11/20
*/
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class PaymentApp {
public static void main(String[] args) {
SpringApplication.run(PaymentApp.class, args);
}
}
添加@EnableDiscoveryClient注解
启动测试:
IDE控制台:
Eureka自我保护机制
概述
保护魔术主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护,一旦进入保护模式。
Eureka Server将会尝试保护其服务注册表的信息,不再删除服务注册表中的数据,也就是不再注销任务微服务。
故障展现:
红色字体显示即是Eureka自我保护体现。
总结
某时刻某一个微服务不可用,Eureka不会立刻清理,依旧会对该微服务信息进行保存。属于分布式CAP定理中的AP。(A:可用性 P:分区容错性)
为什么会产生Eureka自我保护机制?
为了防止Eurekaclient可以正常运行,但是与 EurekaServer网络不通情况下,EurekaServer不我立刻將EurekaClient服务剔除。
什么是自我保护模式?
默认情况下,如果EurekaServer在一定时问内没有接收到某个微服务实例的心跳Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、 卡顿、拥挤)时,微服务与EurekaServer之间无法正正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丟失过多客户端时 (可能发生了网络分区故障),那么这个节点就会进人自我保护模式。
自我保护机制:默认情况下EurekaClent定时向Eurekaserver端发送心跳包。如果Eureka在server端在一定时间内(默认90秒)没有收到EurekaClient发送心跳包,便会直接从服务注册列表中剔除该服务,但是在短时间(90秒中)内丢失了大量的服务实例心跳,这时候Eurekaserver会开启自我保护机制,不会剔除该服务(该现象可能出现在如果网络不通但是EurekaClient以为出现宕机,此时如果换做别的注册中心如果一定时间内没有收到心跳会将剔除该服务,这样就出现了严重失误,因为客户端还能正常发送心跳,只是网络延迟问题,保护机制是为了解决此问题而产生的)。
在自我保护模式中,Eureka Server会保护服务注册表中的信息,不在注销任何服务实例。
自我保护模式就是一种应对网络异常的安全保护措施,它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留)也不会盲目注销任何健康的微服务,使用自我保护模式,可以让Eureka集群更加健壮、稳定。
Eureka禁止自我保护
正常都不会禁用。
Eureka Server端配置
application.yaml
eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己
fetch-registry: false #false表示不需要去检索服务
service-url:
#与Eureka Server交互的地址查询服务与注册都需要依赖这个地址
defaultZone: http://eureka7001.com:7001/eureka/
server:
enable-self-preservation: false #关闭自我保护机制
eviction-interval-timer-in-ms: 2000 #清理间隔(单位毫秒,默认60*1000)
说明:
enable-self-preservation: false
#关闭自我保护机制,默认true
eviction-interval-timer-in-ms: 2000
#清理间隔
Eureka Client端配置
application.yaml
eureka:
client:
register-with-eureka: true #表示是否将自己注册进EurekaServer 默认为true
fetch-registry: true #是否从EurekaServer抓取已有注册信息,默认true。单节点无所谓,集群必须设置true,才能配合ribbon负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
instance-id: payment8001
prefer-ip-address: true #访问路径可以显示ip地址
lease-renewal-interval-in-seconds: 1 #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认30s)
lease-expiration-duration-in-seconds: 2 #Eureka服务端在收到最后一次心跳后等待的时间上限,单位为秒(默认90s),超时将剔除服务
说明:
lease-renewal-interval-in-seconds: 1
#每一次Eureka client向Eureka Server发送心跳的时间间隔。
lease-expiration-duration-in-seconds: 2
#如果90秒内Eureka Server没有收到Eureka Client的心跳包,则剔除该服务。
启动测试:
将payment服务关闭后,会立刻被剔除。
总结:
Eureka Client与Eureka Server之间,每间隔一定时间会续约一次,可配置客户端的lease-renewal-interval-in-seconds属性值,默认30s一次。
如果客户端一定时间仍未向服务端发送心跳包,服务端将客户端剔除。可通过配置lease-expiration-duration-in-seconds属性值,默认90s。
Eureka Server剔除的时间间隔,可通过配置eviction-interval-timer-in-ms属性值,默认60s一次,会去扫描有问题的服务,将有问题的服务进行剔除。
Eureka停更
Eureka停更!!!
Zookeeper
Springcloud整合Zookeeper替代Eureka
- zookeeper是一个分布式协调工具,可以实现注册中心功能
- 关闭Linux服务器后启动zookeeper服务器
- zookeeper服务器取代Eureka服务器,zk作为服务注册中心
# 关闭防火墙
systemctl stop firewalld
# 查看防火墙状态
systemctl status firewalld
# 查询zookeeper的ip
ifconfig
服务提供者注册进zookeeper
新建cloud-provider-payment3 模块
pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud</artifactId>
<groupId>com.test.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-payment3</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringBoot整合Zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
</dependencies>
</project>
SpringBoot整合Zookeeper的依赖
<!--SpringBoot整合Zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
application.yaml
server:
port: 8004 # 服务注册到zookeeper服务器的服务提供者端口号
spring:
application:
name: cloud-provider-payment #注册到zookeeper注册中心的名称
cloud:
zookeeper:
connect-string: 192.168.111.144:2181 # zookeeper机器的ip:端口
启动类
/**
* @author zhangzengxiu
* @date 2022/11/27
*/
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class PaymentApp3 {
public static void main(String[] args) {
SpringApplication.run(PaymentApp3.class, args);
}
}
controller入口
/**
* @author zhangzengxiu
* @date 2022/11/27
*/
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String port;
@GetMapping
public Object paymentZk() {
return "springcloud with zookeeper:" + port + "\t" + UUID.randomUUID().toString();
}
}
如果启动出现jar冲突,解决:
排除掉spring-cloud-starter-zookeeper-discovery自带的zookeeper的依赖,自己引入一个新版本。
<!--SpringBoot整合Zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
启动测试:
ls /
ls /services
ls /services/cloud-provider-payment
zookeeper创建的节点是临时的节点,在一定时间内每有收到心跳包,节点会被剔除。
服务消费者注册进zookeeper
新建cloud-consumerzk-order
pom.xml
<dependencies>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--SpringBoot整合Zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.9</version>
</dependency>
</dependencies>
application.yaml
server:
port: 80 # 服务注册到zookeeper服务器的服务提供者端口号
spring:
application:
name: cloud-consumerzk-order #注册到zookeeper注册中心的名称
cloud:
zookeeper:
connect-string: 192.168.111.144:2181 # zookeeper机器的ip:端口
主启动类
/**
* @author zhangzengxiu
* @date 2022/11/27
*/
@SpringBootApplication
@EnableDiscoveryClient
public class OrderZkApp {
public static void main(String[] args) {
SpringApplication.run(OrderZkApp.class, args);
}
}
配置
/**
* @author zhangzengxiu
* @date 2022/11/27
*/
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
controller入口
/**
* @author zhangzengxiu
* @date 2022/11/27
*/
@RestController
@RequestMapping("/payment")
public class PaymentController {
public static final String INVOKE_URL = "http://cloud-provider-payment";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/zk")
public Object paymentInfo() {
return restTemplate.getForObject(INVOKE_URL + "/payment/zk", Object.class);
}
}
启动测试!
Consul
Consul官网 Consul是一套开源的分布式服务发现和配置管理系统,由 Hashicorp 公司用 Go 语言开发。
提供了微服务系统中的服务治理、配置中心、控制总线等功能。这些功能中的每一个都可以根据需要单独使用,也可以一起使用以构建全方位的服务网格,总之Consul提供了一种完整的服务网格解决方案。
它具有很多优点。包括:基于 raft 协议,比较简洁;支持健康检查,同时支持 HTTP 和 DNS 协议支持跨数据中心的 WAN 集群提供图开果面跨平台,支持 Linux Mac、Windows
Consul安装
Consul下载地址中文文档地址 Windows版本命令:开发模式启动
consul agent -dev
启动成功后,访问本地 http://localhost:8500端口
注册服务提供者
新建cloud-providerconsul-payment模块
pom.xml
<dependencies>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringBoot整合web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<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>
<!--SpringCloud consul-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>
consul依赖:
<!--SpringCloud consul-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
配置application.yml
server:
port: 8006 #端口
spring:
application:
name: consul-provider-payment
# consul注册中心地址
cloud:
consul:
host: 192.168.10.105
port: 8500
discovery:
service-name: ${spring.application.name}
启动类
/**
* @author zhangzengxiu
* @date 2022/11/28
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ConsulPaymentApp {
public static void main(String[] args) {
SpringApplication.run(ConsulPaymentApp.class, args);
}
}
controller:
/**
* @author zhangzengxiu
* @date 2022/11/28
*/
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String port;
@GetMapping("/payment/consul")
public Object paymentConsul() {
return "springcloud with consul:" + port + "\t" + UUID.randomUUID().toString();
}
}
启动测试:
注册服务消费者
新建cloud-consumerconsul-order模块
pom.xml
<dependencies>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<!--SpringBoot整合web组件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<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>
<!--SpringCloud consul-server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>
配置 application.yml
server:
port: 80 #端口
spring:
application:
name: cloud-consumer-order
# consul注册中心地址
cloud:
consul:
host: 192.168.10.105
port: 8500
discovery:
service-name: ${spring.application.name}
config配置:
/**
* @author zhangzengxiu
* @date 2022/11/27
*/
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
controller:
/**
* @author zhangzengxiu
* @date 2022/11/27
*/
@RestController
@RequestMapping("/payment")
public class PaymentController {
public static final String INVOKE_URL = "http://cloud-provider-payment";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/consul")
public Object paymentInfo() {
return restTemplate.getForObject(INVOKE_URL + "/payment/consul", Object.class);
}
}
启动测试:
Eureka VS Zookeeper VS Consul
组件名 | 语言 | CAP | 服务健康检查 | 对外暴露接口 | SpringCloud集成 |
Eureka | Java | AP | 可配支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |
CAP:
分布式架构要么是CP要么是AP。
AP:
CP:
负载均衡
Ribbon
概述
Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。
简单的说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。我们很容易使用Ribbon实现自定义的负载均衡算法。
LB负载均衡
LB负载均衡(Load Balance)是什么
简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。
常见的负载均衡有软件Nginx, LVS,硬件F5等。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡区别
Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用微服务接口时候,会在注册中心上获取注朋信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
LB(负载均衡)
- 集中式LB
即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
Nginx属于集中式LB
- 进程内LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB,它只是个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
总结:Ribbon就是负载均衡+RestTemplate调用
大白话:
相当于去医院看病,进医院大门时间的负载均衡是Nginx。
到达科室后,选择哪个医生,是Ribbon。
总结:Ribbon其实就是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和eureka结合只是其中的一个实例。
架构图
Ribbon在工作时分成两步
第一步先选择EurekaServer ,它优先选择在同一个区域内负载较少的server.
第二步再根据用户指定的策略,在从server取到的服务注册列表中选择个地址。
其中Ribbon提供了很多种策略:比如轮询、随机和根据响应时间加权。
依赖
坐标:
spring-cloud-starter-netflix-ribbon可以使用Ribbon
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
Eureka本质上已经自己和Ribbon做了整合,所以不需要额外导入Ribbon坐标。
RestTemplate
getForObject VS getForEntity
getForObject:返回对象为响应体中数据转换成的对象,基本可以理解为json。(推荐使用)
getForEntity:返回对象包含ResponseEntity对象,包含响应中的一些重要信息,比如相应头、响应状态码、响应体等。
@GetMapping("/consumer/payment/getPayment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URI + "/payment/getPayment/" + id, CommonResult.class);
}
@GetMapping("/consumer/payment/getPaymentEntity/{id}")
public CommonResult<Payment> getPaymentEntityById(@PathVariable("id") Long id) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URI + "/payment/getPayment/" + id, CommonResult.class);
//状态码
HttpStatus statusCode = entity.getStatusCode();
System.out.println(statusCode);
int statusCodeValue = entity.getStatusCodeValue();
System.out.println(statusCodeValue);
HttpHeaders headers = entity.getHeaders();
System.out.println(headers);
CommonResult body = entity.getBody();
System.out.println(body);
if (statusCode.is2xxSuccessful()) {
return body;
}
return null;
}
entity包含更多信息,如相应头、状态码、响应体等等。
负载均衡
IRule接口:
说明:
策略替换方式
细节:
官方文档明确警告⚠️:
这个自定义的配置类不能放在#ComponentScan所扫描的当前包以及子包下,
否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享,达不到特殊化的目的。
解决方案:
分开在不同的包下:
配置:
/**
* @author zhangzengxiu
* @date 2022/11/29
*/
@Configuration
public class RibbonRule {
@Bean
public IRule myRule(){
//随机
return new RandomRule();
}
}
主启动类配置:
/**
* @author zhangzengxiu
* @date 2022/11/21
*/
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = RibbonRule.class)
public class OrderApp {
public static void main(String[] args) {
SpringApplication.run(OrderApp.class, args);
}
}
启动测试即可!
负载均衡算法
手写负载均衡算法
操作步骤:
- ApplicationContextBean去掉注解@LoadBalanced
接口:
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
/**
* @author zhangzengxiu
* @date 2022/12/8
*/
public interface LoadBalance {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
实现类:
package com.test.springcloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author zhangzengxiu
* @date 2022/12/8
*/
@Component
public class MyLb implements LoadBalance {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement() {
int current;
int next;
do {
current = atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!atomicInteger.compareAndSet(current, next));
System.out.println("访问次数:" + next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
OrderController:
/**
* @author zhangzengxiu
* @date 2022/11/21
*/
@RestController
@RequestMapping("/order")
public class OrderController {
//public static final String PAYMENT_URI = "http://localhost:8001";
public static final String PAYMENT_URI = "http://CLOUD-PAYMENT-SERVICE";
@Autowired
private RestTemplate restTemplate;
@Autowired
private MyLb myLb;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/payment/lb")
public String getPaymentlb() {
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (CollectionUtils.isEmpty(instances)) {
return null;
}
ServiceInstance serviceInstance = myLb.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
}
providerController:
@GetMapping("/lb")
public String getPaymentlb() {
return serverPort;
}
启动测试:
http://localhost/order/consumer/payment/lb
服务调用
Spring Cloud OpenFeign
官网地址 Feign是一个声明式WebService客户端。使用Feign能让编写WebService客户端更加简单。
它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可插拔式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了SpringMVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。
Feign其实就是在Ribbon的基础之上,又做了一套服务接口加注解方式调用的一个整合器。
我们只需要创建一个接口,并在上面添加一个注解就可以了。
已经有了Ribbon,为什么还需要OpenFeign?
Feign
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注Feign注解即可,即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用
服务提供者有哪些接口,我们直接通过Feign直接调用,接口对接口。
Feign VS OpenFeign
Feign | OpenFeign |
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务 | OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,@RequesMapping 等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 |
pom依赖:
<!--feign-->
<dependency>
<groupId›org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<!--openfeign-->
<dependency>
<groupId›org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
接口使用
Feign是用在服务端的。
新建模块:cloud-consumer-feign-order
openfeign已经包含了ribbon。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud</artifactId>
<groupId>com.test.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-order</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author zhangzengxiu
* @date 2022/12/12
*/
@SpringBootApplication
@EnableFeignClients
public class FeignOrderApp {
public static void main(String[] args) {
SpringApplication.run(FeignOrderApp.class, args);
}
}
注意:
@EnableFeignClients 需要在主启动类申请后,才可使用@FeignClient
配置文件:application.yml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka/
业务类:
接口+注解
/**
* @author zhangzengxiu
* @date 2022/12/12
*/
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
@PostMapping("/payment/addPayment")
public CommonResult addPayment(@RequestBody Payment payment);
@GetMapping("/payment/getPayment/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id);
}
注意点:
如果路径是分开的,比如类注解上有一部分,方法上有一部分,一定要把两者拼接起来。
feign的接口一定要补全完整的路径!!!
微服务名来自:
@EnableFeignClients是用来激活@FeignClient的
流程:
- PaymentFeignController:暴露给客户端的controller
- PaymentFeignService:客户端的Feign接口
- PaymentController:服务提供者的controller,feign客户端调用到这个controller
代码:
PaymentFeignController
package com.test.springcloud.controller;
import com.test.springcloud.entities.Payment;
import com.test.springcloud.result.CommonResult;
import com.test.springcloud.service.PaymentFeignService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author zhangzengxiu
* @date 2022/12/12
*/
@RestController
@RequestMapping
public class PaymentFeignController {
@Autowired
private PaymentFeignService paymentFeignService;
/**
* 添加payment
*
* @param payment
* @return
*/
@PostMapping("consumer/payment/addPayment")
public CommonResult addPayment(@RequestBody Payment payment) {
return paymentFeignService.addPayment(payment);
}
/**
* 根据id查询payment
*
* @param id
* @return
*/
@GetMapping("consumer/payment/getPayment/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
}
PaymentFeignService:
package com.test.springcloud.service;
import com.test.springcloud.entities.Payment;
import com.test.springcloud.result.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @author zhangzengxiu
* @date 2022/12/12
*/
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentFeignService {
@PostMapping("/payment/addPayment")
public CommonResult addPayment(@RequestBody Payment payment);
@GetMapping("/payment/getPayment/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id);
}
PaymentController:
package com.test.springcloud.controller;
import com.test.springcloud.entities.Payment;
import com.test.springcloud.result.CommonResult;
import com.test.springcloud.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* @author zhangzengxiu
* @date 2022/11/20
*/
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
/**
* 添加payment
*
* @param payment
* @return
*/
@PostMapping("/addPayment")
public CommonResult addPayment(@RequestBody Payment payment) {
int res = paymentService.addPayment(payment);
if (res > 0) {
return new CommonResult(200, "插入成功,serverPort=" + serverPort);
}
return new CommonResult(444, "插入失败,serverPort=" + serverPort);
}
/**
* 根据id查询payment
*
* @param id
* @return
*/
@GetMapping("/getPayment/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentService.getPaymentById(id);
if (payment != null) {
return new CommonResult(200, "查询成功,serverPort=" + serverPort, payment);
}
return new CommonResult(444, "查询失败,serverPort=" + serverPort);
}
}
启动测试:
测试结果:
8001 8002交替出出现,说明负载均衡为轮寻。
因为Feign已经集成了Ribbon,自带负载均衡。
测试成功!!!
总结:
超时控制
Feign本质上就是客户端服务接口。根据FeignClient所显示的微服务名称,去调用服务侧对外暴露的方法。
消费者去调用服务提供者。两个不同的微服务,一定会出现超时的情况。
OpenFeign默认等待1秒钟,超时报错:
代码:
PaymentFeignController:
/**
* 测试超时接口
*
* @return
*/
@GetMapping("consumer/payment/timeout")
public CommonResult getPaymentById() throws InterruptedException {
return paymentFeignService.timeout();
}
PaymentFeignService:Feign接口
@GetMapping("/payment/timeout")
public CommonResult timeout() throws InterruptedException;
PaymentController:服务提供者接口
/**
* 测试超时接口
*
* @return
*/
@GetMapping("/timeout")
public CommonResult timeout() throws InterruptedException {
Thread.sleep(3000); //模拟3秒处理
return new CommonResult(200, serverPort);
}
默认Feign客户端只等待1秒钟,但是服务端处理需要超过1秒钟,导致Feign客户端不再继续等待,直接返回报错,为避免这样的情况,有时候我们需要设置Feign客户端的超时控制。
由于Feign天生整合了Ribbon,所以,Feign的超时也由Ribbon进行超时控制:
配置超时时间:
application.yml:
#设置Feign客户端超时时间(OpenFeign默认支持Ribbon)
ribbon:
ReadTimeout: 5000 #指建立连接所用时间,适用于网络状况正常情况下,两端连接所用的时间
ConnecTimeOut: 5000 #指建立连接后从服务器读取到可用资源所用的时间
重启应用,测试:
没有再报超时的错。
日志打印
Feign提供了日志打印功能,我们可通过配置来调整日志级别,从而了解Feign中的Http请求的细节。
说白了就是对Feign接口的调用情况进行监控和输出。
日志级别:
级别 | 描述 |
NONE | 默认,不显示任何日志 |
BASIC | 仅记录请求方法,URL、响应状态码及执行时间 |
HEADERS | 除了BASIC中定义的信息之外,还有请求和响应的头信息 |
FULL | 除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据 |
配置bean:
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zhangzengxiu
* @date 2022/12/12
*/
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
application.yml配置文件:
logging:
level: #feign日志以什么级别监控哪个接口
com.test.springcloud.service.PaymentFeignService: debug
重启应用,发送请求:
后台日志详情:debug+FULL 最全日志
Hystrix
背景
分布式面临的问题,一条链路去调用时,需要去调用多个服务,如果其中一个服务出现故障,如网络卡顿,调用超时等,就可能会出现级联失败,雪崩效应。
服务雪崩
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”
如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的"雪崩效应”
对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源兴张,号致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。
所以,通常当你发现一个模块下的某个实例失败后,这时候这个模块依然还会接收流量,然后这个有问题的模块还调用了其他的模块,这样就会发生级联故障,或者叫雪崩。
概述
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。
Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
"断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保丝),向调用方返回一个符合预期的、可处理的备选响应 (FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。用来兜底
Hystrix已停更。
重要概念
服务降级
用来兜底。如:服务器忙,不让客户等待,立刻返回一个友好提示fallback
服务降级情况 :
- 程序运行异常
- 超时
- 服务熔断触发服务降级
- 线程池/信号量打满
服务熔断
类似于保险丝的概念。直接拒绝访问,然后调用服务降级的方法,返回友好提示。
降级—>熔断—>恢复
服务限流
秒杀高并发等操作
案例实现
构建新模块cloud-provider-hystrix-payment
pom.xml
<dependencies>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</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>
新引入的pom坐标:
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
application.yml 配置文件
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 #单机版
主启动:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author zhangzengxiu
* @date 2022/12/13
*/
@SpringBootApplication
@EnableEurekaClient
public class HystrixPaymentApp {
public static void main(String[] args) {
SpringApplication.run(HystrixPaymentApp.class, args);
}
}
业务类:
PaymentController
import com.test.springcloud.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangzengxiu
* @date 2022/12/13
*/
@RestController
@RequestMapping
public class PaymentController {
@Autowired
private PaymentService paymentService;
/**
* 正常返回
*
* @param id
* @return
*/
@GetMapping("/payment/ok/{id}")
public String paymentok(@PathVariable("id") Integer id) {
return paymentService.paymentok(id);
}
/**
* 测试异常情况 超时等
*
* @param id
* @return
*/
@GetMapping("/payment/err/{id}/{time}")
public String paymentErr(@PathVariable("id") Integer id, @PathVariable("time") Long time) throws InterruptedException {
return paymentService.paymentErr(id, time);
}
}
PaymentService
package com.test.springcloud.service;
/**
* @author zhangzengxiu
* @date 2022/12/13
*/
public interface PaymentService {
/**
* 正常返回
*
* @param id
* @return
*/
public String paymentok(Integer id);
/**
* 测试异常情况 超时等
*
* @param id
* @param time 睡眠时间
* @return
*/
public String paymentErr(Integer id, Long time) throws InterruptedException;
}
实现类
PaymentServiceImpl
import com.test.springcloud.service.PaymentService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* @author zhangzengxiu
* @date 2022/12/13
*/
@Service
public class PaymentServiceImpl implements PaymentService {
@Value("${server.port}")
private String serverPort;
@Override
public String paymentok(Integer id) {
String name = Thread.currentThread().getName();
return "线程名:" + name + "paymentok,id=" + id;
}
@Override
public String paymentErr(Integer id, Long time) throws InterruptedException {
Thread.sleep(time);
String name = Thread.currentThread().getName();
return "线程名:" + name + "paymentErr,id=" + id + "time=" + time;
}
}
jmeter
使用jmeter来模拟高并发场景,压力测试。
jmeter压测配置:
线程组:
HTTP请求:
压测请求发送后,原本秒回的请求也被拖慢:出现等待、卡顿、延时的现象
http://localhost:8001/payment/ok/1 本来就很慢的请求:http://localhost:8001/payment/err/1/3000
原因:
因为SpeingBoot默认集成了一个Tomcat,Tomcat的默认工作线程数被打满了,没有多余的线程来分解压力。
总结:
上面时8001自己测试,假如此时外部的消费者80也来访问,那消费者只能等着,会最终导致80不满意,服务端8001直接被拖死。
新建模块 cloud-consumer-feign-hystrix-order
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud</artifactId>
<groupId>com.test.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-feign-hystrix-order</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
application.yml
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author zhangzengxiu
* @date 2022/12/17
*/
@SpringBootApplication
@EnableFeignClients
public class HystrixOrderApp {
public static void main(String[] args) {
SpringApplication.run(HystrixOrderApp.class, args);
}
}
PaymentHystrixService:
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @author zhangzengxiu
* @date 2022/12/17
*/
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
/**
* 正常返回
*
* @param id
* @return
*/
@GetMapping("/payment/ok/{id}")
public String paymentok(@PathVariable("id") Integer id);
/**
* 测试异常情况 超时等
*
* @param id
* @return
*/
@GetMapping("/payment/err/{id}/{time}")
public String paymentErr(@PathVariable("id") Integer id, @PathVariable("time") Long time);
}
OrderHystrixController:
import com.test.springcloud.service.PaymentHystrixService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangzengxiu
* @date 2022/12/17
*/
@RestController
@RequestMapping
public class OrderHystrixController {
@Autowired
private PaymentHystrixService paymentHystrixService;
/**
* 正常返回
*
* @param id
* @return
*/
@GetMapping("/consumer/payment/ok/{id}")
public String paymentok(@PathVariable("id") Integer id) {
return paymentHystrixService.paymentok(id);
}
/**
* 测试异常情况 超时等
*
* @param id
* @return
*/
@GetMapping("/consumer/payment/err/{id}/{time}")
public String paymentErr(@PathVariable("id") Integer id, @PathVariable("time") Long time) throws InterruptedException {
return paymentHystrixService.paymentErr(id, time);
}
}
项目结构:
启动服务,测试:
http://localhost/consumer/payment/ok/1 启动jmeter线程组进行压测。
现象:上面接口返回速度变慢,出现卡顿,客户端响应缓慢
解决:
说明:
1、8001服务超时,调用者80不能一直等待,超时需要降级
2、调用的服务器宕机了,超时了,不能一直等待,必须有服务降级
3、客户端只等待3秒,而服务提供者响应时间超过了3秒钟,则,客户端自己降级
服务降级
降级配置
8001 服务提供者配置
设置自身调用超时时间峰值,峰值内可正常运行,超过了则需要有兜底方法处理,做服务降级fallback
实现步骤
业务类启用:
/**
* 一旦调用方法失败并报错错误信息后,会自动调用@HystrixCommand中标注的fallbackMethod方法
* @ HystrixProperty 表示哪个线程,多长时间内走正常的业务逻辑
*
* @param id
* @param time 睡眠时间
* @return
* @throws InterruptedException
*
*/
@HystrixCommand(fallbackMethod = "paymentErrHandler", commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")})
@Override
public String paymentErr(Integer id, Long time) throws InterruptedException {
Thread.sleep(time);
String name = Thread.currentThread().getName();
return "线程名:" + name + "paymentErr,id=" + id + "time=" + time;
}
/**
* 用来处理paymentErr这个方法出问题时的兜底
*
* @param id
* @param time
* @return
* @throws InterruptedException
*/
public String paymentErrHandler(Integer id, Long time) throws InterruptedException {
String name = Thread.currentThread().getName();
return "线程名:" + name + "paymentErrHandler 服务忙,稍后重试";
}
主启动类激活:
/**
* @author zhangzengxiu
* @date 2022/12/13
*/
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker //用来激活:@HystrixCommand
public class HystrixPaymentApp {
public static void main(String[] args) {
SpringApplication.run(HystrixPaymentApp.class, args);
}
}
激活注解:@EnableCircuitBreaker
运行结果:
注意:
当我们的运行时间小于我们配置的时间时,走正常的业务逻辑
当我们运行的时间超过我们配置的时间,会走兜底
hystrix做了专门的线程池来做隔离
当前服务不可用,做服务降级,兜底方案都是:paymentErrHandler方法
当我们在开发过程中,不能让程序一直等待,超过一定时间,要返回兜底信息,否则线程池和CPU都会被打满。
80服务调用者配置
80客户端设置服务降级保护
Hystrix服务降级,既可用于服务端,也可以用于客户端,一般是用在客户端
实现步骤
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
feign:
hystrix:
enabled: true
新增配置feign.hystrix.enabled配置
主启动类
/**
* @author zhangzengxiu
* @date 2022/12/17
*/
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class HystrixOrderApp {
public static void main(String[] args) {
SpringApplication.run(HystrixOrderApp.class, args);
}
}
新添加注解:@EnableHystrix
业务类修改:
/**
* 测试异常情况 超时等
*
* @param id
* @return
*/
@GetMapping("/consumer/payment/err/{id}/{time}")
@HystrixCommand(fallbackMethod = "paymentErrHandler", commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")})
public String paymentErr(@PathVariable("id") Integer id, @PathVariable("time") Long time) throws InterruptedException {
return paymentHystrixService.paymentErr(id, time);
}
/**
* 用来处理paymentErr这个方法出问题时的兜底
*
* @param id
* @param time
* @return
* @throws InterruptedException
*/
public String paymentErrHandler(Integer id, Long time) throws InterruptedException {
String name = Thread.currentThread().getName();
return "线程名:" + name + "客户端:paymentErrHandler 服务忙,稍后重试";
}
现在是对客户端进行修改从而服务保护,降级,不再一直等待
启动测试:
客户端降级:
客户端正常执行:
全局服务
当前服务降级所存在的问题:
1、业务逻辑代码与服务降级代码,混在一起
2、每个方法都写降级方法,造成代码膨胀
解决方案:
1、配置全局服务降级
2、需要单个配置的服务降级,单独配置服务降级方法
解决:每个方法都需要配置个服务降级方法:
配置全局的服务降级:
配置方式:
业务类:
/**
* @author zhangzengxiu
* @date 2022/12/17
*/
@RestController
@RequestMapping
@DefaultProperties(defaultFallback = "globalFallback")
public class OrderHystrixController {
/**
* 测试异常情况 超时等
*
* @param id
* @return
*/
@GetMapping("/consumer/payment/err/{id}/{time}")
//@HystrixCommand(fallbackMethod = "paymentErrHandler", commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")})
@HystrixCommand
public String paymentErr(@PathVariable("id") Integer id, @PathVariable("time") Long time) throws InterruptedException {
return paymentHystrixService.paymentErr(id, time);
}
/**
* 用来处理paymentErr这个方法出问题时的兜底
*
* @param id
* @param time
* @return
* @throws InterruptedException
*/
public String paymentErrHandler(Integer id, Long time) throws InterruptedException {
String name = Thread.currentThread().getName();
return "线程名:" + name + "客户端:paymentErrHandler 服务忙,稍后重试";
}
/**
* 全局fallback兜底
*
* @return
*/
public String globalFallback() {
return "全局fallback兜底";
}
}
配置方式:
类上添加注解,配置全局降级
@DefaultProperties(defaultFallback = "globalFallback")
public class OrderHystrixController {
....
}
对应的降级方法:
/**
* 全局fallback兜底
*
* @return
*/
public String globalFallback() {
return "全局fallback兜底";
}
对应的需要降级的方法,需要添加注解 @HystrixCommand
@GetMapping("/consumer/payment/err/{id}/{time}")
@HystrixCommand
public String paymentErr(@PathVariable("id") Integer id, @PathVariable("time") Long time) throws InterruptedException {
return paymentHystrixService.paymentErr(id, time);
}
如果不配置@HystrixCommand注解,则说明,没有降级,会走报错页面。
若配置了自己专属的降级方法,则会走自己单独配置的降级方法。
配置完成,启动测试:
至此, 已解决代码膨胀问题。
业务逻辑与降级代码混乱问题:
新建一个类实现Feign的对应接口:
接口:
/**
* @author zhangzengxiu
* @date 2022/12/17
*/
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentHystrixFallbackService.class)
public interface PaymentHystrixService {
/**
* 正常返回
*
* @param id
* @return
*/
@GetMapping("/payment/ok/{id}")
public String paymentok(@PathVariable("id") Integer id);
/**
* 测试异常情况 超时等
*
* @param id
* @return
*/
@GetMapping("/payment/err/{id}/{time}")
public String paymentErr(@PathVariable("id") Integer id, @PathVariable("time") Long time);
}
@FeignClient注解必须配置fallback属性,指定对应的降级类。
实现Feign的类:内重写所有方法,实现降级逻辑
/**
* @author zhangzengxiu
* @date 2022/12/18
*/
@Component
public class PaymentHystrixFallbackService implements PaymentHystrixService {
@Override
public String paymentok(Integer id) {
return "客户端:paymentok fallback 服务降级";
}
@Override
public String paymentErr(Integer id, Long time) {
return "客户端:paymentErr fallback 服务降级";
}
}
配置文件配置:必须有以下配置
feign:
hystrix:
enabled: true
启动测试:
调用:http://localhost/consumer/payment/ok/1 正常调用:
异常调用:
手动停止8001 服务提供者,模拟服务器宕机
服务熔断
概述
熔断机制概述
熔断机制是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。
当检测到该节点微服务调用响应正常后,恢复调用链路。
在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,
当失败的调用到一定國值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
配置步骤:
配置方式:
配置的属性,查看类:HystrixCommandProperties
实操
cloud-provider-hystrix-payment模块:
PaymentServiceImpl:
//------------------服务熔断--------------------------------
/**
* 下述配置:
* 10秒内,10次请求,60%的失败率就会触发断路器
*
* @param id
* @return
*/
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", 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 uid = UUID.randomUUID().toString();
return Thread.currentThread().getName() + "调用成功,流水号:" + uid;
}
public String paymentCircuitBreakerFallback(@PathVariable("id") Integer id) {
return "服务降级 id不能为负";
}
PaymentController:
/**
* 测试服务熔断
*
* @param id
* @return
*/
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
return paymentService.paymentCircuitBreaker(id);
}
启动测试:
http://localhost:8001/payment/circuit/1
错误调用:http://localhost:8001/payment/circuit/-1
模拟触发10秒内,10次请求,60%的错误率,从而触发服务熔断:
服务熔断后,发送正常请求:也被熔断,无法请求。
等待服务链路恢复正常:
熔断总结
官网熔断器流程图:
断路器开启或关闭的条件:
断路器被打开之后:
Hystrix官网流程图:
官网:https://github.com/Netflix/Hystrix/wiki/How-it-Works 流程:
流程图:
https://raw.githubusercontent.com/wiki/Netflix/Hystrix/images/hystrix-command-flow-chart.png
服务监控HystrixDashboard
除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控 (Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控。Spring Cloud也提供了Hystrix Dashboard的整合,对监控内容转化成可视化界面。
仪表盘
新建模块:cloud-consumer-hystrix-dashboard
pom依赖:
<artifactId>cloud-consumer-hystrix-dashboard</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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>
application.yml
server:
port: 9001
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
/**
* @author zhangzengxiu
* @date 2022/12/18
*/
@EnableHystrixDashboard
@SpringBootApplication
public class HystrixDashboardApp {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApp.class, args);
}
}
项目结构:
所有微服务需要图形化展现,必须依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动仪表盘,测试:
http://localhost:9001/hystrix 若出现如下页面,则说明,搭建成功:
配置被监控服务:
必须有的pom依赖:
<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>
SpringCloud升级后留下的坑:
被监控的服务,需要添加下面的这个Bean
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
/**
* @author zhangzengxiu
* @date 2022/12/13
*/
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class HystrixPaymentApp {
public static void main(String[] args) {
SpringApplication.run(HystrixPaymentApp.class, args);
}
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
* 只需要在自己的项目中配置下述的servlet即可
*
* @return
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
启动测试:
访问正确的:
http://localhost:8001/payment/circuit/1
访问异常的:http://localhost:8001/payment/circuit/-1
过段时间,再放个正确的请求进来,显示:
熔断器再次关闭。
曲线解释:
- 7色
- 1圈
- 1线
7色:
对于7种状态,7种颜色。
1圈:
实心圆:共有两种含义。它通过颜色的变化代表了实例的健康程度,它的颜色从绿色<橙色<红色递减。
该实心圆除了颜色的变化之外,它的大小也会根据实例的请求流量发生变化,流量越大该实心圆越大,所以通过该实心圆的展示,就可以在大量的实例中快速发现故障实例和高压力实例。
1线:
曲线:用来记录2分钟内流量的相对变化,可以通过它来观察流量的上升和下降趋势。
当前弊端:需要我们单独搭建一个服务来监控其他服务。
服务网关
Spring Cloud Gateway
概述
官网
Zuul官网:https://github.com/Netflix/zuul/wiki
Gateway官网:https://spring.io/projects/spring-cloud-gateway
Spring Cloud全家桶很重要的组件就是网关,在1.x版本中都是采用Zuul网关;
后续,Spring Cloud Gateway替代了Zuul
概述:
SpringCloud Gateway 是 Spring cloud 的一个全新项目,基于 Spring 5.0+ Spring Boot 2.0 和Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API 路由管理方式。
SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 zuul,在Spring Cloud 2.0以上版本中,没有对新版本的ZuuI 2.0以上版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,Spring Cloud Gateway是基于WebFux框架实现的,而webFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目标提供统一的路由方式且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
微服务架构图
特性
Spring Cloud Gateway 具有如下特性:
基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0 进行构建;
动态路由:能够匹配任何请求属性;
可以对路由指定 Predicate(断言)和 Filter(过滤器);
集成Hystrix的断路器功能;
集成 Spring Cloud 服务发现功能;
易于编写的 Predicate(断言)和Filter(过滤器);
请求限流功能:
支持路径重写。
Gateway VS Zuul
在SpringCloud Finchley 正式版之前,Spring Cloud 推存的网关是 Netflix 提供的Zuul:
1、zuul 1.x,是一个基于阻塞1/0的API Gateway
2、Zuul 1.x 基于Servlet 2.5使用阻塞架构它不支持任何长连接(如 WebSocket) zuul 的设计模式和Nginx较像,每次I/0操作都是从工作线程中选择一个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx 用C++ 实现,Zuul 用Java 实现,而JVM 本身会有第一次加载较慢的情况,使得zuul 的性能相对较差。
3、Zuul 2.x理念更先进,想基于Netty非阻塞和支持长连接。但SpringCloud目前还没有整合。
zuul 2.x的性能较 zuul 1.x 有较大提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway 的 RPS(每秒请求数)是zuul 的 1.6倍。
4. Spring Cloud Gateway 建立在Spring Framework 5、Project Reactor 和 Spring Boot 2之上,使用非阻塞 APl。
5、Spring Cloud Gateway 还支持 WebSocket,并日与Spring紧密集成拥有更好的开发体验
Zuul模型
上述模式的缺点:
servlet是一个简单的网络IO模型,当请求进入servlet container时, servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(比如用jemeter压),线程数量就会上涨,而线程资源代价是昂贵的 (上线文切换,内存消耗大)严重影响请求的处理时间。
在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。
所以Zuul 1.x是基于servlet之上的一个阻塞式处理模型,即spring实现了处理所有request清求的一个servlet(Dispatcher Servlet)并由该servlet塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端
Gateway模型
传统的Web框架,比如说:struts2, springmvc等都是基于Servlet AP1与Servlet容器基础之上运行的。
但是在Servlet3.1之后有了异步非阻塞的支持。而webFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关APl实现的。相对于传统的web框架来说,它可以运行在诸如Netty, Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程 (Spring5必须让你使用Eljava8)
Spring WebFlux 是Spring 5.0引入的新的响应式框架,区别于Spring MVC, 它不需要依赖Servlet APl,它是完全异步非阻塞的,并且基于 Reactor 来实现响应式流规范。
核心概念
路由(Route)
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
断言(Predicate)
开发人员可以匹配HTTP请求中的所有内容(例如请求头或者请求参数),如果请求与断言相互匹配规则进行路由
过滤(Filter)
指的是Spring框架中Gateway Filter的实例,使用了过滤器,可以在请求被路由前或者之后对请求进行修改
web请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制。
predicate就是我们匹配的条件,而filter就可以理解为一个无所不能的拦截器。只要有了这两个元素,再加上URI,就可以实现一个具体的路由。
每个请求进来之后,需要经过一个个的路由,每个路由需要先判断Predicate断言,再经过过滤链。
工作流程
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Hardler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。
Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前( pre〞)或之后("post”)执行业务逻辑。
Filter在"pre"类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,
在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有看非常重要的作用。
搭建网关微服务
新建模块:cloud-gateway-gateway
pom依赖
<artifactId>cloud-gateway-gateway</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.test.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
server:
port: 9527
spring:
application:
name: cloud-gateway-gateway
cloud:
gateway:
routes:
- id: payment_routh #路由的id,没有固定的规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配提供服务的路由地址
predicates:
- Path=/payment/getPayment/** #断言,路径相匹配的进行路由
- id: payment_routh2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/**
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
如果不希望暴露8001端口,而是希望通过网关9527来访问,则需要在上述配置中配置8001的地址
routes可以配置多个。此时相当于9527网关在8001外套了一层。
上述配置,相当于对payment的这两个方法配置了路由
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author zhangzengxiu
* @date 2022/12/19
*/
@SpringBootApplication
@EnableEurekaClient
public class GatewayApp {
public static void main(String[] args) {
SpringApplication.run(GatewayApp.class, args);
}
}
踩坑:
启动报错:
spring-cloud-gateway网关,不要添加web的依赖坐标
<!--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>
踩坑:Path配置,P一定要大写
gateway启动报错:
Unable to find RoutePredicateFactory with name path
修改为大写P之后,可以正常启动。
eureka当前状态:
测试:
直接访问8001
http://localhost:8001/payment/getPayment/1
通过网关访问
http://localhost:9527/payment/getPayment/1
也可以访问到。
这样就可以不用暴露真实的8001端口,而是通过网关进行访问。
Gateway网关配置
配置有两种方式:
- 在yaml配置文件中配置(如上)
- 代码中注入RouteLocator的Bean
官网示例:
配置:
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author zhangzengxiu
* @date 2022/12/20
*/
@Configuration
public class RoutesConfig {
/**
* 配置一个id为route-name的路由规则
* 当访问地址http://localhost:9527/guonei的时候,会转发到https://news.baidu.com/guonei
*
* @param routeLocatorBuilder
* @return
*/
@Bean
public RouteLocator getRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("path_route", r -> r.path("/guonei").uri("https://news.baidu.com/guonei")).build();
return routes.build();
}
}
启动测试访问:
当前配置存在的问题:
uri: http://localhost:8001 #匹配提供服务的路由地址。该配置写死,而且没有做负载均衡
动态路由
默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上的微服务名为路径创建动态路由进行转发,从而实现路由的功能。
启动:eureka7001+payment8001+payment8002
修改yaml配置文件
server:
port: 9527
spring:
application:
name: cloud-gateway-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的id,没有固定的规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配提供服务的路由地址
uri: lb://cloud-payment-service #匹配提供服务的路由地址
predicates:
- Path=/payment/getPayment/** #断言,路径相匹配的进行路由
- id: payment_routh2
# uri: http://localhost:8001
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/**
eureka:
instance:
hostname: cloud-gateway-service
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri
现在我们开启动态路由后,会根据注册到注册中心上的微服务找到对应对外提供的服务。
从而实现负载均衡
启动gateway网关微服务。
访问:http://localhost:9527/payment/lb
亦可实现,动态路由。
Predicate(断言)
predicates中的Path只是其中一种
官网地址:
路由地址Predicate全部是true才能访问
After
在配置的时间之后才能访问
获取时间的方式:
import java.time.ZonedDateTime;
/**
* @author zhangzengxiu
* @date 2022/12/20
*/
public class Test {
public static void main(String[] args) {
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now);
}
}
配置示例:
spring:
application:
name: cloud-gateway-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh2
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/**
- After=2022-12-20T18:12:25.104+08:00[Asia/Shanghai]
说明:必须是在After这个时间之后,才能访问。
重启测试:
访问:http://localhost:9527/payment/lb 重启后,服务可正常访问。
修改时间到一小时之后,重启测试。
改为:- After=2022-12-20T19:12:25.104+08:00[Asia/Shanghai]
重启,测试访问
访问不到,显示404。
After、Before、Between同理
predicates:
- Path=/payment/lb/**
- Before=2022-12-20T19:12:25.104+08:00[Asia/Shanghai]
- After=2022-12-20T18:12:25.104+08:00[Asia/Shanghai]
- Between=2022-12-20T18:12:25.104+08:00[Asia/Shanghai],2022-12-20T19:12:25.104+08:00[Asia/Shanghai]
Cookie
Cookie Route Predicate需要两个参数,一个是Cookie name,一个是正则表达式。
路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
官网示例:
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p
网关微服务配置:
spring:
application:
name: cloud-gateway-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh2
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/**
- Cookie=username,xiu
不带Cookie访问
curl http://localhost:9527/payment/lb
访问报错:
带Cookie访问
curl http://localhost:9527/payment/lb --cookie "username=xiu"
可以正常访问
Header
官网示例:
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
两个参数,一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
与Cookie较为相似
配置网关微服务:
spring:
application:
name: cloud-gateway-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh2
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/**
- Header=X-Request-Id, \d+ #请求头要有X-Request-Id属性并且值为整数的正则表达式
访问:
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"
可以正常访。
访问
curl http://localhost:9527/payment/lb -H "X-Request-Id:-123"
-123 不满足匹配规则。报错:404
Method
官网示例:
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://example.org
predicates:
- Method=GET,POST
Predicate就是实现了一组匹配规则,让请求过来找到对应的Route进行处理。
其他示例参考:
https://docs.spring.io/spring-cloud-gateway/docs/4.0.0-SNAPSHOT/reference/html/#gateway-request-predicates-factories
Filter(过滤)
是指Spring框架中的GatewayFilter的实例,使用过滤器,可以在请求被路由前或者后对请求进行修改。
过滤器可以是过滤器链,当所有的过滤器满足条件之后,才能允许访问。
官网地址:
https://docs.spring.io/spring-cloud-gateway/docs/4.0.0-SNAPSHOT/reference/html/#gatewayfilter-factories 路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应,路由顾虑器智能指定路由进行使用。
Spring Cloud Gateway内置了多种路由过滤器,他们都由GatewayFilter的工厂类来产生。
- 生命周期
- pre(之前)
- post(之后)
- 种类
- GatewayFilter(单一)
- GlobalFilter(全局)
配置方式类似Predicate
自定义全局过滤器
- 常常用于全局日志记录
- 统一网关鉴权
网关挡在所有微服务最前,只要通过网关放行之后,才会去放行,才能调用微服务。
自定义全局过滤使用较多
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author zhangzengxiu
* @date 2022/12/20
*/
@Component
@Slf4j
public class LogGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("****LogGlobalFilter****");
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (StringUtils.isBlank(uname)) {
log.error("uname is null");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
//放行到下一个过滤器链
return chain.filter(exchange);
}
/**
* 过滤器顺序
* 数字越小,优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
启动服务:
正确访问:http://localhost:9527/payment/lb?uname=xxx
错误访问:http://localhost:9527/payment/lb
IDE控制台:
分布式配置
Spring Cloud Config
分布式系统面临的问题:
系统中存在大量且重复的配置文件,牵一发而动全身,只要一个位置配置错了,就会很麻烦。
微服务意味着将单体应用中的业务拆分成一个个子服务,每个服务的颗粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。
方便版本切换,一处配置,处处生效。 Spring Cloud提供了ConfigServer来解决这个问题,每个微服务有自己的配置文件,上百个微服务,配置相当繁琐。
概述
Sping Cloud Config为微服务结构中的服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
Sping Cloud Config氛围服务端和客户端两部分。
服务端也称为分布式配置中心,它是一个独立的微服务应用。用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问端口。
客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置服务器默认采用git来存储配置信息,这样有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。
功能:
- 集中管理配置文件
- 不同环境不同配置,动态化的配置更新,分环境部署如dev/test/prod/beta/release
- 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息
- 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
- 将配置信息以REST接口的形式暴露
Config服务端配置与测试(Config Server)
在gitee上创建一个仓库springcloud-config
新建模块cloud-config-center
pom
<artifactId>cloud-config-center</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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>
配置文件:application.yml
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进eureka微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/zhangzengxiu/spring-cloud-config.git #git地址
search-paths:
- springCloudConfig #搜索目录
label: master #读取分支
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
/**
* @author zhangzengxiu
* @date 2022/12/20
*/
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterApp {
public static void main(String[] args) {
SpringApplication.run(ConfigCenterApp.class, args);
}
}
添加本地host
127.0.0.1 config-3344.com
启动服务,访问测试。
访问:http://config-3344.com:3344/master/config-dev.yml
gitee上的配置文件中的信息
配置读取规则
说明:
label:分支
name:服务名
profiles:环境
Config客户端配置
新建模块cloud-config-client
client与server的pom区别
<!--server-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<!--client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
pom
<artifactId>cloud-config-client</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<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: cloud-config-client
cloud:
config: #Config客户端配置
label: master #分支名称
name: config #配置文件名称
profile: dev #文件环境后缀 三个联合起来便是:http://config-3344.com:3344/master/config-dev.yml
uri: http://config-3344.com:3344 #配置中心地址
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
说明:
applicaiton.yml是用户级的资源配置项
bootstrap.yml是系统级的,优先级更加高
Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context 的父上下文。初始化的时候,BootstrapContext 负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment。
Bootstrap属性有高优先级,默认情况下,它们不会被本地配置覆盖。 Bootstrap context和 Application Context 有着不同的约定,所以新增了一个bootstrap.yml文件,保证Bootstrap Context和Application Context 配置的分离。
要将Client模块 下的application.yml文件改为bootstrap.yml,这是很关键的,
因为bootstrap.yml是比application.yml先加载的。bootstrap.yml优先级高于application.yml
启动类
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author zhangzengxiu
* @date 2022/12/21
*/
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientApp {
public static void main(String[] args) {
SpringApplication.run(ConfigClientApp.class, args);
}
}
业务类:
配置信息会以REST风格暴露。
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangzengxiu
* @date 2022/12/21
*/
@RestController
public class ConfigController {
@Value("${xxx}")
private String config;
@GetMapping("/config")
public String getConfig() {
return config;
}
}
启动测试
eureka:
访问:
http://config-3344.com:3344/master/config-dev.ymlhttp://localhost:3355/config
成功实现了客户端3355访问springcloud Config3344通过gitee获取配置信息。
分布式配置动态刷新问题
- Linux运维修改gitee上的配置文件内容
- 刷新3344,发现Configserver配置中心立刻响应
- 刷新3355,发现Config客户端并没有响应
- 3355没有变化,除非重启或者重新加载
现在需要解决的就是每次动态刷新都需要重启客户端
修改cloud-config-client模块
必须引入的pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
如果发生变化,可以被别人监控到
修改yaml,暴露监控端口
#暴露监控端口
management:
endpoints:
web:
exposure:
include: "*"
业务类添加注解:@RefreshScope
启动测试:
访问:http://config-3344.com:3344/master/config-dev.yml
访问:
手动修改gitee上的值。将值修改为222
再次访问:http://config-3344.com:3344/master/config-dev.yml
此时server已经拉取到了最新值。
访问:http://localhost:3355/config
仍然是旧值!!!
需要运维去发送一个post请求,刷新客户端
curl -X POST "http://localhost:3355/actuator/refresh"
再次访问:http://localhost:3355/config
此时即可刷新到最新值,此时并未重启client微服务。
当前存在的问题:若修改一次,有上百个微服务,需要发送上百次的post请求,可以通过自动化运维脚本,来实现,但是也很繁琐。如何精准通知?
消息总线
Spring Cloud Bus
概述
- 分布式刷新自动配置功能,Spring Cloud Bus配合Sping Cloud Config使用可以实现配置动态刷新。
- Bus支持两种消息代理:RabbitMQ和Kafka
SpringCloud Bus是用来将分布式系统的节点与轻量级消息系统连接起来的框架。
它整合了Java的事件处理机制和消息中间件的功能
Spring Cloud Bus目前支持RabbitMQ和Kafka。
本次采用RabbitMQ
作用
Spring Cloud Bus能管理和传播分布式系统的消息,就像一个分布式执行器,可用于广播状态更改、事件推送,有可以当作微服务间的通信通道。
总线
在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。
基本原理
ConfigClient实例都监听MQ中同一个topic(默认是springCloudBus)。当一个服务刷新数据的时候,它会把这个信息放入到Topic中,这样其它监听同一Topic的服务就能得到通知,然后去更新自身的配置。
RabbitMQ配置环境
Bus动态刷新全局广播通知
设计思想:
- 利用消息总线触发一个客户端/bus/refresh,而刷新所有客户端的配置
- 利用消息总线触发一个服务端ConfigServer的/bus/server端点,而刷新所有客户端的配置
- 所以,下述方式,方式二更加适合
方式一:
方式二:
方式一不合适原因:
- 打破微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责
- 破坏了微服务各节点的对等性
- 局限性。微服务在迁移时,它的网络地址会发生变化,如果想要做到自动刷新,那吗需要增加更多变化
cloud-config-center模块添加pom
<!--消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
application.yml 配置文件:
server:
port: 3344
spring:
application:
name: cloud-config-center #注册进eureka微服务名
cloud:
config:
server:
git:
uri: https://gitee.com/zhangzengxiu/spring-cloud-config.git #git地址
search-paths:
- springCloudConfig #搜索目录
label: master #读取分支
rabbitmq: #RabbitMQ相关配置
host: 172.16.124.100
port: 5672
username: guest
password: guest
#RabbitMQ相关配置,暴露bus刷新配置的端点
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
新添加的配置:
spring:
#RabbitMQ相关配置
rabbitmq:
host: 172.16.124.100
port: 5672
username: guest
password: guest
#RabbitMQ相关配置,暴露bus刷新配置的端点
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
cloud-config-center模块添加客户端消息总线支持
添加依赖
<!--消息总线RabbitMQ支持-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
配置文件 bootstrap.yml
server:
port: 3355
spring:
application:
name: cloud-config-client
cloud:
config: #Config客户端配置
label: master #分支名称
name: config #配置文件名称
profile: dev #文件环境后缀 三个联合起来便是:http://config-3344.com:3344/master/config-dev.yml
uri: http://config-3344.com:3344 #配置中心地址
rabbitmq:
host: 172.16.124.100
port: 5672
username: guest
password: guest
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
#暴露监控端口
management:
endpoints:
web:
exposure:
include: "*"
新添加的配置:
spring:
#RabbitMQ相关配置
rabbitmq:
host: 172.16.124.100
port: 5672
username: guest
password: guest
此时,运维只需要发送一个POST请求给server,即可。
curl -X POST "http://localhost:3344/actuator/refresh"
登录RabbitM的web端:
Bus动态刷新定点通知
如果我只想通知其中一个而不是全部通知,如何做?
公式:http://ip:配置中心端口号/actuator/bus-refresh/{destination}
/bus/refresh请求不再发送到具体的服务实例上,而是发给configserver并通过destination参数来指定需要更新配置的服务或实例。
以刷新3355端口上的config-client为例,只通知3355,而不通知3366
curl -X POST "http://localhost:3344/actuator/refresh/cloud-config-client:3355"
消息驱动
Spring Cloud Stream
官网
官网地址:https://spring.io/projects/spring-cloud-stream
概述
屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
官方定义Spring Cloud Stream是一个构建消息驱动微服务框架。
应用程序通过inputs或者outputs来与Spring Cloud Stream中binder对象交互。
通过我们配置来binding(绑定),而Spring Cloud Stream交互就可以方便使用消息驱动的方式。
通过使用Spring Intergration来连接消息代理中间件以实现消息事件驱动
Spring Cloud Stream为一些供应商的消息中间件提供了个性化的自动化配置实现, 引用了发布-订阅、消费组、分区的三个核心概念。
目前仅支持RabbitMQ、Kafka。
只需要找Binder就可以,屏蔽了底层的细节差异。就好似JDBC可以操作MySQL也可以操作Oracle
设计思想
标准MQ
- 生产者/消费者之间靠消息媒介传递消息内容
- 消息必须走特定通道
- 消息通道内的消息消费、收发、处理:消息通道MessageChannel的子接口SubscribableChannel,由MessageHandler消息处理器所订阅
引入Stream
比如我们用到了RabbitMQ和Kafka,由于两种消息中间件的架构上的不同,RabbitMQ有exchange,Kafka有Topic(主题)和Partitions(分区)
中间件的差异性会给我们开发项目造成困扰,我们如果用了两个消息队列中的一种,后面的业务需求,我们想往另一个消息队列进行迁移时,这无疑是灾难性的。一堆东西推倒重来,因为它跟我们的系统耦合了,这个时候Spring Cloud Stream给我们提供了一种解藕的方式。
Binder
在没有绑定器这个概念的情况下,我们的SpingBoot应用要直接与消息中间件进行信息交互时,
由于各消息中间件构建的初衷不同,它们的实现细节会有比较大的差异性
通过定义绑定器作为中间层,完美实现应用程序与消息中间件细节之间的隔离
通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑不同的消息中间件实现。
Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(RabbitMQ切换Kafka),使得微服务开发的高度解藕。服务可以更多关注自己的业务流程
Stream中的消息通信方式遵循了发布-订阅模式
- Topic主题进行广播
- 在RabbitMQ就是Exchange
- 在Kafka中就是Topic
通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
通过向应用程序暴露统一的Channel通道,似的应用程序不再考虑各种不同的消息中间件实现。
Stream中的消息通信方式遵循来发布-订阅模式。
Topic主题进行广播:
- 在RabbitMQ就是Exchange
- 在Kafka就是Topic
目前只支持RabbitMQ和Kafka两种!!!
- Binder:很方便的连接中间件,屏蔽差异
- Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
- Source和Sink:简单理解就是参照对象是SpringCloudStream自身,从Stream发布消息就是输出,接受就是输入
注解:
消息驱动生产者
新建模块:cloud-stream-rabbitmq-provider
pom.xml
<artifactId>cloud-stream-rabbitmq-provider</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<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-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>
新增pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
application.yml
server:
port: 8801
spring:
application:
name: cloud-stream-provider
cloud:
stream:
binders: #配置要绑定的rabbitmq
defaultRabbit: #表示定义的名称,用于binding整合
type: rabbit #消息组件类型
environment: #设置rabbitmq的相关环境配置
spring:
rabbitmq:
host: 172.16.124.100
port: 5672
username: guest
password: guest
bindings: #服务的整合处理
output: #通道名称 表示是生产者
destination: exchangeDemo #要使用的Exchange名称定义
content-type: application/json #消息类型,还可设置为text/plain
binder: defaultRabbit #要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #心跳间隔时间
lease-expiration-duration-in-seconds: 5 #5秒间隔
instance-id: send-8801.com
prefer-ip-address: true
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author zhangzengxiu
* @date 2023/1/3
*/
@SpringBootApplication
public class StreamProviderApp {
public static void main(String[] args) {
SpringApplication.run(StreamProviderApp.class, args);
}
}
service:
/**
* @author zhangzengxiu
* @date 2023/1/3
*/
public interface IMessageProducer {
/**
* 发送消息
*
* @return
*/
public String sendMsg();
}
/**
* @author zhangzengxiu
* @date 2023/1/3
*/
@EnableBinding(Source.class) //定义消息的推送管道而不再是Service注解
public class IMessageProducerImpl implements IMessageProducer {
/**
* 消息发送管道
*/
@Autowired
private MessageChannel output;
@Override
public String sendMsg() {
String uid = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(uid).build());
System.out.println("*******uid=" + uid);
return null;
}
}
controller
import com.test.springcloud.service.IMessageProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhangzengxiu
* @date 2023/1/3
*/
@RestController
public class SendMessageController {
@Autowired
private IMessageProducer iMessageProducer;
@GetMapping("/send")
public String sendMsg() {
return iMessageProducer.sendMsg();
}
}
注意:
启动测试:
- eureka7001
- rabbitmq
- 访问8801
消息驱动消费者
新建模块:cloud-stream-rabbitmq-consumer
pom
<artifactId>cloud-stream-rabbitmq-consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
cloud:
stream:
binders: #配置要绑定的rabbitmq
defaultRabbit: #表示定义的名称,用于binding整合
type: rabbit #消息组件类型
environment: #设置rabbitmq的相关环境配置
spring:
rabbitmq:
host: 172.16.124.100
port: 5672
username: guest
password: guest
bindings: #服务的整合处理
input: #通道名称
destination: exchangeDemo #要使用的Exchange名称定义
content-type: application/json #消息类型,还可设置为text/plain
binder: defaultRabbit #要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://localhost:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 #心跳间隔时间
lease-expiration-duration-in-seconds: 5 #5秒间隔
instance-id: receive-8802.com
prefer-ip-address: true
启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author zhangzengxiu
* @date 2023/1/3
*/
@SpringBootApplication
public class StreamConsumerApp {
public static void main(String[] args) {
SpringApplication.run(StreamConsumerApp.class, args);
}
}
业务类
/**
* @author zhangzengxiu
* @date 2023/1/3
*/
@Component
@EnableBinding(Sink.class)
public class ReceiveMsgController {
@Value("${server.port}")
private String port;
@StreamListener(Sink.INPUT)
public void receive(Message<String> msg) {
System.out.println("消费者接收到的消息=" + msg.getPayload() + "port=" + port);
}
}
区别:
多消费者
重复消费问题
- 分组和持久化group来解决
场景:
分组
通过分组来解决重复消费问题
原理:微服务应用放置于同一个group中,就能保证消息只会被其中一个应用消费一次。
不同的组是可以消费的,同一个组内会发声竞争关系 ,只有其中一个可以消费。
产生竞争关系时,只允许一个实例消费,从而避免了重复消费问题。
持久化
分布式请求链路跟踪
Spring Cloud Sleuth
存在问题:
Spring Cloud Sleuth提供了一套完整的服务跟踪的解决方案,在分布式系统中提供追踪解决方案并且支持了zipkin。
官网
https://github.com/spring-cloud/spring-cloud-sleuth
zipkin安装搭建链路监控
下载地址
https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkin-server&v=LATEST&c=exec 启动
java -jar zipkin-server-2.12.9-exec.jar
访问:http://localhost:9411/zipkin
流程
Trace:类似于树结构的Span集合,表示一条调用链路,存在唯一标识。
span:表示调用链路来源,通俗理解span就是一次请求信息。
配置
依赖
<!--sleuth+zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
application.yml配置
spring:
application:
name: cloud-payment-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 #采样率值介于0到1之间,1表示全部采集
SpringCloud暂时告一段落!!!