Featured image of post Spring Cloud Alibaba 笔记

Spring Cloud Alibaba 笔记

Spring Cloud Alibaba 原理和实战读书笔记

# Spring Cloud alibaba 笔记

# SOA 与微服务的区别:

  1. SOA 关注的是服务的重用性及解决信息孤岛问题
  2. 微服务关注的是解耦,虽然解耦和可重用性从特定的角度来看是一样的,但本质上是有区别的,解耦是降低业务之间的耦合度,而重用性关注的是服务的复用。
  3. 微服务会更多地关注在 DevOps 的持续交付上,因为服务粒度细化之后使得开发运维变得更加重要,因此微服务与容器化技术的结合更加紧密。

# Spring Cloud Alibaba 与 Spring Cloud Netflix 的对比

  1. Alibaba 开源组件在没有织入 Spring Cloud 生态之前,已经在各大公司广泛应用,所以容易实现技术整合及迁移。
  2. Alibaba 开源组件在服务治理上和处理高并发的能力上有天然的优势。

# 什么是 Spring Boot?

帮助开发者快速构建一个基于 Spring Framework 及 Spring 生态体系的应用解决方案,也是对于“约定优于配置”理念的最佳实践。

# IOC/DI(控制反转与依赖注入)

  1. IOC:把对象的生命周期托管到 Spring 容器中,而反转是指对象的获取方式被反转了。
  2. 当使用 IOC 容器之后,客户端类不需要通过 new 来创建这些对象,而是直接从 IOC 容器中获得。早期的 Spring 中,主要通过 XML 的方式来定义 Bean,Spring 会解析 XML 文件,把定义的 Bean 转载到 IOC 容器中。
  3. DI:IOC 容器在运行期间,动态地把某种依赖关系注入组件中。
  4. DI 的三种方法:接口注入、构造方法注入、setter 方法注入;目前是基于注解的形式:有@Autowired、@Inject 和@Resource

# Spring 发展过程

  1. J2EE 的 EJB 时代
  2. Spring XML 配置文件时代
  3. JavaConfig 的无配置化注入时代
  4. Spring Boot 时代:约定优于配置,核心为:
    1. Starter 组件:开箱即用
    2. 自动装配:自动根据上下文完成 Bean 的装配
    3. Actuator:应用监控
    4. Spring Boot CLI:脚手架

# 自动装配的实现

  1. 实现原理:@EnableAutoConfiguration,这个注解的声明在启动类注解@SpringBootApplication 内。进一步又涉及到@Enable 注解(本质上是对@Configuration 和@Bean 的封装);使用 Enable 注解后,Spring 会解析到@Import 导入的配置类,从而根据这个配置类中的描述来实现 Bean 的装配。

  2. 例子:可以直接使用@Autowired 来注入 RedisTemplate 实例。

  3. EnableAutoConfiguration 的原理

    @Import:导入一个 AutoConfigurationImportSelector 类。

    @AutoConfigurationPackage:把使用了该注解的类所在的类所在的包及子包下所有组件扫描到 Spring IoC 容器中

  4. AutoConfigurationImportSelector:是 ImportSelector 的实现类,只有一个 selectImports 抽象方法,并且返回一个 String 数组,在这个数组中可以指定需要装配到 IOC 容器的类,当@Import 中导入一个 ImportSelectord 的实现类后,会把该实现类中返回的 Class 名称都装载到 IOC 容器中。

  5. ImportSelector 与@Configuration 的区别:前者可以实现批量装配,并且还可以通过逻辑处理来实现 Bean 的选择性装配,也就是根据上下文来决定哪些类能够被 IOC 容器初始化。

  6. 自动装配原理总结:

    1. 通过@Import(AutoConfigurationImportSelector)实现配置类的导入
    2. AutoConfigurationImportSelector 类实现了 ImportSelector 接口,重写了方法 selectImports,用于实现选择性批量配置类的装配。
    3. 通过 Spring 提供的 SpringFactoriesLoader 机制,扫描 classpath 路径下的 META-INF/spring.factories,读取需要实现自动装配的配置类。
    4. 通过条件筛选的方式,把不符合条件的配置类移除,最终完成自动装配。
  7. @Conditional 条件装配

    是 Spring Framework 提供的一个核心注解,这个注解的作用是提供自动装配的条件约束,一般与@Configuration 和**@Bean**配合使用。

    简单来说,Spring 在解析@Configuration 配置类时,如果该配置类增加了@Conditional 注解,那么就会根据该注解配置的条件来决定是否要实现 Bean 的装配。

    @Configuration
    public class ConditionConfig {
    
        @Bean
        @Conditional(GpCondition.class)
        public ThirdClass thirdClass() {
            return new ThirdClass();
        }
    }
    

    表示:如果 GpCondition 类中的 matches 返回 true,则装载 ThirdClass 这个类。

  8. @Conditional 在 Spring Boot 中的扩展

    image-20210224200015800

    常用装配注解:

    @ConditionalOnBean

    @ConditionalOnMissingBean

    @ConditionalOnResource

    @ConditionalOnProperties

  9. spring-autoconfigure-metadata

    用于实现批量自动装配条件配置,作用和@Conditional 一致,只是把这些条件配置放在了配置文件中。

    两个条件:

    (1)配置文件的路径和名称必须是/META-INF/spring-autoconfigure-metadata.properties

    (2)配置文件中 key 的配置格式:自动配置类的类全路径名.条件=值

    好处:有效降低 Spring Boot 的启动时间,通过这种过滤方式可以减少配置类的加载数量,因为这个过滤发生在配置类的装载之前,所以它可以降低 Spring Boot 启动时装载 Bean 的耗时。

# 手写实现一个 Starter

# 1 Starter 的功能

  • 涉及相关组件的 Jar 包依赖
  • 自动实现 Bean 的装配
  • 自动声明并且加载 application.properties 文件中的属性配置。

# 2 Starter 的命名规范

Starter 的命名主要分为官方命名和自定义组件命名两类,这种命名格式不是强制性的,也是一种约定俗成的方式。

  • 官方命名格式:spring-boot-starter-模块名称
  • 自定义命名格式:模块名称-spring-boot-starter

# 3 实现基于 Redis 的 Starter

  • 创建一个工程,命名为 redis-spring-boot-starter
  • 添加 Jar 包依赖
  • 定义属性类,实现在 application.properties 中配置 Redis 的连接参数,使用@ConfigurationProperties,把当前类中的属性和配置文件中的配置进行绑定,并且规定前缀。
  • 定义需要自动装配的配置类,主要就是把 RedissonClient 装配到 IOC 容器中。

# Apache Dubbo

  1. 什么是 Dubbo:一个分布式服务框架,主要实现多个系统之间的高性能、透明化调用,简单来说就是一个 RPC 框架,但是和普通的 RPC 框架不同,它提供了服务治理功能,比如服务注册、监控、路由、容错等。

  2. 服务提供者开发流程:

    1. 创建一个普通的 Maven 工程 provider,并创建两个模块:api 和 provider,其中 provider 是一个 Spring Boot 工程
    2. 在 api 模块中定义接口,并且通过 mvn install 安装到本地仓库
    3. 在 provider 模块的 pom 文件中引入 api 和 dubbo 组件。
    4. 在 provider 中实现接口,并且使用@DubboService 注解发布服务
    5. 在 application.properties 文件(或 yml)中添加 Dubbo 服务的配置信息,包括 application.name、protocal.name、protocol.port 和 registry.address
    6. 启动 Spring Boot
  3. 服务调用者的开发流程:

    1. 创建一个 Spring Boot 项目 consumer,添加 Jar 包依赖(Dubbo 和 api)
    2. 在 application.properties 中配置 dubbo.application.name
    3. 使用@DubboReference 注解获取一个远程代理对象。

# Zookeeper

  1. Zookeeper 是一个高性能的分布式协调中间件,基于 Java 编写。

  2. Zookeeper 的数据结构:数据模型和分布式文件系统类似,是一种层次化的属性结构,区别是:Zookeeper 的数据是结构化存储的,并没有在物理上体现出文件和目录。Zookeeper 树中的每个节点被称为 Znode,Znode 维护了一个 stat 状态信息,其中包含数据变化的时间和版本等。并且每个 Znode 可以设置一个 value 值,Zookeeper 并不用于通用的数据库或者大容量的对象存储,它只是管理和协调有关的数据,所以 value 的数据大小不建议设置得非常大,否则会带来更大的网络开销。Zookeeper 上的每一个节点的数据都是允许读和写的,读表示指定获得 Znode 上的 value 数据,写表示修改 Znode 上的 value 数据。另外,节点的创建规则和文件系统中文件的创建规则类似,必须按照层次创建。例如:创建/node/node1/node1-1,先要创建/node/node1 这两个层次节点。

  3. Zookeeper 的特性:Znode 在被创建后,需要指定节点的类型,节点类型分为:

  4. Watcher 机制:

    1. Znode 的订阅/通知机制:当 Znode 节点状态发生变化时或者 Zookeeper 客户端连接状态发生变化时,会触发事件通知。这个机制在服务注册与发现中,针对服务调用者及时感知到服务提供者的变化提供了非常好的解决方案。

    2. Zookeeper 提供的 Java API 中,提供了三种机制来针对 Znode 进行注册监听,分别是:

      image-20210225003945831
    3. 常用应用场景分析

      1. 分布式锁:(1)多线程中 Synchronized 和 Lock 用于解决共享资源访问的数据安全性问题,但范围是线程级别的。(2)在分布式架构中,多个进程对同一个共享资源的访问,也存在数据安全性问题,因此也需要使用锁的形式来解决这类问题,而解决分布式环境下多进程对于共享资源访问带来的安全性问题的方案就是使用分布式锁。锁的本质是排他性,也就是避免同一时刻多个进程同时访问某一个共享资源。(3)如果使用 Zookeeper 实现分布式锁来达到排他性的目的,只需要用到节点的特性:临时节点,以及同级节点的唯一性。(4)具体实现:a.获得锁的过程:所有客户端可以去 Zookeeper 服务器上/Exclusive_Locks 节点下创建一个临时节点/lock。Zookeeper 基于同级节点的唯一性,会保证所有客户端中只有一个客户端能创建成功,创建成功的客户端获得了排它锁,没有获得锁的客户端就需要通过 Watcher 机制监听/Exclusive_Locks 节点下子节点的变更事件,用于实时监听/lock 节点的变化情况以作出反应。 b.释放锁的过程:①获得锁的客户端因为异常断开了和服务端的连接,临时节点会自动删除。②获得锁的客户端执行完业务逻辑后,主动删除创建的 lock 节点。
      2. Master 选举:分布式系统中的集群模式,某一机器宕机后,其他节点会接替故障节点继续工作。(1)Zookeeper 有两种方式来实现 Master 选举的场景。假设集群中有 3 个节点,需要选举出 Master,那么三个节点同时去 Zookeeper 服务器上创建一个临时节点/master-election,由于节点的唯一性,只会有一个客户端创建成功,创建成功就称为 Master。同时,其他没有创建成功的客户端,针对该节点注册 Watcher 事件,监控 master,一旦/master-election 节点被删除,其他客户端重新发起 master 选举。(2)方法二:利用临时有序节点的特性来实现。所有参与选举的节点在/master 节点下创建一个临时有序节点,编号最小的节点表示 master,后续的节点监听上一个节点的删除事件,用于触发重新选举。

# Dubbo 集成 Zookeeper

# 1 需要解决的问题

  • 服务动态上下线感知:服务调用者要感知到服务提供者上下线的变化。
  • 负载均衡

# 2 实现步骤

  1. 在 provider 模块中添加 Zookeeper 相关依赖
  2. 修改 application.properties 配置文件,修改 dubbo 的 registry-addr 为 zookeeper 服务器的地址,表示当前 Dubbo 服务需要注册到 Zookeeper 上。
  3. consumer 只需要修改 application.properties,设置 dubbo 的 registry-addr 即可

# 3 原理

  1. Dubbo 服务注册到 Zookeeper 上之后,可以在 Zookeeper 服务器上看到图下所示的树形结构。

    image-20210225005911001
  2. 其中 URL 是临时节点,其他皆为持久化节点,如果注册该节点的服务器下线了,那么这个服务器的 URL 地址就会被移除。

  3. 当 Dubbo 服务消费者启动时,会对/providers 下的子节点注册 Watcher 监听,这样就可以感知到服务提供方的上下线变化,从而防止请求发送到已经下线的服务器造成访问失败。同时,服务消费者会在/consumers 下写入自己的 URL,这样可以在监控平台上看到某个 Dubbo 服务正在被哪些服务调用。最重要的是,如果服务消费者需要调用一个服务,那么它会先去/providers 路径下获得所有该服务的提供方 URL 列表,然后通过负载均衡算法计算出一个地址进行远程访问。

  4. 此外,Dubbo 还可以针对不同的情况实现以下功能:

    1. 基于临时节点的特性,当服务器宕机或者下线时,注册中心会自动删除该服务提供者的信息。
    2. 注册中心重启时,Dubbo 能自动恢复注册数据及订阅请求。
    3. 为了保证节点操作的安全性,Zookeeper 提供了 ACL 权限控制,在 Dubbo 中可以通过 register.username 和 password 来设置节点的验证信息。
    4. 注册中心默认的根节点为/dubbo,如果需要针对不同环境设置不同的根节点,可以使用 registry.group 修改根节点名称。

# 4 实战 Dubbo Spring Cloud

  1. 创建 service-provider 工程,创建两个子模块 api 和 provider,前者为 maven 工程,后者为 Spring Boot 工程
  2. 在 api 中声明接口,并执行 mvn install
  3. 在 provider 中添加 api、Spring Boot、Spring Cloud 和 Spring Cloud Alibaba 相关组件的依赖。(包括 spring-cloud-starter、spring-cloud-starter-dubbo、api、discovery)
  4. 在父 pom 中显示声明 dependencyManagement 配置版本。
  5. 在 provider 中创建接口的实现类,并且声明@DubboService
  6. 在 application.properties 中配置 Dubbo 相关信息。
  7. 启动 provider 服务。
  8. 创建 consumer,依赖与 provider 类似,同样在 application.properties 中配置 Dubbo 相关信息。注意:dubbo-cloud-subscribed-services 表示服务调用者订阅的服务提供方的应用名称列表,如果有多个应用名称,可以通过",“分开,默认值为“*”
  9. 使用@DubboReference 消费服务,启动即可。

# Dubbo 的高级应用

# 1 集群容错

Dubbo 默认提供 6 种容错模式,默认为 Failover Cluster,此外可以根据实际需求自行扩展。

image-20210225012015079
image-20210225012046078
  • 配置方式:在@DubboService 中增加参数 cluster=“failfast” 即可。
  • 推荐:查询语句容错策略建议使用默认的 Failover Cluster,而增删改操作建议使用 Failfast Cluster 或者使用 Failover Cluster(retries=0),防止出现数据重复添加等其他问题!建议在设计接口的时候把查询接口方法单独做成一个接口提供查询。

# 2 负载均衡

Dubbo 提供了 4 种负载均衡策略,默认为 random,也可以自行扩展(基于 SPI 机制)。

image-20210225012419926

# 3 服务降级

服务降级是一种系统保护策略,当服务器访问压力较大时,可以根据当前业务情况对不重要的服务进行降级,以保证核心业务的正常运行。所谓的降级,就是把一些非必要的功能在流量较大的时间段暂时关闭,比如在双十一大促时,淘宝会把查看历史订单、商品评论等功能关闭。

降级的分类:

  • 是否自动化:人工降级、自动降级
  • 功能划分:读服务降级和写服务降级

自动降级更多来自于系统出现某些异常时自动触发“兜底的流畅”,比如:

  • 故障降级:调用的远程服务挂了,网络故障或者 RPC 服务返回异常。这类情况在业务情况下可以通过设置兜底数据响应给客户端。
  • 限流降级:为了保护系统不被压垮,在系统中会针对核心业务进行限流,当请求流量达到阈值时,后续的请求会被拦截。

Dubbo 提供了一种 Mock 配置来实现服务降级,也就是当服务提供方出现网络异常无法访问时,客户端不抛出异常,步骤如下:

  1. 在 consumer 中创建 MockService,这个类只需要实现降级的接口即可,重写接口中的抽象方法实现本地数据的返回。
  2. 在@DubboReference 中增加 mock 参数,制定 MockService 的位置。
  3. 在不启动 Dubbo 服务或者服务端的返回值超过默认的超时时间时,得到的数据就是 MockService 中的数据。

# 主机绑定规则

主机绑定表示的是 Dubbo 服务对外发布的 IP 地址,默认情况下 Dubbo 会按照以下顺序来查找并绑定主机 IP 地址。

  • 查找环境变量 DUBBO_IP_TO_BIND 属性配置的 IP 地址。

  • 查找 dubbo.protocol.host 属性的 IP 地址,默认是空,如果没有配置或者 IP 地址不合法则继续查找。

  • 通过 LocalHost.getHostAddress 获取本机 IP 地址,获取失败则继续。

  • 如果配置了注册中心的地址,则使用 Socket 通信连接到注册中心的地址后,使用 for 循环通过 socket.getLocalAddress().getHostAddress()扫描各个网卡来获取网卡 IP 的地址。

  • 建议:通过 dubbo.protocal.host 设置主机地址,防止注册错误的 IP 地址,使服务消费者无法调用。

  • docker 部署解决方案:使用–net=host 绑定网络,然后配置 application.yml

    image-20210301020007424

    配置 inetutils 下的两个参数

# Dubbo 源码分析

# 1 核心点

  • SPI 机制
  • 自适应扩展点
  • IOC 和 AOP
  • Dubbo 如何与 Spring 集成。

# 2 生成 IDE 工程的命令

  • mvn idea:idea
  • mvn eclipse:eclipse

# 3 SPI(Service Provider Interface)

  • 自适应扩展点:AdaptiveExtension
  • 指定名称扩展点:Extension(name)
  • 激活扩展点:ActivateExtension(url,key)

SPI 是 JDK 内置的一种服务提供发现机制,主要用于服务的扩展实现。SPI 机制在很多场景中都有运用,比如数据库连接,JDK 提供了 Driver 接口,这个驱动类由不同的数据库厂商来实现,然后 JDK 利用 SPI 机制从 classpath 下找到相应的驱动来获得指定数据库的连接。这种插拔式的扩展加载方式,也同样遵循一定的协议约定,比如所有的扩展点必须要放在 resources/META-INF/services 目录下,SPI 机制会默认扫描这个路径下的属性文件以完成加载。

# 4 Dubbo 中的 SPI 思想

Dubbo 或者 SpringFactoriesLoader 并没有使用 JDK 内置的 SPI 机制,只是利用了 SPI 的思想。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader 我们可以加载指定的实现类。

Dubbo 的 SPI 扩展有两个规则:

  • 和 JDK 内置的 SPI 一样,需要在 resources 目录下创建任一目录结构:META-INF/dubbo、META-INF/dubbp/internal、META-INF/services,在对应的目录下创建以接口全路径名命名的文件,Dubbo 会去三个目录下加载相应扩展点。
  • 文件内容和 JDK 内置的 SPI 不一样,内容是 key-value 形式的数据,key 是一个字符串,value 是一个对应扩展点的实现,这样的方式可以按照需要加载指定的实现类。

实现步骤如下:

  • 在一个依赖了 Dubbo 框架的工程中,创建一个扩展点及一个实现。其中,扩展点需要声明@SPI 注解。
  • 在 resources/META-INF/dubbo 目录下创建以 SPI 接口命名的文件
  • 使用 ExtensionLoader.getExtensionLoader.getExtension(key)获得指定名称的扩展点实现。

# 5 Dubbo 中的 SPI 原理

(1)ExtensionLoader.getExtensionLoader:这个方法用于返回一个 ExtensionLoader 实例,逻辑如下:

  • 先从缓存中获取与扩展类对应的 ExtensionLoader
  • 缓存未命中,则创建一个新的实例,保存到 eEXTENXION_LOADERS 集合中缓存起来。
  • 在 ExtensionLoader 构造方法中,初始化一个 objectFactory
image-20210225020109248
image-20210225020130724

(2)getExtension:这个方法用于根据指定名称获取对应的扩展点并返回。

  • name 用于参数的判断,如果 name=“true”,则返回一个默认的扩展实现。
  • 创建一个 Holder 对象,用户缓存该扩展点的实例。
  • 如果缓存中不存在,则通过 createExtension(name)创建一个扩展点。
image-20210225020354825

(3)createExtension():去指定的路径下查找 name 对应的扩展点的实现。

  • 通过 getExtensionClasses().get(name)获取一个扩展类
  • 通过反射实例化之后缓存到 EXTENSION_INSTANCES 集合中。
  • injectExtension 实例依赖注入
  • 把扩展类对象通过 Wrapper 进行包装。
image-20210225020624033

(4)getExtensionClasses()

  • 从缓存中换取已经被加载的扩展类
  • 如果缓存未命中,则调用 loadExtensionClasses 加载扩展类。
image-20210225020823393

(5)loadExtensionClasses()

  • 通过 cacheDefaultExtensionName 方法获取当且扩展接口的默认扩展对象,并且缓存。
  • 调用 loadDirectory 方法加载指定文件目录下的配置文件。

(6)cacheDefaultExtensionName()

  • 获得指定扩展接口的@SPI 注解
  • 得到@SPI 注解中的名字,保存到 cacheDefaultName 属性中。

# 6 自适应扩展点

Adaptive Extension:能够根据上下文动态匹配一个扩展类,使用方式如下:

ExtensionLoader.getExtensionLoader(class).getAdaptiveExtension();

自适应扩展点通过@Adaptive 注解声明,有两种使用方式

(1)@Adaptive 注解定义在类上面,表示当前类为自适应扩展点。

(2)@Adaptive 注解定义上方法层面,会通过动态代理的方式生成一个动态字节码,进行自适应匹配。

# 7 Protocol 自适应扩展点源码

ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

首先是 getExtensionLoader:

(1)从缓存中获取自适应扩展点实例。

(2)如果缓存未命中,则通过 createAdaptiveExtension 创建自适应扩展点。

然后是 createAdaptiveExtension:

(1)getAdaptiveExtensionClass:获取一个自适应扩展类的实例。

(2)injectExtension 完成依赖注入。

接着是 getAdaptiveExtensionClass:

(1)通过getExtensionClasses方法加载当前传入类型的所有扩展点,缓存在一个集合中。

(2)如果 cachedAdaptiveClass 为空,则调用 createAdaptiveExtensionClass 进行创建。

# 8 IOC

上文中的 injectExtension 就是依赖注入的实现,整体逻辑为:

(1)遍历被加载的扩展类中的所有 set 方法。

(2)得到 set 方法中的参数类型,如果参数类型是对象类型,则获得这个 set 方法中的属性名称。

(3)使用自适应扩展点加载该属性名称对应的扩展类。

(4)调用 set 完成赋值。

image-20210225192001158
image-20210225192017631

简单来说,injectExtension 方法的主要功能是,如果当前加载的扩展类中存在一个成员对象,并且为它提供了 set 方法,那么就会通过自适应扩展点进行加载并赋值。

# 9 AOP

面向切面编程,意图是把业务逻辑和功能逻辑分离,然后在运行期间或者类加载期间进行织入,可以降低代码的复杂性,以及提高重用性。

instance = injectExtension((T)WrapperClass.getConstructor(type).newInstance(instance));

这段代码分别用到了依赖注入和 AOP,AOP 体现在基于 Wrapper 装饰器类实现对原有的扩展类 instance 进行包装。

# 10 Dubbo 集成 Spring 机制(略)

p89

# 什么是 Nacos?

Nacos 致力于解决微服务中的统一配置、服务注册与发现等问题。它提供了一组简单易用的特性集,帮助开发者快速实现动态服务发现、服务配置、服务元数据以及流量管理。

# 1 关键特性

  • 服务发现和服务健康监测

    Nacos 基于 DNS 和基于 RPC 的服务发现。服务提供者通过原生 SDK、OpenAPI 或一个独立的 Agent TODO 注册 Service 后,服务消费者可以使用 DNS 或 HTTP&API 查找和发现服务。

    Nacos 提供对服务的实时的健康检查,阻止向不健康的主机或服务实例发送请求。Nacos 支持传输层(PING 或 TCP)和应用层(如 HTTP、MYSQL、用户自定义)的健康检查。对于复杂的云环境和网络拓扑环境(如 VPC、边缘网络等)服务的健康检查,Nacos 提供了agent 上报服务端主动监测两种健康检查模式。Nacos 还提供了统一的健康检查仪表盘。

  • 动态配置服务

    业务服务一般都会维护一个本地配置文件,然后把一些常量配置到这个文件中。这种方式在某些场景会存在某些问题,比如配置变更时需要重新部署应用。而动态配置服务可以以中心化、外部化和动态化的方式管理所有环境的应用配置和服务配置。

  • 动态 DNS 服务

    支持权重路由,让开发者更容易实现中间层负载均衡、更灵活的路由策略、流量控制,以及数据中心内网的简单 DNS 服务。

  • 服务及其元数据管理

# 2 Nacos 集群

包含一个 Leader 节点和多个 Follower 节点。

数据一致性算法采用的 Raft(Etcd、Redis 哨兵选举也是这个算法)

3 个或 3 个以上 Nacos 节点才能构成集群。

# 搭建 Nacos 注册中心的注意点

  • dubbo.scan.base-packages 功能等同于@DubboComponentScan
  • dubbo.registry.address:Dubbo 服务注册中心的配置地址,它的值 spring-cloud://url 表示挂载到 Spring Cloud 注册中心,不配置的话会提示没有配置注册中心的错误。
  • spring.cloud.nacos.discovery.server-addr:Nacos 服务注册中心的地址。

# Nacos 实现原理

# 1 模块组成

  • Provider App
  • Consumer App
  • Name Server
  • Nacos Server
  • Nacos Console

整体来说,服务提供者通过 Virtual IP 访问 Nacos Server 高可用集群,基于 Open API 完成服务的注册和服务的查询。Nacos Server 本身可以支持主备模式,所以底层会采用数据一致性算法来完成主从节点的整体同步。服务消费者也是如此。

# 2 注册中心的原理

服务注册的功能主要体现在:

  • 服务实例在启动时注册到服务注册表,并在关闭时注销。(Open API)
  • 服务消费者查询服务注册表,获得可用实例。
  • 服务注册中心需要调用服务实例的健康检查 API 来验证它是否能够处理请求。(心跳机制)

# 3 Nacos 源码(略)

  • 服务注册
  • 服务地址的获取
  • 服务地址变化的感知

# Nacos 实现统一配置管理

各个应用自己独立维护本地配置方式的不足:

image-20210225222922897

# 1 Nacos 集成 Spring Boot

  • 在 application.properties 中配置 nacos.config.server-addr
  • 创建 NacosConfigController,用于从 Nacos Server 动态读取配置。
  • @NacosPropertiesSource:用于加载 dataId 为 example 的配置源,autoRefreshed 表示开启自动更新。
  • @NacosValue:设置属性的值,其中 info 表示 key,而 Local Hello World 表示默认值。也就是说如果 key 不存在,则使用默认值。这是一种高可用的策略。
image-20210225223903497

# 2 Nacos 集成 Spring Cloud

  • spring.cloud.nacos.config.prefix 表示 Nacos 配置中心上的 DataID 的前缀。
  • spring.cloud.nacos.config.server-addr 表示 Nacos 配置中心的地址。
  • 在 Nacos Console 创建配置
  • 在启动类中,读取配置中心的数据。
  • 注意坑:配置文件必须用 bootstrap.yml 这个名称,因为 bootstrap 加载顺序优于 application,因为需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息。

# 3 动态更新配置

通过一个 while 循环不断读取 info 属性,当 info 属性发生变化时,控制台可以监听到。

# 4 基于 DataID 配置 yaml 的文件扩展名

DataID 默认规则是${prefix}-${spring.profile.active}.${file-extension}

  • 在默认情况下,会去 Nacos 服务器上加载 DataID 以${spring.application.name}.${file-extension:properties}为前缀的基础配置。例如:在不通过 spring.cloud.nacos.config.prefix 指定 DataID 时,会默认读取 DataID 为 nacos-config-demo.properties 的配置信息。
  • 如果明确指定了 spring.cloud.nacos.config.prefix,则会加载 DataID 为指定值的配置。
  • spring.profile.active 表示多环境支持。

在实际应用中,如果使用 YAML 格式配置,则需要声明 spring.cloud.nacos.config.file-extension=yaml

# 5 不同环境的配置切换

Spring Boot 多环境支持配置步骤如下:

  • 在 resource 目录下根据不同环境创建不同的配置:
    • application-dev.properties
    • application-test.properties
    • application-prod.properties
  • 定义一个 application.properties 默认配置,在该配置中通过 spring.profile.active=${env}来指定使用哪个环境的配置,如果${env}的值为 prod,表示使用 prod 环境。
  • 也可以通过设置 VM Options=-Dspring.profiles.active=prod 来指定。

Nacos Config 配置步骤如下:

  • 在 bootstrap.properties 中声明 spring.profiles.active=prod
  • 在 Nacos 控制台新增 DataID 为 nacos-config-demo-prod.properties 的配置项。

# 6 自定义 Namespace 和 Group

  • Namespace:解决多环境及多租户数据的隔离问题。
    • 使用:在 bootstrap.properties 里指定 spring.cloud.nacos.config.namespace
  • Group:用于分组管理 Data ID
    • 使用:在 bootstrap.properties 里指定 spring.cloud.nacos.config.group

# Nacos Config 实现原理(略)

  • 获取配置
  • 监听配置
  • 发布配置
  • 删除配置

分为两类:配置的 CRUD 和配置的动态监听

# Spring Cloud 加载配置的原理(略)

# Nacos 源码(略)

# Sentinel 限流及熔断

# 1 服务限流的作用及实现

主要作用:损失一部分用户的可用性,为大部分用户提供稳定可靠的服务。

  • 计算器算法:在制定周期内累加访问次数,当访问次数达到阈值时,触发限流策略。

    image-20210225232943900
  • 滑动窗口算法:源于 TCP 拥塞控制,原理是在固定窗口中分割出多个小时间窗口,分别在每个小时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内所有小时间窗口总的计数即可。(Sentinel 的原理)

  • 令牌桶算法:每一个请求,都需要从令牌桶中获取一个令牌,如果没有获得令牌,则触发限流策略。

    image-20210225233303914

    特性:短时间内新增的流量系统能够正常处理。

  • 漏桶限流算法:用于控制数据注入网络的速度,平滑网络上的突发流量。

image-20210225233550970

# 2 服务熔断和降级

在微服务架构中,由于服务拆分粒度较细,会出现请求链路较长的情况,用户发起一个请求操作,需要调用多个微服务才能完成。

雪崩效应:某个服务因为网络延迟或者请求超时等原因不可用时,就会导致当前请求阻塞,一旦某个链路上被依赖的服务不可用,很可能出现请求堆积而产生雪崩。

所以,服务熔断就是用来解决这个问题的方案,它指的是当某个服务提供者无法正常为服务调用者提供服务时,为了防止整个系统出现雪崩效应,暂时将出现故障的接口隔离出来,断绝与外部接口的联系,当触发熔断后,后续一段时间内该服务调用者的请求都会直接失败,直至目标服务恢复正常。

image-20210225234104602

# 3 Sentinel 的特性

  • 丰富的应用场景:秒杀、消息削峰填谷、集群流量控制等。
  • 实时监控
  • 开源生态支持
  • SPI 扩展点支持

# 4 Sentinel 的组成:

  • 核心库(Java 客户端):不依赖任何框架与库,能够运行于所有 Java 运行时环境。
  • 控制台(Dashboard)

# 5 Sentinel 基本应用:

步骤如下:

(1)定义资源:限流保护的最基本元素,比如一个方法。

(2)定义限流规则

(3)检验规则是否生效

限流规则:通过 initFlowRules 方法设置

  • grade:限流阈值类型,有 QPS 模式和并发线程数模式。
  • count:限流阈值
  • resource:设置需要保护的资源

# 6 Sentinel 资源保护规则

Sentinel 支持多种保护规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则、热点参数规则。

  • 限流规则:先通过 FlowRules 来定义限流规则,然后通过 FlowRuleManager.loadRules 来加载规则列表。

# 1 QPS 流量控制行为

通过 controlBehavior 设置,包含:

  • 直接拒接
  • Warm UP,冷启动
  • 匀速排队
  • 冷启动 + 匀速排队

# 7 Sentinel 实现服务熔断

通过 DegradeRule 实现:

  • grade:熔断策略,支持秒级 RT、秒级异常比例、分钟异常数。默认是秒级 RT。
  • timeWindow:熔断降级的时间窗口,单位为 s。也就是出发熔断降级之后多长时间内自动熔断。
  • rtSlowRequestAmount:在 RT 模式下,1s 内持续多少个请求的平均 RT 超出阈值后出发熔断,默认值是 5
  • minRequestAmout:触发的异常熔断最小请求数,请求数小于该值时即使异常比例超出阈值也不会触发熔断,默认值是 5.

三种熔断策略:

  • 平均响应时间 RT:如果 1s 内持续进来 5 个请求,对应的平均响应时间都超过了阈值(count,单位为 ms),那么在接下来的时间窗口内,对这个方法的调用都会自动熔断,抛出 DegradeException
  • 异常比例
  • 最近一分钟异常数:如果 timeWindow 小于 60s,则结束熔断状态后仍然可能再进入熔断状态。

# Sentinel 集成 Spring Cloud

步骤如下:

  • 创建项目,集成 Spring Cloud 依赖。

  • 添加 Sentinel 依赖。

  • 创建一个 REST 接口,并且通过@SentinelResource 配置限流保护资源。

    image-20210226000233876
  • 在上述代码中,配置限流资源有几种情况

    • Sentinel starter 在默认情况下会为所有的 HTTP 服务提供限流埋点,所以如果只想对 HTTP 服务进行限流,只需添加依赖即可。
    • 如果想要对特定的方法进行限流或降级,则需要通过@SentinelResource 注解来定义资源。
    • 可以通过 SphU.entry()方法来配置资源。
  • 手动配置流控规则,可以借助 Sentinel 的 InitFunc SPI 扩展接口来实现,只需要实现自己的 InitFunc 接口,并在 init 方法中编写规则加载的逻辑即可。

# 基于 Sentinel Dashboard 来实现流控配置

步骤如下:

  • 启动 Sentinel Dashboard

  • 在 application.yml 中增加以下配置

    image-20210226000900895
  • 提供一个 REST 接口

  • 进入 Sentinel Dashboard 中配置流控规则。

  • 访问簇点链路,找到资源名称。

  • 单机流控按钮设置流控规则

注意 sentinel 的坑:

image-20210226015008780

# Sentinel 自定义 URL 限流异常

默认情况下,URL 触发限流后会返回 Blocked by Sentinel 字符串

在实际应用中,大都采用 JSON 格式,所以如果希望修改触发限流之后的返回结果形式,则可以通过自定义限流异常来处理,实现UrlBlockHandler并且重写 blocked 方法。

还有一种场景,当触发限流后,希望跳转到一个降级页面,可以通过下面这个配置来实现。

spring.cloud.sentinel.servlet.block-page={url}

# Sentinel 对 URL 资源清洗

Sentinel 中 HTTP 服务的限流默认由 Sentinel-Web-Servlet 包中的 CommonFilter 来实现,这个 Filter 会把每个不同的 URL 都作为不同的资源来处理。

举例:

image-20210226194317550
  • 限流统计不准确,实际需求是控制 clean 方法总的 QPS,结果统计的是每个 URL 的 QPS
  • 导致 Sentinel 中资源数量过多,默认资源数量阈值为 6000,对于多出的资源规则将不会生效。

针对这个问题可以通过URLCleaner接口来实现资源清洗,也就是对于/clean/{id}这个 URL,我们可以统一归集到/clean/*资源下,具体代码如下:

image-20210226194545852

# Sentinel 集成 Nacos 实现动态流控规则

Sentinel 的理念是只需要开发者关注资源的定义,默认会对资源进行流控。当然我们还需要自定义流控规则,前面有两种方式:

  • 通过 FlowRuleManager.loadRules(List rules)手动加载流控规则
  • 在 Sentinel Dashboard 上针对资源动态创建流控规则。

针对第一种方式,如果接入 Sentinel Dashboard,那么同样支持动态修改流控规则。但是,这里会存在一个问题,基于 Sentinel Dashboard 所配置的流控规则,都是保存在内存中的,一旦应用重启,这些规则都会被清除。为了解决这个问题,Sentinel 提供了动态数据源支持。

目前,Sentinel 支持 Consul、Zookeeper、Redis、Nacos、Apollo、etcd 等数据源的扩展,我们使用 Nacos 的方式来扩展。

步骤如下:

  • 添加 Nacos 数据源依赖包

  • 创建一个 REST 接口用于测试。

  • 在 application.yml 中添加数据源配置。

    image-20210226195049493

    配置说明:

    rule-type:flow、degrade、param-flow、gw-flow 等

    data-type:Spring Cloud Alibaba 提供了 JSON 和 XML 两种格式。如果需要自定义,则可以将值配置为 custom,并配置 converter-class 指向 converter 类。

  • 登录 Nacos 控制台,创建流控配置规则,配置信息如下:

    image-20210226195526683
  • 最后,登录 Sentinel Dashboard,找到执行项目名称菜单下的“流控规则”,就可以看到在 Nacos 上所配置的流控规则已经被加载了。

  • 当在 Nacos 控制台修改流控规则后,可以同步在 Sentinel Dashboard 上看到流控规则的变化。

    • 注意:在 Sentinel Dashboard 上修改无法同步到 Nacos 上。
  • 强烈建议:不要在 Nacos 上修改流控规则,因为这种修改的危险系数很高。这就意味着流控规则的管理应该集中在 Sentinel Dashboard 上,所以我们需要实现 Sentinel Dashboard 来动态维护规则并同步到 Nacos 上,目前官方还没有提供支持,但可以自己实现。

  • 这里有一个坑:出现了空指针异常org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘ds1-sentinel-nacos-datasource’: FactoryBean threw exception on object creation; nested exception is java.lang.NullPointerException,出现原因是 Spring-Cloud-Alibaba 与 Sentinel 的版本对应不上,解决办法是把 Spring Cloud Alibaba 的版本升到 2.2.5.RELEASE 即可。

# Sentinel 集成 Nacos 实现规则同步

Sentinel Dashboard 的“流控规则”下的所有操作,都会调用 Sentinel 源码中的 FlowControllerV1 类,这个类包含流控规则本地化的 CRUD

另外,在 com.alibaba.csp.sentinel.dashboard.controller.v2 包下存在一个 FlowControllerV2 类,这个类同样提供流控规则的 CRUD,和 V1 版本不同的是,它可以实现指定数据源的规则拉取和同步。

FlowControllerV2 依赖以下两个非常重要的类

  • DynamicRuleProvider:动态规则的拉取,从指定数据源中获取流控规则后在 Sentinel Dashboard 中展示。
  • DynamicRulePublisher:动态规则的发布,将在 Sentinel Dashboard 中修改的规则同步到指定数据源中。

这里我们扩展这两个类,然后集成 Nacos 来实现 Sentinel Dashboard 规则的同步。

# 1 Sentinel Dashboard 源码修改:

具体步骤如下:

  • 打开 sentinel-dashboard 工程,在 pom.xml 中把 sentinel-datasource-nacos 依赖的 scope 注释掉。

    image-20210226220250575
  • 修改 resouces/app/scripts/directives/sidebar/sidebar.html 文件下的代码,将 dashboard.flowV1 改成 dashboard.flow

    image-20210226220240096

    修改之后,会调用 FlowControllerV2 中的接口。

  • 在 com.alibaba.csp.sentinel.dashboard.rule 包中创建一个 nacos 包,并创建一个类用来加载外部化配置。

    image-20210226220601939
  • 创建一个 Nacos 配置类 NacosConfiguration

    • 注入 Converter 转换器,将 FlowRuleEntity 转化为 FlowRule,以及反向转化。
    • 注入 Nacos 配置服务 ConfigService
  • 创建一个常量类 NacosConstants,分别表示默认的 GROUP_ID 和 DATA_ID 的后缀。

  • 实现动态从 Nacos 配置中心获取流控规则。

  • 创建一个流控规则发布类,在 Sentinel Dashboard 上修改完配置后,需要调用该发布方法将数据持久化到 Nacos 中。

  • 修改 FlowControllerV2 类,将上面配置的两个类注入进来,表示规则的拉取和规则的发布统一用我们前面自定义的两个实例。

  • 在 application.properties 文件中添加 nacos 服务端的配置信息。

  • 将代码打包成一个 fat jar

  • 详见 https://blog.csdn.net/weixin_42073629/article/details/107117433 或者 test 包中的 nacos 代码

# 2 Sentinel Dashboard 规则同步

应用程序需要修改的地方比较少,只需注意配置文件中 data-id 的命名要以-sentinel-flow 结尾即可。

# Sentinel 集成 Dubbo 实现限流

Sentinel 提供了与 Dubbo 整合的模块 Sentinel Apache Dubbo Adapter,可以针对服务提供者和服务消费者进行流控,在使用的时候,只需要添加以下依赖。

image-20210227193606798

添加后该依赖后,Dubbo 服务中的接口和方法(包括服务端和消费端)就会成为 Sentinel 中的资源,只需针对指定资源配置流控规则就可以实现 Sentinel 流控功能。

Sentinel Apache Dubbo Adapter 实现限流的核心原理是基于 Dubbo 的 SPI 机制实现 Filter 扩展,Dubbo 的 Filter 机制是专门为服务提供者和服务消费者调用过程进行拦截设计的,每次执行远程方法,该拦截都会被执行。

同时,Sentinel Apache Dubbo Adapter 还可以自定义开启或者关闭某个 Filter 的功能,下面表示关闭消费端的过滤器。

#
image-20210227193903284

# 1 Dubbo 服务接入 Sentinel Dashboard

  • 引入 sentinel-transport-simple-http 依赖

  • 添加启动参数

    image-20210227194238456
  • 登录 Sentinel Dashboard 之后,进入“簇点链路”,就可以看到资源信息。

  • 需要注意的是,限流可以通过服务接口或服务方法设置

    • 服务接口:resourceName 为接口的全限定名(包+接口名)
    • 服务方法:resourceName 为接口全限定名:方法名(包+接口名:方法名)

# 2 Dubbo 服务限流规则

两种方式

  • Sentinel Dashboard
  • FlowRuleManager.loadRules(List rules)

Sentinel Apache Dubbo Adapter 组件中没有实现规则持久化,因此有以下步骤来支持:

  • 在 dubbo 服务中添加 sentinel-datasource-nacos 依赖
  • 通过 Sentinel 提供的 InitFunc 扩展点,实现 Nacos 数据源的配置
image-20210227204336472
  • 访问 Sentinel Dashboard,在针对某个资源创建流控规则时,这个规则会同步保存到 Nacos 的配置中心,而当 Nacos 配置中心发生变化时,会触发事件机制通知 Dubbo 应用重新加载流控规则。

# Sentinel 热点限流

热点数据表示经常访问的数据,在有限场景中我们希望针对这些访问频次非常高的数据进行限流,比如针对一段时间内频繁访问的用户 ID 地址进行限流,或者针对频繁访问的某个用户 ID 进行限流。

Sentinel 提供了热点参数限流的规则,它是一种特殊的限流,在普通限流的基础上对同一个受保护的资源区根据请求中的参数分别处理,该策略只对包含热点参数的资源调用生效。热点限流在以下场景使用较多:

  • 服务网关层:例如防止网络爬虫和恶意攻击,一种常用方法就是限制爬虫的 IP 地址。
  • 写数据的服务:例如业务系统提供写数据的服务,数据会写入数据库之类的存储系统。存储系统的底层会加锁写磁盘上的文件,部分存储系统会将某一类数据写入同一个文件中。如果底层写同一文件,会出现抢占锁的情况,导致出现大量超时和失败。出现这种情况时一般有两种解决方法:修改存储设计、对热点参数限流。

Sentinel 通过LRU 策略结合滑动窗口机制来实现热点参数的统计,其中 LRU 策略可以统计单位时间内最常访问的热点数据,滑动窗口机制可以协助统计每个参数的 QPS。

# 1 热点参数限流的使用

  • 引用热点参数限流依赖包 sentinel-parameter-flow-control
  • 接下来创建一个 REST 接口,并定义限流埋点,此处针对参数 ID 配置热点限流规则。
  • 针对不同的热点参数,需要通过 SphU.entry(resourceName,EntryType.IN,1,id)方法设置,其最后一个参数是一个数组,有多个热点参数就按照次序依次传入,该配置表示后续会针对该参数进行热点限流。
  • 通过 ParamFlowRuleManager.loadRules 加载热点参数规则。

# 2 @SentinelResource

如果是通过@SentinelResource 注解来定义资源,当注解所配置得方法上有参数时,Sentinel 会把这些参数传入 SphU.entry 中

image-20210227231844217

# 3 热点参数规则说明

  • durationInSec:统计窗口时间长度,单位为 s
  • maxQueueingTimeMS:最长排队等待时长,只有当流控为 controlBehavior 设置为匀速排队模式时生效。
  • paramIdx:热点参数的索引,属于必填项,对应的是 SphU.entry 中的参数索引位置。
  • paramFlowItemList:针对指定参数值单独设置限流阈值,不受 count 阈值的限制。

# Sentinel 的工作原理(略)

  • 工作流程:由各个 Slot 插槽组成(责任链模式)
  • p229

# Spring Cloud Sentinel 工作原理(略)

  • starter 自动装配
  • p232

# Sentinel 核心源码分析(略)

  • sentinel-adapter
  • sentinel-core
  • sentinel-dashboard
  • sentinel-demo
  • sentinel-extension
  • sentinel-transport

# 1 限流的源码实现

# 2 实时指标数据统计

# 3 服务降级的实现原理

# 什么是分布式事务?

事务:作为单个逻辑工作单元执行的多个数据库操作,要么同时成功,要么同时失败,必须满足 ACID 特性。(单库多表)

在微服务架构下,随着业务服务的拆分及数据库的拆分,举例说,订单和库存分别拆分成两个独立的数据库,当客户端发起一个下单操作,需要在订单服务对应的数据库创建订单,同时基于 RPC 通信调用库存服务完成商品库存的扣减。

这样,原来的单库事务操作就变成了多个数据库的事务操作 => 数据不一致问题。

# 1 分布式事务问题的理论模型

核心原因:存储资源的分布性

在实际应用中,应该尽可能从设计层面去避免分布式事务的问题。

# 1 X/Open 分布式模型

X/Open DTP 是 X/Open 这个组织定义的一套分布式事务的标准。这个标准提出了两阶段提交(2PC,2-phase-commit)来保证分布式事务的完整性。X/Open DTP 包含以下三种角色。

  • AP:Application
  • RM:Resource Manager
  • TM:Transaction Manager

如果 TM 需要能够管理多个数据库的事务,则实现步骤如下:

  • 配置 TM,把多个 RM 注册到 TM,相当于 TM 注册 RM 作为数据源。
  • AP 从 TM 管理的 RM 中获取连接,如果 RM 是数据库则获取 JDBC 连接。
  • AP 向 TM 发起一个全局事务,生成全局事务 ID(XID),XID 会通知各个 RM。
  • AP 通过第二步获得的连接直接操作 RM 完成数据库操作。这时,AP 在每次操作会把 XID 传递给 RM。
  • AP 结束全局事务,TM 会通知各个 RM 全局事务结束。
  • 根据各个 RM 的事务执行结果,执行提交或者回滚操作。

其中,TM 和多个 RM 之间的事务控制,是基于 XA 协议来完成的。目前 Oracle、MySQL、DB2 都实现了 XA 接口,因此都能作为 RM。

image-20210227233821400

# 2 两阶段提交协议

第一阶段:事务的准备阶段

第二阶段:事务的提交或回滚阶段

这两个阶段都是由事务管理器发起的,流程如下:

  • 准备阶段:TM 通知 RM 准备分支事务,记录事务日志,并告知 TM 的准备结果。
  • 提交/回滚阶段:如果所有的 RM 在准备阶段都明确返回成功,TM 向所有 RM 发起提交指令完成数据的变更;反之,则 TM 向所有 RM 发送回滚指令。

然而,它并不是完美的,也有缺点:

  • 同步阻塞:所有 RM 都是事务阻塞型的,对于任何一次指令都必须要有明确的响应才能进行下一步,否则会处于阻塞状态。
  • 过于保守:任何一个节点失败都会导致数据回滚。
  • TM 的单点故障:如果 TM 在第二阶段故障,则所有 RM 会一直处于锁定状态。
  • “脑裂”导致数据不一致问题:在第二阶段中,TM 向所有 RM 发送 commit 请求后,发生局部网络异常导致只有一部分 RM 接受到 commit,剩余未收到请求的则没提交,导致数据出现不一致问题。

# 3 三阶段提交协议

利用超时机制解决了同步阻塞的问题

  • CanCommit(询问阶段):TM 向 RM 发送事务执行请求,询问是否可以完成指令,参与者只需回答是或者不是即可,不需要做真正的事务操作,这个阶段会有超时中止机制。
  • PreCommit(准备阶段):TM 根据 RM 的反馈结果决定是否继续,如果在询问阶段所有 RM 都能执行操作,则 TM 向所有 RM 发送 PreCommit 请求,RM 收到请求后写 redo 和 undo 日志,执行事务操作但是不提交事务,然后返回 ACK 响应等待 TM 的下一步通知。如果询问阶段任意参与者返回不能执行操作的结果,则 TM 发送事务中断请求。
  • DoCommit(提交或回滚阶段):根据上一步骤的执行结果,如果每个 RM 都返回成功,则 TM 发送事务提交指令,反之则中止。

三阶段提交协议与二阶段提交协议的区别

  • 增加了一个 CanCommit 阶段,可以尽早发现无法执行操作而中止后续的行为。
  • 在准备阶段之后,TM 和 RM 都引入超时机制,一旦超时,TM 和 RM 会继续提交事务,并且认为处于成功状态,因为这种情况下事务默认为成功的可能性比较大。

实际上,一旦超时,在三阶段提交协议下仍然可能出现数据不一致的问题,当然概率是比较小的。另外,最大的好处是基于超时机制来避免资源的永久锁定。

# 4 CAP 定理和 BASE 理论

XA 协议:二阶段提交和三阶段提交,数据一致性强,但可用性低。

CAP 定理:布鲁尔定理,指在分布式系统中不可能同时满足一致性 C、可用性 A、分区容错性 P,最多同时满足两个。

  • C:数据在多个副本中要保持强一致
  • A:系统对外提供的服务必须一直处于可用状态。
  • P:在分布式系统中遇到任何网络分区故障,系统仍然能够正常对外提供服务。

在分布式系统中,要么满足 CP,要么满足 AP,不可能实现 CAP 或者 CA,因为网络通信不是绝对可靠的。

  • AP:放弃强一致性,实现最终的一致。(很多互联网公司的主要选择)
  • CP:放弃高可用性,实现强一致性。(2PC 和 3PC,存在问题:用户完成一个操作可能会等待较长的时间,用户体验差)

BASE 理论:由于 CAP 中 CA 不可兼得衍生出来的一种新的思想。核心思想是:牺牲数据的强一致性来获得高可用性,有三个特性:

  • Basically Avaliable(基本可用):分布式系统出现故障时,允许损失一部分功能的可用性,保证核心功能的可用。
  • Soft State(软状态):允许系统中的数据存在中间状态,这个状态不影响系统的可用性,也就是允许系统中不同节点的数据副本之间的同步存在延时。
  • Eventually Consistent(最终一致性):中间状态的数据在经过一段时间之后,会达到一个最终的数据一致性。

# 2 分布式事务问题的常见解决方案

# 1 TCC 补偿性方案

TCC(Try-Confirm-Cancel)是一种比较成熟的分布式数据一致性解决方案,它实际上是把一个完整的业务拆分为如下三个步骤

  • Try:这个阶段主要是对数据的校验或者资源的预留。
  • Confirm:确定真正执行的任务,只操作 Try 阶段预留的资源。
  • Cancel:取消执行,释放 Try 阶段预留的资源。

本质:二阶段提交的思想,第一阶段通过 Try 准备,第二阶段通过 Confirm/Cancel

image-20210228000708843

# 2 基于可靠性消息的最终一致性方案

基于可靠性消息的最终一致性方案是互联网公司比较常用的分布式数据一致性解决方案,它主要利用消息中间件(Kafka、RocketMQ 或 RabbitMQ)的可靠性机制来实现数据一致性的投递。

image-20210228001428049

总结:消费者没有向消息中间件服务器发送确认之前,这个消息会被重复投递,确保消息的可靠性消费。

# 3 最大努力通知型

与基于可靠性消息的最终一致性方案实现类似,是一种比较简单的柔性事务解决方案。

如果没有返回一个消息确认时,则不断进行重试,直到收到一个消息确认或者达到最大重试次数。

# 3 分布式事务框架 Seata

提供了 AT、TCC、Saga 和 XA 四种事务模式。

# 1 AT 模式

Seata 最主推的分布式事务解决方案,基于 XA 演进而来,分为 TM、RM 和 TC,TC 作为 Seata 的服务器独立部署。

# 2 Saga 模式

又称长事务解决方案,主要描述的是在没有 2PC 的情况下如何解决分布式事务问题。其核心思想是:把一个业务流程中的长事务拆分为多个本地短事务,业务流程中的每个参与者都提交真实提交给本地段事务,当其中一个参与者失败,则通过补偿机制补偿前面已经成功的参与者。

两种补偿恢复方式:

  • 向后恢复:如果任一子事务失败,则撤销执行结果。
  • 向前恢复:不进行补偿,而是对失败的事务进行 redo,这种方式比较适合于事务必须要执行成功的场景。

优点:

  • 一阶段直接提交本地事务
  • 没有锁等待,性能较高
  • 在事件驱动的模式下,短事务可以异步执行。
  • 补偿机制的实现比较简单。

缺点:不提供原子性和隔离性支持

协调模式:

  • 事件/编排式
  • 命令/协同式
本博客已稳定运行
总访客数: Loading
总访问量: Loading
发表了 73 篇文章 · 总计 323.73k

使用 Hugo 构建
主题 StackJimmy 设计
基于 v3.27.0 分支版本修改