Two-phase Commit [2PC],二阶段协议提交是一种应用在分布式系统中的保持系统一致性的协议,尽管它有着明显的缺陷,而后续改进的协议或者算法也有很多,但是它的设计思想还是很值得学习的。

首先,2PC有如下三个assumption

  1. 该分布式系统中,存在一个节点作为协调者[Coordinator],其他节点作为参与者[Cohorts]。且节点之间可以进行网络通信。
  2. 所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失。
  3. 所有节点不会永久性损坏,即使损坏后仍然可以恢复。

这里说插入一点关于预写式日志[Write-Ahead Logging,WAL]的介绍。

WAL是关系数据库系统中用于提供原子性和持久性[ACID属性中的两个]的一系列技术。在使用WAL的系统中,所有的修改在提交之前都要先写入log文件中。也就是说所有修改都是先记录log而不是直接修改磁盘,这就降低了I/O操作,具体什么时候写入磁盘,不同的数据库有不同的实现机制。但是都是要先写入log,再写入磁盘。所以当写入磁盘失败,系统崩溃,都可以通过log进行恢复,这里面涉及两个主要功能undo和redo。

从概念上讲,undo正好与redo相对。你对数据执行修改时,数据库会生成undo信息,这样万一你执行的事务或语句由于某种原因失败了,或者如果你用一条ROLLBACK语句请求回滚,就可以利用这些undo信息将数据向后回滚到修改前的样子。同样是失败了,你希望再次尝试或者继续这个修改,那么这个操作将会根据日志记录进行重做,这叫向前滚动恢复,就是redo。所以,redo用于在失败时重做或继续事务[即恢复事务],undo则用于取消一条语句或一组语句的作用。

好了,在这个场景中,如果Coordinator不出错,Cohorts也不出错,这就是一个完美的系统,可以正常运转[但是这几乎不可能],所以对于出错,有三种情况:

  1. Coordinator不出错,Cohorts出错 [2PC解决了这个问题]
  2. Coordinator出错,Cohorts不出错 [2PC中,有选新Coordinator的机制,解决了这个问题]
  3. Coordinator出错,Cohorts出错 [2PC解决不了,3PC应运而生]

其核心思想是在问题1的情况下依然保证强一致性,也就是说要么所有节点都进行更新操作,要么就不进行。其选新的Coordinator的机制也解决了问题2,也就是只要Cohorts不出错,我们再选一个新的Coordinator,原来出错的Coordinator变为Cohorts,这就变成了问题1。但在具体部署中,问题3无法解决。

2PC的具体阶段两个,准备阶段,提交阶段。一句话解释,一个负责人问所有其他人,能不能干,如果所有人都说能干,那么负责人就说那咱们干吧,就干了。如果有一个或几个人说不行,或者不说话,那这个具有严格集体主义精神的负责人就说,不行,现在干不了,要不等等,要不就不干了。


准备阶段

事务协调者[Coordinator]给每个参与者[Cohort]发送Prepare消息,每个参与者要么直接返回失败[如权限验证失败],要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种万事俱备,只欠东风的状态。可以进一步将准备阶段分为以下三个步骤:

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

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

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

提交阶段

  • 当协调者节点从所有参与者节点获得的相应消息都为同意时: 1. 协调者节点向所有参与者节点发出正式提交[commit]的请求。2. 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。3. 参与者节点向协调者节点发送完成消息。4. 协调者节点受到所有参与者节点反馈的完成消息后,完成事务。

  • 如果任一参与者节点在第一阶段返回的响应消息为中止,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时: 1. 协调者节点向所有参与者节点发出回滚操作[rollback]的请求。2. 参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。3. 参与者节点向协调者节点发送回滚完成消息。4. 协调者节点受到所有参与者节点反馈的回滚完成消息后,取消事务。

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


二阶段提交看起来确实能够提供原子性的操作并且保持一致性,但是,如我们在开头简述,二阶段提交还是有几个缺点的:

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

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

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

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