如果一个操作涉及多个分布式节点,为了保证事务的ACID特性,需要引入一个“协调者”组件来统一调度所有分布式节点的执行逻辑,这些被调度的分布式节点被称为“参与者”。协调者负责调度参与者的行为,并最终决定这些参与者是否真正地提交事务。
分布式事务通常采用二阶段提交协议(2PC),它是几乎所有分布式事务算法的基础,后续的分布式事务算法几乎都由此改进而来。
(一)2PC:
二阶段提交(Two-phase Commit,简称2PC),是指为了使基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法(Algorithm)。通常2PC也被称为是一种协议(Protocol)。
在此协议中,一个事务管理器(Transaction Manager,简称 TM,也被称之为“协调者”)协调 1 个或多个资源管理器(Resource Manager,简称 RM,也被称之为“参与者”)的活动,所有资源管理器(参与者)向事务管理器(协调者)汇报自身活动状态,由事务管理器(协调者)根据各资源管理器(协调者)汇报的状态(完成准备或准备失败)来决定各资源管理器(协调者)是“提交”事务还是进行“回滚”操作。
因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
第一阶段:
协调者通知各个参与者准备提交它们的事务分支。如果参与者判断自己进行的工作可以被提交,那就对工作内容进行持久化,再给协调者肯定答复;要是发生了其他情况,那给协调者的都是否定答复。在发送了否定答复并回滚了已经的工作后,参与者就可以丢弃这个事务分支信息。
以MySQL数据库为例,在第一阶段,事务管理器(协调者)向所有涉及到的数据库服务器(参与者)发出Prepare “准备提交"请求,数据库(参与者)收到请求后执行数据修改和日志记录等处理,处理完成后只是把事务的状态改成"可以提交”,然后把结果返回给事务管理器(协调者)。
第二阶段:
协调者根据第一阶段中各个参与者 Prepare的结果,决定是提交还是回滚事务。如果所有的参与者都Prepare成功,那么协调者通知所有的参与者进行提交;如果有参与者Prepare失败的话,则协调者通知所有参与者回滚自己的事务分支。
![](/qrcode.jpg)
还是以MySQL数据库为例,如果第一阶段中所有数据库(参与者)都Prepare成功,那么事务管理器(协调者)向数据库服务器(参与者)发出"确认提交"请求,数据库服务器(参与者)把事务的"可以提交"状态改为"提交完成"状态,然后返回应答。如果在第一阶段内有任何一个数据库(参与者)的操作发生了错误,或者事务管理器(协调者)收不到某个数据库(参与者)的回应,则认为事务失败,回撤所有数据库(参与者)的事务。数据库服务器(参与者)收不到第二阶段的确认提交请求,也会把"可以提交"的事务回撤。
2PC提交的优点是尽量保证了数据的强一致,但不是 100% 一致。但是2PC也有明显的缺点:
- 单点故障:由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞,尤其是在第二阶段,协调者发生故障,那么所有的参与者都处于锁定事务资源的状态中,而无法继续完成事务操作。
- 同步阻塞:由于所有节点在执行操作时都是同步阻塞的,当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 数据不一致:在第二阶段中,当协调者向参与者发送提交事务请求之后,发生了局部网络异常或者在发送提交事务请求过程中协调者发生了故障,这会导致只有一部分参与者接收到了提交事务请求。而在这部分参与者接到提交事务请求之后就会执行提交事务操作。但是其他部分未接收到提交事务请求的参与者则无法提交事务。从而导致分布式系统中的数据不一致。
二阶段提交的问题:如果协调者在第二阶段发送提交请求之后挂掉,而唯一接受到这条消息的参与者执行之后也挂掉了,即使协调者通过选举协议产生了新的协调者并通知其他参与者进行提交或回滚操作的话,都可能会与这个已经执行的参与者执行的操作不一样。
(三)3PC:
三阶段提交(Three-phase Commit,简称3PC),是为解决2PC中的缺点而设计的。参考维基百科:https://en.wikipedia.org/wiki/Three-phasecommitprotocol。与两阶段提交不同的是,三阶段提交是“非阻塞”协议。
对应于2PC,3PC有两个改动点:
- 1:引入超时机制。同时在协调者和参与者中都引入超时机制。
- 2:在两阶段提交的第一阶段与第二阶段之间插入了一个准备阶段,使得原先在两阶段提交中,参与者在投票之后,由于协调者发生崩溃或错误而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
第一阶段CanCommit:
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送Commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
- 1:事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
- 2:响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No。
第二阶段PreCommit:
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能。
-
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。
a) 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段。
b) 事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
c) 响应反馈:如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
-
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
a) 发送中断请求:协调者向所有参与者发送Abort请求。
b) 中断事务:参与者收到来自协调者的Abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
第三阶段DoCommit:
该阶段进行真正的事务提交,也可以分为以下两种情况。
-
Case 1:执行提交。
a) 发送提交请求:协调者接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送DoCommit请求。
b) 事务提交:参与者接收到DoCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
c) 响应反馈:事务提交完之后,向协调者发送ACK响应。
d) 完成事务:协调者接收到所有参与者的ACK响应之后,完成事务。
-
Case 2:中断事务。协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
a) 发送中断请求:协调者向所有参与者发送Abort请求。
b) 事务回滚:参与者接收到Abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
c) 反馈结果:参与者完成事务回滚之后,向协调者发送ACK消息。
d) 中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
在三阶段提交中,如果在第三阶段协调者发送提交请求之后挂掉,并且唯一的接受的参与者执行提交操作之后也挂掉了,这时协调者通过选举协议产生了新的协调者。在二阶段提交时存在的问题就是新的协调者不确定已经执行过事务的参与者是执行的提交事务还是中断事务。但是在三阶段提交时,肯定得到了第二阶段的再次确认,那么第二阶段必然是已经正确的执行了事务操作,只等待提交事务了。所以新的协调者可以从第二阶段中分析出应该执行的操作,进行提交或者中断事务操作,这样即使挂掉的参与者恢复过来,数据也是一致的。
所以,三阶段提交解决了二阶段提交中存在的由于协调者和参与者同时挂掉可能导致的数据一致性问题和单点故障问题,并减少阻塞。因为一旦参与者无法及时收到来自协调者的信息之后,他会默认执行提交事务,而不会一直持有事务资源并处于阻塞状态。
不过3PC也存在自身的问题:在提交阶段如果发送的是中断事务请求,但是由于网络问题,导致部分参与者没有接到请求。那么参与者会在等待超时之后执行提交事务操作,这样这些由于网络问题导致提交事务的参与者的数据就与接受到中断事务请求的参与者存在数据不一致的问题。所以无论是 2PC 还是 3PC 都不能保证分布式系统中的数据 100% 一致。