文章目录
1、系统架构演变
单体架构(三阶段开发使用到的架构)
优点:
- 所有的功能集成在一个项目工程中。
- 项目架构简单,前期开发成本低,周期短,小型项目的首选。
缺点:
- 全部功能集成在一个工程中,对于大型项目不易开发、扩展及维护。
- 系统性能扩展只能通过扩展集群结点,成本高、有瓶颈。
- 技术栈受限。
=》垂直架构
优点:
- 项目架构简单,前期开发成本低,周期短,小型项目的首选。
- 通过垂直拆分,原来的单体项目不至于无限扩大
- 不同的项目可采用不同的技术。
缺点:
- 全部功能集成在一个工程中,对于大型项目不易开发、扩展及维护。
- 系统性能扩展只能通过扩展集群结点,成本高、有瓶颈。
=》分布式SOA架构(Service Oriented Architecture面向服务架构)(RPC)
优点:
- 抽取公共的功能为服务,提高开发效率
- 对不同的服务进行集群化部署解决系统压力
- 基于DUBBO减少系统耦合
缺点:
- 抽取服务的粒度较大
- 服务提供方与调用方接口耦合度较高
=》微服务架构(HTTP)
优点:
- 通过服务的原子化拆分,以及微服务的独立打包、部署和升级,小团队的交付周期将缩短,运维成本也将大幅度下降
- 微服务遵循单一原则。微服务之间采用Restful等轻量协议传输
缺点:
- 微服务过多,服务治理成本高,不利于系统维护。
- 分布式系统开发的技术成本高(容错、分布式事务等)
补充: SOA与微服务的关系 :
微服务和SOA架构类似,微服务实在SOA上做的升华。微服务架构强调的一个重点是“业务需要彻底的组件化和服务化”。
功能 | SOA | 微服务 |
---|---|---|
组件大小 | 大块业务逻辑(一整个service) | 单独任务或小块业务逻辑 |
耦合 | 通常松耦合 | 总是松耦合 |
管理 | 着重中央管理 | 着重分散管理 |
目标 | 确保应用能够交互操作 | 执行新功能、快速拓展开发团队 |
2、分布式核心知识
2.1 远程调用
常见的远程调用方式有以下2种:
• RPC:Remote Produce Call远程过程调用,RPC基于Socket,工作在会话层。自定义数据格式,速度快,效率高。早期的webservice,现在热门的dubbo,都是RPC的典型代表
• Http:http其实是一种网络传输协议,基于TCP,工作在应用层,规定了数据传输的格式。现在客户端浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。现在热门的Rest风格,就可以通过http协议来实现
2.2 CAP原理
Consistency(一致性):更新操作成功后,所有节点在同一时间的数据完全一致
Availability(可用性):用户访问数据时,系统能否在正常响应时间返回预期结果
Partition tolerance(分区容忍性):当分布式系统遇到节点或网络故障的时候,依然能够对外提供满足一致性或可用性
AC:不再拆分数据系统,在一个数据库的一个事务中完成操作,也就是单体架构。
CP:订单创建后一直等待库存减少后才返回结果。
AP:订单创建后不等待库存减少直接返回处理结果(库存异步方式通知订单系统。当数据不一致时,系统采用补偿机制:重新发起请求、人工进行补录、数据校对程序来把数据补全)
3.SpringCloud概述
3.1 微服务相关概念
3.1.1 服务注册与发现
服务注册:服务实例将自身服务信息注册到注册中心。这部分服务信息包括服务所在主机IP和提供服务的Port,暴露服务自身状态以及访问协议等信息。
服务发现:服务实例请求注册中心获取所依赖服务信息。服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。
3.1.2 负载均衡
负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。
3.1.3 熔断
熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
3.1.4 链路追踪
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的多个服务链路进行日志记录,性能监控即链路追踪
3.1.5 API网关
随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:
- 客户端需要调用不同的url地址,增加难度
- 在一定的场景下,存在跨域请求的问题
- 每个微服务都需要进行单独的身份认证
针对这些问题,API网关顺势而生。
API 网关字面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题
3.2 SpringCloud介绍
1)定义:
- Spring Cloud是一个开发工具集,包含多个子项目;为微服务开发提供一站式解决方案,是一组独立组件的集合。
- SpringCloud是在SpringBoot的基础上构建的,使开发者可以轻松入门并快速提高工作效率。
2)版本
SpringCloud是一个由许多子项目组成的综合项目,为了避免SpringCloud版本号与子项目版本号混淆,SpringCloud版本采用了名称而非版本号的命名方式,这些版本的名称采用了伦敦地铁站的名字来命名,根据字母表的顺序来对应版本时间顺序,例如Angel是第一个版本,
Brixton是第二个版本。 当SpringCloud的发布内容积累到临界点或者一个重大BUG被解决后,会发布一个”service
releases”版本,简称SRX版本,比如Greenwich.SR2就是SpringCloud发布的Greenwich版本的第2个SRX版本。
3.3 SpringCloud架构
3.3.1 Spring Cloud Netflix组件
组件名称 | 作用 |
---|---|
Eureka | 服务注册中心 |
Ribbon | 客户端负载均衡 |
Feign | 声明式服务调用 |
Hystrix | 客户端容错保护 |
Zuul | API服务网关 |
3.3.2 Spring Cloud Alibaba组件
组件名称 | 作用 |
---|---|
Nacos | 服务注册中心,配置中心 |
Sentinel | 客户端容错保护 |
3.3.3 Spring Cloud 原生及其他组件
组件名称 | 作用 |
---|---|
Consul | 服务注册中心 |
Config | 分布式配置中心 |
Gateway | API服务网关 |
Sleuth/Zipkin | 分布式链路追踪 |
4、远程调用
定义:现在我们有两个微服务,商品微服务与订单微服务,如果在订单微服务中去调用商品微服务,那么也称之为远程调用。
4.1 restTemplate
定义:Spring提供了一个RestTemplate模板工具类,对基于Http的客户端进行了封装,并且实现了对象与json的序列化和反序列化,非常方便。RestTemplate并没有限定Http的客户端类型,而是进行了抽象,目前常用的3种都有支持:
- HttpClient
- OKHttp
- URLConnection
使用:
1⃣️注入restTemplate
@SpringBootApplication
@MapperScan(basePackages = "com.yxz.order.mapper")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
2⃣️在控制器自动注入 RestTemplate接口,以及调用方法
Result result = restTemplate.getForObject("http://localhost:8081/product/" + id, Result.class);
5、服务注册中心
服务注册中心(下称注册中心)是微服务架构非常重要的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:
- 服务发现:
• 服务注册/反注册:保存服务提供者和服务调用者的信息
• 服务订阅/取消订阅:服务调用者订阅服务提供者的信息,最好有实时推送的功能
• 服务路由(可选):具有筛选整合服务提供者的能力。 - 服务配置:
• 配置订阅:服务提供者和服务调用者订阅微服务相关的配置
• 配置下发:主动将配置推送给服务提供者和服务调用者 - 服务健康检测
• 检测服务提供者的健康情况
常见注册中心:
组件名 | 语言 | CAP | 服务健康检查 | 对外暴露的接口 |
---|---|---|---|---|
Eureka | java | AP | 可配支持 | HTTP |
Consul | GO | CP | 支持 | HTTP |
Zookeeper | java | CP | 支持 | 客户端(RPC) |
Nacos | java | AP/CP | 支持 | HTTP |
6、Eureka 注册中心
定义:
Eureka注册中心是SpringCloud微服务架构核心组件,每个微服务都会向注册中心去注册自己的地址及端口信息,注册中心负责维护服务名称与服务实例的对应关系。每个微服务都会定时从注册中心获取服务列表,同时汇报自己的运行情况,这样当有的服务需要调用其他服务时,就可以从自己获取到的服务列表中获取实例地址进行调用,Eureka实现了这套服务注册与发现机制。
配置
1)创建eureka工程:
1⃣️新建模块
2⃣️添加依赖
<dependencies>
<!--web工程启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--eureka服务端启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
3⃣️配置文件
server:
port: 8050
eureka:
client:
# 不向注册中心注册自己
register-with-eureka: false
# 不从注册中心拉取服务
fetch-registry: false
# eureka客户端向eureka服务端注册的地址
service-url:
defaultZone: http://localhost:8050/eureka
4⃣️编写启动引导类
@SpringBootApplication
// 激活eureka的配置,表示该工程师一个eureka的服务端
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
2)服务注册到Eureka
1⃣️添加依赖
<!--eureka客户端的启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2⃣️添加注解(可选)
@SpringBootApplication
@MapperScan(basePackages = "com.woniu.product.mapper")
// 表示该工程是eureka的客户端,该注解可写可不写,那就不写
//@EnableEurekaClient
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
3⃣️配置eureka信息
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka
4⃣️配置服务的名称
spring:
# 配置服务的名称
application:
name: product-server
5⃣️修改远程调用
在控制器自动注入 DiscoveryClient 接口,以及调用方法
// 获取指定服务的实例
List<ServiceInstance> instances = discoveryClient.getInstances("product-server");
ServiceInstance productServer = instances.get(0);
Result result = restTemplate.getForObject("http://"+productServer.getHost()+":"+productServer.getPort()+"/product/" + id, Result.class);
7、Eureka高级
7.1 服务注册
定义:服务提供者在启动时,会检测配置属性中的: eureka.client.register-with-erueka=true 参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,EurekaServer会把这些信息保存到一个双层Map结构中
- 第一层Map的Key就是服务id,一般是配置中的 spring.application.name 属性
- 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如: localhost:product-server:8081。值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群
7.2 服务发现
定义:当服务消费者启动时,会检测 eureka.client.fetch-registry=true 参数的值,如果为true,则会从EurekaServer服务的列表拉取只读备份,然后缓存在本地。并且每隔30秒会重新拉取并更新数据
# 拉取服务的间隔时间
registry-fetch-interval-seconds: 30
7.3 服务续约
定义:在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew)
instance:
# 服务续约间隔时间
lease-renewal-interval-in-seconds: 30
# 服务失效时间
lease-expiration-duration-in-seconds: 90
也就是说,默认情况下每隔30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会定时(eureka.server.eviction-interval-timer-in-ms设定的时间)从服务列表中移除,这两个值在生产环境不要修改,默认即可
7.4 服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态
7.5 失效剔除
定义:有时我们的服务可能由于内存溢出或网络故障等原因使得服务不能正常的工作,而服务注册中心并未收到“服务下线”的请求。相对于服务提供者的“服务续约”操作,服务注册中心在启动时会创建一个定时任务,默认每隔一段时间(默认为60秒)将当前清单中超时(默认为90秒)没有续约的服务剔除,这个操作被称为失效剔除
可以通过 eureka.server.eviction-interval-timer-in-ms 参数对其进行修改,单位是毫秒
7.6 自我保护
1)定义
当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的比例是否低于了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。生产环境下这很有效,保证了大多数服务依然可用。当网络故障恢复后,eureka会自动退出保护模式。
2) 触发自我保护机制的条件
- 当每分钟的实际心跳次数(所有在注册的服务)小于期望心跳次数时,并且开启了自动保护模式开关触发自我保护,不在自动过期剔除服务。 例如当前有10个在注册的服务,配置每30秒发送一次心跳:
实际心跳次数:就是当前10个注册服务每分钟的总的实际心跳次数
期望心跳次数:10 x 2(因为每30秒发送一次心跳)
阈值心跳次数:10 x 2 x 0.85 17 - 可以在eureka管理界面看到Renews threshold和Renews(last min),当后者(最后一分钟收到的心跳数)小于前者(心跳阈值)的时候,触发保护机制,会出现红色的警告: 「EMERGENCY!EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT.RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEGING EXPIRED JUST TO BE SAFE」,从警告中可以看到,eureka认为虽然收不到实例的心跳,但它认为实例还是健康的,eureka会保护这些实例,不会把它们从注册表中删掉。
3) server配置:
eureka:
server:
# 检查失效服务的间隔时间.每隔60秒检测一次,把服务列表中超过90秒没有发送心跳续约服务进行剔除
eviction-interval-timer-in-ms: 60000
# 每分钟需要收到的续约次数的阈值,默认值就是:0.85
renewal-percent-threshold: 0.85
# 开启自我保护
enable-self-preservation: true
# 在获取不到服务时,需要等待的时间,之后开启自我保护
wait-time-in-ms-when-sync-empty: 300000
7.7 常见问题
7.7.1 服务注册慢
默认情况下,服务注册到Eureka Server的过程较慢。SpringCloud官方文档中给出了详细的原因
大致含义:服务的注册涉及到心跳,默认心跳间隔为30s。在实例、服务器、客户端都在本地缓存中具有相同的元数据之前,服务不可用于客户端发现(所以可能需要3次心跳)。可以通过配置eureka.instance.leaseRenewalIntervalInSeconds (心跳频率)加快客户端连接到其他服务的过程。在生产中,最好坚持使用默认值,因为在服务器内部有一些计算,他们对续约做出假设。
7.7.2 服务节点剔除
默认情况下,由于Eureka Server剔除失效服务间隔时间为90s且存在自我保护的机制。所以不能有效而迅速的剔除失效节点,这对开发或测试会造成困扰。解决方案如下:
Eureka Server:
配置关闭自我保护,设置剔除无效节点的时间间隔
server:
# 关闭自我保护
enable-self-preservation: false
# 剔除时间间隔,单位:毫秒
eviction-interval-timer-in-ms: 4000
7.7.3 监控页面显示IP
在Eureka Server的管控台中,显示的服务实例名称默认情况下是微服务定义的名称和端口。为了更好的对所有服务进行定位,微服务注册到Eureka Server的时候可以手动配置示例ID。配置方式如下
eureka:
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
7.8 高可用
引入:
Eureka Server可以通过运行多个实例并相互注册的方式实现高可用部署,Eureka
Server实例会彼此增量地同步信息,从而确保所有节点数据一致。事实上,节点之间相互注册是Eureka Server的默认行为。
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka
Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka
Server集群中的任意一个节点,都可以获取到完整的服务列表信息而作为客户端,需要把信息注册到每个Eureka中。
原理:
注册中心之间可以相互同步服务(this.replicateToPeers())
部署步骤:
1⃣️修改eurekaServer
server:
port: 8050
eureka:
client:
# 不向注册中心注册自己
register-with-eureka: true
# 不从注册中心拉取服务
fetch-registry: true
# eureka客户端向eureka服务端注册的地址
service-url:
defaultZone: http://localhost:8050/eureka
server:
# 剔除失效的服务
eviction-interval-timer-in-ms: 60
# 关闭自我保护
enable-self-preservation: true
spring:
application:
# 服务的名称
name: eureka-server
# 日志日志输出级别
logging:
level:
com.netflix: warn
---
# 执行配置1的信息
spring:
profiles: eureka1
server:
port: 8050
eureka:
client:
service-url:
defaultZone: http://localhost:8060/eureka
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
---
# 执行配置2的信息
spring:
profiles: eureka2
server:
port: 8060
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
2⃣️
3⃣️修改eurekaClient(客户端)
# 配置eureka信息
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka,http://localhost:8060/eureka
8、ribbon
引入:
是 Netflix 发布的一个负载均衡器,有助于控制 HTTP 和 TCP客户端行为。在SpringCloud中可以将注册中心和Ribbon配合使用,Ribbon自动的从注册中心中获取服务提供者的列表信息,并基于内置的负载均衡算法,请求服务。
作用:
- 服务调用
基于Ribbon实现服务调用, 是通过拉取到的所有服务列表组成(服务名-请求路径的)映射关系。借助RestTemplate 最终进行调用 - 负载均衡
当有多个服务提供者时,Ribbon可以根据负载均衡的算法自动的选择需要调用的服务地址
原理:
8.1 使用
1⃣️ribbon是客户端的负载均衡技术,所以应该在消费方order模块添加依赖(但是eureka客户端中集成了ribbon的依赖,所以不需要导依赖)
2⃣️修改商品微服务,添加多台服务
3⃣️修改代码,在页面中显示具体调用的是哪一个商品微服务
修改ProductController代码如下:
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private IProductService productService;
@Value("${spring.cloud.client.ip-address}")
private String ip;
@Value("${server.port}")
private String port;
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
Product product = productService.getById(id);
return new Result(200, "查询商品信息成功:" + ip + ":" + port, product);
}
}
4⃣️添加注解
在OrderApplication 启动引导类 注入RestTemplate中的方法上面添加一个@LoadBalanced,启动负载均衡
/**
* 基于ribbon调用服务及负载均衡
* @return
*/
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
5⃣️修改调用的方式
Result result = restTemplate.getForObject("http://product-server/product/" + id, Result.class);
8.2 负载均衡
概述:
意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案
8.2.1 负载均衡方式:
- 服务端负载均衡
先发送请求到负载均衡服务器或者软件,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务器端再进行负载均衡算法分配
例如:Nginx - 客户端负载均衡
客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配
例如:ribbon
8.2.2 负载均衡策略
- com.netflix.loadbalancer.RoundRobinRule :轮询策略
- com.netflix.loadbalancer.RandomRule :随机策略
- com.netflix.loadbalancer.RetryRule :重试策略。
- com.netflix.loadbalancer.WeightedResponseTimeRule :权重策略。会计算每个服务的权重(robin的权重是自动计算响应时间越快的服务,权重越高),越高的被调用的可能性越大。
- com.netflix.loadbalancer.BestAvailableRule :最佳策略。遍历所有的服务实例,过滤掉故障实例,并返回请求数最小的实例返回。
- com.netflix.loadbalancer.AvailabilityFilteringRule :可用过滤策略。过滤掉故障和请求数超过阈值的服务实例,再从剩下的实力中轮询调用。
配置:
1⃣️在服务消费者的application.yml配置文件中修改负载均衡策略
# 修改指定服务的负载均衡策略(若不指定,则不填写服务名称)
product-server:
ribbon:
# 指定负载均衡策略为:随机策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
8.3 请求重试机制
引入:
如果8081端口的实例不可用,那么当负载到8081端口时,服务就不可用了,能不能有种机制,如果负载到8081服务,发现不可用,那么自动会切换到8083端口实例
使用:
1⃣️在客户端(消费端)添加依赖
<!--请求重试的依赖-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
2⃣️添加配置
# 修改指定服务的负载均衡策略
product-server:
ribbon:
# 指定负载均衡策略为:随机策略
#NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
ConnectTimeout: 250 # Ribbon的建立http连接的时间,也即连接超时时间
ReadTimeout: 1000 # Ribbon的请求响应的时间,也即数据读取超时时间,
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
3⃣️结果
9、feign
引入:
Result result = restTemplate.getForObject("http://product-server/product/" + id, Result.class);
这是我们之前写的源码调用的代码,上面的代码存在以下两个问题:
- 远程调用需要拼接地址,不符合我们的使用习惯(注入一个对象,通过方法调用)
- 参数拼接,如果参数比较复杂的话,拼接较为麻烦(参数为DTO的话)
概述:
Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求。 Spring Cloud
Feign是基于Netflix feign实现,整合了Spring Cloud Ribbon和Spring Cloud Hystrix
实现负载均衡和断路器,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。
只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Spring Cloud
Ribbon时自行封装服务调用客户端的开发量。
作用:
解决需要拼接地址和传参复杂的问题
原理:
9.1 使用:
1⃣️在消费端(order)中添加feign启动器依赖
<!--feign的启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2⃣️添加注解
@SpringBootApplication
@MapperScan(basePackages = "com.woniu.order.mapper")
// 开启feign调用支持
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
3⃣️创建feign客户端接口
在消费端order包下面创建一个feign的包,在feign包下面创建一个接口,名称为:OrderFeignClient
// 指定需要调用的服务的名称,该服务是注册到注册中心的服务名称
@FeignClient(name = "product-server")
// 请求的前缀
@RequestMapping("/product")
public interface ProductFeignClient {
// 具体的请求,建议方法直接从controller中复制过来,不要自己写
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id);
}
4⃣️修改order远程调用
1)注入对象
@Autowired
private ProductFeignClient productFeignClient;
2)方法调用
Result result = productFeignClient.findById(id);
9.2 feign高级
9.2.1 feign与ribbon的关系
Ribbon是一个基于 HTTP 和 TCP 客户端 的负载均衡的工具。它可以 在客户端 配置RibbonServerList(服务端列表),使用 HttpClient 或RestTemplate 模拟http请求。 Feign是在 Ribbon的基础上进行了一次改进,是一个使用起来更加方便的 HTTP 客户端。采用接口的方式, 只需要创建一个接口,然后在上面添加注解即可 ,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建http请求。然后就像是调用自身工程的方法调用,而感觉不到是调用远程方法,使得编写客户端变得非常容易
9.2.2 feign的相关配置
# 修改指定服务的负载均衡策略
product-server:
ribbon:
# 指定负载均衡策略为:随机策略
#NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
ConnectTimeout: 250 # Ribbon的连接超时时间 建立http连接的时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间,请求响应的时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
# feign的相关配置 备注:feign底层使用的ribbon,ribbon也有超时时间的配置,以feign的超时时间配置为准
feign:
client:
config:
product-server:
connectTimeout: 3000 # 连接超时时间 默认是2S
readTimeout: 3000 # 处理请求的时间 默认是5S
9.2.3 请求压缩
对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数,即可开启请求与响应的压缩功能:
# feign的相关配置
feign:
client:
config:
product-server:
connectTimeout: 3000 # 连接超时时间 默认是2S
readTimeout: 3000 # 处理请求的时间 默认是5S
compression:
request:
# 开启请求数据压缩(默认值为false)
enabled: true
# 支持压缩的数据类型
mime-types: text/html,application/xml,application/json
# 设置触发压缩的数据大小下限
min-request-size: 2048
response:
# 开启响应压缩(默认值为false)
enabled: true
9.2.4 日志级别
在开发或者运行阶段往往希望看到Feign请求过程的日志记录,默认情况下Feign的日志是没有开启的。要想用属性配置方式来达到日志效果,只需在 application.yml 中添加如下内容即可:
日志级别:
NONE:默认的,不显示任何日志
BASIC:仅记录请求方法、RUL、响应状态码及执行时间(在生产环境使用)
HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据(在开发环境使用)
效果:
10、接口幂等性
概述:
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而对数据库中的数据产生了副作用;
比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条。这就没有保证接口的幂等性
比如说ribbon负载均衡,其中一个服务由于网络延迟触发请求重试机制,在另一个服务重新发起了一次请求,但原来的请求在一段时间后也会发起,造成了两次请求,对数据进行了两次操作。
解决方案:
- 唯一性约束
- 状态机(乐观锁)
- 防重表
- token机制(token+Redis)

10.1 token机制演示:
1)模拟接口不符合幂等性问题
- 商品微服务添加修改库存的方法
@PutMapping("/stock/sub/{id}")
public Result subStock(@PathVariable Integer id){
int row = productService.subStock(id);
return new Result(200, "更新库存成功");
}
- service层
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {
@Autowired
private ProductMapper productMapper;
@Override
public int subStock(Integer id) {
return productMapper.subStock(id);
}
}
- mapper层
public interface ProductMapper extends BaseMapper<Product> {
@Update("update t_product set stock = stock - 1 where id = #{id}")
int subStock(Integer id);
}
- 在ProductFeignClient添加修改库存的方法
@PutMapping("/stock/sub/{id}")
public Result subStock(@PathVariable Integer id);
- 在订单服务的购买中调用修改库存的方法(需要添加线程休眠来模拟请求超时)
productFeignClient.subStock(id, token);
- 测试,查看库存的变化
添加超时重试,发现购买一次,会扣减4次库存
因为在ribbon设置了
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
在当前实例超时之后,会重试一次,接着切换实例,由于只启动了一个实例,实例切换后还是自己,继续重试两次。至此重试了四次。扣减4次库存)
2)解决幂等性问题
- 在订单微服务中添加获取token的请求,并且将token保存到Redis中(之后在调用购买方法时,需要先调用该请求)
// 进入页面之前获取token
@GetMapping("/token")
public Result getToken(){
// 生成token,次数使用雪花算法
IdWorker idWorker = new IdWorker();
long token = idWorker.nextId();
// 将token保存到redis中
stringRedisTemplate.opsForValue().set(token + "", token + "");
return new Result(200, "获取token成功", token);
}
- 在商品微服务修改库存的方法中添加幂等性校验
@PutMapping("/stock/sub/{id}")
public Result subStock(@PathVariable Integer id, HttpServletRequest request){
// 该接口需要保证幂等性
// 1. 首先从header中获取token的值
String token = request.getHeader("token");
// 2. 如果没有token,则是非法请求,直接抛出异常,不予处理
if(StringUtils.isEmpty(token)){
throw new RuntimeException("没有请求token");
}
// 3. 从Redis判断是否存在该token
token = stringRedisTemplate.opsForValue().get(token);
// 4. 如果不存在,则说明已经处理过了,不需要处理
if(StringUtils.isEmpty(token)){
throw new RuntimeException("不是第一次请求了");
}
// 5. 如果存在,则说明是第一次处理
// 6. 处理完需要将token从Redis中删除掉
stringRedisTemplate.delete(token);
try {
// 在product中休眠2S
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int row = productService.subStock(id);
return new Result(200, "更新库存成功");
}
-
使用postman进行测试,header中需要添加token
-
解决feign调用header中的值丢失问题
- 在ProductFeignClient中:
@PutMapping("/stock/sub/{id}")
public Result subStock(@PathVariable Integer id, @RequestHeader(value = "token", required = false) String token);
- 在订单服务的购买中添加从请求header中获取token,并将token传到product服务中
String token = request.getHeader("token");
productFeignClient.subStock(id, token);
11、服务雪崩
定义:服务雪崩效应是一种因“服务提供者的不可用”(原因)导致“服务调用者不可用”(结果),并将不可用逐渐放大的现象。
服务提供者A服务的访问压力过大,或者是网络原因,硬件原因等等多种因素,造成了服务提供者的不可访问。此时,相应的服务调用者B服务,就无法成功调用其提供的接口,并且造成线程阻塞,挤压线程。随着调用次数的增多,挤压的线程越来越多,那么这个服务调用者的抗并发量,就越来越少,直至最后崩掉。依次类推,B服务还为C服务提供了接口,那么B服务崩掉了,C服务的线程也开始挤压,直至C服务崩掉。依次类推,最后的效果就是所有的微服务都崩掉了。这就是服务雪崩效应。
11.1 解决方案之一: 服务隔离
指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。
1)线程池隔离:
给同一服务中的接口分配不同的线程数。来防止一个接口失败,导致整个服务失败的情形。
2)信号量隔离:
11.2 解决方案之二:熔断降级
熔断:当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
降级:就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。 也可以理解为兜底
11.3 解决方案之三:服务限流算法
概述:限流可以认为是服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳固运行,一旦达到需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。比方:推迟解决,拒绝解决,或者部分拒绝解决等等
常见的限流算法有:计数器、令牌桶、漏桶。
限流算法实现方案:
计数器
概述:一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。
弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”
漏桶
概述:为了消除”突刺现象”,可以采用漏桶算法实现限流。
漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。
不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
弊端:无法应对短时间的突发流量。
实现:可以准备一个队列,用来保存请求,另外通过一个线程池定期从队列中获取请求并执行,可以一次性获取多个并发执行。
令牌桶
概述:在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
实现:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
补充:
滑动时间窗口:
以请求过来的时间作为终点,往前推一个时间单位。计算该时间单位中处理的请求是否超出。
12、hystrix
概述:Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。
12.1 降级
概述:就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。 也可以理解为兜底
配置:
1⃣️添加依赖(SpringCloud Fegin默认已为Feign整合了hystrix,所以添加Feign依赖后就不用再添加hystrix)
2⃣️开启hystrix
在Feign中已经内置了hystrix,但是默认是关闭的需要在工程的 application.yml 中开启对hystrix的支持
在订单微服务的配置文件中开启hystrix
# feign的相关配置
feign:
hystrix:
#在feign中开启hystrix熔断
enabled: true
client:
config:
product-server:
connectTimeout: 2000 # 连接超时时间 默认是2S
readTimeout: 3000 # 处理请求的时间 默认是5S
# 设置feign调用的日志级别
loggerLevel: FULL
3⃣️配置FeignClient接口的实现类
在package com.woniu.order.feign 包中添加如下类
@Component
public class ProductFeginClientCallBack implements ProductFeignClient{
/**
* 熔断降级的方法
* @param id
* @return
*/
public Result findById(Integer id) {
return new Result(500, "熔断:触发了降级的方法======");
}
/**
* 熔断降级的方法
* @param id
* @param token
* @return
*/
public Result subStock(Integer id, String token) {
return new Result(500, "熔断:触发了降级的方法======");
}
}
4⃣️修改FeignClient添加hystrix熔断
// 指定需要调用的服务的名称,该服务是注册到注册中心的服务名称 , 指定熔断降级的方法
// path 指定请求的前缀
@FeignClient(name = "product-server", path = "/product", fallback = ProductFeginClientCallBack.class)
public interface ProductFeignClient {
// 具体的请求,建议方法直接从controller中复制过来,不要自己写
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id);
@PutMapping("/stock/sub/{id}")
public Result subStock(@PathVariable Integer id, @RequestHeader(value = "token", required = false) String token);
}
5⃣️配置hystrix的超时
# hystrix的相关配置
hystrix:
command:
default:
execution:
isolation:
thread:
# hystrix的超时时间,默认为1S
timeoutInMilliseconds: 2000
12.2 熔断
概述:当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
12.2.1 三个重要参数
- 快照时间窗:统计的时间范围就是快照时间窗,默认为最近的10秒。
- 请求总数阈值:在快照时间窗内,必须满足请求总数阈值才有资格熔断。默认20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开。
- 错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这时候就会将断路器打开。
12.2.2 断路器的三个状态
- Closed:关闭状态(断路器关闭),所有请求都正常访问。代理类维护了最近调用失败的次数,如果某次调用失败,则使失败次数加1。如果最近失败次数超过了在给定时间内允许失败的阈值,则代理类切换到断开(Open)状态。此时代理开启了一个超时时钟,当该时钟超过了该时间,则切换到半断开(Half-Open)状态。该超时时间的设定是给了系统一次机会来修正导致调用失败的错误。
- Open:打开状态(断路器打开),所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全关闭。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放1次请求通过,若这个请求是健康的,则会关闭断路器,否则继续保持打开,再次进行5秒休眠计时。
12.2.3 配置断路器:
1⃣️application.yml文件添加配置
# hystrix的相关配置
hystrix:
command:
default:
execution:
isolation:
thread:
# hystrix的超时时间,默认为1S
timeoutInMilliseconds: 2000
circuitBreaker:
# 触发熔断的最小请求次数,默认20
requestVolumeThreshold: 20
# 熔断多少秒后去尝试请求,默认5秒
sleepWindowInMilliseconds: 10000
# 触发熔断的失败请求最小占比,默认50%
errorThresholdPercentage: 50
2⃣️修改商品微服务查询的方法
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
if(id != 1){
throw new RuntimeException("抛出自定义异常");
}
Product product = productService.getById(id);
return new Result(200, "查询商品信息成功:" + ip + ":" + port, product);
}
3⃣️测试
在10秒内请求错误的路径20次以上(http://localhost:8081/order/buy2/2),再请求正确的路径(http://localhost:8081/order/buy2/1),发现正确路径也提示熔断降级。需要过一会儿才能继续访问
12.3 hystrix监控平台
12.3.1 单个服务搭建监控
引入:
上面的案例,我们能看到当达到条件之后,会触发断路器。但是哪些服务的断路器是什么状态我们并不知道,要去访问以下才能知道,如果能有一个面板(页面)能够看到服务断路器状态就好了。
搭建监控:
1⃣️添加依赖
<!--监控平台启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hystrix启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--监控面板启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
2⃣️添加注解
- 在order微服务的启动引导类上添加注解
@SpringBootApplication
@MapperScan(basePackages = "com.woniu.order.mapper")
// 开启feign调用支持
@EnableFeignClients
// 开启hystrix
@EnableHystrix
// 开启hystrix控制面板
@EnableHystrixDashboard
public class OrderApplication {}
- 需要监听的方法上添加注解
@HystrixCommand
3⃣️开放所有检测端口
# 开放所有检测端口
management:
endpoints:
web:
exposure:
include: "*"
4⃣️配置熔断的相关参数
# hystrix的相关配置
hystrix:
command:
default:
execution:
isolation:
thread:
# hystrix的超时时间,默认为1S
timeoutInMilliseconds: 2000
circuitBreaker:
# 触发熔断的最小请求次数,默认20
requestVolumeThreshold: 20
# 熔断多少秒后去尝试请求,默认5秒
sleepWindowInMilliseconds: 10000
# 触发熔断的失败请求最小占比,默认50%
errorThresholdPercentage: 50
5⃣️访问测试
访问http://localhost:8081/actuator/hystrix.stream (此处ip和端口为需要监控的微服务的ip和端口)
6⃣️通过面板来查看
访问http://localhost:8081/hystrix/ (此处ip和端口为需要监控的微服务的ip和端口)
备注:
如果在测试的时候出现:Unable to connect to Command Metric Stream.
则在order微服务的配置文件中添加如下配置:proxy-stream-allow-list: “*”
# hystrix的相关配置
hystrix:
command:
default:
execution:
isolation:
thread:
# hystrix的超时时间,默认为1S
timeoutInMilliseconds: 2000
circuitBreaker:
# 触发熔断的最小请求次数,默认20
requestVolumeThreshold: 20
# 熔断多少秒后去尝试请求,默认5秒
sleepWindowInMilliseconds: 10000
# 触发熔断的失败请求最小占比,默认50%
errorThresholdPercentage: 50
dashboard:
proxy-stream-allow-list: "*"
12.3.2 搭建聚合监控
引入:当多个微服务需要监控时,就需要打开多个监控平台。搭建聚合监控可以实现在一个监控平台上查看多个服务的状态。
备注:被监听的微服务也需要配置hystrix的相关配置
搭建步骤:
1⃣️创建一个hystrix_turbine的工程,作为独立的监控
2⃣️添加依赖
<?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_parent</artifactId>
<groupId>com.woniu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hystrix_turbine</artifactId>
<description>hystrix监控平台</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--web启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--turbine启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
<!--hystrix启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<!--监控面板启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!--eureka客户端启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
3⃣️编写配置文件
server:
port: 8070
spring:
application:
name: hystrix-turbine
# eureka的配置
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
# turbine的相关配置
turbine:
app-config: product-server,order-server
clusterNameExpression: "'default'"
# hystrix的相关配置
hystrix:
dashboard:
proxy-stream-allow-list: "*"
# 开放所有检测端口
management:
endpoints:
web:
exposure:
include: "*"
4⃣️编写启动引导类
@SpringBootApplication
@EnableTurbine
@EnableHystrix
@EnableHystrixDashboard
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
5⃣️测试
打开监控面板入口: http://localhost:8070/hystrix
在监控面板填写以下地址:http://localhost:8070/turbine.stream
13、Sentinel
引入:18年底Netflflix官方宣布Hystrix 已经足够稳定,不再积极开发 Hystrix,该项目将处于维护模式。就目前来看Hystrix是比较稳定的,并且Hystrix只是停止开发新的版本,并不是完全停止维护,Bug什么的依然会维护的。因此短期内,Hystrix依然是继续使用的。但从长远来看,Hystrix总会达到它的生命周期。另外,hystrix在限流的方便提供的功能也不够丰富。
sentinel与hystrix的区别:
概述:Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务的稳定性。相较于Hystrix功能更为丰富。
13.1 使用
1)部署仪表盘
下载地址:https://github.com/alibaba/Sentinel/releases/
启动运行:
在终端输入以下命令,以8848端口,用户名为admin,密码为123456启动运行(Mac可以自定义shell脚本快速运行,需要使用命令chmod +x 文件名 给文件添加可执行权限)
java -jar -Dserver.port=8848 -Dsentinel.dashboard.auth.username=admin -Dsentinel.dashboard.auth.password=123456 sentinel-dashboard-1.8.3.jar
2)引入Sentinel
1⃣️ 在父工程中引入cloudalibaba的父工程
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<!--父工程需要指定为pom类型-->
<type>pom</type>
<!--导入上面的pom依赖-->
<scope>import</scope>
</dependency>
2⃣️在需要添加服务的工程添加如下依赖
<!--alibaba sentinel的启动器依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
3⃣️添加配置
spring:
# 配置服务的名称
application:
name: product-server-sentinel
cloud:
sentinel:
# 取消懒加载
eager: true
transport:
# sentinel仪表盘的地址
dashboard: http://localhost:8848
4⃣️ 测试
13.2 流量控制
概述:
从系统稳定性角度考虑,在处理请求的速度上,任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。因此我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状,如下图所示:
因此所谓的流控就是为了应对突发请求对系统带来额外的压力而采取的一种防御措施。通过对请求流量的限制处理,让大量的突发请求变得平滑,降低系统的压力。
13.2.1 阈值
- QPS
全名 Queries Per Second,意思是“每秒查询率”,是一台服务器每秒能够响应的查询次数。QPS = req/sec = 请求数/秒。
含义:该接口每秒只能处理2个请求,1秒内,超出两个请求,直接请求失败,不处理该请求。
- 线程数
线程数就是该接口总共提供几个线程处理请求,假设是5个,那么,如果现在有5个请求在处理,第6个请求过来的话,就直接返回失败,不处理。
Sentinel并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。
13.2.2 流控模式
- 直接
资源调用达到设置的阈值后直接被流控抛出异常
超出的流量直接不处理,返回失败,但是默认的返回的失败不是太友好,最好定义自己的返回失败的信息
方式一:定义失败的返回信息
@RestController
@RequestMapping("/product")
public class ProductController {
@SentinelResource(value = "findById", blockHandler = "findByIdBlock")
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new Result(200, "查询商品数据成功");
}
/**
* 注意:
* 1. 方法名称必须使用public修饰
* 2. 返回值类型必须跟原方法保持一致
* 3. 方法的参数必须与原方法保持一致
* 4. 可以在参数的最后面添加一个异常参数:BlockException
* @param id
* @return
*/
public Result findByIdBlock(@PathVariable Integer id, BlockException e){
// 打印异常信息
e.printStackTrace();
// 返回数据
return new Result(400, "接口被限流了===哈哈哈");
}
}
方式二:定义统一的异常处理信息
统一的异常处理需要定义一个类实现BlockExceptionHandler接口,重写handle方法,并且将该类添加到spring容器中
备注:
添加了SentinelResource注解之后,统一的异常处理就不会生效,而是通过blockHandler指定异常方法。即方式一和方式二并存时,方式一生效。
@Component
public class MyBlockException implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
// 打印异常信息
e.printStackTrace();
// 返回数据
Result result = new Result(400, "接口被限流了(统一异常处理)===哈哈哈");
response.setStatus(400);// 设置响应状态
response.setCharacterEncoding("UTF-8");// 设置编码
response.setContentType(MediaType.APPLICATION_JSON_VALUE);// 设置响应的数据类型
new ObjectMapper().writeValue(response.getWriter(), result);// 设置响应数据
}
}
- 关联
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。
样例:有2个资源 add, findById. findById的关联资源为add,当add方法触发流控的阈值时,流控限制的是findById方法。
使用jmeter来进行测试:
- 链路
根据调用链路入口限流。
样例:比如/product/test01, /product/test02都调用了service层的findProduct方法,那么,我们可以在链路中配置只针对拿个来源的请求进行流控
@GetMapping("/test01")
public Result test01(){
productService.findProduct();
return new Result(200, "test01");
}
@GetMapping("/test02")
public Result test02(){
productService.findProduct();
return new Result(200, "test02");
}
@Service
public class ProductServiceImpl implements ProductService {
@SentinelResource(value = "findProduct")
public void findProduct() {
System.out.println("findProduct=======================");
}
}
如果按照上述的代码进行重启测试,会发现没有效果,这里需要增加一个配置项:web-context-unify: false
spring:
# 配置服务的名称
application:
name: product-server-sentinel
cloud:
sentinel:
# 取消懒加载
eager: true
transport:
# sentinel仪表盘的地址
dashboard: http://localhost:8848
# 配置为 false 即可根据不同的URL 相当于开启链路限流
web-context-unify: false
13.2.3 流控效果
-
快速失败
默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。 -
warmUp
WarmUp(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。
冷加载因子:codeFactor默认是3,即请求QPS从threshold/3开始,经预热时长逐渐升至设定的QPS阈值。
-
排队等待
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
表示一秒内只能处理两个请求,即每500ms处理一个请求,多余的请求进入等待队列。等待时间超过5000ms时,拒绝请求。
13.3 熔断降级
备注:如果熔断跟降级都触发了,优先执行限流的方法。
13.3.1 慢调用比例
选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间,单位毫秒),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
13.3.2 异常比例
当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
13.3.3 异常数
当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
13.3.4 熔断的异常处理
@SentinelResource(value = "findById", blockHandler = "findByIdBlock", fallback = "findByIdBlockFallBack")
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
if(id != 1){
throw new RuntimeException("哈哈哈");
}
return new Result(200, "查询商品数据成功");
}
public Result findByIdBlockFallBack(@PathVariable Integer id){
// 返回数据
return new Result(400, "接口被熔断降级了===嘿嘿嘿");
}
13.5 feign整合sentinel
备注:springcloud Hoxton.SR12 与springboot 2.3.12 使用feign会有循环依赖冲突,需要在父工程依赖里将springcloud 版本从Hoxton.SR12改成Hoxton.SR9
添加依赖,添加注解同上述引入feign和sentinel的笔记。编写配置文件需要新增feign支持sentinel的配置
# feign的相关配置
feign:
sentinel:
# 开启feign对sentinel的支持
enabled: true
13.6 热点规则
热点参数流控规则是一种更细粒度的流控规则,它允许将规则具体到参数上,分别统计参数值相同的请求(或统计参数值相同的资源),是否超过QPS阈值。
所谓的热点就是频繁访问的数据,比如在商品服务中,对携带ID参数查询商品的请求做限流控制,即可采用热点规则限流。
通俗的说:就是同一个接口,对哪些参数进行限流,哪些参数不限流。
- 配置热点规则
- 配置例外
(该规则经常出现配置例外不保存的bug)
13.7 系统规则
表示当cpu使用率超过0.2时,会对所有接口进行限流。
14、gateway
引入:
微服务架构下,随着服务的数量不断累加,当客户端访问这些微服务的时候,往往需要记住即使甚至上百个地址,这对于客户端而言,是非常复杂且难以维护的。
概述:
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring 5,Spring Boot 2和 Project Reactor等技术。提供了相关的过滤器功能, 例如:熔断、限流、重试等。
核心概念:
(1)Route(路由):
路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
(2)Predicate(断言):
路由断言,判断请求是否符合要求,符合则转发到路由目的地。
(3)Filter(过滤器):
指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前后对请求进行修改。
应用场景:
- 统一入口:为全部微服务提供一个唯一的入口,网关起到外部和内部隔离的作用,保障了后台服务的安全性。
- 鉴权校验:识别每个请求的权限,拒绝不符合要求的请求。
- 动态路由:动态的将请求路由到不同的后端集群中。
- 减少客户端与服务端的耦合:服务可以独立发展,通过网关层来做映射。
注意事项:
gateway依赖内嵌了web启动器依赖,如果再添加spring-boot-starter-web启动器依赖,会导致依赖冲突。
14.1 搭建网关工程
1⃣️创建工程
创建网关工程:gateway_server
2⃣️添加依赖
<?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_parent</artifactId>
<groupId>com.woniu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway_server</artifactId>
<description>网关工程</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--eureka客户端依赖-->
<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-gateway</artifactId>
</dependency>
</dependencies>
</project>
3⃣️ 编写配置文件
server:
port: 9001
spring:
application:
name: gateway-server
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
4⃣️编写启动引导类
@SpringBootApplication
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class, args);
}
}
5⃣️ 启动
14.2 路由配置
路由配置就是访问网关的一个地址,如何转发到对应的微服务。
简单路由配置示例:
配置访问网关地址能够访问到商品微服务
1)修改一下商品微服务中的findById方法
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
Product product = productService.getById(id);
return new Result(200, "查询商品信息成功:" + ip + ":" + port, product);
}
2)配置路由
spring:
application:
name: gateway-server
cloud:
# 配置网关
gateway:
# 配置路由
routes:
# 路由的名称,ID一般与服务的名称保持一致,保持唯一
- id: product-server
# 需要映射的微服务的访问地址
uri: http://localhost:8081
# 断言(路由的条件表达式)
predicates:
- Path=/product/**
这段配置的意思是,配置了一个 id 为 product-server的路由规则,当访问网关请求地址以product 开头时,会自动转发到地址: http://localhost:8081/ 。配置完成启动项目即可在浏览器
3)访问进行测试,当我们访问地址 http://localhost:8080/product/1 时会展示页面展示如下:
14.2.1 路由规则
Spring Cloud Gataway 帮我们内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
举例:参数路由
指的是如果参数中包含token这个参数则匹配
spring:
application:
name: gateway-server
cloud:
# 配置网关
gateway:
# 配置路由
routes:
# 路由的名称,ID一般与服务的名称保持一致
- id: product-server
# 需要映射的微服务的访问地址
uri: http://localhost:8081
predicates:
- Path=/product/**
- Query=token
14.2.2 动态路由
如果商品微服务有多台机器,我们目前uri写死的写法就起不到负载均衡的效果。所以应该从注册中心去拉取,并且使用相应的负载均衡策略
动态路由配置:
spring:
application:
name: gateway-server
cloud:
# 配置网关
gateway:
# 配置路由
routes:
# 路由的名称,ID一般与服务的名称保持一致
- id: product-server
# 需要映射的微服务的访问地址
# uri: http://localhost:8081
uri: lb://product-server
predicates:
- Path=/product/**
# - Query=token
14.2.3 根据服务名称转发
spring:
application:
name: gateway-server
cloud:
# 配置网关
gateway:
# 配置路由
routes:
# 路由的名称,ID一般与服务的名称保持一致
- id: product-server
# 需要映射的微服务的访问地址
# uri: http://localhost:8081
uri: lb://product-server
predicates:
- Path=/product/**
# - Query=token
discovery:
locator:
# 开启微服务名称自动转发 默认是false
enabled: true
# 将服务的名称全部转换为小写
lower-case-service-id: true
14.3 过滤器
概述:
- 作用:过滤器就是在请求的传递过程中,对请求和响应做一些手脚
- 分类:局部过滤器(作用在某一个路由上)全局过滤器(作用全部路由上)
14.2.1 局部过滤器
特点:一般应用到单个路由上
spring:
application:
name: gateway-server
cloud:
# 配置网关
gateway:
# 配置路由
routes:
# 路由的名称,ID一般与服务的名称保持一致
- id: product-server
# 需要映射的微服务的访问地址
# uri: http://localhost:8081
uri: lb://product-server
predicates:
- Path=/aaa/**
# - Query=token
filters:
# 去掉请求地址中的一个前缀
- StripPrefix=1
discovery:
locator:
# 开启微服务名称自动转发 默认是false
enabled: true
# 将服务的名称全部转换为小写
lower-case-service-id: true
14.2.2 全局过滤器
全局过滤器(GlobalFilter)作用于所有路由,Spring Cloud Gateway 定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是程序员使用比较多的过滤器。
特点:针对于所有路由生效,全局过滤器只针对于经过路由的请求有效。
配置全局过滤器步骤:
1)创建一个类
2)实现GlobalFilter,Ordered接口
3)重写方法
@Component
public class TestFilter implements GlobalFilter, Ordered {
/**
* 执行过滤器的方法
* @param exchange
* @param chain
* @return
*/
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("进入了TestFilter的filter方法============");
// 放行操作
return chain.filter(exchange);
}
/**
* 过滤器的优先级,返回的值越小,执行的优先级越高
* @return
*/
public int getOrder() {
return 5;
}
}
14.4 统一鉴权示例
引入:
微服务架构里面会有很多个微服务系统,比如有用户微服务,商品微服务,订单微服务等等。假设这三个微服务都需要登陆之后才能调用里面的方法,就需要认证的相关代码。
可以在网关中添加认证的方法(因为所有的请求都需要经过网关才能到达微服务)。同时为了防止用户伪造token,需要使用redis在登录时保存token,之后在网关过滤器中将每一个token与redis中的token进行比较。
1)用户服务的登录方法
@PostMapping("/login")
public Result login(@RequestBody UserLoginDTO userLoginDTO){
// 1. 数据的规则校验 TODO
// 2. 登录功能
User user = userService.login(userLoginDTO);
// 3. 返回数据
if(user == null){
return new Result(200, "登录失败");
}else{
String token = user.getUsername();
// 3.1 将token存储到redis中
stringRedisTemplate.opsForValue().set(token,token,60*30, TimeUnit.SECONDS);
return new Result(200, "登录成功",token);
}
}
2)添加认证的全局过滤器
@Component
public class LoginFilter implements GlobalFilter, Ordered {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 0 特定的请求需要放行
String path = exchange.getRequest().getPath().toString();
System.out.println("请求的路径为"+ path);
String noFilterPath= "/user/login";
if (noFilterPath.contains(path)){
return chain.filter(exchange);
}
System.out.println("进入认证的过滤器方法=============");
// 1、获取request
ServerHttpRequest request = exchange.getRequest();
// 2、从request的header获取token
// 判断token是否存在,不存在,则没有登录,存在,则登录了,放行
String token = request.getHeaders().getFirst("token");
if (StringUtils.isEmpty(token)){
// 设置response的状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 设置响应完成
return exchange.getResponse().setComplete();
}
// 3、从redis中根据token获取token,不存在,没有登录
String redisToken = stringRedisTemplate.opsForValue().get(token);
if (StringUtils.isEmpty(redisToken)){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 4、续签 在每个操作之后都对redisToken进行续签
stringRedisTemplate.expire(redisToken,60*30, TimeUnit.SECONDS);
// 放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 4;
}
}
14.4 网关限流
SpringCloudGateway官方提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory 实现。在过滤器工厂中是通过Redis和lua脚本结合的方式进行流量控制。
配置步骤:
1)添加redis依赖
<!--Redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2)修改配置文件
spring:
application:
name: gateway-server
cloud:
# 配置网关
gateway:
# 配置路由
routes:
# 路由的名称,ID一般与服务的名称保持一致
- id: product-server
# 需要映射的微服务的访问地址
# uri: http://localhost:8081
uri: lb://product-server
predicates:
- Path=/aaa/**
# - Query=token
filters:
# 去掉请求地址中的一个前缀
- StripPrefix=1
- name: RequestRateLimiter
args:
# 使用SP EL表达式从容器中获取对象
key-resolver: '#{@pathKeyResolver}'
# 令牌生成额速率(生成令牌的速度)
redis-rate-limiter.replenishRate: 1
# 令牌桶的最大容量
redis-rate-limiter.burstCapacity: 3
3)配置keyResolver
@Component
public class KeyResolverConfiguration {
/**
* 基于请求路径的限流
* @return
*/
@Bean
public KeyResolver pathKeyResolver(){
return new KeyResolver() {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getPath().toString());
}
};
}
}
4)测试
一秒发送请求超过三次
14.4.5 网关高可用
高可用 HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。
架构示意图:
具体实现:
1)修改配置文件
server:
port: 9001
spring:
application:
name: gateway-server
cloud:
# 配置网关
gateway:
# 配置路由
routes:
# 路由的名称,ID一般与服务的名称保持一致
- id: product-server
# 需要映射的微服务的访问地址
# uri: http://localhost:8081
uri: lb://product-server
predicates:
- Path=/aaa/**
# - Query=token
filters:
# 去掉请求地址中的一个前缀
- StripPrefix=1
- name: RequestRateLimiter
args:
# 使用SP EL表达式从容器中获取对象
key-resolver: '#{@pathKeyResolver}'
# 令牌生成额速率(生成令牌的速度) 每秒
redis-rate-limiter.replenishRate: 1
# 令牌桶的最容量
redis-rate-limiter.burstCapacity: 3
discovery:
locator:
# 开启微服务名称自动转发 默认是false
enabled: true
# 将服务的名称全部转换为小写
lower-case-service-id: true
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka
# 客户端从注册中心拉取服务的时间 默认30S
registry-fetch-interval-seconds: 5
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
---
spring:
profiles: gateway01
server:
port: 9001
---
spring:
profiles: gateway02
server:
port: 9002
2)添加两个gateway启动配置,激活不同配置(gateway01、gateway02)
3)修改Nginx的配置文件
upstream gatewayRibbon{
server localhost:9001 weight=2;
server localhost:9002 weight=1;
}
4)启动测试
-
启动网关及相关微服务
-
启动Nginx
-
访问Nginx的地址
15、sleuth-zipkin
引入:
在大型系统的微服务化架构中,一次请求往往需要涉及到多个服务。随着业务发展,微服务的数量也会越来越多,某个服务出现问题,问题很难排查。
存在的问题:
- 链路梳理难:无法清晰地看到整个调用链路
- 故障难定位:无法快速定位到故障点、无法快速定位哪个环节比较费时
因此,我们需要链路追踪来梳理链路调用,方便快速定位问题。
15.1 sleuth
概述:
sleuth是一个链路追踪工具,通过它在日志中打印的信息可以分析出一个服务的调用链条,也可以得出链条中每个服务的耗时,这为我们在实际生产中,分析超时服务,分析服务调用关系,做服务治理提供帮助。
作用:
- 耗时分析: 通过Sleuth可以很方便的了解到每个采样请求的耗时,从而分析出哪些服务调用比较耗时
- 可视化错误: 对于程序未捕捉的异常,可以通过集成Zipkin服务界面上看到
- 链路优化: 对于调用比较频繁的服务,可以针对这些服务实施一些优化措施
sleuth目前并不是对所有调用访问都可以做链路追踪,它目前支持的有:rxjava、feign、quartz、RestTemplate、hystrix、grpc、kafka、Opentracing、redis、Reator、circuitbreaker、spring的Scheduled。国内用的比较多的dubbo,sleuth无法对其提供支持。
15.1.1相关概念
-
Span
Span是基本工作单位。Span还包含了其他的信息,例如摘要、时间戳事件、Span的ID以及进程ID。SpanId用于唯一标识请求链路到达的各个服务组件。 -
Trace
一组具有相同TraceId的span组成的树状结构,即一个完整的请求链路 -
Annotation
记录一个请求的4个事件,用于计算各个环节消耗的时长
cs (Client Sent ):客户端发送一个请求,开始一个请求的生命。
sr (Server Received ):服务端收到请求开始处理,sr - cs = 网络延迟(服务调用的时间)
ss(Server Sent ):服务端处理完毕准备发送到客户端,ss - sr = 服务器处理请求所用时间
cr (Client Received ):客户端接收到服务端的响应,请求结束,cr - cs = 请求的总时间
15.1.2 基本使用
1)添加依赖
<!--链路追踪sleuth依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
2)修改配置文件
logging:
level:
# 默认的日志打印级别
root: info
# servlet的日志打印级别
org.springframework.web.servlet.DispatcherServlet: DEBUG
# sleuth的日志记录级别
org.springframework.cloud.sleuth: DEBUG
备注:所有需要链路追踪的都需要添加以上配置,所以,针对我们的案例中:网关,订单,商品都需要添加
3)测试
其中 ac7f50430c401643是TraceId,后面跟着的是SpanId,依次调用有一个全局的TraceId,将调用链路串起来。仔细分析每个微服务的日志,不难看出请求的具体过程。
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过Zipkin可以将日志聚合,并进行可视化展示和全文检索。
15.2 zipkin
概述:
Zipkin 是 Twitter 的一个开源项目,它基于 Google Dapper 实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。 我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。除了面向开发的 API 接口之外,它也提供了方便的 UI 组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。 Zipkin 提供了可插拔数据存储方式:InMemory、MySql、Cassandra 以及 Elasticsearch。
15.2.1 组件介绍
上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:
- Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为Zipkin 内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。
- Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中,我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。
- RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接系统访问以实现监控等。
- Web UI:UI 组件,基于 API 组件实现的上层应用。通过 UI 组件用户可以方便而有直观地查询和分析跟踪信息。
Zipkin 分为两端,一个是 Zipkin 服务端,一个是 Zipkin 客户端,客户端也就是微服务的应用。客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。
发送的方式主要有两种,一种是 HTTP 报文的方式,还有一种是消息总线的方式如 RabbitMQ。
不论哪种方式,我们都需要:
- 一个 Eureka 服务注册中心,这里我们就用之前的 eureka 项目来当注册中心。
- 一个 Zipkin 服务端。
- 多个微服务,这些微服务中配置Zipkin 客户端。
15.2.2 基本使用
1)Zipkin Server下载
从spring boot 2.0开始,官方就不再支持使用自建Zipkin Server的方式进行服务链路追踪,而是直接提供了编译好的 jar 包来给我们使用。可以从官方网站下载先下载Zipkin的web UI,我们这里下载的是 zipkin-server-2.12.9-exec.jar
2)启动运行
在命令行输入 java -jar zipkin-server-2.12.9-exec.jar 启动 Zipkin Server,端口默认是:9411
访问地址:http://localhost:9411/zipkin/
3)整合sleuth
1⃣️添加依赖
<!--zipkin启动器依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
2⃣️修改配置文件
zipkin:
# zipkin server服务的地址
base-url: http://127.0.0.1:9411/
sender:
# 请求的方式,默认是以http的方式向zipkin server发送追踪数据
type: web
sleuth:
sampler:
# 日志收集的比例,默认是10%,一般工作中不会使用100%
rate: 100
3⃣️ 测试
15.2.3 持久化
Zipkin Server默认时间追踪数据信息保存到内存,这种方式不适合生产环境。因为一旦Service关闭重启或者服务崩溃,就会导致历史数据消失。Zipkin支持将追踪数据持久化到mysql数据库或者存储到elasticsearch中。这里已mysql为例。
1) 准备数据库
可以从官网找到Zipkin Server持久mysql的数据库脚本。
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`remote_service_name` VARCHAR(255),
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT,
`error_count` BIGINT,
PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
- 配置启动服务端
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql --
MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root -
-MYSQL_PASS=111111
16、Spring Cloud Config
引入:对于传统的单体应用而言,常使用配置文件来管理所有配置,比如SpringBoot的application.yml文件,但是在微服务架构中全部手动修改的话很麻烦而且不易维护。
常见的配置中心:
-
Spring Cloud Config
为分布式系统中的外部配置提供服务器和客户端支持 -
Apollo(阿波罗)
是携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性,适用于微服务配置管理场景 -
Disconf
专注于各种「分布式系统配置管理」的「通用组件」和「通用平台」, 提供统一的「配置管理服务」包括 百度、滴滴出行、银联、网易、拉勾网、苏宁易购、顺丰科技 等知名互联网公司正在使用 -
nacos
阿里巴巴开源的注册中心组件,同时也是配置中心的作用(目前来说使用最广的一种方式)
config简介
是一个解决分布式系统的配置管理方案。它包含了Client和Server两个部分。server(服务端)从配置仓库获取配置信息,提供配置文件的存储、以接口的形式将配置文件的内容提供出去。client(客户端)通过接口从服务端获取数据、并依据此数据初始化自己的应用。
Spring Cloud Config 的配置中心默认采用Git来存储配置信息,所以天然就支持配置信息的版本管理,并且可以使用Git客户端来方便地管理和访问配置信息。
16.1 使用
1⃣️登录gitee账号,创建一个新的仓库,用于存放相关的配置文件
2⃣️服务端搭建
1.创建config-server工程
2.添加依赖
<?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_parent</artifactId>
<groupId>com.woniu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>config-server</artifactId>
<description>配置中心服务端</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--eureka客户端依赖,配置中心需要注册到注册中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--配置中心服务端的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
</project>
3.编写配置文件
server:
port: 12000
spring:
application:
name: config-server
cloud:
config:
server:
git:
# 配置中心git仓库的地址
uri: https://gitee.com/tarnett/cloud-config.git
# git账户的用户名
username: 13794485021
# git账户的密码
password: 21haofengzi
# 搜索仓库的目录
search-paths:
- config
- other
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka
instance:
instance-id: ${spring.cloud.client.ip-address}:${server.port}
4 .编写启动引导类
@SpringBootApplication
// 开启注册中心服务端
@EnableConfigServer
public class ConfigApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigApplication.class, args);
}
}
5.启动测试
注意:文件的命名规则如下:
{application}-{profile}.yml
{application}-{profile}.properties
其中application为应用名称,profile指的开发环境(用于区分开发环境,测试环境、生产环境等)
在浏览器中输入:http://localhost:12000/test-dev.yml
3⃣️客户端搭建
1.添加依赖
<!--配置中心客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
2.删除或者重命名application.yml配置文件.
已经不需要这个配置文件,配置文件需要存放到git上面,通过配置中心的server端获取
3.编写bootstrap.yml文件
bootstrap.yml配置文件的加载顺序最高,启动springBoot工程,先加载bootstrap.yml文件,通过去读其中的配置,在通过配置中心服务端去Git中拉取配置文件
spring:
cloud:
config:
# 应用名称,对应git上面配置文件${application}内容
name: product
# 环境,对应git上面配置文件${profile}内容
profile: dev
# 分支
label: master
# 配置中心服务端的地址
uri: http://localhost:12000
2.3.4 启动测试
16.2 参数刷新
16.2.1 问题引入
在配置文件中添加一个值,在商品微服务中获取这个值并在页面中显示出来。
出现的问题:当git的中配置文件的内容改变之后,微服务不能及时的获取更新之后的内容,需要重启服务器获取。
16.2.2 解决方案
我们已经在客户端取到了配置中心的值,但当我们修改GitHub上面的值时,服务端(Confifig Server)能实时获取最新的值,但客户端(Confifig Client)读的是缓存,无法实时获取最新值。SpringCloud已经为我们解决了这个问题,那就是客户端使用post去触发refresh,获取最新数据,需要依赖spring-boot-starter-actuator
步骤:
1⃣️添加依赖
<!--监控平台启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
2⃣️ 修改配置
# 开放所有检测端口
management:
endpoints:
web:
exposure:
include: refresh
3⃣️添加注解
在需要更新内容的controller中添加注解:@RefreshScope
@Slf4j
@RestController
@RequestMapping("/product")
@RefreshScope
public class ProductController {
4⃣️测试
http://localhost:8082/actuator/refresh 以post的方式发送请求。
注意:访问/actuator/refresh该地址,实际上会发生配置中心客户端服务的重启,是在Tomcat内部执行的。如果程序运行时,在远程仓库的配置文件中修改服务的端口号是不生效的。
问题总结:
如上案例,在更新配置文件内容之后,想要获取更新之后的内容,过程还是比较繁琐的,cloud提供了其它的方案:config可以结合 bus(消息总线) MQ,不用手动刷新,即可获取更新之后的内容。但是,该方案公司内部使用的很少,此处不做简介
16.3 配置中心高可用
引入:在分布式架构中,只要出现集中式的管理,基本上都是要考虑高可用的。配置中心服务端是其它微服务获取配置的入口,相当于集中式管理了所有的配置文件,所以,是需要进行高可用配置的。
16.3.1 配置
1⃣️服务端改造
1)将配置中心服务端注册到注册中心(之前已经注册到注册中心了)
2)启动多个配置中心服务端
2⃣️客户端改造
1)从注册中心连接配置中心服务端
spring:
cloud:
config:
# 应用名称,对应git上面配置文件${application}内容
name: product
# 环境,对应git上面配置文件${profile}内容
profile: dev
# 分支
label: master
# 配置中心服务端的地址
uri: http://localhost:8050
discovery:
# 从注册中心获取服务
enabled: true
# 服务的名称
service-id: config-server
# 配置eureka信息
eureka:
client:
service-url:
defaultZone: http://localhost:8050/eureka
17、Nacos
概述:阿里为 SpringCloud 贡献了一个子项目,叫做 SpringCloud Alibaba,其中包括了微服务开发中的几个基础组件,Nacos 就是此项目中的一项技术。nacos具备:注册中心,配置中心,服务管理(对注册到注册中心的服务进行管理)三个功能。
SpringCloud Alibaba官网
常见注册中心的比较:
17.1 使用
1⃣️安装nacos
SpringCloud 与 SpringCloud alibaba 及 springBoot 及相关组件的版本关系
1)修改startup.sh中的启动方式为standalone,将sh文件拖拽到终端中回车运行
(windows 使用命令行运行startup.cmd -m standalone,如果启动的时候不添加参数,则默认是集群方式启动。或者需要修改startup.cmd中的启动方式为standalone,这样就可以双击启动了)
出现以下语句则为启动成功:
2)运行成功后,访问http://localhost:8848/nacos可以查看Nacos的主页,默认账号密码都是nacos
2⃣️nacos客户端
1)添加依赖
<!--nacos的客户端依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2)修改配置文件
cloud:
nacos:
discovery:
# 添加nacos服务端的地址
server-addr: http://localhost:8848
username: nacos
password: nacos
3)启动测试
4)通过restTemplate远程调用
restTemplate通过服务名访问,即通过nacos的注册中心服务端获取服务,再访问对应的实例
@GetMapping("/buy/{id}")
public Result buy(@PathVariable Integer id, HttpServletRequest request){
// 1. 根据商品的ID查询商品的数据
// Result result = restTemplate.getForObject("http://localhost:8081/product/" + id, Result.class);
Result result = restTemplate.getForObject("http://product-server-nacos/product/" + id, Result.class);
// 4. 返回数据
return new Result(200, "购买成功-----" + result.getMessage(), result.getData());
}
restTemplate通过服务名访问,需要在RestTemplate上添加@LoadBalanced注解
@LoadBalanced
@Bean
public RestTemplate initRestTemplate(){
RestTemplate restTemplate = new RestTemplate();
System.out.println(restTemplate);
return restTemplate;
}
5)通过feign远程调用
添加依赖
<!--feign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
添加注解@EnableFeignClients
@SpringBootApplication
@MapperScan(basePackages = "com.woniu.order.mapper")
// 添加启动feign的注解
@EnableFeignClients
public class OrderApplication {
编写feign客户端接口
@FeignClient(value = "product-server-nacos", path = "/product")
public interface ProductFeignClient {
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id);
}
启动测试
17.2 nacos多实例
注册中心解决的其中一个问题就是负载均衡的问题,订单微服务调用了商品微服务,如果商品微服务有多台服务器,那么,就需要在订单微服务中进行一个客户端的负载均衡。之前我们用的eureka中默认集成了ribbon客户端负载均衡,nacos中也默认集成了ribbon负载均衡。
(1) 启动多个商品微服务
(2)实现负载均衡
因为nacos中默认集成了ribbon,不需要进行额外的配置及可以实现负载均衡的效果,默认的是轮询的机制。
也可以通过配置切换其它方式的负载均衡
# 指定product-server-nacos服务的负载均衡策略
product-server-nacos:
ribbon:
# 执行ribbon的负载均衡为随机的策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
注意:此种方式只能指定product-server-nacos服务的负载均衡策略,如果要指定所有的服务的负载均衡策略,那么则不能使用该种配置
解决方案:在订单微服务启动引导类中添加以下代码:
// 指定所有服务的负载均衡策略,如果配置文件中也配置了,以该配置为准
@Bean
public IRule rule(){
return new RoundRobinRule();// 轮询策略
}
17.3 nacos 控制面板(注册中心)
17.3.1 空间分组介绍
命名空间(namespace):一般用于区分不同的环境(如开发、测试、生产环境)
分组(group):一般用于区分同一个环境中的不同的项目
- 在服务的配置文件中指定命名空间和分组:
cloud:
nacos:
discovery:
# nacos服务端的地址
server-addr: http://localhost:8848
username: nacos
password: nacos
namespace: d03db0a2-3e7f-4d89-86a0-850eba6237d5
group: dev_group
17.3.2 保护阈值介绍
引入:
⼀般情况下,服务消费者要从Nacos获取可用实例有健康/不健康状态之分。Nacos在返回实例时,只会返回健康实例。
但在高并发、大流量场景会存在⼀定的问题。比如,nacos-user-service有10个实例,其中8个实例都处于不健康状态,如果Nacos只返回这两个健康实例的话。流量洪峰的到来可能会直接打垮这两个服务,进一步产生雪崩效应。
定义:本质上,保护阈值是⼀个⽐例值(当前服务健康实例数/当前服务总实例数)。Nacos中可以针对具体的实例设置一个保护阈值,值为0-1之间的浮点类型。
当某个服务健康实例数/总实例数 < 保护阈值时,说明健康的实例不多了,保护阈值会被触发(状态true)。Nacos会把该服务所有的实例信息(健康的+不健康的)全部提供给消费者,消费者可能访问到不健康的实例,请求失败,避免雪崩,起到分流的作用。牺牲了⼀些请求,保证了整个系统的可⽤。
补充:
在nacos注册的实例分为临时实例和持久化实例。
1)临时实例:临时实例向Nacos注册,Nacos不会对其进行持久化存储,只能通过心跳方式保活。默认模式是:客户端心跳上报Nacos实例健康状态,默认间隔5秒,Nacos在15秒内未收到该实例的心跳,则会设置为不健康状态,超过30秒则将实例删除。
2)持久化实例:持久化实例向Nacos注册,Nacos会对其进行持久化处理。当该实例不存在时,Nacos只会将其健康状态设置为不健康,但并不会对将其从服务端删除。
17.4 nacos配置中心
作用:和Spring Cloud Config 相同,但读写性能更为强大。并且无需额外发送post请求到http://localhost:8082/actuator/refresh去触发refresh。
官方文档
17.4.1 使用
1⃣️ 添加配置文件(使用properties格式)
2⃣️添加依赖
<!--nacos配置中心的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
3⃣️创建配置文件
必须使用bootstrap作为配置文件的名称
# 指定nacos配置中心的地址
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
4⃣️打印测试
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext context = SpringApplication.run(OrderApplication.class, args);
while(true){
String name = context.getEnvironment().getProperty("user.name");
String password = context.getEnvironment().getProperty("user.password");
System.err.println("user name :" + name + "; password: " + password);
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
}
}
当配置文件中内容修改时,可以动态感知到。
补充:配置中心默认只能读取properties格式的配置文件,如果要指定其它的格式的配置文件,则需要进行配置,配置如下:
# 指定nacos配置中心的地址
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
config:
# 指定读取配置文件的格式
file-extension: yaml
17.4.2 profile粒度配置
profile粒度的配置主要是针对于不同的环境进行配置,比如开发,测试,生产环境都有自己的配置文件。
配置文件名称格式:
${prefix}- ${spring.profiles.active}. ${file-extension}
prefix: 默认为spring.application.name的值,也可以通过配置项 spring.cloud.nacos.config.prefix
来配置。
spring.profiles.active:当前环境对应的 profile(可填写命名空间),当 spring.profiles.active
为空时,对应的连接符 -
也将不存在,dataId 的拼接格式变成 ${prefix}.${file-extension}
file-exetension:为配置内容的数据格式,可以通过配置项 spring.cloud.nacos.config.file-extension
来配置。目前只支持 properties
和 yaml
类型。
配置中心配置:
# 指定nacos配置中心的地址
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
config:
# 指定读取配置文件的格式
file-extension: yaml
profiles:
# 激活指定的配置文件
active: pro
17.4.3 自定义通用配置
一些通用的配置没有必要的每一个配置文件中都配置一份,所以,在提取出来,然后在每个配置文件中引用即可。
1.操作步骤
1)创建一个通用的配置文件
2)引用通用的配置文件
# 指定nacos配置中心的地址
spring:
cloud:
nacos:
discovery:
server-addr: http://localhost:8848
username: nacos
password: nacos
config:
# 指定读取配置文件的格式
file-extension: yaml
shared-configs:
# 通用的配置文件的ID,一定要带上后缀名
- data-id: cloud.demo.common.yaml
# 自动刷新
refresh: true
3)获取配置,打印测试
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext context = SpringApplication.run(OrderApplication.class, args);
while(true){
String name = context.getEnvironment().getProperty("user.name");
String password = context.getEnvironment().getProperty("user.password");
System.err.println("user name :" + name + "; password: " + password);
String host = context.getEnvironment().getProperty("spring.redis.host");
String port = context.getEnvironment().getProperty("spring.redis.port");
System.err.println("redis host :" + host + "; port: " + port);
// 休眠3秒
TimeUnit.SECONDS.sleep(3);
}
}
2.配置的优先级
通用配置优先级低于独有配置
17.4.4 @RefreshScope注解
@Value注解可以获取到配置中心的值,但是无法动态感知修改后的值,需要利用@RefreshScope注解
@Slf4j
@RestController
@RequestMapping("/order")
@RefreshScope
public class OrderController {
@Autowired
private IOrderService orderService;
@Autowired
private RestTemplate restTemplate;
@Autowired
private ProductFeignClient productFeignClient;
@Value("${user.name}")
private String name;
@GetMapping("/{id}")
public Result findById(@PathVariable Integer id){
Order order = orderService.getById(id);
return new Result(200, "查询订单信息成功: name = " + name, order);
}
17.4.5 使用mysql存储配置信息
Nacos单机模式默认使用内嵌的数据库作为存储引擎,不方便观察数据存储的基本情况。
配置步骤:
1⃣️使用mysql5创建以nacos为名的数据库,字符集为utf8,排序规则为utf8_general_ci
2⃣️运行nacos\conf\目录下的nacos-mysql.sql 文件
3⃣️修改同一目录下的application.properties文件
### If use MySQL as datasource:
spring.datasource.platform=mysql
### Count of DB:
db.num=1
### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=root
18、rabbitMQ基础
18.1 消息队列MQ简介
概述:Message Queue(消息队列),是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。
作用:
-
应用解耦:提高系统容错性和可维护性
-
异步提速:提升用户体验和系统吞吐量
-
削峰填谷:提高系统稳定性
缺点:
- 系统复杂度提高
MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。 - 系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。 - 一致性问题
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理
失败。
常见的消息队列
1.RabbitMQ
RabbitMQ2007年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。
2.ActiveMQ
ActiveMQ是由Apache出品,ActiveMQ 是一个完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现。它非常快速,支持多种语言的客户端和协议,而且可以非常容易的嵌入到企业的应用环境中,并有许多高级功能。
3.RocketMQ
RocketMQ出自 阿里公司的开源产品,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一些改进,消息可靠性上比 Kafka 更好。RocketMQ在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。
4.Kafka
Apache Kafka是一个分布式消息发布订阅系统。它最初由LinkedIn公司基于独特的设计实现为一个分布式的提交日志系统( a distributed commit log),,之后成为Apache项目的一部分。Kafka系统快速、可扩展并且可持久化。它的分区特性,可复制和可容错都是其不错的特性。
18.2 rabbitMQ 简介
官网
概述:RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
架构图:
生产者:负责消息发送,发送到队列
交换机:交换机本身并不存储消息,只做消息的转发
队列:存储消息,先进先出
消费者:监听某个队列中的消息,获取消息并确认,最后处理业务
18.3 安装
(1)macOS安装
1⃣️使用homebrew 安装,自动安装配置erlang的环境变量
brew install rabbitmq
2⃣️访问测试
打开浏览器访问网站http://localhost:15672进入登录页面,默认账号和密码都为guest
(2) docker 安装
docker pull rabbitmq:management
docker run -itd --net host --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=123456 -p 15672:15672 -p 5672:5672 rabbitmq:management
18.4 消息模型
RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
rabbitmq引入步骤:
1⃣️引入依赖
<!--rabbitmq启动器依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2⃣️编写配置文件
spring:
rabbitmq:
# 主机名
host: 192.168.106.139
# 端口号
port: 5672
# 虚拟主机(相当于数据库的库)
virtual-host: my_vhost
# 用户名
username: admin
# 密码
password: admin
18.4.1 简单模型
最简单的消息模型,一个生产者对应一个消费者。
1. 基于原始的java代码
1)生产者
public class ProducerDemo {
public static void main(String[] args) throws Exception {
// 1. 创建连接(connection),需要设置相关参数
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.106.139");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
// 2. 创建信道(channel)
Channel channel = connection.createChannel();
// 3. 申明队列(queue)
/**
* 参数:
* 第一个参数:队列的名称
* 第二个参数:队列是否持久化,默认不是持久化(存在内存中) 如果持久化的话,存在磁盘中 true:持久化 false:不持久化
* 第三个参数:该队列是否共享消息(是否只能被一个消费者消费)true:可以被多个消费者消费 false:不可以
* 第四个参数:是否自动删除。最后一个消费者断开连接后,是否自动删除该队列。 true:自动删除 false:不自动删除
* 第五个参数:其它信息(比如可以配置死信队列)
*/
channel.queueDeclare("hello", true, false, false, null);
// 4. 发送消息,通过信道发送消息
String message = "hello world";
/**
* 参数:
* 第一个参数: 交换机的名称(简单模式可以使用默认的交换机)
* 第二个参数: 路由key,简单模式可以写队列的名称
* 第三个参数: 其它参数
* 第四个参数: 需要发送的消息
*/
channel.basicPublish("", "hello", null, message.getBytes());
System.out.println("生产者发送消息成功,发送的消息为:" + message);
}
}
2)消费者
public class ConsumerDemo {
public static void main(String[] args) throws Exception {
// 1. 创建连接(connection),需要设置相关参数
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.106.139");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
// 2. 创建信道(channel)
Channel channel = connection.createChannel();
// 3. 消费消息
// 定义接收消息回调consumerTag: 通道的名称 delivery: 里面包含了消息内容
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("消费者获取到的消息为:" + message);
};
// 定义取消接收消息回调
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消费者取消消息接收");
};
/**
* 参数:
* 第一个参数: 队列的名称
* 第二个参数: 是否自动应道 true:自动应答 false:不自动应答
* 第三个参数: 接收消息的回调
* 第四个参数: 取消消息的回调
*/
channel.basicConsume("hello", true, deliverCallback, cancelCallback);
}
}
2. 基于springBoot
1)生产者
@RestController
@RequestMapping("/work")
public class WorkerProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public void sendMessage(String message){
// 需要往队列中发送一个消息
/**
* 参数:
* 第一个参数:交换机名称
* 第二个参数:路由key(此处是队列的名称)
* 第三个参数:发送的内容
* 说明:如果队列不存在,则不会自动创建队列
*/
rabbitTemplate.convertAndSend("", "hello_boot", message);
}
}
2)消费者
@Component
public class WorkerConsumer {
/**
* @RabbitListener: 消息监听器注解
* 作用:
* 1. 监听指定队列里面的消息(需要设置监听的队列 queues:不会创建队列 queuesToDeclare:队列不存在,则会创建队列)
* 2. mq会自动将消息设置给该方法的形参(形参的名字可以随意设置)
*/
//@RabbitListener(queues = {"hello"})
@RabbitListener(queuesToDeclare = {@Queue(value = "hello_boot")})
public void getMessage(String message){
System.out.println("消费者C1消费的消息为:" + message);
}
}
18.4.2 工作队列模型(Work queues)
本质也为简单消息模型。不同的是一个生产者对应多个消费者。
当消息的生产速度远超消息的消费速度时候,需要使用工作队列模式,多个消费者(创建多个消费同一个队列的消费者)共同处理消息。
注意:多个消费者是竞争关系,也就是同一个消息要么被C1消费,要么被C2消费。
两个消费者:
public class ConsumerDemo {
public static void main(String[] args) throws IOException, TimeoutException {
// 1. 创建连接(connection),需要设置相关参数
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("39.108.216.146");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("YangXinZhe233!");
Connection connection = connectionFactory.newConnection();
// 2. 创建信道(channel)
Channel channel = connection.createChannel();
// 3. 消费消息
// 定义接收消息回调consumerTag: 通道的名称 delivery: 里面包含了消息内容
DeliverCallback deliverCallback = ( consumerTag, delivery) ->{
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("消费者获取到的消息为:"+message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
// 定义取消接收消息回调
CancelCallback cancelCallback = (consumerTag) ->{
System.out.println("消费者取消消息接收");
};
/**
* 参数:
* 第一个参数: 队列的名称
* 第二个参数: 是否自动应道 true:自动应答 false:不自动应答
* 第三个参数: 接收消息的回调
* 第四个参数: 取消消息的回调
*/
// 设置不公平分发值为2; 公平分发值为1;
channel.basicQos(1);
channel.basicConsume("queue2",false,deliverCallback,cancelCallback);
}
}
public class ConsumerDemo2 {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1. 创建连接(connection),需要设置相关参数
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("39.108.216.146");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("YangXinZhe233!");
Connection connection = connectionFactory.newConnection();
// 2. 创建信道(channel)
Channel channel = connection.createChannel();
// 3. 消费消息
// 定义接收消息回调consumerTag: 通道的名称 delivery: 里面包含了消息内容
DeliverCallback deliverCallback = ( consumerTag, delivery) ->{
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("消费者2获取到的消息为:"+message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
// 定义取消接收消息回调
CancelCallback cancelCallback = (consumerTag) ->{
System.out.println("消费者取消消息接收");
};
/**
* 参数:
* 第一个参数: 队列的名称
* 第二个参数: 是否自动应道 true:自动应答 false:不自动应答
* 第三个参数: 接收消息的回调
* 第四个参数: 取消消息的回调
*/
channel.basicQos(1);
channel.basicConsume("queue2",false,deliverCallback,cancelCallback);
}
}
当其中一个消费者处理速度较慢,可以设置channel.basicQos(1)来将更多消息分发给其他消费者。
18.4.3 扇出(群发)(fanout )
引入:工作队列模式中,一个生产者只能给一个队列发送消息(或者说一个生产者生产的消息只能发送到一个队列中)。如果一个生产者想要给多个队列发送消息,其实是没有办法做到的。也可以理解为,生产者与队列是一对一的关系,耦合死的。
扇出模式:通过新增一个交换机给多个队列发送消息,实现群发。但没有路由,不需要指定路由key。
![在这里插入图片描述](https://img-blog.csdnimg.cn/5e15a300b12c4886a4b0302d1de1f071.png#pic_center
代码实现:
1)生产者
@RestController
@RequestMapping("/fanout")
public class FanoutProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public void sendMessage(String message) throws IOException {
// 需要往队列中发送一个消息
/**
* 参数:
* 第一个参数:交换机名称
* 第二个参数:路由key(此处是队列的名称)
* 第三个参数:发送的内容
* 说明:如果队列不存在,则不会自动创建队列
*/
rabbitTemplate.convertAndSend("test_fanout", "", message);
}
}
2)消费者
@Component
public class FanoutConsumer {
/**
* bindings: 交换机与队列的绑定关系
* @QueueBinding: 对于的绑定关系
* value: 指定路由
* exchange: 指定交换机
*/
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "zhangsan"),
exchange = @Exchange(name = "test_fanout", type = ExchangeTypes.FANOUT)
)
})
public void getMessage01(String message){
System.out.println("消费者C1消费的消息为:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "lisi"),
exchange = @Exchange(name = "test_fanout", type = ExchangeTypes.FANOUT)
)
})
public void getMessage02(String message){
System.out.println("消费者C2消费的消息为:" + message);
}
}
18.4.4 direct 路由模式
引入:fanout模式中,一个生产者能给所有绑定到该交换机中的队列发送消息(全体都有),如果只想给其中的一部分队列发送消息就没有办法做到了,此时,就需要使用路由模式了。
路由模式通过路由,绑定路由key,队列与交换机绑定,发送消息时在消息生产者端指定路由key来实现定向发送消息到指定队列。
代码实现
1)生产者
@RestController
@RequestMapping("/direct")
public class DirectProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public void sendMessage(String message, String key) throws IOException {
// 需要往队列中发送一个消息
/**
* 参数:
* 第一个参数:交换机名称
* 第二个参数:路由key(此处是队列的名称)
* 第三个参数:发送的内容
* 说明:如果队列不存在,则不会自动创建队列
*/
rabbitTemplate.convertAndSend("test_direct", key, message);
}
}
2)消费者
@Component
public class RedirectConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "wangwu"),
exchange = @Exchange(name = "test_direct", type = ExchangeTypes.DIRECT),
key = "wangwu"
)
})
public void getMessage01(String message){
System.out.println("王五消费的消息为:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "zhaoliu"),
exchange = @Exchange(name = "test_direct", type = ExchangeTypes.DIRECT),
key = "zhaoliu"
)
})
public void getMessage02(String message){
System.out.println("赵六消费的消息为:" + message);
}
}
18.4.5 topic 主题模式
引入:
direct模式中,一个生产者能给所有绑定到该交换机中的部分队列发送消息,但是key是完全匹配的,不够灵活,所以,需要更加灵活的配置,则可以使用topic模式(相当于正则表达式匹配)。
例如:当topic模式消费者端代码的routingkey 为“zhang.“时,表示发送消息时,以"zhang."为开头的routingkey的生产者消息都会发送到该消费者。但以“zhang.xx.xx"为开头则不可行。需要将消费者端代码的routingkey 由“zhang.“改为”zhang.#“。表示匹配全部。
2.6.2 代码实现
1)生产者
@RestController
@RequestMapping("/topic")
public class TopicProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public void sendMessage(String message, String key) throws IOException {
// 需要往队列中发送一个消息
/**
* 参数:
* 第一个参数:交换机名称
* 第二个参数:路由key(此处是队列的名称)
* 第三个参数:发送的内容
* 说明:如果队列不存在,则不会自动创建队列
*/
rabbitTemplate.convertAndSend("test_topic", key, message);
}
}
2)消费者
@Component
public class TopicConsumer {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "zhangxing"),
exchange = @Exchange(name = "test_topic", type = ExchangeTypes.TOPIC),
key = "zhang.*"
)
})
public void getMessage01(String message){
System.out.println("张消费的消息为:" + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "lixing"),
exchange = @Exchange(name = "test_topic", type = ExchangeTypes.TOPIC),
key = "li.*"
)
})
public void getMessage02(String message){
System.out.println("李消费的消息为:" + message);
}
}
19、rabbitMQ高级
19.1 生产端创建交换机及队列
问题引入:
先前创建队列、交换机、队列绑定到交换机实在消费者这一端完成的,比如:
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(value = "wangwu"),
exchange = @Exchange(name = "test_direct", type = ExchangeTypes.DIRECT),
key = "wangwu"
)
})
public void getMessage01(String message){
System.out.println("王五消费的消息为:" + message);
}
必须要先启动消费者,创建完队列、交换机后,再执行生产者发消息。不方便测试发送消息。
解决:
方案一:管理界面端创建及绑定
方案二:生产者端创建绑定
代码实现:
1⃣️创建交换机,队列 及 绑定关系
/**
* 创建交换机,队列 及 绑定关系
*/
@Configuration
public class RabbitInitConfig {
//创建direct模式交换机
@Bean
public Exchange exchange(){
return new DirectExchange("test_direct",true,false,null);
}
//创建队列
@Bean
public Queue queueZhaoLiu(){
return new Queue("zhaoliu",true,false,false,null);
}
//创建队列
@Bean
public Queue queueWangQi(){
return new Queue("wangqi",true,false,false,null);
}
//创建交换机和队列的绑定关系
@Bean
public Binding bindingZhaoLiu(@Qualifier(value = "queueZhaoLiu") Queue queue,Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("zhaoliu").noargs();
}
//创建交换机和队列的绑定关系
@Bean
public Binding bindingWangQi(@Qualifier(value = "queueWangQi") Queue queue,Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("wangqi").noargs();
}
}
2⃣️消费者端直接绑定队列
@Component
public class DirectConsumer {
/**
* bindings: 交换机与队列的绑定关系
* @QueueBinding: 队列的绑定关系
* value: 指定路由
* exchange: 指定交换机
*/
/* @RabbitListener(bindings = {
@QueueBinding(
value = @Queue(name = "zhaoliu"),
exchange = @Exchange(name = "test_direct",type = ExchangeTypes.DIRECT),
key = "zhaoliu"
)
})*/
@RabbitListener(queues = "zhaoliu")
public void getMessage01(String message){
System.out.println("消费者C1消费的消息为:"+message);
}
/*@RabbitListener(bindings = {
@QueueBinding(
value = @Queue(name = "wangqi"),
exchange = @Exchange(name = "test_direct",type = ExchangeTypes.DIRECT),
key = "wangqi"
)
})*/
@RabbitListener(queues = "wangqi")
public void getMessage02(String message){
System.out.println("消费者C2消费的消息为:"+message);
}
}
19.2 消息可靠性保证(保证消息不丢失)
主要在以下环节保证消息不丢失:
- 生产者到交换机
- 生产者到队列
- rabbitMQ本身
- 队列到消费者
19.2.1生产者保障
(1)添加配置
# 开启生产者确认回调
publisher-confirm-type: correlated
(2) 交换机确认
@Component
public class CallBackConfig implements RabbitTemplate.ConfirmCallback {
// 特别注意 需要将CallBackConfig注入到RabbitTemplate中
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
*
* @param correlationData 发送的数据及消息的ID
* @param ack 交换机接收消息的状态 false:表示交换机接收失败 true : 表示交换机接收成功
* @param cause 交换机返回的原因
*/
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){// 表示交换机接收成功
System.out.println("交换机接收成功");
}else{// 表示交换机接收失败
System.out.println("交换机接收失败,失败的原因为:" + cause);
System.out.println("交换机接收失败的消息为:" + new String(correlationData.getReturnedMessage().getBody()));
// TODO 可以重发消息或者将消息存储到Redis中记录 或者 使用备用交换机
}
}
}
(2)生产者
@GetMapping("/send")
public void sendMessage(String message, String routingKey){
// 创建correlationData,用于保存发送的信息
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
Message mess = new Message(message.getBytes());
correlationData.setReturnedMessage(mess);
rabbitTemplate.convertAndSend("confirm_exchange1", routingKey, message, correlationData);
}
(3) 队列确认
@Component
public class CallBackConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
// 特别注意 需要将CallBackConfig注入到RabbitTemplate中
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
// 设置队列接收消息失败,将消息返还给生产者。默认是false,将消息丢弃
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(this);
}
/**
*
* @param correlationData 发送的数据及消息的ID
* @param ack 交换机接收消息的状态 false:表示交换机接收失败 true : 表示交换机接收成功
* @param cause 交换机返回的原因
*/
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){// 表示交换机接收成功
System.out.println("交换机接收成功");
}else{// 表示交换机接收失败
System.out.println("交换机接收失败,失败的原因为:" + cause);
System.out.println("交换机接收失败的消息为:" + new String(correlationData.getReturnedMessage().getBody()));
// TODO 可以重发消息或者将消息存储到Redis中记录 或者 使用备用交换机
}
}
/**
*
* @param message 回退的消息
* @param replyCode 失败码
* @param replyText 失败原因
* @param exchange 交换机名称
* @param routingKey 路由key
*/
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("发送到队列失败失败的消息为:" + new String(message.getBody()) + ", 失败的原因为:" + replyText +
", 交换机为:" + exchange + ", 路由key为:" + routingKey);
}
}
19.2.2 rabbitmq保障
队列持久化
创建队列的第二参数即是队列的持久化
@Bean
public Queue confirmQueue(){
return new Queue("confirm_queue", true, false, false, null);
}
19.2.3 消费者保障
问题引入-重试机制
如果消费者无法正确的消费消息,会导致消费者一直在循环消费消息,我们可以配置最大的重试次数,防止消费者循环报错。
这里指定最大重试次数,解决了异常的死循环问题。但是重试3次还是消费失败后,消息就会丢弃,造成消息丢失。一般结合死信队列使用。
server:
port: 8081
spring:
rabbitmq:
# 主机名
host: 192.168.106.139
# 端口号
port: 5672
# 虚拟主机(相当于数据库的库)
virtual-host: /
# 用户名
username: admin
# 密码
password: 123456
listener:
simple:
retry:
# 开启最大重试次数
enabled: true
# 最大重试次数
max-attempts: 3
问题引入-消息幂等性
面临的问题:重试机制有可能造成一条消息被重复消费。也就是经常被问到的问题,mq中如何保证消息不会被重复消费。可以采用ID+Redis的策略
1)生产端给每一条消息添加一条ID
@GetMapping("/send")
public void sendMessage(String message, String routingKey){
// 创建correlationData,用于保存发送的信息
String uuid = UUID.randomUUID().toString();
CorrelationData correlationData = new CorrelationData(uuid);
Message mess = new Message(message.getBytes());
// 设置该条消息的唯一ID
mess.getMessageProperties().setMessageId(uuid);
correlationData.setReturnedMessage(mess);
MessagePostProcessor messagePostProcessor = mess2 -> {
mess2.getMessageProperties().setMessageId(uuid);
System.out.println("生产的消息ID为:" + uuid);
return mess2;
};
rabbitTemplate.convertAndSend("confirm_exchange", routingKey, message, messagePostProcessor, correlationData);
}
2)消费端去判断该ID的消息是否已经被消费
@RabbitListener(queues = "confirm_queue")
public void getMessage(String message, Message mess){
System.out.println("消费者ConfirmConsumer接收到的消息为:" + message);
// 如果此时保存,则不会处理本地业务
//int i = 1 / 0;
// 处理相关业务
// 处理完业务将消息设置到Redis中,使用setnx命令
// 获取消息的ID
String messageId = mess.getMessageProperties().getMessageId();
System.out.println("获取的消息ID为:" + messageId);
}
解决上述问题之后,即可开始配置消费者保障
1)消费端手动应达机制,修改配置
spring:
rabbitmq:
# 主机名
host: 192.168.106.139
# 端口号
port: 5672
# 虚拟主机(相当于数据库的库)
virtual-host: /
# 用户名
username: admin
# 密码
password: 123456
listener:
simple:
retry:
# 开启最大重试次数
enabled: true
# 最大重试次数
max-attempts: 3
# 开启消费端手动应答
acknowledge-mode: manual
2)代码实现
@RabbitListener(queues = "confirm_queue")
public void getMessage(String message, Message mess, Channel channel) throws IOException {
// 是否重投
Boolean redelivered = mess.getMessageProperties().getRedelivered();
System.out.println("是否重投的标记:" + redelivered);
try {
int i = 1 / 0;
System.out.println("消费者ConfirmConsumer接收到的消息为:" + message);
String messageId = mess.getMessageProperties().getMessageId();
System.out.println("消费者ConfirmConsumer接收到的消息ID为: "+ messageId);
// TODO 根据该messageId从redis中查询,若不存在,处理本地业务,处理结束将该id保存到redis中;若存在,不处理
// 如果正常接收到消息,则确认收到消息
channel.basicAck(mess.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
e.printStackTrace();
if(redelivered){// 已经重投过
/**
* 参数1: 消息的标识
* 参数2: true: 继续重试 false:放入死信队列
*/
channel.basicReject(mess.getMessageProperties().getDeliveryTag(), false);
System.out.println("消息消费异常,直接作废,添加到死信队列");
}else{
/**
* 参数1: 消息的标识
* 参数2: 是否批量确认
* 参数3: true: 继续重试 false:放入死信队列
*/
channel.basicNack(mess.getMessageProperties().getDeliveryTag(), false, true);
System.out.println("消息消费异常,消息进行重投");
}
}
/*// 如果此时保存,则不会处理本地业务
//int i = 1 / 0;
// 处理相关业务
// 处理完业务将消息设置到Redis中,使用setnx命令
// 获取消息的ID
String messageId = mess.getMessageProperties().getMessageId();
System.out.println("获取的消息ID为:" + messageId);*/
}
19.3 死信队列
在消息可靠性保证中的消费者保障,消息重试失败后,就会将消息拒绝,如果不作处理,就会造成消息丢失。死信队列可以用来存储该类无法被消费的消息。
哪些消息会放入到死信队列:
- 被拒绝的消息: channel.basicReject() 并且 requeue=false
- 消息过期 TTL
params.put("x-message-ttl", 10000);
- 消息达到最大长度
params.put("x-max-length", 3);
代码演示
1)配置死信队列
@Configuration
public class ConfirmInitConfig {
@Bean
public Exchange confirmExchange(){
return new DirectExchange("confirm_exchange");
}
@Bean
public Queue confirmQueue(){
//return new Queue("confirm_queue", true, false, false, null);
// 该队列与死信交换机进行绑定
Map<String,Object> params = new HashMap<>();
// 设置过期时间
params.put("x-message-ttl", 10000);
// 设置死信交换机
params.put("x-dead-letter-exchange","xdlExchange");
// 设置路由key
params.put("x-dead-letter-routing-key","xdl");
return new Queue("confirm_queue", true, false, false, params);
}
@Bean
public Binding confirmBinding(@Qualifier("confirmQueue") Queue queue, @Qualifier("confirmExchange") Exchange exchange ){
return BindingBuilder.bind(queue).to(exchange).with("confirm").noargs();
}
// 创建死信交换机
@Bean
public Exchange xdlExchange(){
return new DirectExchange("xdlExchange");
}
// 创建死信队列
@Bean
public Queue xdlQueue(){
return new Queue("xdlQueue", true, false, false, null);
}
// 将死信队列绑定到死信交换机上
@Bean
public Binding xdlBinding(@Qualifier("xdlQueue") Queue queue, @Qualifier("xdlExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("xdl").noargs();
}
}
19.4 传递对象类型
代码演示:
(1)在启动引导类配置消息转换器(生产者与消费者都要添加)
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
(2)定义对象
@Data
public class User {
private String username;
private String password;
private Integer age;
}
(3)发送对象消息
@RestController
@RequestMapping("/obj")
public class ObjProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/send")
public void sendMessage(){
User user = new User();
user.setUsername("admin");
user.setPassword("admin");
user.setAge(21);
rabbitTemplate.convertAndSend("", "obj_queue",user);
}
}
(4)接收对象消息
@Component
public class ObjConsumer {
@RabbitListener(queuesToDeclare = {@Queue("obj_queue")})
public void getMessage(User user){
System.out.println(user);
}
}
20、分布式事务
引入:
当用户再次下单时,需同时对订单库 order、库存库 storage、用户库 account 进行操作,可此时我们只能保证自己本地的数据一致性,无法保证调用其他服务的操作是否成功,所以为了保证整个下单流程的数据一致性,就需要分布式事务介入。
概述:
在分布式系统中一次操作需要由多个服务协同完成,这种由不同的服务之间通过网络协同完成的事务称为分布式事务。
20.1 分布式事务类型
20.1.1 强一致性
数据更新成功后,系统不承诺立即可以读到最新写入的值,也不承诺具体多久之后可以读到。
20.1.2 弱一致性
在两个操作进行时,一个操作要读取另一个操作的资源,可能会读取到更新前的资源也可能读取到更新后的资源。
20.1.3 最终一致性
弱一致性的一种形式,数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。
20.2 分布式解决方案
20.2.1 2PC
1)概述
2PC ( Two-Phase Commit缩写),两阶段提交,将事务的提交过程分为资源准备和资源提交两个阶段,并且由事务协调者来协调所有事务参与者,如果准备阶段所有事务参与者都预留资源成功,则进行第二阶段的资源提交,否则事务协调者回滚资源。
2)流程描述
3)总结
二阶段提交确实能够提供原子性的操作,但是还是有几个缺点的:
(1)性能问题:执行过程中,所有参与节点都是事务阻塞性的,当参与者占有公共资源时,其他第三方节点访问公共资源就不得不处于阻塞状态,为了数据的一致性而牺牲了可用性,对性能影响较大,不适合高并发高性能场景
(2)可靠性问题:2PC非常依赖协调者,当协调者发生故障时,尤其是第二阶段,那么所有的参与者就会都处于锁定事务资源的状态中,而无法继续完成事务操作(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
(3)数据一致性问题:在阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。
(4)二阶段无法解决的问题:协调者在发出 commit 消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了,那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
20.2.2 3PC
1)概述
3PC,三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点:
- 在协调者和参与者中都引入超时机制
- 在第一阶段和第二阶段中插入一个准备阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
所以3PC会分为3个阶段,CanCommit 准备阶段、PreCommit 预提交阶段、DoCommit 提交阶段
2)流程描述
3)总结
与2PC相比,3PC降低了阻塞范围,并且在等待超时后,协调者或参与者会中断事务,避免了协调者单点问题,阶段三中协调者出现问题时,参与者会继续提交事务。
数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者因为网络问题无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
20.3 seata
概述:Simple Extensible Autonomous Transaction Architecture 的缩写,由 feascar 改名而来。Seata 是阿里开源的分布式事务框架,属于二阶段提交模式。
20.3.1 名词解释
- TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
- RM ( Resource Manager ) - 资源管理器:管理分支事务处理的资源( Resource ),与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。
20.3.2 前提
- 基于支持本地 ACID 事务的关系型数据库(MySQL版本需要5.6以上)。
- Java 应用,通过 JDBC 访问数据库。
20.3.3 整体机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
- TM 请求 TC,开始一个新的全局事务,TC 会为这个全局事务生成一个 XID。
- XID 通过微服务的调用链传递到其他微服务。
- RM 把本地事务作为这个XID的分支事务注册到TC。
- TM 请求 TC 对这个 XID 进行提交或回滚。
- TC 指挥这个 XID 下面的所有分支事务进行提交、回滚。
20.3.4 配置实现
- TC端处理
1)去GitHub下载,解压缩即可
2)修改配置文件
registry.conf 主要配置nacos的信息
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 采用nacos注册服务,进行事务管理
type = "nacos"
nacos {
# 应用的名称
application = "seata-server"
# nacos的地址
serverAddr = "127.0.0.1:8848"
# 分组名称
group = "DEFAULT_GROUP"
# 命名空间
namespace = "862bbf0e-1d94-4bd3-80be-ed4d1d690bef"
# 集群名称
cluster = "default"
# nacos账号
username = "nacos"
# nacos密码
password = "nacos"
}
2)file.conf:主要配置数据库相关的信息
添加以下内容:
## transaction log store, only used in seata-server
service {
# vgroup->rgroup
# 自定义事务组名称为:my_tx_group,通过名字区分每个事物的具体操作,和客户端自定义的名称对应
vgroup_mapping.my_tx_group = "default"
#only support single node
default.grouplist = "127.0.0.1:8091"
#disable
disableGlobalTransaction = false
}
修改以下内容:
store {
## store mode: file、db、redis
mode = "file"
## rsa decryption public key
publicKey = ""
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://127.0.0.1:3306/seata?rewriteBatchedStatements=true"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
3)新建seata数据库,再执行SQL脚本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for branch_table
-- ----------------------------
DROP TABLE IF EXISTS `branch_table`;
CREATE TABLE `branch_table` (
`branch_id` bigint(20) NOT NULL,
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`resource_group_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`branch_type` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`status` tinyint(4) NULL DEFAULT NULL,
`client_id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime(6) NULL DEFAULT NULL,
`gmt_modified` datetime(6) NULL DEFAULT NULL,
PRIMARY KEY (`branch_id`) USING BTREE,
INDEX `idx_xid`(`xid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of branch_table
-- ----------------------------
-- ----------------------------
-- Table structure for global_table
-- ----------------------------
DROP TABLE IF EXISTS `global_table`;
CREATE TABLE `global_table` (
`xid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`status` tinyint(4) NOT NULL,
`application_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_service_group` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_name` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`timeout` int(11) NULL DEFAULT NULL,
`begin_time` bigint(20) NULL DEFAULT NULL,
`application_data` varchar(2000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`xid`) USING BTREE,
INDEX `idx_gmt_modified_status`(`gmt_modified`, `status`) USING BTREE,
INDEX `idx_transaction_id`(`transaction_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Table structure for lock_table
-- ----------------------------
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`xid` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`transaction_id` bigint(20) NULL DEFAULT NULL,
`branch_id` bigint(20) NOT NULL,
`resource_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`table_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`pk` varchar(36) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`gmt_create` datetime NULL DEFAULT NULL,
`gmt_modified` datetime NULL DEFAULT NULL,
PRIMARY KEY (`row_key`) USING BTREE,
INDEX `idx_branch_id`(`branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
-- ----------------------------
-- Records of lock_table
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;
- RM端处理
需要用到分布式事务的工程都需要做以下三个操作:
1)添加undo_log表
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime(0) NOT NULL,
`log_modified` datetime(0) NOT NULL,
`ext` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
2)添加依赖
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<!--版本较低,1.3.0,因此排除-->
<exclusion>
<artifactId>seata-spring-boot-starter</artifactId>
<groupId>io.seata</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<!--seata starter 采用1.4.2版本-->
<version>1.4.2</version>
</dependency>
3)修改配置文件
# seata相关配置
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
namespace: 862bbf0e-1d94-4bd3-80be-ed4d1d690bef # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
cluster: default
tx-service-group: seata-demo # 事务组名称
service:
vgroup-mapping: # 事务组与cluster的映射关系
seata-demo: default
- TM端处理
添加注解
业务方法上添加@GlobalTransactional,管理全局事物