在分布式系统中,每一个机器节点虽然都能明确的知道自己在事务操作中的结果是成功或失败,但无法直接获取其他节点的操作结果。因此在分布式环境中,为了保持事务的ACID特性,就需要增加一个“协调者”来管理其他节点(“参与者”)事务的提交和回滚。基于这个思想,衍生出二阶段提交和三阶段提交两种协议。
2PC 简述
二阶段提交(Two-Phase Commit),是一个非常经典的强一致、中心化的原子提交协议。目前,绝大多数关系型数据库都采用二阶段提交协议来完成分布式事务处理(例如mysql的XA协议)。因此二阶段提交协议也被广泛运用到分布式系统中。
顾名思义,算法流程就是分为两个阶段提交某一操作,其分为准备阶段、提交阶段。为了更好描述算法过程,为此定义了两种角色:协调者(Coordinator)、参与者(Participant)。
阶段一:准备阶段
准备阶段,又被称为投票阶段(Vote Request),由协调者向参与者发送请求,询问当前事务能否处理成功。参与者则开启本地数据库事务,开始执行数据库操作,但是并不会提交。根据操作结果,返回给协调者“yes/no”,表示事务是否可以提交。
事务询问 协调者向所有参与者发送事务内容,询问是否可以执行事务的提交操作,并等待各参与者的响应
执行事务 各参与者执行事务操作,准备好事务资源,记录undo、redo信息
反馈询问结果 如果参与者成功执行了事务操作,那么返回给协调者yes(表示当前事务可以提交),否者返回给协调者no(表示当前事务不能执行)
阶段二:提交阶段
在准备阶段,由于参与者可以返回yes/no,则在提交阶段也会出现两种可能,即全局提交事务、全局回滚事务。
全局提交事务
当准备阶段所有参与者都返回yes的响应后,协调者将发起全局提交事务请求。
发送提交请求 由协调者向所有参与者发送global_commit请求,要求提交当前事务
事务提交 当参与者收到global_commit请求后,则执行事务提交操作,并释放整个分布式事务期间占用的事务资源
反馈提交结果 参与在执行完事务提交后,向协调者返回ack消息
完成事务提交 协调者收到所有参与者反馈的ack消息后,给客户端返回结果,完成本次事务
全局回滚事务
当准备阶段有一个参与者都返回no的响应后,或者在协调者等待响应超时后,则协调者将发起全局回滚事务请求,中断事务。
发送回滚请求 由协调者向所有参与者发送global_rollback请求,要求中断当前事务
事务回滚 当参与者收到global_rollback请求后,会利用准备阶段记录的undo信息来进行回滚,并释放整个分布式事务期间占用的事务资源
反馈回滚结果 参与在执行完事务提交后,向协调者返回ack消息
中断事务 协调者收到所有参与者反馈的ack消息后,给客户端返回结果,完成中断事务
优缺点
2PC协议明显的优点就是:原理简单、容易实现。但是它的缺点更加明显:
同步阻塞 每个参与者都需要等待其他参与者完成后,才能继续下一阶段,也就是说事务操作逻辑都是处于阻塞状态,极大限制了分布式系统性能
数据不一致 在第二阶段,如果出现网络异常导致一部分参与者收到了commit请求,一部分参与者没有收到commit请求,结果会是一部分参与者提交了事务,一部分参与者无法进行事务提交
单点问题/脑裂 协调者在2PC中,太过重要,当协调者宕机,整个集群将不可用。更可怕的是,协调者在第二阶段之前宕机,那么所有参与者将一直锁定准备阶段的事务资源。脑裂(动态选主情况下)是指因为网络原因,出现多个协调者。
太过保守 任何一个节点故障,都会导致整个事务协调失败,换句话说没有完善的容错机制。
3PC
上面小节讲解了二阶段提交协议的原理,也指出了它所存在的问题,因此有必要在2PC基础上进一步改进,提出三阶段提交协议。
3PC(Three-Phase Commit),在2PC的基础上,将准备阶段一分为二,形成由 CanCommit、PreCommit、DoCommit三个阶段组成的事务提交协议。并且在3PC参与者也增加超时机制(2PC只有协调者拥有超时机制),避免了参与者长时间无法与协调者节点通讯(协调者宕机)的情况下,无法释放资源的问题。在参与者超时后,会自动进行本地commit/rollback从而进行释放资源。这种机制也侧面降低了整个事务的阻塞时间和范围。
阶段一:CanCommit
事务询问 由协调者向所有参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并等待参与者响应
反馈询问响应 参与者收到canCommit请求后,根据自身逻辑判断是否可以顺利执行事务,那么反馈yes,否则反馈no。说白了就是检查下自身状态的健康性,看有没有能力进行事务操作。
阶段二:PreCommit
在阶段一结果中,如果所有参与者都返回yes,则执行事务预提交,协调者发起PreCommit请求。如果有任何一个参与者节点返回的结果是no,或者协调者在等待参与者节点反馈的过程中超时,整个分布式事务就会中断,协调者就会向所有的参与者发送“abort”请求。
注意:上面所说的参与者增加超时时间,在该阶段就可以派上用场了。在该阶段开始之前,如果协调者宕机,参与者在等待超时时间后,各自选择中断事务,最终达成一致。但是协调者在发出一部分preCommit请求后宕机,有一部分没有收到preCommit请求,那情况则是:一部分参与者执行了preCommit操作,一部分参与者没有执行preCommit操作,最终导致各节点之间数据不一致。
执行事务预提交
发送preCommit请求 由协调者向所有参与者发送preCommit请求,并等待参与者响应
事务预提交 参与者收到preCommit请求后,会执行事务操作,将undo和redo信息写入事务日志中
反馈执行响应 参与者将执行结果反馈给协调者,同时等待最终指令提交(commit)或终止(abort)
中断事务
该阶段中断事务相比阶段三中断事务要简单,只需要由协调者向所有参与者发送abort请求,通知中断事务。期间参与者在等待协调者的preCommit请求超时后也会主动中断本地事务。
阶段三:DoCommit
同样,根据上一阶段的结果,该阶段也会存在两种情况。阶段二所有参与者成功反馈,则执行提交事务请求,否则中断事务。
注意:参与者增加的超时时间,在该阶段也有所体现。在该阶段,无论是协调者宕机了,还是网络原因导致参与者没有收到该阶段的(提交/中断)请求,参与者都会执行本地事务提交。倘若在该阶段,协调者需要发送的是提交事务请求,参与者自动提交本地事务,最后能达到一致性。倘若在该阶段,协调者需要发送的是中断事务请求,如果参与者没有收到该请求,则参与者也会自动提交本地事务,最终导致各节点之间数据不一致。
提交事务
发送提交请求 协调者就会从“预提交状态”变为“提交状态”。然后向所有的参与者节点发送"global_commit"请求
事务提交 参与者收到global_commit请求后,将执行本地事务提交操作,并释放整个事务执行期间占用的事务资源
反馈提交结果 参与者向协调者反馈ack提交结果
完成事务 协调者收到所有参与者的ack消息后,完成事务
中断事务
发送中断请求 协调者向所有参与者发送abort请求
事务回滚 参与者根据阶段二中记录的undo信息,来执行回滚操作,并释放占用事务资源
反馈回滚结果 参与者向协调者反馈ack回滚结果
中断事务 协调者收到所有参与者的ack消息后,中断事务
优缺点
相比于2PC,3PC最大的优点就是减少了参与者的阻塞范围,并且能在协调者故障之后某些情况下继续达成一致。
3PC的缺点,也就是我在每个阶段描述中,注意的那一部分,在某些情况下,3PC必然会造成数据的不一致性。