一、“两阶段提交”的架构
对于分布式系统,在两阶段提交的架构中,有两种类型的节点:事务的协调者,事务的参与者。
两阶段提交的程序执行流程如下:
1.请求阶段(commit-request phase,或称 预提交阶段,prepare phase)
在请求阶段,协调者将通知事务参与者 “执行本地事务,并做好提交的准备”,然后参与者答复协调者自己的决策:OK(事务参与者本地作业执行成功)或 Fail 取消(本地作业执行故障)。
2.提交阶段(commit phase)
在该阶段,协调者将综合第一个阶段的投票结果进行决策:提交或取消。参与者在接收到协调者发来的消息后将执行相应的操作。
怎么理解“preCommit(预提交)”,预提交到底是什么样的操作,可否举个实际例子?
以数据库为例,预提交是写本地的redo和undo日志,但是不提交、不释放锁,达到一种“万事具备只欠东风”的状态。也可以理解为实际已经执行了操作,只是还没让它最终生效,而且在生效之前随时可以回滚。
怎么理解“commit(提交)”?
提交就是实际执行生效,释放资源和锁。
怎么理解“rollback(回滚)”?
即回滚已经执行的预提交操作,并释放资源和锁。要注意,回滚操作,务必要保证一定能成功,或者即使不成功,也不会造成多少影响。
怎么理解“canCommit(是否可提交)”?
可以理解为一个状态的判断,判断自己是否具备提交的条件,能否顺利的执行事务。
二阶段提交,用程序表示如下:
// 预提交
canCommit = doAllPreCommit(); (1)
if(canCommit) {
result = doAllCommit(); (2) // 可多次重试
}
if(!result) {
// 执行所有 回滚
doAllRollback(); (3) // 可多次重试
}
3.两阶段提交的缺点
1)同步阻塞问题。
执行过程中,所有参与节点都是事务阻塞型的(在预提交后,必须等待提交或回滚,注意:通常所谓的二阶段提交,没有超时故障处理的概念,所以说会一直等待)。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2)单点故障。
由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。(同样,前提是没有引入超时处理机制)
尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
3)数据不一致。
在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。(前提是,二阶段提交没有引入重试机制)
上述二阶段提交的缺点,都是因为没有 超时机制 和 重试机制 产生的。如果引入超时机制和重试机制,效果又会如何呢?
能否解决“同步阻塞问题”??:预提交之后,如果1分钟没有收到消息,参与者该怎么办?只能选择提交或回滚,但是无论选择哪一项,都可能造成和其他节点数据的不一致。但是引入重试机制之后,基本上能保证所以节点收到消息。
能否解决“单点故障问题”??:若协调者在预提交之后挂掉,所有参与者都懵逼了,他们只能默认选择提交或回滚,问题同上,而且协调者都挂了,没办法重试,只能重新选举协调者,但是新的协调者如何知道 旧协调者的决议?除非旧协调者把每次决议写到数据库中?否则,新协调者无法继续处理之前的事务。
能否解决“数据不一致问题”??:上面已经说了,数据可能不一致。而且在回滚阶段,也可能不一致。
综上,二阶段提交,即使引入 超时机制和 重试机制,依然无法解决 同步阻塞、单点故障和数据不一致的问题。
有人举了一个形象的例子:
假设有一个决策小组由一个主持人负责与多位组员以电话联络方式协调是否通过一个提案,对两阶段提交来说,主持人收到一个提案请求,打电话跟每个组员询问是否通过并统计回复,然后将最后决定打电话通知各组员。
要是主持人在跟第一位组员通完电话后失忆,而第一位组员在得知结果并执行后老人痴呆,那么即使重新选出主持人,也没人知道最后的提案决定是什么,也许是通过,也许是驳回,不管大家选择哪一种决定,都有可能与第一位组员已执行过的真实决定不一致,老板就会不开心认为决策小组沟通有问题而解雇。
二、“三阶段提交”的架构
上述“二阶段提交”最大的问题在于,单点故障问题,如果可以在执行预提交之前,先让个节点达成一致,则可以避免各节点不知道该回滚还是该提交的问题。
“三阶段提交” 的流程分为CanCommit、PreCommit、DoCommit三个阶段。
将这三个阶段串联起来,程序表示如下
// 查询是否可以提交
canCommit = queryAllCanCommit(); (1)
if(canCommit) {
// 执行所有 预提交
result = doAllPreCommit(); (2) // 可多次重试
if(result) {
// 执行所有 提交
result = doAllCommit(); (3) // 可多次重试
}
if(!result) {
// 执行所有 回滚
doAllRollback(); (4) // 可多次重试
}
return result;
} else {
return false;
}
在这些操作过程中,所有的失败均包括两个方面:参与者返回的失败、联系不到参与者(包括联系时出错或者网络阻塞超时)。
1、对于(1)阶段,联系不到参与者,不需要重试,直接失败。
2、对于(2)阶段,联系不到参与者,根据情况可以多次重试,如果最终确实联系不上,然后就发起回滚。
此阶段为预提交,如果有一部分参与者收到通知,另一部分未收到通知该怎么办?如果协调者还活着,他可以重试,重试多次仍然有人没回复,它可以发起回滚。如果协调者挂了,可以重新选举协调者,新的协调者,保持了canCommit状态,所以它可以继续执行旧协调者未完成的重试工作。这个过程,虽然不够完美,但是基本上能解决问题。
3、对于(3)阶段,如果协调者还活着,它可以持续通知参与者执行。但是,如果通知到一半节点时,协调者挂了,新选出的协调者,可以根据自己从旧协调者接收到的指令(commit或rollback)通知其他参与者去继续执行。该方案比较完美。
三阶段提交并非足够完美(但我觉得已经很好了),下面分析它存在的问题(网络分区问题):
如果有部分参与者 与 协调者 失联了,那该怎么办?首先,对于第二阶段的失联,协调者会努力重试,超过重试时间后,它将发起回滚。所以,失联的参与者,如果已经执行了预提交,且在重试时间之后仍未联系到协调者,它就可以执行回滚。
其次 ,对于第三阶段的失联,同理,协调者会努力重试,超过重试时间后,它将发起回滚。失联的参与者,如果在重试时间之后仍未联系到协调者,它就可以执行回滚。
基于重试超时时间的回滚方案,可以让三阶段提交方案的一致性加强,但是重试总时间怎么能控制精确呢?(每个节点都需要一个计时器?)。所以,这个方案一致性还可以,但是需要改进,它仍然没有彻底解决分布式的一致性问题。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的不完整版本。