今天我们来看看2PC协议,不知道大家对2PC协议是不是了解,我们先简单介绍一下。
两阶段提交协议(two phase commit protocol, 2PC)可以保证数据的强一致性,许多分布式关系型数据管理系统采用此协议来完成分布式事务。它是协调所有分布式院子事务参与者,并决定提交或取消(回滚)的分布式算法。同时也是解决一致性问题的一致性算法。该算法能够解决很多的临时性系统故障(包括进程,网络节点,通信等故障),被广泛的使用。但是,它并不能通过配置来解决所有的问题,在某些情况下,它还需要人为的参与才能解决问题。参与者为了能够从故障中恢复,它们都使用日志来记录协议的状态,虽然使用日志降低了性能,但是节点能够从中恢复。
在两阶段提交协议中,系统一般包含两类机器(或者节点):一类为协调者(coordinator),通常一个系统中只有一个;另一个为事务参与者(participants, cohorts或workers),一般包含多个,在数据存储系统中可以理解为数据副本的个数。协议中假设每个节点都会记录写前日志(write-ahead log)并持久性存储,即使节点发生故障,日志也不会丢失。协议中同时假设节点不会发生永久性故障而且任意两个节点都可以互相通信。
当事务的最后一步完成之后,协调器执行协议,参与者根据本地事务能够成功完成回复同意提交事务或者回滚事务。
顾名思义,两阶段提交协议由两个阶段组成,在正常情况下,两个阶段的执行过程如下所述:
阶段1:请求阶段(commit-request phase, 或者表决阶段,Voting phase)
在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。在表决过程中,参与者将告知协助者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。
阶段2:提交阶段(commit phase)
在该阶段,协调者将基于第一个阶段的投票结果进行决策,提交或者取消。当且仅当所有的参与者同意提交事务协调器才通知所有的参与者提交事务,,否则协调者将通知所有参与者取消事务。参与者收到协调者发来的消息后将执行响应的操作。
协议操作流图如下:
两阶段提交最大的劣势是其通过阻塞完成的协议,在节点等待消息的时候处于阻塞状态,节点中其他进程则需要等待阻塞进程释放资源才能使用。如果协调者发生了故障,那么参与者将无法完成事务一直等待下去。以下情况可能导致节点发生永久阻塞:
如果参与者发送同意提交消息给协调者,进程将阻塞直至收到协调者的提交或者混滚的消息。如果协调器发生永久故障,参与者将一直等待,这里可以采用备份的协调器,所有参与者将恢复发给备份协调者,由它来承担协调器的功能。
如果协调器发送“请求提交”消息给参与者,它将被阻塞直到所有参与者回复了,如果某个参与者发生永久性故障,那么协调器也不会一直阻塞,因为协调器在某一时间内未收到参与者的消息,那么它通知其他参与者回滚事务。
同时两阶段提交协议没有容错机制,一个节点发生故障整个事务都要回滚,代价比较大。
下面看一个例子,来说明两阶段提交协议的工作流程:
A组织B、C和D三个人去爬长城:如果所有人都同意去爬长城,那么活动将举行;如果有一人不同意去爬长城,那么活动将取消。用2PC算法解决该问题的过程如下:
首先A将成为该活动的协调者,B、C和D将成为该活动的参与者。
阶段1:
A发邮件给B、C和D,提出下周三去爬山,问是否同意。那么此时A需要等待B、C和D的邮件。
B、C和D分别查看自己的日程安排表。B、C发现自己在当日没有活动安排,则发邮件告诉A它们同意下周三去爬长城。由于某种原因,D白天没有查看邮件。那么此时A、B和C均需要等待。到晚上的时候,D发现了A的邮件,然后查看日程安排,发现周三当天已经有别的安排,那么D回复A说活动取消吧。
阶段2:
此时A收到了所有活动参与者的邮件,并且A发现D下周三不能去爬山。那么A将发邮件通知B、C和D,下周三爬长城活动取消。
此时B、C回复A“太可惜了”,D回复A“不好意思”。至此该事务终止。
通过该例子可以发现,2PC协议存在明显的问题。假如D一直不能回复邮件,那么A、B和C将不得不处于一直等待的状态。并且B和C所持有的资源,即下周三不能安排其它活动,一直不能释放。其它等待该资源释放的活动也将不得不处于等待状态。
基于此,后来有人提出了三阶段提交协议,在其中引入超时的机制,将阶段1分解为两个阶段:在超时发生以前,系统处于不确定阶段;在超时发生以后,系统则转入确定阶段。
2PC协议包含协调者和参与者,并且二者都有发生问题的可能性。假如协调者发生问题,我们可以选出另一个协调者来提交事务。例如,班长组织活动,如果班长生病了,我们可以请副班长来组织。如果协调者出问题,那么事务将不会取消。例如,班级活动希望每个人都能去,假如有一位同学不能去了,那么直接取消活动即可。或者,如果大多数人去的话那么活动如期举行(2PC变种)。为了能够更好地解决实际的问题,2PC协议存在很多的变种,例如:树形2PC协议(或称递归2PC协议)、动态2阶段提交协议(D2PC)等。
好了,说了这么一大段协议的东西,我们接下来看看在PAT里面怎么实现这样的协议。
//Two Phase Commit Protocol (TPCP) #define N 2; //number of pages enum {Yes, No, Commit, Abort}; //constants //channel result 0; //channel inform 0; channel vote 0; var hasNo = false; //The following models the coordinator Coord(decC) = (|||{N}@ request -> Skip); (|||{N}@ vote?vo -> atomic{tau{if (vo == No) {hasNo = true;}} -> Skip}); decide -> (([hasNo == false] (|||{N}@inform.Commit -> Skip); CoordPhaseTwo(Commit)) [] ([hasNo == true] (|||{N}@inform.Abort -> Skip); CoordPhaseTwo(Abort))); CoordPhaseTwo(decC) = |||{N}@acknowledge -> Skip; //The following models a page Page(decP, stable) = request -> execute -> (vote!Yes -> PhaseTwo(decP) [] vote!No -> PhaseTwo(decP)); PhaseTwo(decP) = inform.Commit -> complete -> result.decP -> acknowledge -> Skip [] inform.Abort -> undo -> result.decP -> acknowledge -> Skip; //定义这个alphabet有什么用呢? #alphabet Coord {request, inform.Commit, inform.Abort, acknowledge}; #alphabet Page {request, inform.Commit, inform.Abort, acknowledge}; System = Coord(Abort) || (|||{N}@Page(Abort, true)); Implementation = System \{request, execute, acknowledge, inform.Abort, inform.Commit, decide, result.Abort, result.Commit}; #assert System deadlockfree; #define has hasNo == 1; #assert System |= [](has -> <> undo); #assert System |= [](request -> <> undo); Specification = PC(N); PC(i) = [i == 0](|||{N}@complete -> Skip) [] [i > 0] (vote.Yes -> PC(i-1) [] vote.No -> PU(i-1)); PU(i) = [i == 0](|||{N}@undo -> Skip) [] [i > 0](vote.Yes -> PU(i-1) [] vote.No -> PU(i-1)); #assert Specification deadlockfree; #assert Implementation refines Specification;
代码里面的参与者是pages,这里有两个人参与者。
枚举类型给了4个变量,Yes,No,Commit,Abort。值分别为0,1,2,3,Yes和No表示参与者返回给协调者的信息,如果都是YES(0),那么协调者就会确认(Commit,值为2),如果有一个参与者返回的是No(1),协调者就会让参与者回滚(Abort,值为3)。接下来定义一个通道(channel)vote,接下来定义一个变量(hasNo),表示这个参与者返回给协调者的信息里面是不是有No。
下面来对协调者进行建模:
Coord(decC) = (|||{N}@ request -> Skip); (|||{N}@ vote?vo -> atomic{tau{if (vo == No) {hasNo = true;}} -> Skip}); decide -> (([hasNo == false] (|||{N}@inform.Commit -> Skip); CoordPhaseTwo(Commit)) [] ([hasNo == true] (|||{N}@inform.Abort -> Skip); CoordPhaseTwo(Abort))); CoordPhaseTwo(decC) = |||{N}@acknowledge -> Skip;
首先,协调者先向参与者发送请求,让参与者提交,然后协调者通过vote这个通道收到消息,然后如果收到的消息里面有No就把hasNo置为true。然后来做决定,然后是一个选择,每个选项前面有一个需要满足的条件([hasNo==false]或者[hasNo=true]),当hasNo为false时,说明每个参与者都返回给协调者Yes,那么协调者就会确认(Commit),然后开始第二阶段。第二阶段就是收到来自参与者的答复。
下面是对参与者进行建模:
Page(decP, stable) = request -> execute -> (vote!Yes -> PhaseTwo(decP) [] vote!No -> PhaseTwo(decP)); PhaseTwo(decP) = inform.Commit -> complete -> result.decP -> acknowledge -> Skip [] inform.Abort -> undo -> result.decP -> acknowledge -> Skip;
参与者收到请求,然后开始执行然后进入选择,要么回复给协调者Yes,然后进入第二阶段,要么回复No,进入第二阶段。
第二阶段就是要么收到确认要么收到回滚,确认之后就是完成,然后进行相应的结果处理,然后给协调者一个答复。收到回滚后就开始回滚,然后对相应的结果进行处理,然后给协调者一个答复。
下面是定义了程序的变量集合:
#alphabet Coord {request, inform.Commit, inform.Abort, acknowledge};
#alphabet Page {request, inform.Commit, inform.Abort, acknowledge};
其实这一个我也不知道是什么意思。
First of all, alphabet of processes are calculated only when it is necessary, which means, only when a parallel composition is evaluated.
This saves a lot of computational overhead. Processes in a large number of models only communicate through shared variables.
If no parallel composition is present, there is no need to evaluate alphabets.
We remark that when there is no shared abstract events, process P ||| Q and P || Q are exactly the same. Therefore, we recommend ||| when appropriate.
When a parallel composition is evaluated for the first time, the default alphabet of each sub-process is calculated (if not manually defined).
The procedure for calculating the default alphabet works by retrieving all event constituting the process expression and unfolding every newly-met process reference.
It throws an exception if a process reference is met twice with different parameters (which probably indicates the unfolding is non-terminating).
In such a case, PAT expects the user to specify the alphabet of a process using the following syntax, #alphabet P {...}; where P is process name and {...} is the set of events which is considered its alphabet. Notice that the event expressions may contain variables.
翻译过来就是:
首先,仅当必要时才计算出进程的字母表,这意味着只有当并行组合被评估时。
这节省了大量的计算开销。大量模型中的进程只能通过共享变量进行通信。
如果不存在平行组合,则不需要评估字母。我们说当没有共享抽象事件时,进程P ||| Q和P || Q完全一样。
因此,我们推荐|||适当时候当首次评估并行组合时,会计算每个子进程的默认字母表(如果没有手动定义)。
计算默认字母表的过程通过检索构成过程表达式的所有事件并展开每个新满足的过程引用。
如果一个进程引用被两次不同的参数(这可能表示展开是不终止的),它会抛出异常。
在这种情况下,PAT希望用户使用以下语法来指定进程的字母表, #alphabet P {...}; 其中P是进程名称,{...}是被认为是其字母表的事件集合。请注意,事件表达式可能包含变量。
大概就是说这个字母表是可以自己指定,如果自己不指定那么PAT就会自动生成一个字母表。指定字母表有一个好处就是当程序比较复杂时,PAT就只是照着指定的字母表运行。
然后接下来就是整个系统,整个系统就是协调者和参与者的并发。
接下来Implementation表示在System的基础上隐藏一些行为。
下面就是一些验证:
首先验证System会不会不死锁。
然后定义了has 变量为 hasNo==1;意思就是hasNo为true,也就是说参与者有回复No的,由此可以看出
#assert System |= [](has -> <> undo);
是正确的,因为有人回复No,那么就会回滚。
下面这个验证是不正确的,因为协调者让参与者commit,但是不一定会回滚。
#assert System |= [](request -> <> undo);
接下来是一个行为,
Specification = PC(N); PC(i) = [i == 0](|||{N}@complete -> Skip) [] [i > 0] (vote.Yes -> PC(i-1) [] vote.No -> PU(i-1)); PU(i) = [i == 0](|||{N}@undo -> Skip) [] [i > 0](vote.Yes -> PU(i-1) [] vote.No -> PU(i-1));
这个行为大家看看是什么意思,有点复杂,我也不想解释了。
接下来是两个验证:
#assert Specification deadlockfree;
#assert Implementation refines Specification;
这两个验证都是正确的。至于为什么正确我也不想解释了,因为这一块我不是太懂。