分布式理论及相关设计方案|青训营笔记

80 阅读14分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记

分布式

分布式理论

CAP

CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)这三个基本需求。

  • 一致性 :数据在多个节点之间能够保持一致的特性。
  • 可用性:系统提供的服务一直处于可用的状态,每次请求都能获得正确的响应。
  • 分区容错性:分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。当一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到这个区所有节点里。容忍性就提高了。然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。

总的来说就是,数据存在的节点越多,分区容忍性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。CP和AP不能同时满足

BASE

BASE 指的是 Basically Available(基本可用) 、Soft-state(软状态) 和 Eventually Consistent(最终一致性)

BASE 理论是对 CAP 中一致性 C 和可用性 A 权衡的结果,其来源于对大规模互联网系统分布式实践的总结,是基于 CAP 定理逐步演化而来的,它大大降低了我们对系统的要求。

  • 基本可用: 系统在实现具体业务时,可用允许损失部分可用性

    • 响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在2秒作用返回结果。
    • 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单。但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
  • 软状态: 允许系统数据存在中间状态,允许数据同步过程存在延时

  • 最终一致性: 不要求实时数据一致性,只保证最终的数据一致性

分布式事务

X/A规范

X/A规范主要有两种角色:事务管理器和资源管理器

事务管理器:负责全局事务的开启提交回滚

资源管理器:负责本地事务的开启提交和回滚

实现分布式过程分为两个阶段

  • 第一阶段:事务管理器会首先向所有的资源管理器节点发送Prepare请求。各子节点对数据加锁,执行事务但不提交。执行成功返回信息给事务管理器。
  • 第二阶段:事务管理器收到的信息都是成功信息,则向各子节点发送commit请求,各子节点提交事务并释放锁。分布式事务成功完成。如果事务收到了某个子节点返回的失败信息,则向各子节点发送abort请求,各子节点回滚事务。

特点:处理长事务时所有事务都会长时间占用锁,影响性能

TCC(业务层面)

tcc主要是业务层的解决方案

TCC主要分为三个阶段

  • Try:预留业务资源(完成业务检查+锁定业务资源)

    • 释放Try阶段预留的业务资源
  • Confirm:确认执行业务操作

    • 真正执行业务
    • 不做任何业务检查
    • 只使用Try阶段预留的业务资源
  • Cancel:取消执行业务操作

案例理解见文章链接

空回滚: 在没有调用TCC资源Try方法的情况下,调用来二阶段的Cancel方法,Cancel方法需要识别出这是一个空回滚,然后直接返回成功。 出现原因是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。 解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行来,那就是正常回滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条。再额外增加一张‘分支事务记录表’,其中有全局事务ID和分支事务ID,第一阶段Try方法里会插入一条记录,表示一阶段执行来。Cancel接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存在,则是空回滚,直接返回

幂等 :TCC二阶段一般不是幂等接口,为了保证二阶段的幂等性,解决方案是在 “分支事务记录表”中增加执行状态,每次执行前都查询该状态。

悬挂 : 悬挂就是对于一个分布式事务,其二阶段Cancel接口比Try接口先执行。 出现原因是在RPC调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时RPC调用的网络发生拥堵,通常RPC调用是有超时时间的,RPC超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC请求才到达参与者真正执行,而一个Try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后无法继续处理。 解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,解决方案是在try阶段在“分支事务记录”表中查询是否已经有二阶段事务记录,如果有则不执行Try。

特点:没有全局锁,性能较高。但研发上需要开发者实现TCC接口,以及需要开发者解决空回滚、悬挂、幂等问题

AT(Seata)

AT中一个分布式事务包含3中角色:

Transaction Coordinator (TC): 事务协调器,负责协调并驱动全局事务的提交或回滚

Transaction Manager (TM): 事务管理器,控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。

Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

AT分布式事务的执行过程:

①TM向TC申请开启一个全局事务

②TC创建全局事务唯一标识xid,并将全局事务拆分分发给RM执行

③RM解析sql语义,保存前镜像——>执行sql——>保存后镜像,向TC申请对应记录的全局锁

④RM拿到对应的全局锁后提交本地事务,上报TC执行结果

⑤如果RM都执行成功,TC发起全局提交,各RM删除对应镜像以及释放全局锁。否则,TC发起全局回滚,所有通过保存的镜像执行回滚sql,并释放全局锁

三种模式中只有AT具有全局锁这个概念,主要是因为AT相比XA在第一阶段就已经进行了本地事务的提交。引入全局锁的概念是为了保证全局事务读提交的隔离级别

特点:相比X/A,AT的效率更高。因为AT将锁分为了全局锁和本地锁,当本地事务提交后本地锁会直接释放。而X/A是两个阶段都一直持有本地锁不释放。但AT的效率不如TCC,因为TCC没有全局锁,但AT可用屏蔽分布式事务的管理对开发者的影响,这是TCC所不具备的

AT引入全局锁的原因主要是为了保证事务的隔离性为读提交。因为如果③不加全局锁,在本地事务提交但全局事务还未提交时,其他的全局事务就可以读到本地事务已经提交的数据,这对于全局事务来说,不满足读提交的隔离要求。

当全局事务需要实现读提交的隔离级别时,可以使用slect·····for update查询。

Seata由于一阶段RM自动提交本地事务的原因,默认隔离级别为Read Uncommitted。如果希望隔离级别为Read Committed,那么可以使用SELECT...FOR UPDATE语句。Seata引擎重写了SELECT...FOR UPDATE语句执行逻辑SELECT...FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT...FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是已提交的才返回。出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。

对比

XA:实现简单,但处理长事务时所有事务都会长时间占用锁,影响性能

TCC:把数据库层面的两段提交上升到应用层实现,规避了数据库层两段提交的性能低下问题。TCC的Try、Confirm和Cancel api需业务自身提供,增加了应用层系统的复杂度和开发成本

AT:数据库层面两段提交,与XA不同的是第一阶段就会提交本地事务,规避XA本地事务占用锁时间长导致系统性能低下的问题。但为了保证全局事务的隔离性为读提交以上,又引入了全局锁的概念。性能低于TCC,但向应用层屏蔽了具体的实现细节,降低了应用层的开发成本。

性能:XA<AT<TCC

成本:XA<AT<TCC

分布式锁

集群环境下,单纯的Java并发API并不能提供跨机器的线程同步问题。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

Redis实现

利用 SETNX 和 SETEX

基本命令主要有

  • SETNX(SET If Not Exists):当且仅当 Key 不存在时,则可以设置,否则不做任何动作。
  • SETEX:可以设置超时时间

其原理为:通过 SETNX 设置 Key-Value 来获得锁,随即进入死循环,每次循环判断,如果存在 Key 则继续循环,如果不存在 Key,则跳出循环,当前任务执行完成后,删除 Key 以释放锁。

① 这种方式可能会导致死锁,为了避免死锁,需要设置超时时间

② 设置过期时间expire和setnx并不是原子操作,为了实现原子性,redis2.8之前可用采用lua脚本,Redis2.8后可用使用set指令的扩展参数来保证setnx和setex组合为原子操作

③ 为了保证锁的存活时间大于业务逻辑的处理时间,可用使用守护线程对key增加存活时间

④ 为了避免自己的锁被别人释放,在释放锁的时候需要使用lua脚本保证原子性,或者在value上加上唯一标识(UUID或时间戳)

⑤ 原则上redis分布式锁不支持锁重入,为了避免非重入锁的死锁问题,可用利用threadLocal实现锁重入

image-20220402141915241

这种方式只适用于单机redis模式,如果redis集群的情况下,可能会出现两个客户端持有同一把锁的情况。

在单Matster-Slave的Redis系统中,正常情况下Client向Master获取锁之后同步给Slave,如果Client获取锁成功之后Master节点挂掉,并且未将该锁同步到Slave,之后在Sentinel的帮助下Slave升级为Master但是并没有之前未同步的锁的信息,此时如果有新的Client要在新Master获取锁,那么将可能出现两个Client持有同一把锁的问题。

集群环境下,master节点获得锁,但在同步给slave时发生了宕机。则slave节点被推选为master,但slave上还没有锁数据。此时就会造成一锁二用的情况

Redlock算法过程

Redlock算法是Antirez在单Redis节点基础上引入的高可用模式。

在Redis的分布式环境中,我们假设有N个完全互相独立的Redis节点,在N个Redis实例上使用与在Redis单实例下相同方法获取锁和释放锁。

现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:

  • 获取当前Unix时间,以毫秒为单位。
  • 依次尝试从5个实例获取锁,并在客户端设置超时时间。(这个超时时间应该小于锁的失效时间,这样可以避免客户端死等)
  • 客户端计算获取到锁使用的时间,如果半数以上的节点获取锁时间小于锁失效时间,锁才算获取成功。
  • 锁获取成功,计算锁真正有效时间=key有效时间-获取锁时间
  • 锁获取失败,客户端对所有主节点进行锁释放操作(delete)

幂等性

对于数据变更类接口,两种行为要考虑幂等性:

①用户重复提交/用户恶意攻击

②微服务之间通信时会进行超时重传,导致服务端接口被重复调用

1.唯一索引(订单创建)

2.给请求分配唯一标识(对象序列化+时间戳+md5减少长度),处理过的请求标识存入redis(为了避免mq消息重复消费,可以使用setnx命令把消息写入redis)

3.状态机:一条数据完整运行时状态的转换流程,比如订单状态,因为它的状态只会向前变更,所以多次修改同一数据时,那么对这条数据修改造成的影响只会发生一次

微服务的利弊

优点

  • 微服务影响范围小,风险小,造价成本低
  • 频繁发布版本,快速交付需求
  • 低成本扩容: 微服务的拆解,增加了系统的安全性和故障隔离,可以让我们针对不同的服务,实施不同的扩容和存储技术。更适合云环境
  • 细粒度拆分,解耦性强: 微服务的目的是拆分解耦应用,专注去耦合,让不同不同的业务团队服务不同的微服务,专人专事,缩小迭代影响范围,让微服务更容易进行水平扩展微服务遵循单一职责
  • 微服务负责范围小,上手容易: 相比传统单机和SOA,微服务每个服务负责的范围更小,上手更容易
  • 单微服务部署方式灵活多变: 相比传统单机架构和SOA,微服务可独立拥有存储空间和计算空间,根据自身的需要独立构建部署
  • 更轻量级的系统通信方式: 相比SOA架构使用消息队列作为通信协议,微服务使用更轻量级的rpc/rest通信协议
  • 测试和升级服务更加容易: 当一个微服务规模和功能逐步增长后,可以继续按职责拆分了多个微服务。因此微服务始终保持小型、可管理、自治的特点。这使得测试和升级服务更加容易
  • 避免了单体系统开发效率低下的问题: 单体系统中随着业务功能、数据库数据、项目总代码量变大后,代码的维护和开发难度都会急速上升,而微服务则很好的避免了这些问题,始终保持较好的开发速度
  • 微服务的是可持续性的开发: 单体系统不同,微服务是一个可持续的体系结构,通过添加新的微服务来满足快速变化的业务需求,而不是修改(和破坏)旧的服务

缺点

  • 对开发人员的要求提高:不仅要学习各自微服务组件,还要解决分布式系统所带来的一系列问题(分布式锁、分布式事务、缓存一致性和CAP的相关问题)
  • 增加了运维的复杂度:使用微服务架构可能会增加运维开销。 使用这种方法,您的部署可能需要大量资源。您可能需要更多的时间和精力来创建基础架构。 所有服务可能都需要群集以实现故障转移和弹性。 您的系统可能具有数十个单独的组件,并且在您添加新功能时,它将变得越来越复杂。

文章链接

Redis实现分布式锁+可重入性

blog.csdn.net/jibin295347…

RedisTemplate 分布式锁之使用守护线程为key续命

blog.csdn.net/fuwei52406/…

Seata:Spring Cloud Alibaba分布式事务组件(非常详细)

c.biancheng.net/springcloud…

阿里分布式事务框架Seata原理解析

www.jianshu.com/p/044e95223…

分布式事务TCC

www.cnblogs.com/duanxz/p/52…