赞
踩
1998年,加州大学的计算机科学及Eric Brewer提出,分布式系统有三个指标:
Eric Brewer说,分布式系统无法同时满足这三个指标。这个结论就叫做CAP定理。
Consistency
Consistency(一致性):用户当问分布式系统中的任意节点,得到的数据必须一致
Availability
Avaliability(可用性):用户访问分布式系统时,读或写操作总能成功。只能读不能写,或者只能写不能读,或者两者都不能执行,就说明系统弱可用或不可用。
Partition tolerance
Partition(分区):因为网络故障或其他原因导致分布式系统中的部分节点与其他节点失去连接,形成独立分区。
tolerance(容错):系统要能容忍网络分区现象,出现分区时,整个系统也要持续对外提供服务。
BASE理论是对CAP的一种解决思路,包含三个思想:
而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:
AT模式原理
例如,一个分支业务的SQL是这样的:update tb_account set money = money - 10 where id = 1
第一阶段是记录数据快照,执行并提交事务:
第二阶段根据阶段一的结果来判断:
AT模式的脏写问题
这种模式在大多数情况下并不会有什么问题,不过在极端情况下,特别是多线程并发访问AT模式的分布式事务时,有可能出现脏写问题,如图:(丢失更新)
AT模式的写隔离
解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务操作当前数据。
极端情况:
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码实现数据恢复。需要实现三个方法:
举例,一个扣减用户余额的业务。假设账户A原来是100,需要余额扣减30元。
初始余额:
余额充足,可以冻结:
TCC模式原理
TCC的工作模型图:
假如一个分布式事务中包含两个分支事务,try阶段,一个分支成功执行,另一个分支事务阻塞:
如果阻塞时间过长,可能导致全局事务超时而触发二阶段的cancel操作。两个分支事务都会执行cancel操作:
要知道,其中一个分支是未执行try操作的,直接执行了cancel操作,反而会导致数据错误。因此,这种情况下,尽管cancel方法要执行,但其中不能做任何回滚操作,这就是空回滚。
对于整个空回滚的分支事务,将来try方法阻塞结束依然会执行。但是整个全局事务其实已经结束了,因此永远不会再有confirm或cancel,也就是说这个事务执行了一半,处于悬挂状态,这就是业务悬挂问题。
以上问题都需要我们在编写try、cancel方法时处理
TCC的优点是什么?
TCC的缺点是什么?
最大努力通知是一种最终一致性的分布式事务解决方案。顾名思义,就是通过消息通知的方式来通知事务参与者完成业务执行,如果执行失败会多次通知。无需任何分布式事务组件介入。
企业实际开发中,往往会搭建多个运行环境,例如:开发环境、测试环境、发布环境。不同环境之间需要隔离。或者不同项目使用了一套Nacos,不同项目之间要做环境隔离。
nacos提供了一个默认的namespace,叫作public
默认所有的服务和配置都属于这个namespace,当然我们也可以自己创建新的namespace:
添加完成后,可以在页面看到我们新建的namespace,并且Nacos为我们自动生成了一个命名空间id:
切换到配置列表页,发现dev这个命名空间下没有任何配置(因为我们之前添加的所有配置都在public下)
默认情况下,所有的微服务注册发现、配置管理都是走public这个命名空间。如果要指定命名空间则需要修改application.yml文件。
比如,修改item-service服务的bootstrap.yml文件,添加服务发现配置,指定其namespace(不指定默认为public):
- spring:
- application:
- name: item-service # 服务名称
- profiles:
- active: dev
- cloud:
- nacos:
- server-addr: 192.168.150.101 # nacos地址
- discovery: # 服务发现配置
- namespace: 8c468c63-b650-48da-a632-311c75e6d235 # 设置namespace,必须用id
- # 。。。略
重启item-service,查看服务列表,会发现item-service会在dev下:
而其它服务则出现在public下:
此时,访问http://localhost:8082/doc.html,基于swagger做测试:
会发现查询结果中缺少商品的最新价格信息。
查看cart-service运行日志:
会发现cart-service服务在远程调用item-service时,并没有找到可用的实例。这证明不同namespace之间是相互隔离的,不可访问。
大厂的服务可能部署在多个不同机房,物理上被隔离为多个集群。Nacos支持对于这种集群的划分。一个服务(service)下可以有很多集群(cluster),而一个集群(cluster)下又可以包含很多实例(instance)。
任何一个微服务的实例在注册到Nacos时都会生成以下几个信息,用来确认当前实例的身份,从外到内依次是:
在Nacos内部会有一个服务实例的注册表,是基于Map实现的,其结构与分级模型的对应关系如下:
查看nacos控制台,会发现默认情况下所有服务的集群都是default:
如果我们要修改服务所在集群,只需要修改bootstrap.yml即可:
- spring:
- cloud:
- nacos:
- discovery:
- cluster-name: BJ # 集群名称,自定义
Eureka是Netfix公司开源的一个注册中心组件,目前被集成在SpringCloudNetfix这个模块下。它的工作原理与Nacos类似。由于Eureka和Nacos的starter中提供的功能都是基于SpringCloudCommon规范,因此两者使用起来差别不大。
用IDEA打开课前资料里提供的cloud-demo项目:
结构说明:
启动EurekaApplication后,可以在localhost:10086查看Eureka的控制台:
微服务引入Eureka的步骤:
①引入eureka-client依赖
此处在hmall项目的cart-serive和item-service的pom.xml引入
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
- </dependency>
注意要把 Nacos服务注册发现 的依赖注释掉,把Eureka的依赖放到最下面,不然可能导致依赖冲突问题
②配置eureka地址
item-service和cart-service的application.yaml
(测试完之后改回为 nacos的配置)
- eureka:
- client:
- service-url:
- defaultZone: http://127.0.0.1:10086/eureka
在swagger接口文档中进行测试:
同时Eureka还实现了负载均衡:
Eureka和Nacos都能起到注册中心的作用,用法基本相似,但是还是有一些区别的,例如:
而且在服务注册发现上也有区别。
在Eureka中,健康检测的原理如下:
综上,Eureka是尽量不剔除服务,避免”误杀“,宁可放过一千,也不错杀一个。这就导致当服务真的出现故障时,迟迟不会被剔除,给服务的调用者带来困扰。
不仅如此,当Eureka发现服务宕机并从服务列表中剔除以后,并不会将服务列表的变更信息推动给所有微服务,而是等待微服务自己来拉取时发现服务列表的变化。而微服务每隔30秒才会去Eureka更新一次服务列表,进一步推迟了服务宕机时被发现的时间。
而Nacos中微服务除了自己定时去Nacos中拉取服务列表外,Nacos还会在服务列表变更时主动推送最新的服务列表给所有的订阅者。
Eureka与Nacos的相似点:
Eureka与Nacos的区别:
自SpringCloud2020版本开始,SpringCloud弃用Ribbon,改用Spring自己开源的Spring Cloud LoadBalancer了,我们使用的OpenFeign、Gateway都已经与其整合。
OpenFeign在整合SpringCloudLoadBalancer时,与我们手动服务发现、负载均衡的流程类似。
流程梳理:
Spring在整合OpenFeign时,实现了org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient类,其中定义了OpenFeign发起远程调用的核心流程。也就是四步:
而具体的负载均衡则不是由OpenFiegn组件负责。而是分成了负载均衡的接口规范,以及负载均衡的具体实现两部分。
负载均衡的接口规范是定义在Spring-Cloud-Common模块中,包含下面的接口:
OpenFeign的负载均衡是基于Spring-Cloud-Common模块的相关接口,具体如下:
BlockingLoadBalancerClient:实现了LoadBalancerClient,会根据serviceId选出负载均衡器并调用其算法实现负载均衡。
ReactiveLoadBalancer的实现类有三个:
其中,RoundRobinLoadBalancer和RandomLoadBalalcer是由Spring-Cloud-LoadBalancer模块提供,而NacosLoadBalancer则是由Nacos-Discovery模块提供的。
默认采用的负载均衡策略是RoundRobinLoadBalancer。
查看源码会发现,Spring-Cloud-LoadBalancer模块中会有一个自动配置类
其中定义了默认的负载均衡器:RoundRobinLoadBalancer
这个Bean上添加了@ConditionalOnMissingBean注解,也就是说如果我们自定义了这个类型的bean,则负载均衡的策略就会被改变。
在cart-service模块中添加一个配置类:
代码如下:注意,这个配置类不能加@Configuration注解,也不要被SpringBootApplication扫描到
- package com.hmall.cart.config;
-
- import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
- import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer;
- import org.springframework.cloud.client.ServiceInstance;
- import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
- import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
- import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
- import org.springframework.context.annotation.Bean;
- import org.springframework.core.env.Environment;
-
- public class LoadBalancerConfiguration {
- @Bean
- public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
- Environment environment, NacosDiscoveryProperties properties,
- LoadBalancerClientFactory loadBalancerClientFactory) {
- String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
- return new NacosLoadBalancer(
- loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, properties);
- }
- }
由于这个LoadBalancerConfiguration没有加@Configuration注解,也就没有被Spring加载,因此是不会生效的。接下来,我们要在启动类上通过注解来声明这个配置。
有两种做法:
全局配置:对所有服务生效(我们选择全局配置)
@LoadBalancerClients(defaultConfiguration = LoadBalancerConfiguration.class)
局部配置:只对某个服务生效
@LoadBalancerClients(@LoadBalancerClient(value = "item-service", configuration = LoadBalancerConfiguration.class)
DEBUG重启测试,会发现负载均衡器的类型切换成功:
RoundRobinLoadBalancer是轮询算法,RandomLoadBalancer是随机算法,那么NacosLoadBalancer是什么负载均衡算法呢?
源码分析:
流程:
为什么?
假如我们现在有两个机房,都部署有item-service和cart-service服务:
假如这些服务实例全部都注册到了同一个Nacos。现在,杭州机房的cart-service要调用item-service,会拉取到所有机房的item-service的实例。调用时会出现两种情况:
本机房调用几乎没有网络延迟,速度比较快。而跨机房调用,如果两个机房相距很远,会存在较大的网络延迟。因此,我们应该尽可能避免跨机房调用,优先本地集群调用。
假设现在的情况如下:
cart-service访问item-service时,应该优先访问8081和8082。重启cart-service进行测试:
可以看到原本是3个实例,经过筛选后还剩下2个实例。
查看DEBUG控制台:
同集群还剩下两个实例,接下来就需要做负载均衡,使用的是权重算法。
权重配置
继续跟踪NacosLoadBalancer的源码:
在nacos控制台,进入item-service的服务详情页,可以看到每个实例后面都有应一个编辑按钮:
点击,可以编辑实例的权重:
我们将这里的权重修改为5,可以发现大多数请求都访问到了8083这个实例:
在SpringCloud的早期版本中采用的服务保护技术叫作Hystix,不过后来被淘汰,替换为Spring Cloud Circuit Breaker,其底层实现可以是Spring Retry和Resilience4J。
不过在国内使用较多的还是SpringCloudAlibaba中的Sentinel组件。
优点 | 缺点 | 场景 | |
信号量隔离 | 轻量级,无额外开销 | 不支持主动超时 不支持异步调用 | 高频调用 高扇出 |
线程池隔离 | 支持主动超时 支持异步调用 | 线程的额外开销比较大 | 低扇出 |
问题:Sentinel的线程隔离与Hystix的线程隔离有什么差别?
答:线程隔离可以采用线程池隔离或者信号量隔离。Hystix默认是基于线程池实现的线程隔离,每一个被隔离的业务都要创建一个独立的线程池,线程过多会带来额外的CPU开销,性能一般,但是隔离性更强。Sentinel则是基于信号量隔离的原理,这种方式不用创建线程池,性能较好,但是隔离性一般。
说明:
说明:
一种特殊场景:
说明:假如在第5、6秒,请求数量都为3,没有超过阈值,全部放行
但是,如果第5秒的三次请求都是在4.5~5秒之间进来;第6秒的请求是在5~5.5之间进来。那么第4.5~5.5之间就有6次请求!也就是说,每秒的QPS达到了6,远超阈值。
这就是固定窗口计数算法的问题,它只能统计当前某一时间窗的请求数量是否达到阈值,无法结合前后的时间窗的数据做综合统计。因此,我们就需要滑动时间窗口算法来解决。
固定时间窗口算法中窗口有很多,其跨度和位置是与时间区间绑定,因此是很多固定不动的窗口。而滑动时间窗口算法中只包含1个固定跨度的窗口,但窗口是可移动的,与时间区间无关。
滑动窗口计数器算法会将一个窗口划分为n个更小的区间,例如:
限流阈值依然为3,绿色小块就是请求,上面的数字是其currentTime值。
若第1400ms又来一个请求,会落在1000~1500时区,虽然该时区请求总数是3,但是滑动窗口内总数已经达到4,因此该请求会被拒绝:
假如第1600ms又来一个请求,处于1500~2000时区,根据算法,滑动窗口位置应该是1000~1500和1500~2000这两个时区,也就是向后移动:
这就是滑动窗口计数的原理,解决了我们之前所说的问题。而且滑动窗口内划分的时区越多,这种统计就越准确。
漏桶算法说明:
Sentinel内部基于漏桶算法实现了排队等待效果,桶的容量取决于QPS阈值以及允许等待的最大超时时间。
例如:限流QPS=5,队列超时时间为2000ms。我们让所有请求进入应该队列中,如同进入漏桶中。由于漏桶是固定频率执行,因此QPS为5就是每200ms执行一个请求。那第N个请求的预期的执行时间是第(N-1)*200ms。如果请求预期的执行时间超出最大时长2000ms,说明“桶满了”,新的请求则会被拒绝。
漏桶的优势就是流量整型,桶就像是一个大坝,请求就是水。并发量不断波动,就如图水流时大时小,但都会被大坝拦住。而后大坝按照固定的速度放水,避免下游被洪水淹没。
因此,不管并发量如何波动,经过漏桶处理后的请求一定是相对平滑的曲线:
Sentinel中的限流中的排队等待功能正是基于漏桶算法实现的。
限流的另一种常见算法是令牌桶算法。Sentinel中的热点参数限流正是基于令牌桶算法实现的。
令牌桶算法说明:
基于令牌桶算法,每秒产生的令牌数量基本就是QPS上限。
当然也有例外情况,例如:
因此,在使用令牌桶算法时,尽量不要将令牌上限设定到服务能承受的QPS上限,而是预留一定的波动空间,这样我们才能应对突发流量。
问题:Sentinel的限流与Gateway的限流有什么差别?
答:限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法,而Sentinel内部却比较复杂:
1. Spring Cloud有哪些常用组件?分别是什么作用?
2. 服务注册发现的基本流程是怎样的?
3. Eureka和Nacos有哪些区别?
相似点:
区别:
4. Nacos的分级存储模型是什么意思?
5. OpenFeign是如何实现负载均衡的?
在SpringCloud的早期版本中,负载均衡都是有Netflix公司开源的Ribbon组件来实现的,甚至Ribbon被直接集成到了Eureka-client和Nacos-Discovery中。
但是自SpringCloud2020版本开始,已经弃用Ribbon,改用Spring自己开源的Spring Cloud LoadBalancer了,我们使用的OpenFeign的也已经与其整合。
发现Spring在整合OpenFeign的时候,实现了org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient类,其中定义了OpenFeign发起远程调用的核心流程。也就是四步:
获取请求中的serviceId
根据serviceId负载均衡,找出一个可用的服务实例
利用服务实例的ip和port信息重构url
向真正的url发起请求
而具体的负载均衡则是不是由OpenFeign组件负责。而是分成了负载均衡的接口规范,以及负载均衡的具体实现两部分。
负载均衡的接口规范是定义在Spring-Cloud-Common模块中,包含下面的接口:
OpenFeign的负载均衡是基于Spring-Cloud-Common模块中的负载均衡规则接口,并没有写死具体实现。这就意味着以后还可以拓展其它各种负载均衡的实现。
不过目前SpringCloud中只有Spring-Cloud-Loadbalancer这一种实现。
Spring-Cloud-Loadbalancer模块中,实现了Spring-Cloud-Common模块的相关接口,具体如下:
BlockingLoadBalancerClient:实现了LoadBalancerClient,会根据serviceId选出负载均衡器并调用其算法实现负载均衡。
6. 什么是服务雪崩,常见的解决方案有哪些?
服务雪崩:微服务调用链路中某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
雪崩问题的产生原因:
解决方案:
①尽量避免服务出现故障或阻塞
②服务调用者做好远程调用异常的后备方案,避免故障扩散
服务保护技术 | ||
Sentinel | Hystrix | |
线程隔离 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断策略 | 基于慢调用比例或异常比例 | 基于异常比率 |
限流 | 基于QPS,支持流量整形 | 有限的支持 |
Fallback | 支持 | 支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
配置方式 | 基于控制台,重启后失效 | 基于注解或配置文件,永远生效 |
7. Hystix和Sentinel有什么区别和联系?
8. 限流的常见算法有哪些?
滑动窗口算法:通过统计一段时间内的请求次数来限制流量。算法维护1个固定大小的时间窗口(例如1秒),在这个时间窗口内计算请求次数,如果请求次数超过设定的阈值,则拒绝该请求。随着时间推移,滑动窗口不断向前移动,过期的请求数量被移除,新的请求数量被加入。
令牌桶算法:通过令牌桶的设计来控制流量。算法维护一个固定容量的桶,以固定的速率不断往桶内放入令牌,每次请求到来时,消耗一个令牌,如果桶内的令牌数足够,则接受请求,否则拒绝请求。能够平滑限制请求的速率。
漏桶算法:通过一个固定容量的漏桶来控制流量。算法规定每隔请求都向漏桶中注入一个水滴,漏桶以固定速率漏水。如果漏桶被注满了,新注入的水滴就会被溢出,即请求被拒绝。如果漏桶未满,则会接受请求被按固定速率漏水,这种算法可以平滑流量防止突发请求引起系统崩溃。
9. 什么是CAP理论和BASE思想?
CAP理论是指分布式系统的三个指标:Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性)。
BASE思想是指Basically Available(基本可用)、Soft State(软状态)、Eventually Consistent(最终一致性)。
简单来说,BASE思想是一种取舍的方案,不再追求完美,而是最终达成目标。因此解决分布式事务的思想也是这样,有两个方向:
10. 项目中碰到过分布式事务吗?怎么解决的?
答:黑马商城hmall项目中交易服务、购物车服务、库存服务分别在三个不同的微服务,每个微服务有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。虽然每个单独的业务都能在本地遵循ACID,但是它们互相之间无法感知,不知道有人失败了,无法保证最终结果的统一,也就无法遵循ACID的事务特性了。
出现分布式事务:业务跨多个服务实现,业务跨多个数据源实现。
分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单:就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。
Seata的事务管理者有3个重要的角色:
Seata支持4种不同的分布式事务解决方案:XA、TCC、AT、SAGA。
XA模式:
AT模式:
11. AT模式如何解决脏读和脏写问题的?
答:解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁,避免同一时刻有另外一个事务来操作当前数据。
12. TCC模式与AT模式相比,有哪些优缺点?
答:TCC(Try Confirm Cancel)模式和AT(Try Abort)模式都是分布式事务处理中常见的两种设计模式,各自有一些优缺点:
TCC模式的优点:
TCC模式的缺点:
AT模式的优点:
AT模式的缺点:
13. RabbitMQ是如何确保消息的可靠性的?
答:确保消息的可靠性,要从发送者的可靠性、MQ的可靠性、消费者的可靠性三个方面入手。
发送者的可靠性
MQ的可靠性
消费者的可靠性
14. RabbitMQ是如何解决消息堆积问题的?
资料文档地址:Docs
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。