分布式系统的一致性是特别伤脑筋的事情,当然也可以使用一些现成的协议,比如Raft之类的。今天我们准备来聊下两阶段提交(2PC)和三阶段提交(3PC),以及它们之间的关系。
相对来说,我个人认为这些东西如果只是通过文字表达,若抽象思维不好,理解起来还是有些难度。于是,我画了几个图,尽量用图形的方式来展现它们的流程以及对彼此做一些比较。
预备知识
在分布式系统中,我们都会采用一些分布式协调的工具来协调分布在各个地方的参与者的行为,保障事务结果的一致性。这里就有两个角色,分别是协调者和参与者。在现实中,协调者可能是你发送消息到各个系统的客户端,或者是外部中间件。
两阶段提交
先来看两阶段提交的过程,上图中包含了一个协调者和两个参与者,为了保障事务在整个系统中执行完成后的一致性,协调者分两个阶段来向各参与者提交事务。具体流程如下:
prepare
- 协调者向各个参与者发送提交事务的请求;
- 参与者基于接收到的事务内容,在本地执行:
校验
、锁资源
、执行
、写redo和undo日志
,但不提交
; 并向协调者反馈yes/no
commit/rollback
- 协调者在上一步中收集来自各个参与者的反馈,如果都是yes,那么就正式向各参与者发送commit请求;如果其中有的参与者反馈是no,为了整个系统的一致性,会向各参与者发送rollback请求;
- 参与者接收到请求后,执行log中的redo或者undo操作,然后返回结果。
这块是否很好理解呢?整个过程其实就类似于我们使用mysql的事务操作,一般是先提交一批的操作,然后在最后基于各个操作的执行情况来决定是执行commit还是rollback。只有在第二阶段命令提交之后,操作才会真正在DB中生效。
问题
看起来很完美!但是,两阶段提交也存在着一些无法解决的问题,比如:
prepare
阶段做了锁定资源的操作;所以,要是在第一个阶段执行了之后,假设出现以下两种情况都会导致协调者无法提交第二阶段请求到参与者,那么参与者的资源就会一直被锁定;- 网络中断
- 协调者挂掉
commit
阶段的协调者到参与者的消息,有的发送成功,有的失败;这就会导致有的参与者执行了redo,另外一些什么都没处理,还在锁定资源,数据最终不一致。
三阶段提交
其整个流程如下,接下来会分析为何要这样设计。
- cancommit
只负责对操作条件的校验,不做其它; - precommit/abort
负责锁资源、执行、写redo和undo日志,但不提交; - docmmit/abort
正常完成操作,释放资源
两阶段提交的改良
三阶段提交是对两阶段提交的一种完善,为了解决前面提到的第一个问题,三阶段提交引入了两个解决方案。
解决资源锁定
核心思想是:“增加流程,事前预防,胜过事后解决问题”。它将原来两阶段中的第一阶段prepare
分为两个阶段:
- cancommit
只负责对操作条件的校验,不做其他; - precommit
负责锁资源、执行、写redo和undo日志,但不提交;
通过上图可以清楚看到,从两阶段提交到三阶段提交,通过拆分了prepare
后,可以在cancommit
阶段就能最大概率快速确定该次事务是否能够执行。一旦所有参与者都返回yes后,才通知各个参与者锁定资源;这样就避免了两阶段提交中,一上来各自就开始锁定资源,然后发现某个参与者有问题后又通知各自在rollback操作中去释放资源的补救措施。
解决无法提交第二阶段请求
前面已经提到,由于协调者故障或者网络异常、分片等异常,将导致commit
消息无法提交到某些消费者;为了更大概率的解决该问题,在三阶段提交中,为precommit
后的事务都设置了一个定时器。当在超时后没有接收到docommit
消息后,参与者就会尝试正式执行操作。
当然,这种方法只是一种最大概率的保障,判断的依据是一旦参与者接收到了
precommit
操作之后,意味着它知道所有的参与者其实都同意修改了。所以,由于网络超时等原因,虽然参与者没有收到commit
或者abort
响应,但是他有理由相信:成功提交的几率很大。但是,如果协调者事实上在最后阶段不是提交
docommit
,而是abort
的话,那就愿赌服输了!