【分布式】关于分布式“一致性”的讨论

一、写在前面的话

在分布式来临之前,主流是集中式的计算机架构。`集中式系统对主机的性能和稳定性要求很高。

它由一台或多台主计算机组成中心节点,数据集中存储于这个中心节点中,并且整个系统的所有业务单元都集中部署在这个中心节点上,系统的所有功能均由其集中处理。也就是说,在集中式系统中,每个终端或客户端机器仅仅负责数据的录入和输出,而数据的存储与控制处理完全交由主机来完成。

由于集中式系统往往基于底层性能卓越的大型主机,因此无须考虑如何对服务进行多个节点的部署,也就不用考虑多个节点之间的分布式协作问题。这个时候在数据方面,我们会重点关注数据库的事务,它基本上解决了数据不一致的痛点。

可是一方面由于大型机实在太昂贵了,且在单一大型主机上进行系统的扩容往往比较困难,另外一方面PC机性能的不断提升和网络技术的快速普及,很多企业开始放弃原来的大型主机,而改用小型机和普通PC服务器来搭建分布式的计算机系统,环境驱使下,分布式发展一直很火热。

但是分布式环境存在很多的问题,之前那套保持数据一致性的理论已经不在适用。有人分析所面临的的困难,提出新的理论CAP,在此基础上随着不断的实践,又出现了BASE理论,这些理论为解决分布式面临的问题提供了很好的指导,其中我们要重点关注数据的一致性问题

为了解决分布式一致性问题,在长期的探索硏究过程中,出现很多优秀的一致性协议和算法,比如两阶段提交三阶段提交Paxos算法等。


因为前辈们这部分内容做的太足了,我一直想写点东西,但是却动不了笔,最后索性选了一篇我的文章,摘录了《从Paxos到Zookeeper分布式一致性原理与实践》的一些内容,还有我的偶像Holis的两篇文章。

文章虽然表明转载,但却是我整理的,如果你要转载,请注明我的地址。


二、数据库的事务

这部分当做复习吧

事务( Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元(Unit)。

一方面,当多个应用程序并发访问数据库时,事务可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

另一方面,事务为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持数据一致性的方法。

事务具有四个特征,分别是原子性( Atomicity)、一致性( Consistency)、隔离性( Isolation)和持久性( Durability),简称为事务的ACID特性。

  1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性: 执行事务前后,数据保持一致;由于并发操作带来的不一致性,通常包括以下几种类型:丢失数据修改,读“脏”数据,不可重复读和幻读(也称为产生幽灵数据)。

穿插上述这几个名词的解释:
脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。

  1. 隔离性: 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;

SQL 标准定义了四个隔离级别:
READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
在这里插入图片描述

  1. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

持久化涉及到数据库的恢复问题

三、分布式环境的各种问题

为了从集中式更好地过渡到分布式,这里先说一下分布式环境面临的问题

通信异常

分布式系统需要在各个节点之间进行网络通信,因此每次网络通信都会伴随着网络不可用的风险,网络光纤、路由器等硬件设备或是系统不可用都会导致最终分布式系统无法顺利完成一次网络通信。另外,即使分布式系统各节点之间的网络通信能够正常进行,其延时也会远大于单机操作,消息丟失和消息延迟在分布式环境下变得非常普遍。

网络分区

当网络由于发生异常情况,导致分布式系统中部分节点之间的网络延时不断增大,最终导致组成分布式系统的所有节点中,只有部分节点之间能够进行正常通信,而另一些节点则不能,我们将这个现象称为网络分区,就是俗称的“脑裂”。

当网络分区出现时,分布式系统会出现局部小集群,这就对分布式一致性提出了非常高的要求

三态

分布式系统的每一次请求与响应,存在特有的“三态”概念,即成功、失败与超时。由于网络是不可靠的,虽然在绝大部分情况下,网络通信也能够接收到成功或失败的响应,但是当网络出现异常的情况下,就可能会出现超时现象,通常有以下两种情况:

  • 由于网络原因,该请求(消息)并没有被成功地发送到接收方,而是在发送过程就发生了消息丟失现象。
  • 该请求(消息)成功的被接收方接收后,并进行了处理,但是在将响应反馈给发送方的过程中,发生了消息丢失现象。

当出现这样的超时现象时,网络通信的发起方是无法确定当前请求是否被成功处理的

节点故障

节点故障指的是组成分布式系统的服务器节点出现的宕机或“僵死”现象。这种现象很常见哦。

三、CAP和BASE理论

CAP最初以猜想的形式提出来,经过实践,在此基础上发展出BASE理论

CAP理论告诉我们,一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中的两项。

下面我们依次介绍一下这三个基本需求

(1) 一致性

在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态

如何既保证数据的一致性,同时又不影响系统运行的性能,是每一个分布式系统都需要重点考虑和权衡的。于是,数据一致性级别由此诞生。

强一致性
这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响比较大。

弱一致性
这种一致性级别约東了系统在写入成功后,不承诺立即可以读到写入的值,也不具体承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别
(比如秒级别)后,数据能够达到一致状态。弱一致性还可以再进行细分:

  • 会话一致性:该一致性级别只保证对于写入的值,在同一个客户端会话中可以读到一致的值,但其他的会话不能保证。
  • 用户一致性:该一致性级别只保证对于写入的值,在同一个用户中可以读到一致的值,但其他用户不能保证。


最终一致性
最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性

(2) 可用性

可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

(3) 分区容错性

分区容错性要求一个分布式系统需要具有如下特性:

  • 分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障

  • 网络分区是指在分布式系统中,不同的节点分布在不同的子网络(机房或异地网络等)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状况,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域。需要注意的是,组成一个分布式系统的每个节点的加入与退出都可以看作是一个特殊的网络分区。


CAP理论告诉我们一个分布式系统不可能同时满足一致性、可用性和分区容错性这三个需求。

可是对于一个分布式系统而言,分区容错性可以说是一个最基本的要求(对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题)因此系统架构设计师往往需要把精力花在如何根据业务特点在C(一致性)和A(可用性)之间寻求平衡

在这里插入图片描述


BASE是 Basically Available(基本可用)、 Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的简写

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

(1)基本可用

基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用。以下两个就是“基本可用”的典型例子。

  • 响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
  • 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。

(2)弱状态

弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时

(3)最终一致性

最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

在实际工程实践中,最终一致性存在以下五类主要变种。

  1. 因果一致性(Casual consistency):因果一致性是指,如果进程A在更新完某个数据项后通知了进程B,那么进程B之后对该数据项的访问都应该能够获取到进程A更新后的最新值,并且如果进程B要对该数据项进行更新操作的话,务必基于进程A更新后的最新值,即不能发生丢失更新情况。与此同时,与进程A无因果关系的进程C的数据访问则没有这样的限制。
  2. 读己之所写(Read your writes):读己之所写是指,进程A更新一个数据项之后,它自己总是能够访问到更新过的最新值,而不会看到旧值。也就是说,对于单个数据获取者而言,其读取到的数据一定不会比自己上次写入的值旧。因此,读己之所写也可以看作是一种特殊的因果一致性。
  3. 会话一致性(Session consistency):会话一致性将对系统数据的访问过程框定在了一个会话当中:系统能保证在同一个有效的会话中实现“读己之所写”的一致性,也就是说,执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值。
  4. 单调读一致性(Monotonic read consisency):单调读一致性是指如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。
  5. 单调写一致性(Monotonic write consisency):单调写一致性是指,一个系统需要能够保证来自同一个进程的写操作被顺序地执行。

以上就是最终一致性的五类常见的变种,在实际系统实践中,可以将其中的若干个变种互相结合起来,以构建一个具有最终一致性的分布式系统。

事实上,最终一致性并不是只有那些大型分布式系统才设计的特性,许多现代的关系型数据库都采用了最终一致性模型。在现代关系型数据库中,大多都会采用同步和异步方式来实现主备数据复制技术。在同步方式中,数据的复制通常是更新事务的一部分,因此在事务完成后,主备数据库的数据就会达到一致。而在异步方式中,备库的更新往往存在延时,这取决于事务日志在主备数据库之间传输的时间长短,如果传输时间过长或者甚至在日志传输过程中出现异常导致无法及时将事务应用到备库上,那么很显然,从备库中读取的的数据将是旧的,因此就出现了不一致的情况。当然,无论是采用多次重试还是认为数据订正,关系型数据库还是能搞保证最终数据达到一致——这就是系统提供最终一致性保证的经典案例。

总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。


四、一致性协议


随着大型网站的各种高并发访问、海量数据处理等场景越来越多,如何实现网站的高可用、易伸缩、可扩展、安全等目标就显得越来越重要。为了解决这样一系列问题,大型网站的架构也在不断发展。提高大型网站的高可用架构,不得不提的就是分布式。在分布式一致性一文中主要介绍了分布式系统中存在的一致性问题。本文将简单介绍如何有效的解决分布式的一致性问题,其中包括什么是分布式事务二阶段提交三阶段提交

在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本(replica),这些副本会放置在不同的物理的机器上。为了对用户提供正确的增\删\改\差等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。

为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two Phase Commitment Protocol)、三阶提交协议(Three Phase Commitment Protocol)和Paxos算法


分布式事务

分布式事务是指会涉及到操作多个数据库的事务。其实就是将对同一库事务的概念扩大到了对多个库的事务。目的是为了保证分布式系统中的数据一致性。分布式事务处理的关键是必须有一种方法可以知道事务在任何地方所做的所有动作,提交或回滚事务的决定必须产生统一的结果(全部提交或全部回滚)

在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确的知道其他节点中的事务执行情况。所以从理论上讲,两台机器理论上无法达到一致的状态。如果想让分布式部署的多台机器中的数据保持一致性,那么就要保证在所有节点的数据写操作,要不全部都执行,要么全部的都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果。所以他也就不知道本次事务到底应该commit还是 roolback。所以,常规的解决办法就是引入一个“协调者”的组件来统一调度所有分布式节点的执行。

XA规范

X/Open 组织(即现在的 Open Group )定义了分布式事务处理模型。 X/Open DTP 模型( 1994 )包括应用程序( AP )、事务管理器( TM )、资源管理器( RM )、通信资源管理器( CRM )四部分。一般,常见的事务管理器( TM )是交易中间件,常见的资源管理器( RM )是数据库,常见的通信资源管理器( CRM )是消息中间件。 通常把一个数据库内部的事务处理,如对多个表的操作,作为本地事务看待。数据库的事务处理对象是本地事务,而分布式事务处理的对象是全局事务。 所谓全局事务,是指分布式事务处理环境中,多个数据库可能需要共同完成一个工作,这个工作即是一个全局事务,例如,一个事务中可能更新几个不同的数据库。对数据库的操作发生在系统的各处但必须全部被提交或回滚。此时一个数据库对自己内部所做操作的提交不仅依赖本身操作是否成功,还要依赖与全局事务相关的其它数据库的操作是否成功,如果任一数据库的任一操作失败,则参与此事务的所有数据库所做的所有操作都必须回滚。 一般情况下,某一数据库无法知道其它数据库在做什么,因此,在一个 DTP 环境中,交易中间件是必需的,由它通知和协调相关数据库的提交或回滚。而一个数据库只将其自己所做的操作(可恢复)影射到全局事务中。

XA 就是 X/Open DTP 定义的交易中间件与数据库之间的接口规范(即接口函数),交易中间件用它来通知数据库事务的开始、结束以及提交、回滚等。 XA 接口函数由数据库厂商提供。

二阶提交协议三阶提交协议就是根据这一思想衍生出来的。可以说二阶段提交其实就是实现XA分布式事务的关键(确切地说:两阶段提交主要保证了分布式事务的原子性:即所有结点要么全做要么全不做)

(1)两阶段提交

二阶段提交(Two-phaseCommit)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常,二阶段提交也被称为是一种协议(Protocol))。在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)

准备阶段

事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。

可以进一步将准备阶段分为以下三个步骤:

1)协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。

2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)

3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。

提交阶段

如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)

接下来分两种情况分别讨论提交阶段的过程。

当协调者节点从所有参与者节点获得的相应消息都为”同意”时:

success

1)协调者节点向所有参与者节点发出”正式提交(commit)”的请求。

2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。

3)参与者节点向协调者节点发送”完成”消息。

4)协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。

如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

fail

1)协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。

2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。

3)参与者节点向协调者节点发送”回滚完成”消息。

4)协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。

  不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。

(2)三阶段提交

三阶段提交(Three-phase commit),也叫三阶段提交协议(Three-phase commit protocol),是二阶段提交(2PC)的改进版本。

3

与两阶段提交不同的是,三阶段提交有两个改动点。

1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommitPreCommitDoCommit三个阶段。

CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

1.事务询问 协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。

2.响应反馈 参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。

假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

1.发送预提交请求 协调者向参与者发送PreCommit请求,并进入Prepared阶段。

2.事务预提交 参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

3.响应反馈 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

1.发送中断请求 协调者向所有参与者发送abort请求。

2.中断事务 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

doCommit阶段

该阶段进行真正的事务提交,也可以分为以下两种情况。

执行提交

1.发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。

2.事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。

3.响应反馈 事务提交完之后,向协调者发送Ack响应。

4.完成事务 协调者接收到所有参与者的ack响应之后,完成事务。

中断事务 协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

1.发送中断请求 协调者向所有参与者发送abort请求

2.事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

3.反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息

4.中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )

2PC与3PC的区别

相对于2PC,3PC主要解决的单点故障问题,并减少阻塞,因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行commit。而不会一直持有事务资源并处于阻塞状态。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。


了解了2PC和3PC之后,我们可以发现,无论是二阶段提交还是三阶段提交都无法彻底解决分布式的一致性问题。Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。后面的文章会介绍这个公认为难于理解但是行之有效的Paxos算法。

(3)Paxos算法

这篇文章主要讲解业内公认的比较难的也是最行之有效的paxos算法。

我认为对paxos算法讲解的最清楚的就是维基百科了。但是要看懂维基百科中的介绍需要很强的数学思维(paxos毕竟是一个算法),而且有很多关于定理的推论、证明等过程。那么本篇文章主要站在程序的角度,通俗的,循序渐进的讲解到底什么是paxos算法。

背景

Google Chubby的作者Mike Burrows说过, there is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos. 意即世上只有一种一致性算法,那就是Paxos,所有其他一致性算法都是Paxos算法的不完整版。

Paxos算法是莱斯利·兰伯特(Leslie Lamport,就是 LaTeX 中的”La”,此人现在在微软研究院)于1990年提出的一种基于消息传递的一致性算法。为描述 Paxos 算法,Lamport 讲述了这样一个故事:

在古希腊有一个岛屿叫做Paxos,这个岛屿通过议会的形式修订法律。执法者(legislators,后面称为牧师priest)在议会大厅(chamber)中表决通过法律,并通过服务员传递纸条的方式交流信息,每个执法者会将通过的法律记录在自己的账目(ledger)上。问题在于执法者和服务员都不可靠,他们随时会因为各种事情离开议会大厅、服务员也有可能重复传递消息(或者直接彻底离开),并随时可能有新的执法者(或者是刚暂时离开的)回到议会大厅进行法律表决,因此,议会协议要求保证上述情况下可以能够正确的修订法律并且不会产生冲突。

什么是paxos算法

Paxos 算法是分布式一致性算法用来解决一个分布式系统如何就某个值(决议)达成一致的问题。

人们在理解paxos算法是会遇到一些困境,那么接下来,我们带着以下几个问题来学习paxos算法:

1、paxos到底在解决什么问题?

2、paxos到底如何在分布式存储系统中应用?

3、paxos的核心思想是什么?

paxos解决了什么问题

上面我们提到过,分布式的一致性问题其实主要是指分布式系统中的数据一致性问题。所以,为了保证分布式系统的一致性,就要保证分布式系统中的数据是一致的。

在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。为保证每个节点执行相同的命令序列,需要在每一条指令上执行一个“一致性算法”以保证每个节点看到的指令一致。

所以,paxos算法主要解决的问题就是如何保证分布式系统中各个节点都能执行一个相同的操作序列。

Package Diagram(2)

上图中,C1是一个客户端,N1、N2、N3是分布式部署的三个服务器,初始状态下N1、N2、N3三个服务器中某个数据的状态都是S0。当客户端要向服务器请求处理操作序列:op1op2op3时(op表示operation)(这里把客户端的写操作简化成向所有服务器发送相同的请操作序列,实际上可能通过Master/Slave模式处理)。如果想保证在处理完客户端的请求之后,N1、N2、N3三个服务器中的数据状态都能从S0变成S1并且一致的话(或者没有执行成功,还是S0状态),就要保证N1、N2、N3在接收并处理操作序列op1op2op3时,严格按照规定的顺序正确执行opi,要么全部执行成功,要不就全部都不执行。

所以,针对上面的场景,paxos解决的问题就是如何依次确定不可变操作opi的取值,也就是确定第i个操作什么,在确定了opi的内容之后,就可以让各个副本执行opi操作。

Paxos算法详解

Paxos是一个十分巧妙的一致性算法,但是他也十分难以理解,就连他的作者Lamport都被迫对他做过多种讲解。我认为对paxos算法讲解的最清楚的就是维基百科了。但是要看懂维基百科中的介绍需要很强的数学思维(paxos毕竟是一个算法),而且有很多关于定理的推论、证明等过程。那么本篇文章主要站在程序的角度,通俗的,循序渐进的讲解到底什么是paxos算法。

我们先把前面的场景简化,把我们现在要解决的问题简化为如何确定一个不可变变量的取值(每一个不可变变量可以标识一个操作序列中的某个操作,当确保每个操作都正确之后,就可以按照顺序执行这些操作来保证数据能够准确无误的从一个状态转变成另外一个状态了)。


接下来,请跟我一步一步的学习paxos算法。

要学习paxos算法,我们就要从他要解决的问题出发,假如没有paxos算法,当我们面对如何确定一个不可变变量的取值这样一个吻问题的时候,我们应该如何解决呢?

这里暂不介绍paxos中的角色的概念,读者可以自行从维基百科中了解。不了解的话也可以直接往下看,看着看着就了解了。

问题抽象

我们把确定一个不可变变量的取值问题定义成:

设计一个系统,来存储名称为var的变量。

var的取值可以是任意二进制数

系统内部由多个Accepter组成,负责管理和存储var变量。

系统对外提供api,用来设置var变量的值
propose(var,V) => <ok,f> or <error>

将var的值设置为V,系统会返回ok和系统中已经确定的取值f,或者返回error。

外部有多个Proposer机器任意请求系统,调用系统API(propose(var,V) => <ok,f> or <error>)来设置var变量的值。

如果系统成功的将var设置成了V,那么返回的f应该就是V的值。否则,系统返回的f就是其他的Proposer设置的值。

>

系统需要保证var的取值满足一致性

如果var没有被设置过,那么他的初始值为null

一旦var的值被设置成功,则不可被更改,并且可以一直都能获取到这个值

系统需要满足容错特性

可以容忍任意proposer出现故障
可以容忍少数acceptor故障(半数以下)

暂时忽略网络分化问题和acceptor故障导致var丢失的问题。

到这里,问题已经抽象完成了,读者可以再仔细看看上面的系统描述。如果这样设置一个系统,是不是就可以保证变量var的不可变性了呢?

这里还是再简单讲解一下,上面的系统确实可以保证变量var的不可变性。

因为var的初始值为null,当有proposer请求接口propose(var,v)设置var的值的时候,系统会将var设置为v,并返回f(f==v)。

var变量被初始化以后,再有proposer请求propose(var,v)设置var的值的时候,系统会直接返回系统中已有的var的值f,而放弃proposer提供的v。

系统难点

要设计以上系统存在以下难点:

1、管理多个proposer并发执行

2、容忍var变量的不可变性

3、容忍任意Proposer的故障

4、容忍半数以下的acceptor的故障

解决方案一

先考虑整个系统由单个acceptor组成。通过类似互斥锁的方式来管理并发的proposer的请求。

proposer向acceptor申请acceptor的互斥访问权,当取得互斥访问权之后才能调用api给var变量赋值。
accepter向proposer发放互斥访问权,谁取得了互斥访问权,acceptor就接收谁的请求。
这样通过互斥访问的机制,proposer就要按照获取互斥访问权的顺序来请求系统。
一旦acceptor接收到一个proposer请求,并成功给var变量赋值之后,就不再允许其他的proposer设置var变量的值。每当再有proposer来请求设置var变量的值的时候,acceptor就会将var里面现有的值返回给他。

基于互斥访问权的acceptor的实现

acceptor会保存变量var的值和一个互斥锁Lock。

提供接口prepare()

加互斥锁,给予var的互斥访问权,并返回当前var的取值

提供接口release()

用于释放互斥访问权

提供接口accept(var, v)

如果已经加锁,并且当前var没有值,则将var的值设置成v,并释放锁。

proposer采用两阶段来实现

Step1、通过调用prepare接口来获取互斥性访问权和当前var的取值

如果无法获取到互斥性访问权,则返回,并不能进入到下一个阶段,因为其他proposer获取到了互斥性访问权。

Step2、根据当前var的取值f选择执行

1、如果f的取值为null,说明没有被设置过值,则调用接口accept(var ,v)来将var的取值设置成v,并释放掉互斥性访问权。
2、如果f的取值不为null,说明var已经被其他proposer设置过值,则调用release接口释放掉互斥性访问权。

总结:方案一通过互斥访问的方式来保证所有的proposer能够串行的访问acceptor,这样其实并没有解决多个proposer并发执行的问题。只是想办法绕开了并发执行。虽然可以在一定程度上保证var变量的取值是确定的。但是一旦获取到互斥访问权的proposer在执行过程中出现故障,那么就会导致所有其他proposer无法再获取到互斥访问权,就会发生死锁。。所以,方案一不仅效率低、而且还会产生死锁问题,不能容忍任意Proposer出现故障。
在之前提到的四个系统难点中,方案一可以解决难点1和难点2,但是无法解决难点3和难点4。

解决方案二

通过引入抢占式访问权来取代互斥访问权。acceptor有权让任意proposer的访问权失效,然后将访问权发放给其他的proposer。

在方案二中,proposer向acceptor发出的每次请求都要带一个编号(epoch),且编号间要存在全序关系。一旦acceptor接收到proposer的请求中包含一个更大的epoch的时候,马上让旧的epoch失效,不再接受他们提交的取值。然后给新的epoch发放访问权,让他可以设置var变量的值。

为了保证var变量取值的不变性,不同epoch的proposer之前遵守后者认同前者的原则:

在确保旧的epoch已经失效后,并且旧的epoch没有设置var变量的值,新的epoch会提交自己的值。
当旧的epoch已经设置过var变量的取值,那么新的epoch应该认同旧的epoch设置过的值,并不在提交新的值。

基于抢占式访问权的acceptor的实现

五、写在最后的话

这部分是分布式相关的基础内容(比如服务熔断与服务降级的结合其实就是文中提到的“功能上的损失”的应用),除此之外还能够为后面zookeeper的学习以及消息队列的学习的打下一个基础。

如果也能帮助大家理清这部分的思路(可以不深入),我展示的这篇文章目的就达到了。

发布了127 篇原创文章 · 获赞 3078 · 访问量 36万+

猜你喜欢

转载自blog.csdn.net/qq_42322103/article/details/103234198