分布式事务

CAP定理

1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统有三个指标:.

  • Consistency (一致性)
  • Availability (可用性)
  • Partition tolerance (分区容错性)

Eric Brewer说,分布式系统无法同时满足这三个指标。这个结论就叫做CAP定理。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_服务列表

Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。

比如现在包含两个节点,其中的初始数据是一致的

Availability (可用性):用户访问分布式系统时,读或写操作总能成功。

只能读不能写,或者只能写不能读,或者两者都不能执行,就说明系统弱可用或不可用。

Partition,就是分区,就是当分布式系统节点之间出现网络故障导致节点之间无法通信的情况:

Tolerance,就是容错,即便是系统出现网络分区,整个系统也要持续对外提供服务。

如果此时只允许读,不允许写,满足所有节点一致性。但是牺牲了可用性。符合CP

如果此时允许任意读写,满足了可用性。但由于node3无法同步,导致数据不一致,牺牲了一致性。符合AP

BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:

Basically Available(基本可用)∶分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。

Soft State(软状态)∶在一定时间内,允许出现中间状态,比如临时的不一致状态。

Eventually Consistent(最终一致性)︰虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。

而分布式事务最大的问题是各个子事务的一致性问题,因此可以借鉴CAP定理和BASE理论:

CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。

AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。

AT模式的脏写问题

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_服务列表_02

解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_03

如果在极端情况下,事务2没有被seata管理,快照保存了操作前,和操作后的快照,来对比操作后的快照的数据和数据库的数据进行对比,如果数据不一致,说明在释放全局锁之前有人动了手脚,此时就需要人工介入了

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_限流_04

TCC模式

TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

  • try:资源的检测和预留;
  • confirm:完成资源操作业务;要求 try 成功 confirm 一定要能成功。
  • cancel:预留资源释放,可以理解为try的反向操作。

举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_服务列表_05

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_06

最大努力通知

最大努力通知是一种最终一致性的分布式事务解决方案。顾明思议,就是通过消息通知的方式来通知事务参与者完成业务执行,如果执行失败会多次通知。无需任何分布式事务组件介入。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_07

注册中心

环境隔离

企业实际开发中,往往会搭建多个运行环境,例如:开发环境、测试环境、发布环境。不同环境之间需要隔离。或者不同项目使用了一套Nacos,不同项目之间要做环境隔离。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_08

在Nacos控制台可以创建namespace,用来隔离不同环境

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_09

在微服务中,我们可以通过配置文件指定当前服务所属的namespace:

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_限流_10

分级模型

大厂的服务可能部署在多个不同机房,物理上被隔离为多个集群。Nacos支持对于这种集群的划分。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_服务列表_11

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_12

Eureka与Nacos

Eureka是Netflix公司开源的一个注册中心组件,目前被集成在SpringCloudNetflix这个模块下。它的工作原理与Nacos类似:

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_服务列表_13

Eureka和Nacos都能起到注册中心的作用,用法基本类似。但还是有一些区别的,例如:

Nacos支持配置管理,而Eureka则不支持。 而且服务注册发现上也有区别,我们来做一个实验:

  • 我们停止user-service服务,然后观察Eureka控制台,你会发现很长一段时间过去后,Eureka服务依然没有察觉user-service的异常状态。

这与Eureka的健康检测机制有关。在Eureka中,健康检测的原理如下:

  • 微服务启动时注册信息到Eureka,这点与Nacos一致。
  • 微服务每隔30秒向Eureka发送心跳请求,报告自己的健康状态。Nacos中默认是5秒一次。
  • Eureka如果90秒未收到心跳,则认为服务疑似故障,可能被剔除。Nacos中则是15秒超时,30秒剔除。
  • Eureka如果发现超过85%比例的服务都心跳异常,会认为是自己的网络异常,暂停剔除服务的功能。
  • Eureka每隔60秒执行一次服务检测和清理任务;Nacos是每隔5秒执行一次。

综上,你会发现Eureka是尽量不剔除服务,避免“误杀”,宁可放过一千,也不错杀一个。这就导致当服务真的出现故障时,迟迟不会被剔除,给服务的调用者带来困扰。

不仅如此,当Eureka发现服务宕机并从服务列表中剔除以后,并不会将服务列表的变更消息推送给所有微服务。而是等待微服务自己来拉取时发现服务列表的变化。而微服务每隔30秒才会去Eureka更新一次服务列表,进一步推迟了服务宕机时被发现的时间。

而Nacos中微服务除了自己定时去Nacos中拉取服务列表以外,Nacos还会在服务列表变更时主动推送最新的服务列表给所有的订阅者。

综上,Eureka和Nacos的相似点有:

  • 都支持服务注册发现功能
  • 都有基于心跳的健康监测功能
  • 都支持集群,集群间数据同步默认是AP模式,即最全高可用性

Eureka和Nacos的区别有:

  • Eureka的心跳是30秒一次,Nacos则是5秒一次
  • Eureka如果90秒未收到心跳,则认为服务疑似故障,可能被剔除。Nacos中则是15秒超时,30秒剔除。
  • Eureka每隔60秒执行一次服务检测和清理任务;Nacos是每隔5秒执行一次。
  • Eureka只能等微服务自己每隔30秒更新一次服务列表;Nacos即有定时更新,也有在服务变更时的广播推送
  • Eureka仅有注册中心功能,而Nacos同时支持注册中心、配置管理
  • Eureka和Nacos都支持集群,而且默认都是AP模式

远程调用

负载均衡原理

自SpringCloud2020版本开始,SpringCloud弃用Ribbon,改用Spring自己开源的Spring Cloud LoadBalancer了,我们使用的OpenFeign、Gateway都已经与其整合。

OpenFeign在整合SpringCloudLoadBalancer时,与我们手动服务发现、复杂均衡的流程类似。

1)获取serviceld,也就是服务名称

2)根据serviceld拉取服务列表

3)利用负载均衡算法选择一个服务

4)重构请求的URL路径,发起远程调用

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_14

切换负载均衡算法

自定义配置类

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 OpenFeignConfig {

    @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);
    }

}

然后在启动类加注解使其生效

@LoadBalancerClients(defaultConfiguration = OpenFeignConfig.class)

服务保护

线程隔离

线程隔离有两种方式实现:

  • 线程池隔离( Hystix默认采用)
  • 信号量隔离( Sentinel默认采用)

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_服务列表_15

  • 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
  • 信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求

Sentinel的线程隔离与Hystix的线程隔离有什么差别?

答:线程隔离可以采用线程池隔离或者信号量隔离。

Hystix默认是基于线程池实现的线程隔离,每一个被隔离的业务都要创建一个独立的线程池,线程过多会带来额外的CPU开销,性能一般,但是隔离性更强。

Sentinel则是基于信号量隔离的原理,这种方式不用创建线程池,性能较好,但是隔离性一般。

滑动窗口算法

固定窗口计数器算法

固定窗口计数器算法概念如下:

  • 将时间划分为多个窗口,窗口时间跨度称为Interval,本例中为1000ms;
  • 每个窗口分别计数统计,每有一次请求就将计数器加一,限流就是设置计数器阈值,本例为3
  • 如果计数器超过了限流阈值,则超出阈值的请求都被丢弃。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_16

滑动窗口计数器算法

滑动窗口计数器算法会将一个窗口划分为n个更小的区间,例如

  • 窗口时间跨度Interval为1秒;区间数量n = ,则每个小区间时间跨度为500ms,每个区间都有计数器
  • 限流阈值依然为3,时间窗口(1秒)内请求超过阈值时,超出的请求被限流
  • 窗口会根据当前请求所在时间〈currentTime)移动,窗口范围是从(currentTime-Interval)之后的第一个时区开始,到currentTime所在时区结束。

漏桶算法

说明:

  • 将每个请求视作"水滴"放入"漏桶"进行存储;
  • "漏桶"以固定速率向外"漏"出请求来执行,如果"漏桶"空了则停止"漏水”;
  • 如果"漏桶"满了则多余的"水滴"会被直接丢弃。

漏桶的优势就是流量整型,桶就像是一个大坝,请求就是水。并发量不断波动,就如图水流时大时小,但都会被大坝拦住。而后大坝按照固定的速度放水,避免下游被洪水淹没。

因此,不管并发量如何波动,经过漏桶处理后的请求一定是相对平滑的曲线:

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_线程池_17

Sentinel内部基于漏桶算法实现了排队等待效果,桶的容量取决限流的QPS阈值以及允许等待的最大超时时间:

例如:限流QPS=5,队列超时时间为2000ms。我们让所有请求进入一个队列中,如同进入漏桶中。由于漏桶是固定频率执行,因此QPs为5就是每200ms执行一个请求。那第N个请求的预期的执行时间是第(N-1)*200ms。如果请求预期的执后行时间超出最大时长2000ms,说明“桶满了”,新的请求则会被拒绝。

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_服务列表_18

令牌桶算法

令牌桶算法说明:

  • 以固定的速率生成令牌,存入令牌桶中,如果令牌桶满了以后,停止生反
  • 请求进入后,必须先尝试从桶中获取令牌,获取到令牌后才可以被处理
  • 如果令牌桶中没有令牌,则请求等待或丢弃

分布式事务、注册中心、服务保护(AT模式下脏写的问题、负载均衡原理、服务保护底层原理)_限流_19

Sentinel的限流与Gateway的限流有什么差别?

限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法

而Sentinel内部却比较复杂:

√默认限流模式是基于滑动时间窗口算法,另外Sentinel中断路器的计数也是基于滑动时间窗口算法

√限流后可以快速失败和排队等待,其中排队等待基于漏桶算法

√而热点参数限流则是基于令牌桶算法