第一章 分布式互斥
在分布式系统里,这种排他性的资源访问方式,叫作分布式互斥(Distributed Mutual Exclusion),而这种被互斥访问的共享资源就叫作临界资源(Critical Resource)。
如何才能让分布式系统里的程序互斥地访问临界资源?
1.1 集中式算法
引入一个协调者程序,得到一个分布式互斥算法。每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前没有程序使用这个资源,协调者直接授权请求程序访问;否则,按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源,则通知协调者,协调者从“排号”的队列里取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源。
这个互斥算法,就是我们所说的集中式算法,也可以叫做*服务器算法。
不难看出,集中式算法的优点在于直观、简单、信息交互量少、易于实现,并且所有程序只需和协调者通信,程序之间无需通信。但是,这个算法的问题也出在了协调者身上。
- 一方面,协调者会成为系统的性能瓶颈。协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加。
- 另一方面,容易引发单点故障问题。协调者故障,会导致所有的程序均无法访问临界资源,导致整个系统不可用。
因此,在使用集中式算法的时候,一定要选择性能好、可靠性高的服务器来运行协调者。
1.2 分布式算法
既然引入协调者会带来一些问题,不用协调者是否可以实现对临界资源的互斥访问呢?
当一个程序要访问临界资源时,先向系统中的其他程序发送一条请求消息,在接收到所有程序返回的同意消息后,才可以访问临界资源。其中,请求消息需要包含所请求的资源、请求者的 ID,以及发起请求的时间。
在分布式领域中,我们称之为分布式算法,或者使用组播和逻辑时钟的算法。
一个程序完成一次临界资源的访问,需要进行如下的信息交互:
- 向其他 n-1 个程序发送访问临界资源的请求,总共需要 n-1 次消息交互;
- 需要接收到其他 n-1 个程序回复的同意消息,方可访问资源,总共需要 n-1 次消息交互。
可以看出,一个程序要成功访问临界资源,至少需要 2*(n-1) 次消息交互。假设,现在系统中的 n 个程序都要访问临界资源,则会同时产生 2n(n-1) 条消息。
总结来说,在大型系统中使用分布式算法,消息数量会随着需要访问临界资源的程序数量呈指数级增加,容易导致高昂的“沟通成本”。
分布式算法根据“先到先得”以及“投票全票通过”的机制,让每个程序按时间顺序公平地访问资源,简单粗暴、易于实现。但,这个算法可用性很低,主要包括两个方面的原因:
- 当系统内需要访问临界资源的程序增多时,容易产生“信令风暴”,也就是程序收到的请求完全超过了自己的处理能力,而导致自己正常的业务无法开展。
- 一旦某一程序发生故障,无法发送同意消息,那么其他程序均处在等待回复的状态中,使得整个系统处于停滞状态,导致整个系统不可用。所以,相对于集中式算法的协调者故障,分布式算法的可用性更低。
针对可用性低的一种改进办法是,如果检测到一个程序故障,则直接忽略这个程序,无需再等待它的同意消息。
分布式算法适合节点数目少且变动不频繁的系统,且由于每个程序均需通信交互,因此适合 P2P 结构的系统。比如,运行在局域网中的分布式文件系统,具有 P2P 结构的系统等。
P2P 架构是两个或多个客户端不经过服务器而直接通信的架构。
1.3 令牌环算法
所有程序构成一个环结构,令牌按照顺时针(或逆时针)方向在程序之间传递,收到令牌的程序有权访问临界资源,访问完成后将令牌传送到下一个程序;若该程序不需要访问临界资源,则直接把令牌传送给下一个程序。在分布式领域,这个算法叫作令牌环算法,也可以叫作基于环的算法。
因为在使用临界资源前,不需要像分布式算法那样挨个征求其他程序的意见了,所以相对而言,在令牌环算法里单个程序具有更高的通信效率。同时,在一个周期内,每个程序都能访问到临界资源,因此令牌环算法的公平性很好。
但是,不管环中的程序是否想要访问资源,都需要接收并传递令牌,所以也会带来一些无效通信。假设系统中有 100 个程序,那么程序 1 访问完资源后,即使其它 99 个程序不需要访问,也必须要等令牌在其他 99 个程序传递完后,才能重新访问资源,这就降低了系统的实时性。
综上,令牌环算法非常适合通信模式为令牌环方式的分布式系统,例如移动自组织网络系统。一个典型的应用场景就是无人机通信。
无人机在通信时,工作原理类似于对讲机,同一时刻只能发送信息或接收信息。因此,通信中的上行链路(即向外发送信息的通信渠道)是临界资源。
如下图所示,所有的无人机群组成一个环,按照顺时针方向通信。每个无人机只知道其前一个发送信息的无人机,和后一个将要接收信息的无人机。拥有令牌的无人机可以向外发送信息,其他无人机只能接收数据。拥有令牌的无人机通信完成后,会将令牌传送给后一个无人机。
所有的无人机轮流通信并传输数据,从而消除了多个无人机对通信资源的争夺,使得每个无人机都能接收到其他无人机的信息,降低了通信碰撞导致的丢包率,保证了网络通信的稳定性,提高了多个无人机之间的协作效率。
对于集中式和分布式算法都存在的单点故障问题,在令牌环中,若某一个程序(例如上图的无人机 2)出现故障,则直接将令牌传递给故障程序的下一个程序(例如,上图中无人机 1 直接将令牌传送给无人机 3),从而很好地解决单点故障问题,提高系统的健壮性,带来更好的可用性。但,这就要求每个程序都要记住环中的参与者信息,这样才能知道在跳过一个参与者后令牌应该传递给谁。
1.4 总结
上面提到的集中式、分布式和令牌环 3 个互斥算法,都不适用于规模过大、节点数量过多的系统。那么,什么样的互斥算法适用于大规模系统呢?
由于大规模系统的复杂性,我们很自然地想到要用一个相对复杂的互斥算法。时下有一个很流行的互斥算法,两层结构的分布式令牌环算法,把整个广域网系统中的节点组织成两层结构,可以用于节点数量较多的系统,或者是广域网系统。
在该算法中,局域网是较低的层次,广域网是较高的层次。每个局域网中包含若干个局部进程和一个协调进程。局部进程在逻辑上组成一个环形结构,在每个环形结构上有一个局部令牌 T 在局部进程间传递。局域网与局域网之间通过各自的协调进程进行通信,这些协调进程同样组成一个环结构,这个环就是广域网中的全局环。在这个全局环上,有一个全局令牌在多个协调进程间传递。
问题:
(1)你认为集中式算法、分布式算法和令牌环算法,还有什么可以改进的地方吗?
- 集中式算法:可参照redis集群通信模式,通过hash key将大量的请求分散到不同的master,以处理大量请求,每个master由小集群主从节点来保障单点故障
- 分布式算法:分布式算法可在集群中过半数同意就识为其同意,降低通信数,如分布式选举场景
- 令牌环算法:可根据参与者使用频率列出权重,结合平滑加权轮询算法选出下一个参与者
(2)传统单机上的互斥方法,为什么不能用于分布式环境呢?
传统单机上的互斥只能针对单台机器上的程序相互间通信,而分布式环境往往是多台服务器上的程序相互通信
第二章 分布式选举
集群一般是由两个或两个以上的服务器组建而成,每个服务器都是一个节点。
对于一个集群来说,要选一个leader来负责调度和管理其他节点。
主节点,在一个分布式集群中负责对其他节点的协调和管理,也就是说,其他节点都必须听从主节点的安排。
主节点的存在,就可以保证其他节点的有序运行,以及数据库集群中的写入数据在每个节点上的一致性。这里的一致性是指,数据在每个集群节点中都是一样的,不存在不同的情况。
总结来说,选举的作用就是选出一个主节点,由它来协调和管理其他节点,以保证集群有序运行和节点间数据的一致性。
目前常见的选主方法有基于序号选举的算法( 比如,Bully 算法)、多数派算法(比如,Raft 算法、ZAB 算法)等。
2.1 Bully 算法
Bully 算法选举原则是在所有活着的节点中,选取 ID 最大的节点作为主节点。
在 Bully 算法中,节点的角色有两种:普通节点和主节点。
初始化时,所有节点都是平等的,都是普通节点,并且都有成为主的权利。但是,当选主成功后,有且仅有一个节点成为主节点,其他所有节点都是普通节点。当且仅当主节点故障或与其他节点失去联系后,才会重新选主。
Bully 算法在选举过程中,需要用到以下 3 种消息:
- Election 消息,选举消息 A->B 发送选举消息,表示 A 支持 B 当 Leader。
- Alive 消息,响应选举消息,刚才 A->B 选举 B,B 给 A 回复 Answer Messge。
- Victory 消息,宣布胜利消息,如果 B 最终当选为 Leader,则 B 向其他节点发送 Victory Message。
Bully 算法假设条件是,集群中每个节点均知道其他节点的 ID。在此前提下,其具体的选举过程是:
- 集群中每个节点判断自己的 ID 是否为当前活着的节点中 ID 最大的,如果是,则直接向其他节点发送 Victory 消息,宣誓自己的主权;
- 如果自己不是当前活着的节点中 ID 最大的,则向比自己 ID 大的所有节点发送 Election 消息,并等待其他节点的回复;
- 若在给定的时间范围内,本节点没有收到其他节点回复的 Alive 消息,则认为自己成为主节点,并向其他节点发送 Victory 消息,宣誓自己成为主节点;若接收到来自比自己 ID 大的节点的 Alive 消息,则等待其他节点发送 Victory 消息;
- 若本节点收到比自己 ID 小的节点发送的 Election 消息,则回复一个 Alive 消息,告知其他节点,我比你大,重新选举。
早期MongoDB 的副本集故障转移功能采用Bully 算法。MongoDB 的分布式选举中,采用节点的最后操作时间戳来表示 ID,时间戳最新的节点其 ID 最大,也就是说时间戳最新的、活着的节点是主节点。
Bully 算法的选择特别霸道和简单,谁活着且谁的 ID 最大谁就是主节点,其他节点必须无条件服从。
这种算法的优点是,选举速度快、算法复杂度低、简单易实现。但这种算法的缺点在于,需要每个节点有全局的节点信息,因此额外信息存储较多;其次,任意一个比当前主节点 ID 大的新节点或节点故障后恢复加入集群的时候,都可能会触发重新选举,成为新的主节点,如果该节点频繁退出、加入集群,就会导致频繁切主。
2.2 Raft 算法
Raft 算法是典型的多数派投票选举算法,核心思想是“少数服从多数”。也就是说,Raft 算法中,获得投票最多的节点成为主。
采用 Raft 算法选举,集群节点的角色有 3 种:
- Leader,即主节点,同一时刻只有一个 Leader,负责协调和管理其他节点;
- Candidate,即候选者,每一个节点都可以成为 Candidate,节点在该角色下才可以被选为新的 Leader;
- Follower,Leader 的跟随者,不可以发起选举。
Raft 选举的流程,可以分为以下几步:
- 初始化时,所有节点均为 Follower 状态。
- 开始选主时,所有节点的状态由 Follower 转化为 Candidate,并向其他节点发送选举请求。
- 其他节点根据接收到的选举请求的先后顺序,回复是否同意成为主。这里需要注意的是,在每一轮选举term 中,一个节点只能投出一张票。
- 若发起选举请求的节点获得超过一半的投票,则成为主节点,其状态转化为 Leader,其他节点的状态则由 Candidate 降为 Follower。Leader 节点与 Follower 节点之间会定期发送心跳包,以检测主节点是否活着。
- 当 Leader 节点的任期到了,即发现其他服务器开始下一轮选主周期时,Leader 节点的状态由 Leader 降级为 Follower,进入新一轮选主。
节点的状态迁移如下所示(图中的 term 指的是选举周期):
raft投票的依据(暂不考虑PreCandidate状态):
1. raft节点与leader心跳超时,成为Candidate, 会投自己一票;
2. follower收到Candidate投票请求后,会比较log的term和index如果至少比自己新才会将票投给Candidate;
3. 节点在任何状态收到比自己新的term和index消息则立刻成为follower;
https://zhuanlan.zhihu.com/p/32052223
Google 开源的 Kubernetes,擅长容器管理与调度,为了保证可靠性,通常会部署 3 个节点用于数据备份。这 3 个节点中,有一个会被选为主,其他节点作为备。Kubernetes 的选主采用的是开源的 etcd 组件。而etcd 的集群管理器 etcds,是一个高可用、强一致性的服务发现存储仓库,就是采用了 Raft 算法来实现选主和一致性的。
Raft 算法具有选举速度快、算法复杂度低、易于实现的优点;
缺点是,它要求系统内每个节点都可以相互通信,且需要获得过半的投票数才能选主成功,因此通信量大。该算法选举稳定性比 Bully 算法好,这是因为当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点获得投票数过半,才会导致切主。
2.3 ZAB 算法
ZAB(ZooKeeper Atomic Broadcast)选举算法是为 ZooKeeper 实现分布式协调功能而设计的。相较于 Raft 算法的投票机制,ZAB 算法增加了通过节点 ID 和数据 ID 作为参考进行选主,节点 ID 和数据 ID 越大,表示数据越新,优先成为主。相比较于 Raft 算法,ZAB 算法尽可能保证数据的最新性。所以,ZAB 算法可以说是对 Raft 算法的改进。
使用 ZAB 算法选举时,集群中每个节点拥有 3 种角色:
- Leader,主节点;
- Follower,跟随者节点;
- Observer,观察者,无投票权。
选举过程中,集群中的节点拥有 4 个状态:
- Looking 状态,即选举状态。当节点处于该状态时,它会认为当前集群中没有 Leader,因此自己进入选举状态。
- Leading 状态,即领导者状态,表示已经选出主,且当前节点为 Leader。
- Following 状态,即跟随者状态,集群中已经选出主后,其他非主节点状态更新为 Following,表示对 Leader 的追随。
- 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。
ZAB 算法性能高,对系统无特殊要求,采用广播方式发送信息,若节点中有 n 个节点,每个节点同时广播,则集群中信息量为 n*(n-1) 个消息,容易出现广播风暴;且除了投票,还增加了对比节点 ID 和数据 ID,这就意味着还需要知道所有节点的 ID 和数据 ID,所以选举时间相对较长。但该算法选举稳定性比较好,当有新节点加入或节点故障恢复后,会触发选主,但不一定会真正切主,除非新节点或故障后恢复的节点数据 ID 和节点 ID 最大,且获得投票数过半,才会导致切主。
2.4 总结
从消息传递内容、选举机制和选举过程的维度,对这 3 种分布式选举算法进行一个对比分析。
zab的性能好,是因为,zab选举出的主节点,在所有节点中,是数据最全的,这样在把数据从主节点同步到其他节点时,同步的数据较少。因为zab比较了事务id,事务id,可以理解为最新数据的id。也就是说。选举出的主节点上的数据相对其他节点来说,是比较新的。
问题:
(1)分布式选举和一致性的关系是什么?
分布式选举就是为保障一致性的一次协商,要求全局认可同一个节点做主节点。选举的目的是为了简化一致性协商的流程,让选出的master来协调各成员针对某项决议达成一致。
(3)你是否见到过一个集群中存在双主的场景呢?
双主是可能发生的,例如原主网络与外部中断,集群发生脑裂,则老的集群主还存在,分裂的剩余节点由于与老主失联,大家重新选了新主出来。此时就会产生双主。
规避双主的影响,需要通过租约机制,让老主发现在租约到期后与大多数节点失联主动降备;新主的选举也要等待超过这个租约时间后去选举新主,避免业务同一时刻看到双主。但是由于各个服务器资源、负载、调度等问题,时间并不是一个精确的可靠保障,例如定时器失真,还是可能导致同一时刻出现双主,所以每个地方的租约时间配置是个技术点。另外新主产生,生成新的epoch(+1),这样可以避免大家去处理老消息,从而进一步规避双主的问题。
第三章 分布式共识
分布式选举问题,是从多个节点中选出一个主节点,相关的选举方法几乎都有一个共同特点:每个节点都有选举权和被选举权。大部分选举方法采用多数策略,也就是说一个节点只有得到了大部分节点的同意或认可才能成为主节点,然后主节点向其他节点宣告主权。
其实,这个选主过程就是一个分布式共识问题,因为每个节点在选出主节点之前都可以认为自己会成为主节点,也就是说集群节点“存异”;而通过选举的过程选出主节点,让所有的节点都认可该主节点,这叫“求同”。由此可见,分布式共识的本质就是“存异求同”。
所以,从本质上看,分布式选举问题,其实就是传统的分布式共识方法,主要是基于多数投票策略实现的。基于多数投票策略的分布式选举方法,如果用于分布式在线记账一致性问题中,那么记账权通常会完全掌握到主节点的手里,这使得主节点非常容易造假,且存在性能瓶颈。因此,分布式选举不适用于分布式在线记账的一致性问题。
分布式共识是在多个节点均可独自操作或记录的情况下,使得所有节点针对某个状态达成一致的过程。通过共识机制,我们可以使得分布式系统中的多个节点的数据达成一致。这里说的分布式在线记账,就是近几年比较火的区块链技术解决的问题。而分布式共识技术,就是区块链技术共识机制的核心。
在传统的交易方式中,用户 A 给用户 B 转账,需要银行来实行具体的转账操作并记录交易,银行会从中收取相应的手续费。而采用分布式在线记账的话,参与记录这笔交易的服务器,也可以从中获得一些奖励(这些奖励,在区块链技术中可以换成钱)。所有服务器帮助记录交易并达成一致的过程,就是区块链中的“挖矿”。
区块链相关的知识参考:https://www.cnblogs.com/wkfvawl/category/1631062.html
下来,将介绍 3 种主流的解决分布式在线记账一致性问题的共识技术,即:PoW(Proof-of-Work,工作量证明)、PoS(Proof-of-Stake,权益证明)和 DPoS(Delegated Proof of Stake,委托权益证明)。
3.1 PoW
从分布式选举问题可以看出,同一轮选举中有且仅有一个节点成为主节点。同理,在分布式在线记账问题中,针对同一笔交易,有且仅有一个节点或服务器可以获得记账权,然后其他节点或服务器同意该节点或服务器的记账结果,达成一致。
也就是说,分布式共识包括两个关键点,获得记账权和所有节点或服务器达成一致。
PoW(Proof-of-Work,工作量证明) 算法,是以每个节点或服务器的计算能力(即“算力”)来竞争记账权的机制,因此是一种使用工作量证明机制的共识算法。也就是说,谁的计算力强、工作能力强,谁获得记账权的可能性就越大。
如何体现节点的“算力”呢?答案就是,每个节点都去解一道题,谁能先解决谁的能力就强。
假设每个节点会划分多个区块用于记录用户交易,PoW 算法获取记账权的原理是:利用区块的 index、前一个区块的哈希值、交易的时间戳、区块数据和 nonce 值,通过 SHA256 哈希算法计算出一个哈希值,并判断前 k 个值是否都为 0。如果不是,则递增 nonce 值,重新按照上述方法计算;如果是,则本次计算的哈希值为要解决的题目的正确答案。谁最先计算出正确答案,谁就获得这个区块的记账权。
nonce 值是用来找到一个满足哈希值的数字;k 为哈希值前导零的个数,标记了计算的难度,0 越多计算难度越大。
达成共识的过程,就是获得记账权的节点将该区块信息广播给其他节点,其他节点判断该节点找到的区块中的所有交易都是有效且之前未存在过的,则认为该区块有效,并接受该区块,达成一致。
以分散在世界各地的 5 台服务器为例,说明基于 PoW 的共识记账过程。
假设客户端 A 产生一个新的交易,基于 PoW 的共识记账过程为:
- 客户端 A 产生新的交易,向全网进行广播,要求对交易进行记账。
- 每个记账节点接收到这个请求后,将收到的交易信息放入一个区块中。
- 每个节点通过 PoW 算法,计算本节点的区块的哈希值,尝试找到一个具有足够工作量难度的工作量证明。
- 若节点 D 找到了一个工作量证明向全网广播。当然,当且仅当包含在该区块中的交易都是有效且之前未存在过的,其他节点才会认同该区块的有效性。其
- 他节点接收到广播信息后,若该区块有效,接受该区块,并跟随在该区块的末尾,制造新区块延长该链条,将被接受的区块的随机哈希值视为新区块的随机哈希值。
可以看出,PoW 算法中,谁的计算能力强,获得记账权的可能性就越大。但必须保证其记账的区块是有效的,并在之前未存在过,才能获得其他节点的认可。
目前,比特币平台采用了 PoW 算法,属于区块链 1.0 阶段,其重心在于货币,比特币大约 10min 才会产生一个区块,区块的大小也只有 1MB,仅能够包含 3000~4000 笔交易,平均每秒只能够处理 5~7(个位数)笔交易。PoW 通过“挖矿”的方式发行新币,把比特币分散给个人,实现了相对的公平。PoW 的容错机制,允许全网 50% 的节点出错,因此,如果要破坏系统,则需要投入极大成本(若你有全球 51% 的算力,则可尝试攻击比特币)。
但,PoW 机制每次达成共识需要全网共同参与运算,增加了每个节点的计算量,并且如果题目过难,会导致计算时间长、资源消耗多;而如果题目过于简单,会导致大量节点同时获得记账权,冲突多。这些问题,都会增加达成共识的时间。所以,PoW 机制的缺点也很明显,共识达成的周期长、效率低,资源消耗大。
3.2 PoS
为了解决 PoW 算法的问题,引入了 PoS(Proof-of-Stake,权益证明) 算法。它的核心原理是,由系统权益代替算力来决定区块记账权,拥有的权益越大获得记账权的概率就越大。
这里所谓的权益,就是每个节点占有货币的数量和时间,而货币就是节点所获得的奖励。PoS 算法充分利用了分布式在线记账中的奖励,鼓励“利滚利”。
在股权证明 PoS 模式下,根据你持有货币的数量和时间,给你发利息。每个币每天产生 1 币龄,比如你持有 100 个币,总共持有了 50 天,那么,你的币龄就为 5000。这个时候,如果你发现了一个 PoS 区块,你的币龄就会被减少 365。每被减少 365 币龄,你就可以从区块中获得 0.05 个币的利息 (可理解为年利率 5%)。
在这个案例中,利息 = (5000*5% )/365 = 0.68 个币。这下就有意思了,持币有利息。
基于 PoS 算法获得区块记账权的方法与基于 PoW 的方法类似,不同之处在于:节点计算获取记账权的方法不一样,PoW 是利用区块的 index、前一个区块的哈希值、交易的时间戳、区块数据和 nonce 值,通过 SHA256 哈希算法计算出一个哈希值,并判断前 k 个值是否都为 0,而 PoS 是根据节点拥有的股权或权益进行计算的。
通过 PoS 算法决定区块记账权的流程和 PoW 算法类似,唯一不同的就是,每个节点在计算自己记账权的时候,通过计算自己的股权或权益来评估,如果发现自己权益最大,则将自己的区块广播给其他节点,当然必须保证该区块的有效性。
以太坊平台属于区块链 2.0 阶段,在区块链 1.0 的基础上进一步强调了合约,采用了 PoS 算法(以太坊1.0采用的是PoW共识机制,2.0转向PoS)。12 年发布的点点币(PPC),综合了 PoW 工作量证明及 PoS 权益证明方式,从而在安全和节能方面实现了创新。
可以看出,PoS 将算力竞争转变成权益竞争。与 PoW 相比,PoS 不需要消耗大量的电力就能够保证区块链网络的安全性,同时也不需要在每个区块中创建新的货币来激励记账者参与当前网络的运行,这也就在一定程度上缩短了达成共识所需要的时间。所以,基于 PoS 算法的以太坊每秒大概能处理 30 笔左右的交易。
关于以太坊PoS的内容:https://zhuanlan.zhihu.com/p/326469979
但,PoS 算法中持币越多或持币越久,币龄就会越高,持币人就越容易挖到区块并得到激励,而持币少的人基本没有机会,这样整个系统的安全性实际上会被持币数量较大的一部分人掌握,容易出现垄断现象。
3.3 DPoS
为了解决 PoS 算法的垄断问题,2014 年比特股(BitShares)的首席开发者丹尼尔 · 拉里默(Dan Larimer)提出了委托权益证明法,也就是 DPoS 算法。
DPoS 算法的原理,类似股份制公司的董事会制度,普通股民虽然拥有股权,但进不了董事会,他们可以投票选举代表(受托人)代他们做决策。DPoS 是由被社区选举的可信帐户(受托人,比如得票数排行前 101 位)来拥有记账权。
为了成为正式受托人,用户要去社区拉票,获得足够多的信任。用户根据自己持有的货币数量占总量的百分比来投票,好比公司股票机制,假设总的发行股票为 1000,现在股东 A 持股 10,那么股东 A 投票权为 10/1000=1/100。如下图所示,根据自己拥有的权益,投票选出可代表自己的受托节点,受托节点之间竞争记账权。
在 DPos 算法中,通常会选出 k(比如 101) 个受托节点,它们的权利是完全相等的。受托节点之间争取记账权也是根据算力进行竞争的。只要受托节点提供的算力不稳定,计算机宕机或者利用手中的权力作恶,随时可以被握着货币的普通节点投票踢出整个系统,而后备的受托节点可以随时顶上去。
DPoS 在比特股和 Steem 上已运行多年,整个网络中选举出的多个节点能够在 1s 之内对 99.9% 的交易进行确认。此外,DPoS 在 EOS(Enterprise Operation System,为商用分布式应用设计的一款区块链操作系统)中也有广泛应用,被称为区块链 3.0 阶段。
DPoS 是在 PoW 和 PoS 的基础上进行改进的,相比于 PoS 算法,DPoS 引入了受托人,优点主要表现在:
- 由投票选举出的若干信誉度更高的受托人记账,解决了所有节点均参与竞争导致消息量大、达成一致的周期长的问题。也就是说,DPoS 能耗更低,具有更快的交易速度。
- 每隔一定周期会调整受托人,避免受托人造假和独权。
但是,在 DPoS 中,由于大多数持币人通过受托人参与投票,投票的积极性并不高;且一旦出现故障节点,DPoS 无法及时做出应对,导致安全隐患。
3.4 总结
三种算法放在一起做下对比,如下图所示。
问题:
一致性与共识的区别是什么?
- 一致性是指在分布式系统中,针对同一数据或状态以多个副本形式保存在不同节点上;当对某个数据或状态副本做出修改后,能保证多副本达到对外表现的数据一致性。
- 共识是指一个或多个进程提议某些修改后,采用一种大家认可的方法,使得系统中所有进程对该修改达成一致意见,该方法称为共识机制。
也就是说,共识重点在于达成一致的过程或方法,一致性问题在于最终对外表现的结果。
第四章 分布式事务
事务(Transaction)提供一种机制,将包含一系列操作的工作序列纳入到一个不可分割的执行单元。只有所有操作均被正确执行才能提交事务;任意一个操作失败都会导致整个事务回滚(Rollback)到之前状态,即所有操作均被取消。简单来说,事务提供了一种机制,使得工作要么全部都不做,要么完全被执行,即 all or nothing。
通常情况下,我们所说的事务指的都是本地事务,也就是在单机上的事务。事务具备四大基本特征 ACID,具体含义如下。
- A:原子性(Atomicity),即事务最终的状态只有两种,全部执行成功和全部不执行,不会停留在中间某个环节。若处理事务的任何一项操作不成功,就会导致整个事务失败。一旦操作失败,所有操作都会被取消(即回滚),使得事务仿佛没有被执行过一样。
- C:一致性(Consistency),是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
- I:隔离性(Isolation),是指当系统内有多个事务并发执行时,多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
- D:持久性(Durability),也被称为永久性,是指一个事务被执行后,那么它对数据库所做的更新就永久地保存下来了。即使发生系统崩溃或宕机等故障,重新启动数据库系统后,只要数据库能够重新被访问,那么一定能够将其恢复到事务完成时的状态。
分布式事务,就是在分布式系统中运行的事务,由多个本地事务组合而成。在分布式场景下,对事务的处理操作可能来自不同的机器,甚至是来自不同的操作系统。比如常见的电商处理订单问题
对于网上购物的每一笔订单来说,电商平台一般都会有两个核心步骤:一是订单业务采取下订单操作,二是库存业务采取减库存操作。通常,这两个业务会运行在不同的机器上,甚至是运行在不同区域的机器上。针对同一笔订单,当且仅当订单操作和减库存操作一致时,才能保证交易的正确性。也就是说一笔订单,只有这两个操作都完成,才能算做处理成功,否则处理失败。
分布式事务由多个事务组成,因此基本满足 ACID,其中的 C 是强一致性,也就是所有操作均执行成功,才提交最终结果,以保证数据一致性或完整性。但随着分布式系统规模不断扩大,复杂度急剧上升,达成强一致性所需时间周期较长,限定了复杂业务的处理。为了适应复杂业务,出现了 BASE 理论,该理论的一个关键点就是采用最终一致性代替强一致性。
实际上,分布式事务主要是解决在分布式环境下,组合事务的一致性问题。实现分布式事务有以下 3 种基本方法:
- 基于 XA 协议的二阶段提交协议方法
- 三阶段提交协议方法
- 基于消息的最终一致性方法
其中,基于 XA 协议的二阶段提交协议方法和三阶段提交协议方法,采用了强一致性,遵从 ACID。
基于消息的最终一致性方法,采用了最终一致性,遵从 BASE 理论。
4.1 基于 XA 协议的二阶段提交方法
XA 是一个分布式事务协议,规定了事务管理器和资源管理器接口。因此,XA 协议包括事务管理器和本地资源管理器两个部分。
事务管理器相当于协调者,负责各个本地资源的提交和回滚;而资源管理器就是分布式事务的参与者,通常由数据库实现,比如 Oracle、DB2 等商业数据库都实现了 XA 接口。
两阶段提交协议的执行过程,分为投票(Voting)和提交(Commit)两个阶段。
首先,我们看一下第一阶段投票:在这一阶段,协调者(Coordinator,即事务管理器)会向事务的参与者(Cohort,即本地资源管理器)发起执行操作的 CanCommit 请求,并等待参与者的响应。参与者接收到请求后,会执行请求中的事务操作,将操作信息记录到事务日志中但不提交(即不会修改数据库中的数据),待参与者执行成功,则向协调者发送“Yes”消息,表示同意操作;若不成功,则发送“No”消息,表示终止操作。
当所有的参与者都返回了操作结果(Yes 或 No 消息)后,系统进入了第二阶段提交阶段(也可以称为,执行阶段)。在提交阶段,协调者会根据所有参与者返回的信息向参与者发送 DoCommit(提交)或 DoAbort(取消)指令。具体规则如下:
- 若协调者从参与者那里收到的都是“Yes”消息,则向参与者发送“DoCommit”消息。参与者收到“DoCommit”消息后,完成剩余的操作(比如修改数据库中的数据)并释放资源(整个事务过程中占用的资源),然后向协调者返回“HaveCommitted”消息;
- 若协调者从参与者收到的消息中包含“No”消息,则向所有参与者发送“DoAbort”消息。此时投票阶段发送“Yes”消息的参与者,则会根据之前执行操作时的事务日志对操作进行回滚,就好像没有执行过请求操作一样,然后所有参与者会向协调者发送“HaveCommitted”消息;
- 协调者接收到来自所有参与者的“HaveCommitted”消息后,就意味着整个事务结束了。
二阶段提交的算法思路可以概括为:协调者向参与者下发请求事务操作,参与者接收到请求后,进行相关操作并将操作结果通知协调者,协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。
虽然基于 XA 的二阶段提交算法尽量保证了数据的强一致性,而且实现成本低,但依然有些不足。主要有以下三个问题:
- 同步阻塞问题:二阶段提交算法在执行过程中,所有参与节点都是事务阻塞型的。也就是说,当本地资源管理器占有临界资源时,其他资源管理器如果要访问同一临界资源,会处于阻塞状态。因此,基于 XA 的二阶段提交协议不支持高并发场景。
- 单点故障问题:该算法类似于集中式算法,一旦事务管理器发生故障,整个系统都处于停滞状态。尤其是在提交阶段,一旦事务管理器发生故障,资源管理器会由于等待管理器的消息,而一直锁定事务资源,导致整个系统被阻塞。
- 数据不一致问题:在提交阶段,当协调者向所有参与者发送“DoCommit”请求时,如果发生了局部网络异常,或者在发送提交请求的过程中协调者发生了故障,就会导致只有一部分参与者接收到了提交请求并执行提交操作,但其他未接到提交请求的那部分参与者则无法执行事务提交。于是整个分布式系统便出现了数据不一致的问题。
4.2 三阶段提交方法
三阶段提交协议(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 阶段,参与者仅会判断是否可以顺利执行事务,并返回结果。而操作信息记录到事务日志但不提交的操作由第二阶段预提交阶段执行。
CanCommit 阶段不同节点之间的事务请求成功和失败的流程,如下所示。
当协调者接收到所有参与者回复的消息后,进入预提交阶段(PreCommit 阶段)。
PreCommit 阶段
协调者根据参与者的回复情况,来决定是否可以进行 PreCommit 操作(预提交阶段)。
如果所有参与者回复的都是“Yes”,那么协调者就会执行事务的预执行:
- 协调者向参与者发送 PreCommit 请求,进入预提交阶段。
- 参与者接收到 PreCommit 请求后执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中。
- 如果参与者成功执行了事务操作,则返回 ACK 响应,同时开始等待最终指令。
假如任何一个参与者向协调者发送了“No”消息,或者等待超时之后,协调者都没有收到参与者的响应,就执行中断事务的操作:
- 协调者向所有参与者发送“Abort”消息。
- 参与者收到“Abort”消息之后,或超时后仍未收到协调者的消息,执行事务的中断操作。
预提交阶段,不同节点上事务执行成功和失败的流程,如下所示。
预提交阶段保证了在最后提交阶段(DoCmmit 阶段)之前所有参与者的状态是一致的。
DoCommit 阶段
DoCmmit 阶段进行真正的事务提交,根据 PreCommit 阶段协调者发送的消息,进入执行提交阶段或事务中断阶段。
执行提交阶段:
- 若协调者接收到所有参与者发送的 Ack 响应,则向所有参与者发送 DoCommit 消息,开始执行阶段。
- 参与者接收到 DoCommit 消息之后,正式提交事务。完成事务提交之后,释放所有锁住的资源,并向协调者发送 Ack 响应。
- 协调者接收到所有参与者的 Ack 响应之后,完成事务。
事务中断阶段:
- 协调者向所有参与者发送 Abort 请求。
- 参与者接收到 Abort 消息之后,利用其在 PreCommit 阶段记录的 Undo 信息执行事务的回滚操作,释放所有锁住的资源,并向协调者发送 Ack 消息。
- 协调者接收到参与者反馈的 Ack 消息之后,执行事务的中断,并结束事务。
执行阶段不同节点上事务执行成功和失败 (事务中断) 的流程,如下所示。
3PC 协议在协调者和参与者均引入了超时机制。即当参与者在预提交阶段向协调者发送 Ack 消息后,如果长时间没有得到协调者的响应,在默认情况下,参与者会自动将超时的事务进行提交,从而减少整个集群的阻塞时间,在一定程度上减少或减弱了 2PC 中出现的同步阻塞问题。
但三阶段提交仍然存在数据不一致的情况,比如在 PreCommit 阶段,部分参与者已经接受到 ACK 消息进入执行阶段,但部分参与者与协调者网络不通,导致接收不到 ACK 消息,此时接收到 ACK 消息的参与者会执行任务,未接收到 ACK 消息且网络不通的参与者无法执行任务,最终导致数据不一致。
4.3 基于分布式消息的最终一致性方案
2PC 和 3PC 核心思想均是以集中式的方式实现分布式事务,这两种方法都存在两个共同的缺点,一是,同步执行,性能差;二是,数据不一致问题。为了解决这两个问题,通过分布式消息来确保事务最终一致性的方案便出现了。
在 eBay 的分布式系统架构中,架构师解决一致性问题的核心思想就是:将需要分布式处理的事务通过消息或者日志的方式异步执行,消息或日志可以存到本地文件、数据库或消息队列中,再通过业务规则进行失败重试。这个案例,就是使用基于分布式消息的最终一致性方案解决了分布式事务的问题。
基于分布式消息的最终一致性方案的事务处理,引入了一个消息中间件(在本案例中,我们采用 Message Queue,MQ,消息队列),用于在多个应用之间进行消息传递。实际使用中,阿里就是采用 RocketMQ 机制来支持消息事务。
基于消息中间件协商多个节点分布式事务执行操作的示意图,如下所示。
以网上购物为例。假设用户 A 在某电商平台下了一个订单,需要支付 50 元,发现自己的账户余额共 150 元,就使用余额支付,支付成功之后,订单状态修改为支付成功,然后通知仓库发货。
在该事件中,涉及到了订单系统、支付系统、仓库系统,这三个系统是相互独立的应用,通过远程服务进行调用。
根据基于分布式消息的最终一致性方案,用户 A 通过终端手机首先在订单系统上操作,通过消息队列完成整个购物流程。然后整个购物的流程如下所示。
- 订单系统把订单消息发给消息中间件,消息状态标记为“待确认”。
- 消息中间件收到消息后,进行消息持久化操作,即在消息存储系统中新增一条状态为“待发送”的消息。
- 消息中间件返回消息持久化结果(成功 / 失败),订单系统根据返回结果判断如何进行业务操作。失败,放弃订单,结束(必要时向上层返回失败结果);成功,则创建订单。
- 订单操作完成后,把操作结果(成功 / 失败)发送给消息中间件。
- 消息中间件收到业务操作结果后,根据结果进行处理:失败,删除消息存储中的消息,结束;成功,则更新消息存储中的消息状态为“待发送(可发送)”,并执行消息投递。
- 如果消息状态为“可发送”,则 MQ 会将消息发送给支付系统,表示已经创建好订单,需要对订单进行支付。支付系统也按照上述方式进行订单支付操作。
- 订单系统支付完成后,会将支付消息返回给消息中间件,中间件将消息传送给订单系统。若支付失败,则订单操作失败,订单系统回滚到上一个状态,MQ 中相关消息将被删除;若支付成功,则订单系统再调用库存系统,进行出货操作,操作流程与支付系统类似。
在上述过程中,可能会产生如下异常情况,其对应的解决方案为:
- 订单消息未成功存储到 MQ 中,则订单系统不执行任何操作,数据保持一致;
- MQ 成功将消息发送给支付系统(或仓库系统),但是支付系统(或仓库系统)操作成功的 ACK 消息回传失败(由于通信方面的原因),导致订单系统与支付系统(或仓库系统)数据不一致,此时 MQ 会确认各系统的操作结果,删除相关消息,支付系统(或仓库系统)操作回滚,使得各系统数据保持一致;
- MQ 成功将消息发送给支付系统(或仓库系统),但是支付系统(或仓库系统)操作成功的 ACK 消息回传成功,订单系统操作后的最终结果(成功或失败)未能成功发送给 MQ,此时各系统数据可能不一致,MQ 也需确认各系统的操作结果,若数据一致,则更新消息;若不一致,则回滚操作、删除消息。
基于分布式消息的最终一致性方案采用消息传递机制,并使用异步通信的方式,避免了通信阻塞,从而增加系统的吞吐量。同时,这种方案还可以屏蔽不同系统的协议规范,使其可以直接交互。
在不需要请求立即返回结果的场景下, 这些特性就带来了明显的通信优势,并且通过引入消息中间件,实现了消息生成方(如上述的订单系统)本地事务和消息发送的原子性,采用最终一致性的方式,只需保证数据最终一致即可,一定程度上解决了二阶段和三阶段方法要保证强一致性而在某些情况导致的数据不一致问题。
可以看出,分布式事务中,当且仅当所有的事务均成功时整个流程才成功。所以,分布式事务的一致性是实现分布式事务的关键问题,目前来看还没有一种很简单、完美的方案可以应对所有场景。
4.4 刚性事务与柔性事务
什么是刚性事务、柔性事务,以及两者之间有何区别?
- 刚性事务,遵循 ACID 原则,具有强一致性。比如,数据库事务。
- 柔性事务,其实就是根据不同的业务场景使用不同的方法实现最终一致性,也就是说我们可以根据业务的特性做部分取舍,容忍一定时间内的数据不一致。
总结来讲,与刚性事务不同,柔性事务允许一定时间内,数据不一致,但要求最终一致。而柔性事务的最终一致性,遵循的是 BASE 理论。
那,什么是 BASE 理论呢?eBay 公司的工程师 Dan Pritchett 曾提出了一种分布式存储系统的设计模式——BASE 理论。 BASE 理论包括基本可用(Basically Available)、柔性状态(Soft State)和最终一致性(Eventual Consistency)。
- 基本可用:分布式系统出现故障的时候,允许损失一部分功能的可用性,保证核心功能可用。比如,某些电商 618 大促的时候,会对一些非核心链路的功能进行降级处理。
- 柔性状态:在柔性事务中,允许系统存在中间状态,且这个中间状态不会影响系统整体可用性。比如,数据库读写分离,写库同步到读库(主库同步到从库)会有一个延时,其实就是一种柔性状态。
- 最终一致性:事务在操作过程中可能会由于同步延迟等问题导致不一致,但最终状态下,所有数据都是一致的。
BASE 理论为了支持大型分布式系统,通过牺牲强一致性,保证最终一致性,来获得高可用性,是对 ACID 原则的弱化。ACID 与 BASE 是对一致性和可用性的权衡所产生的不同结果,但二者都保证了数据的持久性。ACID 选择了强一致性而放弃了系统的可用性。与 ACID 原则不同的是,BASE 理论保证了系统的可用性,允许数据在一段时间内可以不一致,最终达到一致状态即可,也即牺牲了部分的数据一致性,选择了最终一致性。
4.5 总结
从算法一致性、执行方式、性能等角度对比三种实现方式:
思维导图
第五章 分布式锁
在单机系统中,经常会有多个线程访问同一种资源的情况,我们把这样的资源叫做共享资源,或者叫做临界资源。为了维护线程操作的有效性和正确性,我们需要某种机制来减少低效率的操作,避免同时对相同数据进行不一样的操作,维护数据的一致性,防止数据丢失。也就是说,我们需要一种互斥机制,按照某种规则对多个线程进行排队,依次、互不干扰地访问共享资源。
这个机制指的是,为了实现分布式互斥,在某个地方做个标记,这个标记每个线程都能看到,到标记不存在时可以设置该标记,当标记被设置后,其他线程只能等待拥有该标记的线程执行完成,并释放该标记后,才能去设置该标记和访问共享资源。这里的标记,就是我们常说的锁。
也就是说,锁是多线程同时访问同一资源的场景下,为了让线程互不干扰地访问共享资源,从而保证操作的有效性和正确性的一种标记。
与普通锁不同的是,分布式锁是指分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁。为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、Memcached、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。
在大规模分布式系统中,单个机器的线程锁无法管控多个机器对同一资源的访问,这时使用分布式锁,就可以把整个集群当作一个应用一样去处理,实用性和扩展性更好。
分布式锁的三种主流实现方法
- 基于数据库实现分布式锁,这里的数据库指的是关系型数据库;
- 基于缓存实现分布式锁;
- 基于 ZooKeeper 实现分布式锁
5.1 基于数据库实现分布式锁
实现分布式锁最直接的方式通过数据库进行实现,首先创建一张表用于记录共享资源信息,然后通过操作该表的数据来实现共享资源信息的修改。
当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保证只有一个操作可以成功,操作成功的那个线程就获得了访问共享资源的锁,可以进行操作。
基于数据库实现的分布式锁,是最容易理解的。但是,因为数据库需要落到硬盘上,频繁读取数据库会导致 IO 开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。对于双 11、双 12 等需求量激增的场景,数据库锁是无法满足其性能要求的。而在平日的购物中,我们可以在局部场景中使用数据库锁实现对资源的互斥访问。
基于数据库实现关键在于创建一张锁表,为申请者在锁表里建立一条记录,记录建立成功则获得锁,消除记录则释放锁。该方法依赖于数据库,主要有两个缺点:
- 单点故障问题。一旦数据库不可用,会导致整个系统崩溃。
- 死锁问题。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。倘若已获得共享资源访问权限的进程突然挂掉、或者解锁操作失败,使得锁记录一直存在数据库中,无法被删除,而其他进程也无法获得锁,从而产生死锁现象。
5.2 基于缓存实现分布式锁
所谓基于缓存,也就是说把数据存放在计算机内存中,不需要写入磁盘,减少了 IO 读写。以 Redis 为例展开这部分内容。
Redis 通常可以使用 setnx(key, value) 函数来实现分布式锁。key 和 value 就是基于缓存的分布式锁的两个属性,其中 key 表示锁 id,value = currentTime + timeOut,表示当前时间 + 超时时间。也就是说,某个进程获得 key 这把锁后,如果在 value 的时间内未释放锁,系统就会主动释放锁。
setnx 函数的返回值有 0 和 1:
- 返回 1,说明该服务器获得锁,setnx 将 key 对应的 value 设置为当前时间 + 锁的有效时间。
- 返回 0,说明其他服务器已经获得了锁,进程不能进入临界区。该服务器可以不断尝试 setnx 操作,以获得锁。
Redis 通过队列来维持进程访问共享资源的先后顺序。Redis 锁主要基于 setnx 函数实现分布式锁,当进程通过 setnx<key,value> 函数返回 1 时,表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁,或者等到时间超时才能获得锁。
基于缓存实现的分布式锁的优势表现在以下几个方面:
- 性能更好。数据被存放在内存,而不是磁盘,避免了频繁的 IO 操作。
- 很多缓存可以跨集群部署,避免了单点故障问题。
- 使用方便。很多缓存服务都提供了可以用来实现分布式锁的方法,比如 Redis 的 setnx 和 delete 方法等。
- 可以直接设置超时时间(例如 expire key timeout)来控制锁的释放,因为这些缓存服务器一般支持自动删除过期数据。
这个方案的不足是,通过超时时间来控制锁的失效时间,并不是十分靠谱,因为一个进程执行时间可能比较长,或受系统进程做内存回收等影响,导致时间超时,从而不正确地释放了锁。
redis 官方已经明确说明不推荐 setnx 来实现分布式锁了(https://redis.io/commands/setnx) 官方给出了 the Redlock algorithm 各语言的实现版本是来实现分布式锁的机制,比如JAVA的实现版本(https://github.com/redisson/redisson) 所以从可靠性来讲并不比zk的方案差
5.3 基于 ZooKeeper 实现分布式锁
ZooKeeper 基于树形数据存储结构实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题。ZooKeeper 的树形数据存储结构主要由 4 种节点构成:
- 持久节点(PERSISTENT)。这是默认的节点类型,一直存在于 ZooKeeper 中。
- 持久顺序节点(PERSISTENT_SEQUENTIAL)。在创建节点时,ZooKeeper 根据节点创建的时间顺序对节点进行编号命名。
- 临时节点(EPHEMERAL)。当客户端与 Zookeeper 连接时临时创建的节点。与持久节点不同,当客户端与 ZooKeeper 断开连接后,该进程创建的临时节点就会被删除。
- 临时顺序节点(EPHEMERAL_SEQUENTIAL)。就是按时间顺序编号的临时节点。
根据它们的特征,ZooKeeper 基于临时顺序节点实现了分布锁。
临时顺序节点结构大致如下,/locker节点为持久节点,该节点下有多个子节点,这些子节点由不同客户端创建。
locker/
├── foo0000000001
├── foo0000000002
├── foo0000000003
├── foo0000000004
└── foo0000000005
参考:https://blog.csdn.net/pysense/article/details/100638437
以电商售卖吹风机的场景为例。假设用户 A、B、C 同时在 11 月 11 日的零点整提交了购买吹风机的请求,ZooKeeper 会采用如下方法来实现分布式锁:
在与该方法对应的持久节点 shared_lock 的目录下,为每个进程创建一个临时顺序节点。如下图所示,吹风机就是一个拥有 shared_lock 的目录,当有人买吹风机时,会为他创建一个临时顺序节点。
- 每个进程获取 shared_lock 目录下的所有临时节点列表,注册 Watcher,用于监听子节点变更的信息。当监听到自己的临时节点是顺序最小的,则可以使用共享资源。
- 每个节点确定自己的编号是否是 shared_lock 下所有子节点中最小的,若最小,则获得锁。例如,用户 A 的订单最先到服务器,因此创建了编号为 1 的临时顺序节点 LockNode1。该节点的编号是持久节点目录下最小的,因此获取到分布式锁,可以访问临界资源,从而可以购买吹风机。
- 若本进程对应的临时节点编号不是最小的,则分为两种情况:
- 本进程为读请求,如果比自己序号小的节点中有写请求,则等待;
- 本进程为写请求,如果比自己序号小的节点中有请求,则等待。
例如,用户 B 也想要买吹风机,但在他之前,用户 C 想看看吹风机的库存量。因此,用户 B 只能等用户 A 买完吹风机、用户 C 查询完库存量后,才能购买吹风机。
可以看到,使用 ZooKeeper 实现的分布式锁,可以解决前两种方法提到的各种问题,比如单点故障、不可重入、死锁等问题。但该方法实现较复杂,且需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。
5.4 总结
对比一下这三种方式的特点
这里的实现复杂性,是针对同样的分布式锁的实现复杂性,与之前提到的基于数据库的实现非常简易不一样。
基于数据库实现的分布式锁存在单点故障和死锁问题,仅仅利用数据库技术去解决单点故障和死锁问题,是非常复杂的。而 ZooKeeper 已定义相关的功能组件,因此可以很轻易地解决设计分布式锁时遇到的各种问题。所以说,要实现一个完整的、无任何缺陷的分布式锁,ZooKeeper 是一个最简单的选择。
总结来说,ZooKeeper 分布式锁的可靠性最高,有封装好的框架,很容易实现分布式锁的功能,并且几乎解决了数据库锁和缓存式锁的不足,因此是实现分布式锁的首选方法。
从上述分析可以看出,为了确保分布式锁的可用性,我们在设计时应考虑到以下几点:
- 互斥性,即在分布式系统环境下,对于某一共享资源,需要保证在同一时间只能一个线程或进程对该资源进行操作。
- 具备锁失效机制,防止死锁。即使出现进程在持有锁的期间崩溃或者解锁失败的情况,也能被动解锁,保证后续其他进程可以获得锁。
- 可重入性,即进程未释放锁时,可以多次访问临界资源。
- 有高可用的获取锁和释放锁的功能,且性能要好。
如何解决分布式锁的羊群效应问题?
在分布式锁问题中,会经常遇到羊群效应。所谓羊群效应,就是在整个 ZooKeeper 分布式锁的竞争过程中,大量的进程都想要获得锁去使用共享资源。每个进程都有自己的“Watcher”来通知节点消息,都会获取整个子节点列表,使得信息冗余,资源浪费。
当共享资源被解锁后,Zookeeper 会通知所有监听的进程,这些进程都会尝试争取锁,但最终只有一个进程获得锁,使得其他进程产生了大量的不必要的请求,造成了巨大的通信开销,很有可能导致网络阻塞、系统性能下降。
那如何解决这个问题呢?具体方法可以分为以下三步。
- 在与该方法对应的持久节点的目录下,为每个进程创建一个临时顺序节点。
- 每个进程获取所有临时节点列表,对比自己的编号是否最小,若最小,则获得锁。
- 若本进程对应的临时节点编号不是最小的,则注册 Watcher,监听自己的上一个临时顺序节点,当监听到该节点释放锁后,获取锁。
分布式锁与分布式互斥的关系是什么呢?
分布式互斥是在分布式系统中存在多个节点共享某个资源或临界区,任何时刻只允许一个进程访问资源或执行临界区,而分布式锁正好是解决分布式互斥的一种方法。