由于分布式系统的各个服务可能分布在不同的节点上,如果各节点直接没有相互的通信获取其他节点状态,那么各个节点是无法知道其他节点的任务处理结果的。
如果在分布式系统中发起一个事务,该事务涉及多个不同节点,那么为了保证事务 ACID 特性,就需要引入一个协调者来统一调度事务涉及的多个节点,被调度的节点称为事务参与者。由此衍生出 2PC 和 3PC 协议,本文就来详细介绍 2PC 和 3PC 的工作机制。
2PC(两阶段提交,Two-Phase Commit)
顾名思义,分为两个阶段:Prepare 和 Commit
Prepare:提交事务请求
基本流程如下图:
2PC1.png-
询问
协调者向所有参与者发送事务请求,询问是否可执行事务操作,然后等待各个参与者的响应。 -
执行
各个参与者接收到协调者事务请求后,执行事务操作(例如更新一个关系型数据库表中的记录),并将 Undo 和 Redo 信息记录事务日志中。 -
响应
如果参与者成功执行了事务并写入 Undo 和 Redo 信息,则向协调者返回 YES 响应,否则返回 NO 响应。当然,参与者也可能宕机,从而不会返回响应。
Commit:执行事务提交
执行事务提交分为两种情况,正常提交和回退。
正常提交事务
流程如下图:
2PC2.png-
commit 请求
协调者向所有参与者发送 Commit 请求。 -
事务提交
参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。 -
反馈结果
参与者执行事务提交后向协调者发送 Ack 响应。 -
完成事务
接收到所有参与者的 Ack 响应后,完成事务提交。
中断事务
在执行 Prepare 步骤过程中,如果某些参与者执行事务失败、宕机或与协调者之间的网络中断,那么协调者就无法收到所有参与者的 YES 响应,或者某个参与者返回了 No 响应,此时,协调者就会进入回退流程,对事务进行回退。流程如下图红色部分(将 Commit 请求替换为红色的 Rollback 请求):
2PC4.png-
rollback 请求
协调者向所有参与者发送 Rollback 请求。 -
事务回滚
参与者收到 Rollback 后,使用 Prepare 阶段的 Undo 日志执行事务回滚,完成后释放事务执行期占用的所有资源。 -
反馈结果
参与者执行事务回滚后向协调者发送 Ack 响应。 -
中断事务
接收到所有参与者的 Ack 响应后,完成事务中断。
2PC 的问题
-
同步阻塞
参与者在等待协调者的指令时,其实是在等待其他参与者的响应,在此过程中,参与者是无法进行其他操作的,也就是阻塞了其运行。
倘若参与者与协调者之间网络异常导致参与者一直收不到协调者信息,那么会导致参与者一直阻塞下去。 -
单点
在 2PC 中,一切请求都来自协调者,所以协调者的地位是至关重要的,如果协调者宕机,那么就会使参与者一直阻塞并一直占用事务资源。如果协调者也是分布式,使用选主方式提供服务,那么在一个协调者挂掉后,可以选取另一个协调者继续后续的服务,可以解决单点问题。但是,新协调者无法知道上一个事务的全部状态信息(例如已等待 Prepare 响应的时长等),所以也无法顺利处理上一个事务。
-
数据不一致
Commit 事务过程中 Commit 请求/Rollback 请求可能因为协调者宕机或协调者与参与者网络问题丢失,那么就导致了部分参与者没有收到 Commit/Rollback 请求,而其他参与者则正常收到执行了 Commit/Rollback 操作,没有收到请求的参与者则继续阻塞。这时,参与者之间的数据就不再一致了。当参与者执行 Commit/Rollback 后会向协调者发送 Ack,然而协调者不论是否收到所有的参与者的 Ack,该事务也不会再有其他补救措施了,协调者能做的也就是等待超时后像事务发起者返回一个“我不确定该事务是否成功”。
-
环境可靠性依赖
协调者 Prepare 请求发出后,等待响应,然而如果有参与者宕机或与协调者之间的网络中断,都会导致协调者无法收到所有参与者的响应,那么在 2PC 中,协调者会等待一定时间,然后超时后,会触发事务中断,在这个过程中,协调者和所有其他参与者都是出于阻塞的。这种机制对网络问题常见的现实环境来说太苛刻了。
3PC(三阶段提交,Three-Phase Commit)
上面说明了 2PC 协议的多个缺点,那么 3PC 就是在 2PC 的基础上,为了解决 2PC 的某些缺点而设计的,3PC 分为三个阶段:CanCommit,PreCommit 和 doCommit。
CanCommit
流程如下图:
3PC1.png-
事务询问
协调者向所有参与者发送事务 canCommit 请求,请求中包含事务内容,询问是否可以执行事务提交操作,并开始等待响应。 -
反馈询问结果
参与者收到 canCommit 请求后,分析事务内容,判断自身是否可以执行事务,如果可以,那么就返回 Yes 响应,进入预备状态,否则返回 No 响应。注意:此过程中并没有执行事务(对比 2PC 的 Prepare 阶段,参与者是执行了事务的)。
PreCommit
流程图如下:
3PC2.pngPreCommit 阶段根据各参与者返回的 CanCommit 响应,决定下一步动作。如果收到了所有参与者的 Yes 响应,则执行事务预提交,否则(收到了至少一个 No 响应或一定时长内没有收到所有参与者的 Yes 响应,如 3PC 第一张图片中红色部分),执行事务中断。
事务预提交
-
发送 PreCommit 请求
协调者发送 PreCommit 请求,并进入 Prepared 阶段。 -
参与者处理 PreCommit
参与者收到 PreCommit 请求后,执行事务操作,并将 Undo 和 Redo 信息记录事务日志中。 -
反馈执行结果
如果参与者成功执行了事务并写入 Undo 和 Redo 信息,那么反馈 Ack 给协调者,并等待下一步指令。
事务中断
上图中,红色的 Abort 表示协调者发送的不是 PreCommit 请求,而是 Abort 请求。
-
发送事务中断请求
协调者向所有参与者发送 Abort 请求。 -
中断事务
参与者收到 Abort 请求后,会触发事务中断。此外,如果参与者在等待协调者指令超时,会自己触发事务中断,在 2PC 中,参与者会一直阻塞的等待协调者指令,所以 3PC 中解决了因为这种情况带来的阻塞。
doCommit
流程图如下:
3PC3.png协调者根据第二阶段的响应决定最终操作,如果协调者收到了所有参与者在 PreCommit 阶段的 Ack 响应,那么会进入执行事务提交阶段,否则执行事务中断。
事务提交
-
发送提交请求
协调者收到所有参与者在 PreCommit 阶段返回的 Ack 响应后,向所有参与者发送 doCommit 请求,并进入提交状态。 -
事务提交
参与者收到 Commit 请求后,执行事务提交,提交完成后释放事务执行期占用的所有资源。 -
反馈结果
参与者完成事务提交之后,向协调者返回 Ack 响应。 -
完成事务
协调者收到所有参与者的 Ack 响应后,完成事务。
事务中断
-
发送事务中断请求
协调者向所有参与者发送 Abort 请求。 -
事务回滚
参与者收到 Abort 请求后,会使用第二阶段记录的 Undo 信息进行事务回滚,并在完成回滚后释放所有事务资源。注意:因为第一阶段并没有任何参与者实际执行事务,所以在第二阶段(PreCommit 阶段)执行事务中断,是不需要事务回滚的,也就不需要下面的反馈结果,直接中断事务即可。
-
反馈回滚结果
参与者执行事务回滚后向协调者发送 Ack 响应。 -
中断事务
协调者接收到所有参与者反馈的 Ack 响应后,完成事务中断。
3PC 的改进和缺点
改进
-
降低了阻塞
-
参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若等待超时,则自动 abort,降低了阻塞;
-
参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若等待超时,则自动 commit 事务,也降低了阻塞;
-
-
解决单点故障问题
-
参与者返回 CanCommit 请求的响应后,等待第二阶段指令,若协调者宕机,等待超时后自动 abort,;
-
参与者返回 PreCommit 请求的响应后,等待第三阶段指令,若协调者宕机,等待超时后自动 commit 事务;
-
缺点
数据不一致问题仍然是存在的,比如第三阶段协调者发出了 abort 请求,然后有些参与者没有收到 abort,那么就会自动 commit,造成数据不一致。
总结
从上面讲述来看,2PC 和 3PC 都无法完美解决分布式数据一致性问题,虽然无法保证事务的 ACID 特性,但两阶段的思想在很多实际架构中有这广泛应用,例如 JTA 事务以及一些数据库的数据同步。
引用一句话,是 Google Chubby 作者说的:
“There is only one consensus protocol, and that’s Paxos” – all other approaches are just broken versions of Paxos.”
Paxos 是兰伯特提出的一个解决分布式一致性的算法,后面再写文讲述其原理。
网友评论