分布式一致性那些事儿


1 什么是微服务、微服务架构、分布式

  • 微服务:它是一个单一的、完整的服务,这个服务只做部分功能。甚至一个微服务只负责创建数据、一个微服务只负责修改数据,也是可用的。不同的微服务之间通过rpc相互交互。一个微服务是一个完整的服务,它的开发、联调、测试、部署可以由一个团队负责,与其他服务没有任何关系。
  • 微服务架构:当拿到一个服务的时候,从它的用户量、功能复杂度、并发性等角度考量,决定是否需要拆分成多个微服务。如果用户量很少,或者功能比较简单,或者并发性很低,则可以考虑只用一个服务实现所有功能;如果用户量很多,或者功能比较复杂,或者并发性很高,则可以考虑按功能模块划分成多个微服务,每个微服务实现某一个模块的功能。这就是微服务架构
  • 分布式:一个系统拆分成了多个微服务,并且部署到不同的逻辑服务器或者虚拟机,则称之为分布式部署。分布式部署的应用不一定是微服务架构,比如集群部署,它是把相同的服务部署到不同的机器上,功能上仍属于单体应用。因此微服务架构一定是多个功能不同的微服务并且部署在不同的逻辑服务器或者虚拟机上。

2 分布式的优点

传统单体服务 分布式微服务
新功能开发 复杂 工作量少、容易实现
架构设计 难度小 难度大
系统性能 相应时间快,吞吐量小 相应时间慢、吞吐量大
系统运维 运维简单 运维复杂
技术 技术单一且封闭 技术多样且开放
测试和查错 简单 复杂
扩展性 扩展性很差 扩展性非常好

3 分布式带来的问题

分布式提高了系统可用性、吞吐量,增强了系统扩展性,但同时也带来了一些问题。比如增加了系统复杂性、排查问题的难度、服务之间调度产生的各种问题,最大的问题就是数据一致性。

某一件事情分配给了多个不同的服务取实现,由于网络传输问题,只要有任何一个服务执行失败则整件事情就失败了,其他已经执行成功的服务就产生了脏数据,造成了数据不一致。这是分布式带来的、不可避免的问题。业界对这个问题做了许多研究,提出了多种解决方案。

4 CAP理论

CAP定理又称CAP原则,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),最多只能同时三个特性中的两个,三者不可兼得。

4.1 CAP原则简介

描述
Consistency(一致性) 指数据在多个副本之间能够保持一致的特性(严格的一致性)
Availability(可用性) 指系统提供的服务必须一直处于可用的状态,每次请求都能获取到非错的响应(不保证获取的数据为最新数据)
Partition tolerance(分区容错性) 分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障

什么是分区?
在分布式系统中,不同的节点分布在不同的子网络中,由于一些特殊的原因,这些子节点之间出现了网络不通的状态,但他们的内部子网络是正常的。从而导致了整个系统的环境被切分成了若干个孤立的区域,同时每个孤立的区域能够对外提供完整的功能,这就是分区。

4.2 CAP原则的权衡

通过CAP理论,我们知道无法同时满足一致性、可用性和分区容错性这三个特性,那要舍弃哪个呢?
(1) C A without P
如果不要求P(不允许分区),即单一系统或者所有微服务都在一个网络内,比如一台逻辑服务器或者虚拟机少给你,则C(强一致性)和A(可用性)是可以保证的。比如单一数据库、单一服务。但是分布式系统分区一定存在分区,因此P一定需要考虑。所以CAP理论其实是允许P之后,考虑如何尽最大努力保证C和A。
(2) C P without A
如果不要求A(可用),相当于每个请求都需要在Server之间强一致,而P(分区)会导致同步时间无限延长,如此CP也是可以保证的。很多传统的数据库分布式事务都属于这种模式,当发生C或者P的时候,系统不可用,必须强制确保C和P。
(3) A P without C
要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性,但是整个系统对外展示还是可用的,如果测试有数据修改则更会造成数据不一直,因此这个时候可用对系统做一些控制,比如允许只读,不允许写数据,相当于降低了部分A的功能,来确保数据不一致程度最小。还有一种方法就是在各个分区内记录下写的操作,等P回复之后再同步数据,确保数据最终一致性,但是这样有风险。

上面三种权衡策略有各自的优缺点,需要根据不同的场景选择不同的权衡策略。比如对于普通的、影响不大的应用,可用选择A P without C,舍弃数据一致性来确保系统可用;但是对于金融、支付系统必须强制确保C,当发生了P的时候,可用舍弃A,即宁可系统不可用,也要保证数据强一致性。

CAP理论之间的关系如下图:



ACID就是C&A without P, 对于单一数据库,没有P,因此单一数据库可用保证ACID特性。

BASE就是牺牲了数据强一致性,保证了A&P,但它能确保数据最终一致性。

5 BASE理论

BASE理论是Basically Available(基本可用),Soft State(软状态)和Eventually Consistent(最终一致性)三个短语的缩写。

核心思想
既是无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

5.1 基本可用(Basically Available)

什么是基本可用呢?就是发生了P,牺牲C,还能保证A。

假设系统,出现了不可预知的故障,但还是能用,相比较正常的系统而言:

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

5.2 软删除(Soft State)

什么是软状态呢?相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。

软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。

5.3 最终一致性(Eventually Consistent)

上面说软状态,然后不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间期限取决于网络延时、系统负载、数据复制方案设计等等因素。

实际上,不只是分布式系统使用最终一致性,关系型数据库在某个功能上,也是使用最终一致性的。比如备份,数据库的复制过程是需要时间的,这个复制过程中,业务读取到的值就是旧的。当然,最终还是达成了数据一致性。这也算是一个最终一致性的经典案例。

6 分布式一致性的五种解决方案

分布式涉及到数据一致性问题,数据库中通过事务确保数据一致性。因此分布式系统也可用通过分布式事务来确保分布式数据一致性。

6.1 2PC

2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备和提交两个阶段。

注意这只是协议或者说是理论指导,只阐述了大方向,具体落地还是有会有差异的。

让我们来看下两个阶段的具体流程。

  • 准备阶段:协调者会给各参与者发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了。
  • 提交阶段:同步等待所有资源的响应之后就进入第二阶段即提交阶段(注意提交阶段不一定是提交事务,也可能是回滚事务)。

    假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,返回事务执行成功。

    让我们来看一下流程图:



假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败



那可能就有人问了,那第二阶段提交失败的话呢?

这里有两种情况:

  • 第一种是第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。
  • 第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功,到最后真的不行只能人工介入处理。
    大体上二阶段提交的流程就是这样,我们再来看看细节。

首先 2PC 是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。

在第二阶段协调者的没法超时,因为按照我们上面分析只能不断重试!

2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。

6.2 3PC

3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。

3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit。

  • 准备阶段:什么都不做,就是问候一下,确认对方是否网络通畅。
  • 预提交阶段:对应2PC的准备阶段,除了提交什么都做了。
  • 提交阶段:提交或者回滚。
    让我们来看一下图:



不管哪一个阶段有参与者返回失败都会宣布事务失败,这和 2PC 是一样的(当然到最后的提交阶段和 2PC 一样只要是提交请求就只能不断重试)。

相对于2PC, 3PC多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。

那么引入了超时机制,参与者就不会傻等了,如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。

然而超时机制也会带来数据不一致的问题,比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了.

3PC 的引入是为了解决提交阶段 2PC 协调者和某参与者都挂了之后新选举的协调者不知道当前应该提交还是回滚的问题。

新协调者来的时候发现有一个参与者处于预提交或者提交阶段,那么表明已经经过了所有参与者的确认了,所以此时执行的就是提交命令。

所以说 3PC 就是通过引入预提交阶段来使得参与者之间的状态得到统一,也就是留了一个阶段让大家同步一下。

但是这也只能让协调者知道该如果做,但不能保证这样做一定对,这其实和上面 2PC 分析一致,因为挂了的参与者到底有没有执行事务无法断定。

所以说 3PC 通过预提交阶段可以减少故障恢复时候的复杂性,但是不能保证数据一致,除非挂了的那个参与者恢复。

让我们总结一下, 3PC 相对于 2PC 做了一定的改进:引入了参与者超时机制,并且增加了预提交阶段使得故障恢复之后协调者的决策复杂度降低,但整体的交互过程更长了,性能有所下降,并且还是会存在数据不一致问题。

所以 2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制

6.3 TCC

2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!

TCC 指的是Try - Confirm - Cancel。

  • Try 指的是预留,即资源的预留和锁定,注意是预留。
  • Confirm 指的是确认操作,这一步其实就是真正的执行了。
  • Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
    其实从思想上看和 2PC 差不多,都是先试探性的执行,如果都可以那就真正的执行,如果不行就回滚。

比如说一个事务要执行A、B、C三个操作,那么先对三个操作执行预留动作。如果都预留成功了那么就执行确认操作,如果有一个预留失败那就都执行撤销动作。

我们来看下流程,TCC模型还有个事务管理者的角色,用来记录TCC全局事务状态并提交或者回滚事务。



可以看到流程还是很简单的,难点在于业务上的定义,对于每一个操作你都需要定义三个动作分别对应Try - Confirm - Cancel。

因此 TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。

还有一点要注意,撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。

相对于 2PC、3PC ,TCC 适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。

6.4 本地消息表

本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。

本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。

然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。

如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。

这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。

可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。

6.5 消息队列

RocketMQ 就很好的支持了消息事务,让我们来看一下如何通过消息实现事务。

第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。

再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。

并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。

如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。

如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。

可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。



可以看到消息事务实现的也是最终一致性。消息事务与本地消息表差不多,只是它的小i西保存在消息队列。

7 一致性协议

8 一致性算法

9 参考

【1】分布式理论(一) - CAP定理

【2】分布式理论(二) - BASE理论

【3】分布式理论(三) - 2PC协议

【4】分布式理论(四) - 3PC协议

【5】分布式理论(五) - 一致性算法Paxos

【6】分布式理论(六) - 一致性协议Raft

【7】面试必问:分布式事务六种解决方案

【8】一分钟弄懂什么是分布式和微服务

【9】分布式优缺点


文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录