在分布式系统中,每一个机器节点虽然都能明确的知道自己在事务操作中的结果是成功或失败,但无法直接获取其他节点的操作结果。因此在分布式环境中,为了保持事务的ACID特性,就需要增加一个“协调者”来管理其他节点(“参与者”)事务的提交和回滚。基于这个思想,衍生出二阶段提交和三阶段提交两种协议。

2PC 简述

二阶段提交(Two-Phase Commit),是一个非常经典的强一致中心化的原子提交协议。目前,绝大多数关系型数据库都采用二阶段提交协议来完成分布式事务处理(例如mysql的XA协议)。因此二阶段提交协议也被广泛运用到分布式系统中。

顾名思义,算法流程就是分为两个阶段提交某一操作,其分为准备阶段、提交阶段。为了更好描述算法过程,为此定义了两种角色:协调者(Coordinator)、参与者(Participant)。

阶段一:准备阶段

2pc 准备阶段
准备阶段,又被称为投票阶段(Vote Request),由协调者向参与者发送请求,询问当前事务能否处理成功。参与者则开启本地数据库事务,开始执行数据库操作,但是并不会提交。根据操作结果,返回给协调者“yes/no”,表示事务是否可以提交。

  • 事务询问
    协调者向所有参与者发送事务内容,询问是否可以执行事务的提交操作,并等待各参与者的响应
  • 执行事务
    各参与者执行事务操作,准备好事务资源,记录undo、redo信息
  • 反馈询问结果
    如果参与者成功执行了事务操作,那么返回给协调者yes(表示当前事务可以提交),否者返回给协调者no(表示当前事务不能执行)

阶段二:提交阶段

在准备阶段,由于参与者可以返回yes/no,则在提交阶段也会出现两种可能,即全局提交事务、全局回滚事务。

全局提交事务

2pc 提交阶段
当准备阶段所有参与者都返回yes的响应后,协调者将发起全局提交事务请求。

  • 发送提交请求
    由协调者向所有参与者发送global_commit请求,要求提交当前事务
  • 事务提交
    当参与者收到global_commit请求后,则执行事务提交操作,并释放整个分布式事务期间占用的事务资源
  • 反馈提交结果
    参与在执行完事务提交后,向协调者返回ack消息
  • 完成事务提交
    协调者收到所有参与者反馈的ack消息后,给客户端返回结果,完成本次事务
全局回滚事务

2pc 提交阶段
当准备阶段有一个参与者都返回no的响应后,实际场景中,协调者还会怎加等待响应时间,如果超时后,则协调者将发起全局回滚事务请求,中断事务。

  • 发送回滚请求
    由协调者向所有参与者发送global_rollback请求,要求中断当前事务
  • 事务回滚
    当参与者收到global_rollback请求后,会利用准备阶段记录的undo信息来进行回滚,并释放整个分布式事务期间占用的事务资源
  • 反馈回滚结果
    参与在执行完事务提交后,向协调者返回ack消息
  • 中断事务
    协调者收到所有参与者反馈的ack消息后,给客户端返回结果,完成中断事务

宕机(故障)恢复

2PC宕机故障,分为多种情况。部分参与者宕机、协调者宕机、部分参与者和协调者都宕机。

  • 部分参与者宕机
    参与者恢复后询问协调者宕机期间处理情况便可完成一致性
  • 协调者宕机
    新选一个协调者替换,询问所有参与者最后那条事务执行情况,然后决定进行commit/rollback,或者不处理。
  • 部分参与者和协调者都宕机
    新竞选的协调者询问所有参与者最后那条事务执行情况。则又分为几种情况:
    • 如果有参与者第一阶段返回no,或者第二阶段执行了rollback。则协调者命令所有参与者执行rollback
    • 如果所有参与者第一阶段返回yes,第二阶段有参与者执行了commit,则协调者命令所有参与者执行commit
    • 如果所有参与者第一阶段返回yes,第二阶段没有参与者执行任何操作,此时协调者则不知道宕机的参与者执行了commit/rollback。即不能明确的恢复过来。

优缺点

2PC协议明显的优点就是:原理简单、容易实现。但是它的缺点更加明显:

  • 同步阻塞
    每个参与者都需要等待协调者的消息,才能继续下一阶段。当协调者在发送第二阶段的消息之前宕机,那么所有参与者将一直锁定准备阶段的事务资源,事务推进不下去了,造成事务阻塞。只有等到协调者恢复,事务才能继续进行。
  • 数据不一致
    在第二阶段,如果出现网络异常导致一部分参与者收到了commit请求,一部分参与者没有收到commit请求,结果会是一部分参与者提交了事务,一部分参与者无法进行事务提交。
  • 宕机恢复,上一小节描述的部分情况下不能保证一致性
  • 单点问题/脑裂
    协调者在2PC中,太过重要,当协调者宕机,整个集群将不可用。脑裂是指因为网络原因,出现多个协调者。
  • 太过保守
    任何一个节点故障,都会导致整个事务协调失败,换句话说没有完善的容错机制。

3PC

上面小节讲解了二阶段提交协议的原理,也指出了它所存在的问题,三阶段提交协议则是为了解决2PC的同步阻塞、数据不一致的问题而诞生。

在2PC的基础上,3PC将准备阶段一分为二,形成由 CanCommit、PreCommit、DoCommit三个阶段组成的事务提交协议。并且在3PC参与者也增加超时机制(2PC只有协调者拥有超时机制),避免了参与者长时间无法与协调者节点通讯(协调者宕机)的情况下,无法释放资源的问题。在参与者超时后,会自动进行本地commit/abort从而进行释放资源。

但是3PC并没有完美解决2PC的问题,例如:

  • 同步阻塞,3PC只是减少了阻塞范围,这里的阻塞包含两方面:
    • 参与者在长时间无法与协调者通讯,无法释放资源阻塞的问题。参与者等待协调者消息超时后,自行commit/abort来释放资源。
    • 协调者和参与者都宕机情况下的阻塞。新晋升的协调者不知道宕机的参与者执行了什么操作,依然不能释放事务资源,而造成的阻塞问题。
  • 数据不一致,也只解决了一部分情况下的不一致。
    比如第三阶段,部分参与者没有收到DoCommit消息,根据规定会自行提交,最后达成一致。但是如果第三阶段需要abort,部分参与者没有收到abort消息,根据规定也会自行提交,最后数据不一致。
  • 宕机恢复,解决了2PC中协调者和参与者在第二阶段宕机不能恢复一致性的问题

可以看出3PC提出的解决方案,自身本就引入了新的问题,比如自动提交后造成的数据不一致。还有增加了一轮通讯,增加了复杂度和协商效率。所以实际上,很少会有系统实现3PC,多数情况下,通过状态机复制来解决2PC的同步阻塞的问题。

阶段一:CanCommit

3pc CanCommit

  • 事务询问
    由协调者向所有参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并等待参与者响应
  • 反馈询问响应
    参与者收到canCommit请求后,根据自身逻辑判断是否可以顺利执行事务,那么反馈yes,否则反馈no。说白了就是检查下自身状态的健康性,看有没有能力进行事务操作。

阶段二:PreCommit

在阶段一结果中,如果所有参与者都返回yes,则执行事务预提交,协调者发起PreCommit请求。如果有任何一个参与者节点返回的结果是no,或者协调者在等待参与者节点反馈的过程中超时,整个分布式事务就会中断,协调者就会向所有的参与者发送“abort”请求。

注意:上面所说的参与者增加超时时间,在该阶段就可以派上用场了。在该阶段开始之前,如果协调者宕机,参与者在等待超时时间后,各自选择中断事务,最终达成一致。但是协调者在发出一部分preCommit请求后宕机,有一部分没有收到preCommit请求,那情况则是:一部分参与者执行了preCommit操作,一部分参与者没有执行preCommit操作,最终导致各节点之间数据不一致。

执行事务预提交

3pc PreCommit

  • 发送preCommit请求
    由协调者向所有参与者发送preCommit请求,并等待参与者响应
  • 事务预提交
    参与者收到preCommit请求后,会执行事务操作,将undo和redo信息写入事务日志中
  • 反馈执行响应
    参与者将执行结果反馈给协调者,同时等待最终指令提交(commit)或终止(abort)
中断事务

该阶段中断事务相比阶段三中断事务要简单,只需要由协调者向所有参与者发送abort请求,通知中断事务。期间参与者在等待协调者的preCommit请求超时后也会主动中断本地事务。

阶段三:DoCommit

同样,根据上一阶段的结果,该阶段也会存在两种情况。阶段二所有参与者成功反馈,则执行提交事务请求,否则中断事务。

注意:参与者增加的超时时间,在该阶段也有所体现。在该阶段,无论是协调者宕机了,还是网络原因导致参与者没有收到该阶段的(提交/中断)请求,参与者都会执行本地事务提交。倘若在该阶段,协调者需要发送的是提交事务请求,参与者自动提交本地事务,最后能达到一致性。倘若在该阶段,协调者需要发送的是中断事务请求,如果参与者没有收到该请求,则参与者也会自动提交本地事务,最终导致各节点之间数据不一致。

提交事务

3pc DoCommit

  • 发送提交请求
    协调者就会从“预提交状态”变为“提交状态”。然后向所有的参与者节点发送”global_commit”请求
  • 事务提交
    参与者收到global_commit请求后,将执行本地事务提交操作,并释放整个事务执行期间占用的事务资源
  • 反馈提交结果
    参与者向协调者反馈ack提交结果
  • 完成事务
    协调者收到所有参与者的ack消息后,完成事务
中断事务
  • 发送中断请求
    协调者向所有参与者发送abort请求
  • 事务回滚
    参与者根据阶段二中记录的undo信息,来执行回滚操作,并释放占用事务资源
  • 反馈回滚结果
    参与者向协调者反馈ack回滚结果
  • 中断事务
    协调者收到所有参与者的ack消息后,中断事务

宕机(故障)恢复

直接分析2PC中的协调者和参与者都宕机情况下,新晋升的协调者怎么恢复一致性的。

这种情况下,新晋升的协调者同样会询问所有的参与者事务处理情况。这看上去和2PC一样啊?但是深入分析下就会发现不一样。

我们假设第三阶段,协调者和参与者宕机了,并且参与者执行了第三阶段的操作,没人知道它执行了什么,也就存在两种可能:

  • 宕机的参与者没执行第三阶段任何操作或者执行了第三阶段的commit操作。那么协调者在询问所有的参与者事务执行情况时,得到的ACK响应一定是,所有参与者处于第二阶段成功完成的状态或者第三阶段执行了commit操作的状态。据此,协调者如果收到这样的回复,那一定可以断定宕机的参与者执行了commit操作或者没执行第三阶段的操作,那么协调者可以大胆的发送DoCommit消息。
  • 宕机的参与者执行第三阶段的abort操作。那么协调者在询问所有的参与者事务执行情况时,得到的ACK响应一定是,一定存在一个参与者的第二阶段的响应是no,否则第三阶段不会发起abort操作。据此,协调者如果收到存在一个参与者处于第二阶段的abort状态时,则可以大胆的发送abort操作给所有的参与者。

优缺点

相比于2PC,3PC最大的优点就是:

  • 通过增加超时和自动提交,减少了参与者的阻塞范围。
  • 通过增加PreCommit阶段,能在协调者和参与者都宕机情况下,新晋升的协调者能够使数据达成一致。(维基百科中描述的则是:当协调者和参与者都宕机后,新晋升的协调者可以自信的完成后续的事务操作。)

3PC的缺点:

  • 在某些情况下,因为自动提交/中断本地事务的约定,必然会造成数据的不一致性。
  • 增加一轮消息,增加了复杂度和协商效率。