解决分布式状态及数据的一致性问题
分布式互斥,分布式选举,分布式共识,分布式事务
分布式互斥
对于同一共享资源,一个程序正在使用的时候也不希望被其他程序打扰。这,就要求同一时刻只能有一个程序能够访问这种资源
在分布式系统里,这种排他性的资源访问方式,叫作分布式互斥(Distributed Mutual Exclusion),而这种被互斥访问的共享资源就叫作临界资源(Critical Resource)
集中式算法
增加一个“协调者”来约束大家使用,每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前没有程序使用这个资源,协调者直接授权请求程序访问;否则,按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源,则通知协调者,协调者从“排号”的队列里取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源

从上述流程可以看出,一个程序完成一次临界资源访问,需要如下几个流程和消息交互:
1.向协调者发送请求授权信息,1 次消息交互;
2.协调者向程序发放授权信息,1 次消息交互;
3.程序使用完临界资源后,向协调者发送释放授权,1 次消息交互。
因此,每个程序完成一次临界资源访问,需要进行 3 次消息交互。不难看出,集中式算法的优点在于直观、简单、信息交互量少、易于实现,并且所有程序只需和协调者通信,程序之间无需通信。但是,这个算法的问题也出在了协调者身上
问题:
1.协调者会成为系统的性能瓶颈。想象一下,如果有 100 个程序要访问临界资源,那么协调者要处理 100*3=300 条消息。也就是说,协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加。
2.容易引发单点故障问题。协调者故障,会导致所有的程序均无法访问临界资源,导致整个系统不可用。
集中式算法具有简单、易于实现的特点,但可用性、性能易受协调者影响。在可靠性和性能有一定保障的情况下,比如中央服务器计算能力强、性能高、故障率低,或者中央服务器进行了主备,主故障后备可以立马升为主,且数据可恢复的情况下,集中式算法可以适用于比较广泛的应用场景
分布式算法
当一个程序要访问临界资源时,先向系统中的其他程序发送一条请求消息,在接收到所有程序返回的同意消息后,才可以访问临界资源。其中,请求消息需要包含所请求的资源、请求者的 ID,以及发起请求的时间.
分布式算法是一个“先到先得”和“投票全票通过”的公平访问机制,但通信成本较高,可用性也比集中式算法低,适用于临界资源使用频度较低,且系统规模较小的场景


从上述流程可以看出,一个程序完成一次临界资源的访问,需要进行如下的信息交互:
1.向其他 n-1 个程序发送访问临界资源的请求,总共需要 n-1 次消息交互;
2.需要接收到其他 n-1 个程序回复的同意消息,方可访问资源,总共需要 n-1 次消息交互。
这个算法可用性很低,主要包括两个方面的原因:
1.当系统内需要访问临界资源的程序增多时,容易产生“信令风暴”,也就是程序收到的请求完全超过了自己的处理能力,而导致自己正常的业务无法开展。
2.一旦某一程序发生故障,无法发送同意消息,那么其他程序均处在等待回复的状态中,使得整个系统处于停滞状态,导致整个系统不可用。所以,相对于集中式算法的协调者故障,分布式算法的可用性更低。
分布式算法适合节点数目少且变动不频繁的系统,且由于每个程序均需通信交互,因此适合 P2P 结构的系统。比如,运行在局域网中的分布式文件系统,具有 P2P 结构的系统等。
其中的分布式文件系统 HDFS 的文件修改就是一个典型的应用分布式算法的场景。
处于同一个局域网内的计算机 1、2、3 中都有同一份文件的备份信息,且它们可以相互通信。这个共享文件,就是临界资源。当计算机 1 想要修改共享的文件时,需要进行如下操作:
1.计算机 1 向计算机 2、3 发送文件修改请求;
2.计算机 2、3 发现自己不需要使用资源,因此同意计算机 1 的请求;
3.计算机 1 收到其他所有计算机的同意消息后,开始修改该文件;
4.计算机 1 修改完成后,向计算机 2、3 发送文件修改完成的消息,并发送修改后的文件数据;
5.计算机 2 和 3 收到计算机 1 的新文件数据后,更新本地的备份文件

令牌环算法
程序访问临界资源问题也可按照轮值 CEO 的思路实现。 如下图所示,所有程序构成一个环结构,令牌按照顺时针(或逆时针)方向在程序之间传递,收到令牌的程序有权访问临界资源,访问完成后将令牌传送到下一个程序;若该程序不需要访问临界资源,则直接把令牌传送给下一个程序。

因为在使用临界资源前,不需要像分布式算法那样挨个征求其他程序的意见了,所以相对而言,在令牌环算法里单个程序具有更高的通信效率。同时,在一个周期内,每个程序都能访问到临界资源,因此令牌环算法的公平性很好。
对于集中式和分布式算法都存在的单点故障问题,在令牌环中,若某一个程序出现故障,则直接将令牌传递给故障程序的下一个程序,从而很好地解决单点故障问题,提高系统的健壮性,带来更好的可用性。但,这就要求每个程序都要记住环中的参与者信息,这样才能知道在跳过一个参与者后令牌应该传递给谁

分布式选举
主节点,在一个分布式集群中负责对其他节点的协调和管理,也就是说,其他节点都必须听从主节点的安排。主节点的存在,就可以保证其他节点的有序运行,以及数据库集群中的写入数据在每个节点上的一致性。这里的一致性是指,数据在每个集群节点中都是一样的,不存在不同的情况
Bully 算法
Bully 算法是一种霸道的集群选主算法,为什么说是霸道呢?因为它的选举原则是“长者”为大,即在所有活着的节点中,选取 ID 最大的节点作为主节点
Bully 算法在选举过程中,需要用到以下 3 种消息:
1.Election 消息,用于发起选举;
2.Alive 消息,对 Election 消息的应答;
3.Victory 消息,竞选成功的主节点向其他节点发送的宣誓主权的消息

Bully 算法选举的原则是“长者为大”,意味着它的假设条件是,集群中每个节点均知道其他节点的 ID。在此前提下,其具体的选举过程是:
1.集群中每个节点判断自己的 ID 是否为当前活着的节点中 ID 最大的,如果是,则直接向其他节点发送 Victory 消息,宣誓自己的主权;
2.如果自己不是当前活着的节点中 ID 最大的,则向比自己 ID 大的所有节点发送 Election 消息,并等待其他节点的回复;若在给定的时间范围内,本节点没有收到其他节点回复的 Alive 消息,则认为自己成为主节点,并向其他节点发送 Victory 消息,宣誓自己成为主节点;
3.若接收到来自比自己 ID 大的节点的 Alive 消息,则等待其他节点发送 Victory 消息;
4.若本节点收到比自己 ID 小的节点发送的 Election 消息,则回复一个 Alive 消息,告知其他节点,我比你大,重新选举
MongoDB 的分布式选举中,采用节点的最后操作时间戳来表示 ID,时间戳最新的节点其 ID 最大,也就是说时间戳最新的、活着的节点是主节点
总结:
Bully 算法的选择特别霸道和简单,谁活着且谁的 ID 最大谁就是主节点,其他节点必须无条件服从。这种算法的优点是,选举速度快、算法复杂度低、简单易实现。但这种算法的缺点在于,需要每个节点有全局的节点信息,因此额外信息存储较多;其次,任意一个比当前主节点 ID 大的新节点或节点故障后恢复加入集群的时候,都可能会触发重新选举,成为新的主节点,如果该节点频繁退出、加入集群,就会导致频繁切主
民主投票:Raft 算法
Raft 算法是典型的多数派投票选举算法,其选举机制与我们日常生活中的民主投票机制类似,核心思想是“少数服从多数”。也就是说,Raft 算法中,获得投票最多的节点成为主
采用 Raft 算法选举,集群节点的角色有 3 种:
Leader,即主节点,同一时刻只有一个 Leader,负责协调和管理其他节点;
Candidate,即候选者,每一个节点都可以成为 Candidate,节点在该角色下才可以被选为新的 Leader;
Follower,Leader 的跟随者,不可以发起选举。
Raft 选举的流程,可以分为以下几步:
1.初始化时,所有节点均为 Follower 状态。
2.开始选主时,所有节点的状态由 Follower 转化为 Candidate,并向其他节点发送选举请求。
3.其他节点根据接收到的选举请求的先后顺序,回复是否同意成为主。这里需要注意的是,在每一轮选举中,一个节点只能投出一张票。
4.若发起选举请求的节点获得超过一半的投票,则成为主节点,其状态转化为 Leader,其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包,以检测主节点是否活着。
5.当 Leader 节点的任期到了,即发现其他服务器开始下一轮选主周期时,Leader 节点的状态由 Leader 降级为 Follower,进入新一轮选主

Google 开源的 Kubernetes,擅长容器管理与调度,为了保证可靠性,通常会部署 3 个节点用于数据备份。这 3 个节点中,有一个会被选为主,其他节点作为备。Kubernetes 的选主采用的是开源的 etcd 组件。而,etcd 的集群管理器 etcds,是一个高可用、强一致性的服务发现存储仓库,就是采用了 Raft 算法来实现选主和一致性的
总结:Raft 算法具有选举速度快、算法复杂度低、易于实现的优点;缺点是,它要求系统内每个节点都可以相互通信,且需要获得过半的投票数才能选主成功,因此通信量大。该算法选举稳定性比 Bully 算法好,这是因为当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点获得投票数过半,才会导致切主。
ZAB 算法
ZAB(ZooKeeper Atomic Broadcast)选举算法是为 ZooKeeper 实现分布式协调功能而设计的。相较于 Raft 算法的投票机制,ZAB 算法增加了通过节点 ID 和数据 ID 作为参考进行选主,节点 ID 和数据 ID 越大,表示数据越新,优先成为主。相比较于 Raft 算法,ZAB 算法尽可能保证数据的最新性。所以,ZAB 算法可以说是对 Raft 算法的改进。
使用 ZAB 算法选举时,集群中每个节点拥有 3 种角色:
Leader,主节点;
Follower,跟随者节点;
Observer,观察者,无投票权
选举过程中,集群中的节点拥有 4 个状态:
1.Looking 状态,即选举状态。当节点处于该状态时,它会认为当前集群中没有 Leader,因此自己进入选举状态。2.Leading 状态,即领导者状态,表示已经选出主,且当前节点为 Leader。
3.Following 状态,即跟随者状态,集群中已经选出主后,其他非主节点状态更新为 Following,表示对 Leader 的追随。
4.Observing 状态,即观察者状态,表示当前节点为 Observer,持观望态度,没有投票权和选举权。
投票过程中,每个节点都有一个唯一的三元组 (server_id, server_zxID, epoch),其中 server_id 表示本节点的唯一 ID;server_zxID 表示本节点存放的数据 ID,数据 ID 越大表示数据越新,选举权重越大;epoch 表示当前选取轮数,一般用逻辑时钟表示
ZAB 选举算法的核心是“少数服从多数,ID 大的节点优先成为主”,因此选举过程中通过 (vote_id, vote_zxID) 来表明投票给哪个节点,其中 vote_id 表示被投票节点的 ID,vote_zxID 表示被投票节点的服务器 zxID。ZAB 算法选主的原则是:server_zxID 最大者成为 Leader;若 server_zxID 相同,则 server_id 最大者成为 Leader
第一步:当系统刚启动时,3 个服务器当前投票均为第一轮投票,即 epoch=1,且 zxID 均为 0。此时每个服务器都推选自己,并将选票信息 <epoch, vote_id, vote_zxID> 广播出去。

第二步:根据判断规则,由于 3 个 Server 的 epoch、zxID 都相同,因此比较 server_id,较大者即为推选对象,因此 Server 1 和 Server 2 将 vote_id 改为 3,更新自己的投票箱并重新广播自己的投票

第三步:此时系统内所有服务器都推选了 Server 3,因此 Server 3 当选 Leader,处于 Leading 状态,向其他服务器发送心跳包并维护连接;Server1 和 Server2 处于 Following 状态

ZAB 算法性能高,对系统无特殊要求,采用广播方式发送信息,若节点中有 n 个节点,每个节点同时广播,则集群中信息量为 n*(n-1) 个消息,容易出现广播风暴;且除了投票,还增加了对比节点 ID 和数据 ID,这就意味着还需要知道所有节点的 ID 和数据 ID,所以选举时间相对较长。但该算法选举稳定性比较好,当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点数据 ID 和节点 ID 最大,且获得投票数过半,才会导致切主。

分布式共识
用于解决分布式在线记账一致性问题的分布式共识技术
这里所说的分布式在线记账,是指在没有集中的发行方,也就是没有银行参与的情况下,任意一台接入互联网的电脑都能参与买卖,所有看到该交易的服务器都可以记录这笔交易,并且记录信息最终都是一致的,以保证交易的准确性。而如何保证交易的一致性,就是该场景下的分布式共识问题。
在传统方法中,我们通过银行进行转账并记录该笔交易。但分布式在线记账方法中,没有银行这样的一个集中方,而是由上述 5 台服务器来记录该笔交易。但是,这 5 台服务器均是有各自想法的个体,都可以自主操作或记录,那么如何保证记录的交易是一致的呢?这,就是分布式共识技术要解决的问题。
分布式共识就是在多个节点均可独自操作或记录的情况下,使得所有节点针对某个状态达成一致的过程。
所有服务器帮助记录交易并达成一致的过程,就是区块链中的“挖矿”。
PoW
从分布式选举问题可以看出,同一轮选举中有且仅有一个节点成为主节点。同理,在分布式在线记账问题中,针对同一笔交易,有且仅有一个节点或服务器可以获得记账权,然后其他节点或服务器同意该节点或服务器的记账结果,达成一致
PoW 算法,是以每个节点或服务器的计算能力(即“算力”)来竞争记账权的机制,因此是一种使用工作量证明机制的共识算法。也就是说,谁的计算力强、工作能力强,谁获得记账权的可能性就越大。
那么,如何体现节点的“算力”呢?
答案就是,每个节点都去解一道题,谁能先解决谁的能力就强。假设每个节点会划分多个区块用于记录用户交易,PoW 算法获取记账权的原理是:利用区块的 index、前一个区块的哈希值、交易的时间戳、区块数据和 nonce 值,通过 SHA256 哈希算法计算出一个哈希值,并判断前 k 个值是否都为 0。如果不是,则递增 nonce 值,重新按照上述方法计算;如果是,则本次计算的哈希值为要解决的题目的正确答案。谁最先计算出正确答案,谁就获得这个区块的记账权。

假设客户端 A 产生一个新的交易,基于 PoW 的共识记账过程为:
1.客户端 A 产生新的交易,向全网进行广播,要求对交易进行记账。
2.每个记账节点接收到这个请求后,将收到的交易信息放入一个区块中。
3.每个节点通过 PoW 算法,计算本节点的区块的哈希值,尝试找到一个具有足够工作量难度的工作量证明。
4.若节点 D 找到了一个工作量证明向全网广播。当然,当且仅当包含在该区块中的交易都是有效且之前未存在过的,其他节点才会认同该区块的有效性。
5.其他节点接收到广播信息后,若该区块有效,接受该区块,并跟随在该区块的末尾,制造新区块延长该链条,将被接受的区块的随机哈希值视为新区块的随机哈希值
PoW 通过“挖矿”的方式发行新币,把比特币分散给个人,实现了相对的公平。
PoW 的容错机制,允许全网 50% 的节点出错,因此,如果要破坏系统,则需要投入极大成本(若你有全球 51% 的算力,则可尝试攻击比特币)。
PoW 机制每次达成共识需要全网共同参与运算,增加了每个节点的计算量,并且如果题目过难,会导致计算时间长、资源消耗多;而如果题目过于简单,会导致大量节点同时获得记账权,冲突多。这些问题,都会增加达成共识的时间。所以,PoW 机制的缺点也很明显,共识达成的周期长、效率低,资源消耗大
PoS
为了解决 PoW 算法的问题,引入了 PoS 算法。它的核心原理是,由系统权益代替算力来决定区块记账权,拥有的权益越大获得记账权的概率就越大。这里所谓的权益,就是每个节点占有货币的数量和时间,而货币就是节点所获得的奖励。PoS 算法充分利用了分布式在线记账中的奖励,鼓励“利滚利”。
基于 PoS 算法获得区块记账权的方法与基于 PoW 的方法类似,不同之处在于:节点计算获取记账权的方法不一样,PoW 是利用区块的 index、前一个区块的哈希值、交易的时间戳、区块数据和 nonce 值,通过 SHA256 哈希算法计算出一个哈希值,并判断前 k 个值是否都为 0,而 PoS 是根据节点拥有的股权或权益进行计算的。
假设一个公链网络中,共有 3 个节点,A 、B 和 C。其中 A 节点拥有 10000 个币,总共持有 30 天,而 B 和 C 节点分别有 1000 和 2000 个币,分别持有 15 和 20 天。通过 PoS 算法决定区块记账权的流程和 PoW 算法类似,唯一不同的就是,每个节点在计算自己记账权的时候,通过计算自己的股权或权益来评估,如果发现自己权益最大,则将自己的区块广播给其他节点,当然必须保证该区块的有效性。

以太坊平台属于区块链 2.0 阶段,在区块链 1.0 的基础上进一步强调了合约,采用了 PoS 算法。12 年发布的点点币(PPC),综合了 PoW 工作量证明及 PoS 权益证明方式,从而在安全和节能方面实现了创新。可以看出,PoS 将算力竞争转变成权益竞争。与 PoW 相比,PoS 不需要消耗大量的电力就能够保证区块链网络的安全性,同时也不需要在每个区块中创建新的货币来激励记账者参与当前网络的运行,这也就在一定程度上缩短了达成共识所需要的时间。所以,基于 PoS 算法的以太坊每秒大概能处理 30 笔左右的交易。但,PoS 算法中持币越多或持币越久,币龄就会越高,持币人就越容易挖到区块并得到激励,而持币少的人基本没有机会,这样整个系统的安全性实际上会被持币数量较大的一部分人掌握,容易出现垄断现象。
DPoS
为了解决 PoS 算法的垄断问题,2014 年比特股(BitShares)的首席开发者丹尼尔 · 拉里默(Dan Larimer)提出了委托权益证明法,也就是 DPoS 算法
DPoS 算法的原理,类似股份制公司的董事会制度,普通股民虽然拥有股权,但进不了董事会,他们可以投票选举代表(受托人)代他们做决策。DPoS 是由被社区选举的可信帐户(受托人,比如得票数排行前 101 位)来拥有记账权。
DPoS 是在 PoW 和 PoS 的基础上进行改进的,相比于 PoS 算法,DPoS 引入了受托人,优点主要表现在:由投票选举出的若干信誉度更高的受托人记账,解决了所有节点均参与竞争导致消息量大、达成一致的周期长的问题。也就是说,DPoS 能耗更低,具有更快的交易速度。每隔一定周期会调整受托人,避免受托人造假和独权。

一致性与共识的区别是什么
一致性是指,分布式系统中的多个节点之间,给定一系列的操作,在约定协议的保障下,对外界呈现的数据或状态是一致的。
共识是指,分布式系统中多个节点之间,彼此对某个状态达成一致结果的过程。
一致性强调的是结果,共识强调的是达成一致的过程,共识算法是保障系统满足不同程度一致性的核心技术。
选主的本质是希望中央集权,即所有节点默认为最终要听主节点的协调与管理,但这样会有随着规模增加主节点存在性能瓶颈问题、以及篡改或破坏主节点后(比如篡改元数据)产生的安全问题。因此人们想到了”去中心化“

分布式事务
事务(Transaction)提供一种机制,将包含一系列操作的工作序列纳入到一个不可分割的执行单元。只有所有操作均被正确执行才能提交事务;任意一个操作失败都会导致整个事务回滚(Rollback)到之前状态,即所有操作均被取消。简单来说,事务提供了一种机制,使得工作要么全部都不做,要么完全被执行,即 all or nothing
事务具备四大基本特征 ACID
具体含义如下
A:原子性(Atomicity),即事务最终的状态只有两种,全部执行成功和全部不执行,不会停留在中间某个环节。若处理事务的任何一项操作不成功,就会导致整个事务失败。一旦操作失败,所有操作都会被取消(即回滚),使得事务仿佛没有被执行过一样。就好比买一件商品,购买成功时,则给商家付了钱,商品到手;购买失败时,则商品在商家手中,消费者的钱也没花出去。
C:一致性(Consistency),是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有 800 元和 600 元,总共 1400 元,用户 A 给用户 B 转账 200 元,分为两个步骤,从 A 的账户扣除 200 元和对 B 的账户增加 200 元。一致性就是要求上述步骤操作后,最后的结果是用户 A 还有 600 元,用户 B 有 800 元,总共 1400 元,而不会出现用户 A 扣除了 200 元,但用户 B 未增加的情况(该情况,用户 A 和 B 均为 600 元,总共 1200 元)
I:隔离性(Isolation),是指当系统内有多个事务并发执行时,多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的
D:持久性(Durability),也被称为永久性,是指一个事务被执行后,那么它对数据库所做的更新就永久地保存下来了。即使发生系统崩溃或宕机等故障,重新启动数据库系统后,只要数据库能够重新被访问,那么一定能够将其恢复到事务完成时的状态。就像消费者在网站上的购买记录,即使换了手机,也依然可以查到
分布式事务由多个事务组成,因此基本满足 ACID,其中的 C 是强一致性,也就是所有操作均执行成功,才提交最终结果,以保证数据一致性或完整性。但随着分布式系统规模不断扩大,复杂度急剧上升,达成强一致性所需时间周期较长,限定了复杂业务的处理。为了适应复杂业务,出现了 BASE 理论,该理论的一个关键点就是采用最终一致性代替强一致性
什么是 BASE 理论
eBay 公司的工程师 Dan Pritchett 曾提出了一种分布式存储系统的设计模式——BASE 理论。 BASE 理论包括基本可用(Basically Available)、柔性状态(Soft State)和最终一致性(Eventual Consistency)。
基本可用:分布式系统出现故障的时候,允许损失一部分功能的可用性,保证核心功能可用。比如,某些电商 618 大促的时候,会对一些非核心链路的功能进行降级处理。
柔性状态:在柔性事务中,允许系统存在中间状态,且这个中间状态不会影响系统整体可用性。比如,数据库读写分离,写库同步到读库(主库同步到从库)会有一个延时,其实就是一种柔性状态。
最终一致性:事务在操作过程中可能会由于同步延迟等问题导致不一致,但最终状态下,所有数据都是一致的。BASE 理论为了支持大型分布式系统,通过牺牲强一致性,保证最终一致性,来获得高可用性,是对 ACID 原则的弱化。
ACID 与 BASE 是对一致性和可用性的权衡所产生的不同结果,但二者都保证了数据的持久性。ACID 选择了强一致性而放弃了系统的可用性。与 ACID 原则不同的是,BASE 理论保证了系统的可用性,允许数据在一段时间内可以不一致,最终达到一致状态即可,也即牺牲了部分的数据一致性,选择了最终一致性
实现分布式事务有以下 3 种基本方法:
基于 XA 协议的二阶段提交协议方法
两阶段提交协议的执行过程,分为投票(Voting)和提交(Commit)两个阶段
我们看一下第一阶段投票:在这一阶段,协调者(Coordinator,即事务管理器)会向事务的参与者(Cohort,即本地资源管理器)发起执行操作的 CanCommit 请求,并等待参与者的响应。
参与者接收到请求后,会执行请求中的事务操作,将操作信息记录到事务日志中但不提交(即不会修改数据库中的数据),待参与者执行成功,则向协调者发送“Yes”消息,表示同意操作;若不成功,则发送“No”消息,表示终止操作。当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了第二阶段提交阶段(也可以称为,执行阶段)。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit(提交)或 DoAbort(取消)指令
当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了第二阶段提交阶段(也可以称为,执行阶段)。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit(提交)或 DoAbort(取消)指令。具体规则如下:
1.若协调者从参与者那里收到的都是“Yes”消息,则向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后,完成剩余的操作(比如修改数据库中的数据)并释放资源(整个事务过程中占用的资源),然后向协调者返回“HaveCommitted”消息;
2.若协调者从参与者收到的消息中包含“No”消息,则向所有参与者发送“DoAbort”消息。此时投票阶段发送“Yes”消息的参与者,则会根据之前执行操作时的事务日志对操作进行回滚,就好像没有执行过请求操作一样,然后所有参与者会向协调者发送“HaveCommitted”消息;
3.协调者接收到来自所有参与者的“HaveCommitted”消息后,就意味着整个事务结束了。
二阶段提交的算法思路可以概括为:协调者向参与者下发请求事务操作,参与者接收到请求后,进行相关操作并将操作结果通知协调者,协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作


虽然基于 XA 的二阶段提交算法尽量保证了数据的强一致性,而且实现成本低,但依然有些不足。主要有以下三个问题:
1.同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。因此,基于 XA 的二阶段提交协议不支持高并发场景。
2.单点故障问题:该算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
3.数据不一致问题:在提交阶段,当协调者向所有参与者发送“DoCommit”请求时,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。
三阶段提交协议方法
三阶段提交协议(Three-phase Commit Protocol,3PC),是对二阶段提交(2PC)的改进。为了更好地处理两阶段提交的同步阻塞和数据不一致问题,三阶段提交引入了超时机制和准备阶段
与 2PC 只是在协调者引入超时机制不同,3PC 同时在协调者和参与者中引入了超时机制。如果协调者或参与者在规定的时间内没有接收到来自其他节点的响应,就会根据当前的状态选择提交或者终止整个事务,从而减少了整个集群的阻塞时间,在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。
在第一阶段和第二阶段中间引入了一个准备阶段,或者说把 2PC 的投票阶段一分为二,也就是在提交阶段之前,加入了一个预提交阶段。在预提交阶段尽可能排除一些不一致的情况,保证在最后提交之前各参与节点的状态是一致的。
三阶段提交协议就有 CanCommit、PreCommit、DoCommit 三个阶段。
第一,CanCommit 阶段。
协调者向参与者发送请求操作(CanCommit 请求),询问参与者是否可以执行事务提交操作,然后等待参与者的响应;参与者收到 CanCommit 请求之后,回复 Yes,表示可以顺利执行事务;否则回复 No。
3PC 的 CanCommit 阶段与 2PC 的 Voting 阶段相比:类似之处在于:协调者均需要向参与者发送请求操作(CanCommit 请求),询问参与者是否可以执行事务提交操作,然后等待参与者的响应。参与者收到 CanCommit 请求之后,回复 Yes,表示可以顺利执行事务;否则回复 No。
不同之处在于,在 2PC 中,在投票阶段,若参与者可以执行事务,会将操作信息记录到事务日志中但不提交,并返回结果给协调者。但在 3PC 中,在 CanCommit 阶段,参与者仅会判断是否可以顺利执行事务,并返回结果。
而操作信息记录到事务日志但不提交的操作由第二阶段预提交阶段执行。
当协调者接收到所有参与者回复的消息后,进入预提交阶段(PreCommit 阶段)。
第二,PreCommit 阶段。
协调者根据参与者的回复情况,来决定是否可以进行 PreCommit 操作(预提交阶段)。
如果所有参与者回复的都是“Yes”,那么协调者就会执行事务的预执行:协调者向参与者发送 PreCommit 请求,进入预提交阶段。
参与者接收到 PreCommit 请求后执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中。如果参与者成功执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
假如任何一个参与者向协调者发送了“No”消息,或者等待超时之后,协调者都没有收到参与者的响应,就执行中断事务的操作:协调者向所有参与者发送“Abort”消息。参与者收到“Abort”消息之后,或超时后仍未收到协调者的消息,执行事务的中断操作。
第三,DoCommit 阶段。
DoCmmit 阶段进行真正的事务提交,根据 PreCommit 阶段协调者发送的消息,进入执行提交阶段或事务中断阶段。
执行提交阶段:若协调者接收到所有参与者发送的 Ack 响应,则向所有参与者发送 DoCommit 消息,开始执行阶段。
参与者接收到 DoCommit 消息之后,正式提交事务。
完成事务提交之后,释放所有锁住的资源,并向协调者发送 Ack 响应。协调者接收到所有参与者的 Ack 响应之后,完成事务。
事务中断阶段:协调者向所有参与者发送 Abort 请求。参与者接收到 Abort 消息之后,利用其在 PreCommit 阶段记录的 Undo 信息执行事务的回滚操作,释放所有锁住的资源,并向协调者发送 Ack 消息。协调者接收到参与者反馈的 Ack 消息之后,执行事务的中断,并结束事务。
基于消息的最终一致性方法
2PC 和 3PC 核心思想均是以集中式的方式实现分布式事务,这两种方法都存在两个共同的缺点,一是,同步执行,性能差;二是,数据不一致问题。为了解决这两个问题,通过分布式消息来确保事务最终一致性的方案便出现了
将需要分布式处理的事务通过消息或者日志的方式异步执行,消息或日志可以存到本地文件、数据库或消息队列中,再通过业务规则进行失败重试。

文档信息
- 本文作者:Jessica
- 本文链接:https://jessica0530.github.io/2020/09/20/%E5%88%86%E5%B8%83%E5%BC%8F%E5%8D%8F%E5%90%8C/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)