强一致性事务实现原理
2PC,即二阶段提交,是分布式事务中一个很重要的协议,当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个coordinator,即协调者作为的组件来统一掌控所有节点(称作参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交或回滚。
二阶段提交的算法思路可以概括为: 参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是回滚操作。
各参与者成功提交事务流程:
(1)在一个有A,B,C参与的事务中,引入一个协调者,用来协调和通知各参与者执行相应的事务动作,其中黑线为协调者发起的指令,红线为参与者响应的指令。
(2)协调者通知所有参与者节点准备提交事务,参与者节点执行事务操作,并将Undo信息和Redo信息写入日志,但此时不提交事务。
(3)协调者收到各参与者准备提交完成的指令,没有任何节点出错,此时通知所有节点提交事务。
(4)参与者节点正式完成提交操作,并释放在整个事务期间内占用的资源。
(5)协调者收到所有参与者确认提交完毕的答复,此时整个事务完成。
各参与者提交事务失败流程:
(1)在一个有A,B,C参与的事务中,引入一个协调者,用来协调和通知各参与者执行相应的事务动作,其中黑线为协调者发起的指令,红线为参与者响应的指令。
(2)协调者通知所有参与者节点准备提交事务,参与者节点执行事务操作,并将Undo信息和Redo信息写入日志,但此时不提交事务,但此时B节点在准备执行操作事务时出现了异常,则返回给协调者准备失败的响应信息
(3)协调者虽然收到A跟C准备完成的响应,但B返回的状态是准备失败,此时进入通知所有参与者回滚的阶段
(4)参与者节点正式完成回滚操作,并释放在整个事务期间内占用的资源。
(5)协调者收到所有参与者确认回滚完毕的答复,此时整个事务完成
结论:不管最后结果如何,第二阶段都会结束当前事务。
那可能就有人问了,那第二阶段提交失败的话呢?
这里有两种情况。
第一种是第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。
第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功,到最后真的不行只能人工介入处理。
大体上二阶段提交的流程就是这样,我们再来看看细节。
首先 2PC 是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。
在第二阶段协调者的没法超时,因为按照我们上面分析只能不断重试!
协调者故障分析
协调者是一个单点,存在单点故障问题。
假设协调者在发送准备命令之前挂了,还行等于事务还没开始。
假设协调者在发送准备命令之后挂了,这就不太行了,有些参与者等于都执行了处于事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统其它操作。
假设协调者在发送回滚事务命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功参与者都阻塞着。
假设协调者在发送回滚事务命令之后挂了,这个还行,至少命令发出去了,很大的概率都会回滚成功,资源都会释放。但是如果出现网络分区问题,某些参与者将因为收不到命令而阻塞着。
假设协调者在发送提交事务命令之前挂了,这个不行,傻了!这下是所有资源都阻塞着。
假设协调者在发送提交事务命令之后挂了,这个还行,也是至少命令发出去了,很大概率都会提交成功,然后释放资源,但是如果出现网络分区问题某些参与者将因为收不到命令而阻塞着。
协调者故障,通过选举得到新协调者
因为协调者单点问题,因此我们可以通过选举等操作选出一个新协调者来顶替。
如果处于第一阶段,其实影响不大都回滚好了,在第一阶段事务肯定还没提交。
如果处于第二阶段,假设参与者都没挂,此时新协调者可以向所有参与者确认它们自身情况来推断下一步的操作。
假设有个别参与者挂了!这就有点僵硬了,比如协调者发送了回滚命令,此时第一个参与者收到了并执行,然后协调者和第一个参与者都挂了。
此时其他参与者都没收到请求,然后新协调者来了,它询问其他参与者都说OK,但它不知道挂了的那个参与者到底O不OK,所以它傻了。
问题其实就出在每个参与者自身的状态只有自己和协调者知道,因此新协调者无法通过在场的参与者的状态推断出挂了的参与者是什么情况。
虽然协议上没说,不过在实现的时候我们可以灵活的让协调者将自己发过的请求在哪个地方记一下,也就是日志记录,这样新协调者来的时候不就知道此时该不该发了?
但是就算协调者知道自己该发提交请求,那么在参与者也一起挂了的情况下没用,因为你不知道参与者在挂之前有没有提交事务。
如果参与者在挂之前事务提交成功,新协调者确定存活着的参与者都没问题,那肯定得向其他参与者发送提交事务命令才能保证数据一致。
如果参与者在挂之前事务还未提交成功,参与者恢复了之后数据是回滚的,此时协调者必须是向其他参与者发送回滚事务命令才能保持事务的一致。
所以说极端情况下还是无法避免数据不一致问题。
2PC 在执行过程中可能发生 Coordinator 或者参与者突然宕机的情况,在不同时期宕机可能有不同的现象。
情况 | 分析及解决方案 |
---|---|
Coordinator 挂了,参与者没挂 | 这种情况其实比较好解决,只要找一个 Coordinator 的替代者。当他成为新的 Coordinator 的时候,询问所有参与者的最后那条事务的执行情况,他就可以知道是应该做什么样的操作了。所以,这种情况不会导致数据不一致。 |
参与者挂了(无法恢复),Coordinator 没挂 | 如果挂了之后没有恢复,那么是不会导致数据一致性问题。 |
参与者挂了(后来恢复),Coordinator 没挂 | 恢复后参与者如果发现有未执行完的事务操作,直接取消,然后再询问 Coordinator 目前我应该怎么做,协调者就会比对自己的事务执行记录和该参与者的事务执行记录,告诉他应该怎么做来保持数据的一致性。 |
还有一种情况是:参与者挂了,Coordinator 也挂了,需要再细分为几种类型来讨论:
情况 | 分析及解决方案 |
---|---|
Coordinator 和参与者在第一阶段挂了 | 由于这时还没有执行 commit 操作,新选出来的 Coordinator 可以询问各个参与者的情况,再决定是进行 commit 还是 roolback。因为还没有 commit,所以不会导致数据一致性问题。 |
Coordinator 和参与者在第二阶段挂了,但是挂的这个参与者在挂之前还没有做相关操作 | 这种情况下,当新的 Coordinator 被选出来之后,他同样是询问所有参与者的情况。只要有机器执行了 abort(roolback)操作或者第一阶段返回的信息是 No 的话,那就直接执行 roolback 操作。如果没有人执行 abort 操作,但是有机器执行了 commit 操作,那么就直接执行 commit 操作。这样,当挂掉的参与者恢复之后,只要按照 Coordinator 的指示进行事务的 commit 还是 roolback 操作就可以了。因为挂掉的机器并没有做 commit 或者 roolback 操作,而没有挂掉的机器们和新的 Coordinator 又执行了同样的操作,那么这种情况不会导致数据不一致现象。 |
Coordinator 和参与者在第二阶段挂了,挂的这个参与者在挂之前已经执行了操作。但是由于他挂了,没有人知道他执行了什么操作。 | 这种情况下,新的 Coordinator 被选出来之后,如果他想负起 Coordinator 的责任的话他就只能按照之前那种情况来执行 commit 或者 roolback 操作。这样新的 Coordinator 和所有没挂掉的参与者就保持了数据的一致性,我们假定他们执行了 commit。但是,这个时候,那个挂掉的参与者恢复了怎么办,因为他已经执行完了之前的事务,如果他执行的是 commit 那还好,和其他的机器保持一致了,万一他执行的是 roolback 操作呢?这不就导致数据的不一致性了么?虽然这个时候可以再通过手段让他和 Coordinator 通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了! |
所以,2PC协议中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。为了解决这个问题,衍生出了3PC。
2PC 优缺点
简单总结一下 2PC 的优缺点:
- 优点:原理简洁清晰、实现方便;
- 缺点:同步阻塞、单点问题、某些情况可能导致数据不一致。
关于这几个缺点,在实际应用中,都是对2PC 做了相应的改造:
- 同步阻塞:2PC 有几个过程(比如 Coordinator 等待所有参与者表决的过程中)都是同步阻塞的,在实际的应用中,这可能会导致长阻塞问题,这个问题是通过超时判断机制来解决的,但并不能完全解决同步阻塞问题;
- Coordinator 单点问题:实际生产应用中,Coordinator 都会有相应的备选节点;
- 数据不一致:这个在前面已经讲述过了,如果在第二阶段,Coordinator 和参与者都出现挂掉的情况下,是有可能导致数据不一致的。
三阶段提交协议(3PC)
三阶段提交协议(Three-Phase Commit, 3PC)最关键要解决的就是 Coordinator 和参与者同时挂掉导致数据不一致的问题,所以 3PC 把在 2PC 中又添加一个阶段,这样三阶段提交就有:CanCommit、PreCommit 和 DoCommit 三个阶段。
3PC 过程
三阶段提交协议的过程如下图(图来自 维基百科:三阶段提交,知乎,傲丙)所示:
3PC 的详细过程如下:
阶段一 CanCommit
- 事务询问:Coordinator 向各参与者发送 CanCommit 的请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应;
- 参与者向 Coordinator 反馈询问的响应:参与者收到 CanCommit 请求后,正常情况下,如果自身认为可以顺利执行事务,那么会反馈 Yes 响应,并进入预备状态,否则反馈 No。
阶段二 PreCommit
执行事务预提交:如果 Coordinator 接收到各参与者反馈都是Yes,那么执行事务预提交:
- 发送预提交请求:Coordinator 向各参与者发送 preCommit 请求,并进入 prepared 阶段;
- 事务预提交:参与者接收到 preCommit 请求后,会执行事务操作,并将 Undo 和 Redo 信息记录到事务日记中;
- 各参与者向 Coordinator 反馈事务执行的响应:如果各参与者都成功执行了事务操作,那么反馈给协调者 ACK 响应,同时等待最终指令,提交 commit 或者终止 abort,结束流程;
中断事务:如果任何一个参与者向 Coordinator 反馈了 No 响应,或者在等待超时后,Coordinator 无法接收到所有参与者的反馈,那么就会中断事务。
- 发送中断请求:Coordinator 向所有参与者发送 abort 请求;
- 中断事务:无论是收到来自 Coordinator 的 abort 请求,还是等待超时,参与者都中断事务。
阶段三 doCommit
执行提交
- 发送提交请求:假设 Coordinator 正常工作,接收到了所有参与者的 ack 响应,那么它将从预提交阶段进入提交状态,并向所有参与者发送 doCommit 请求;
- 事务提交:参与者收到 doCommit 请求后,正式提交事务,并在完成事务提交后释放占用的资源;
- 反馈事务提交结果:参与者完成事务提交后,向 Coordinator 发送 ACK 信息;
- 完成事务:Coordinator 接收到所有参与者 ack 信息,完成事务。
中断事务:假设 Coordinator 正常工作,并且有任一参与者反馈 No,或者在等待超时后无法接收所有参与者的反馈,都会中断事务
- 发送中断请求:Coordinator 向所有参与者节点发送 abort 请求;
- 事务回滚:参与者接收到 abort 请求后,利用 undo 日志执行事务回滚,并在完成事务回滚后释放占用的资源;
- 反馈事务回滚结果:参与者在完成事务回滚之后,向 Coordinator 发送 ack 信息;
- 中断事务:Coordinator 接收到所有参与者反馈的 ack 信息后,中断事务。
3PC 分析
3PC 虽然解决了 Coordinator 与参与者都异常情况下导致数据不一致的问题,3PC 依然带来其他问题:比如,网络分区问题,在 preCommit 消息发送后突然两个机房断开,这时候 Coordinator 所在机房会 abort, 另外剩余参与者的机房则会 commit。
而且由于3PC 的设计过于复杂,在解决2PC 问题的同时也引入了新的问题,所以在实际上应用不是很广泛。
TCC(Try-Confirm-Cancel)
2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,就像我前面说的分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!
TCC 指的是Try - Confirm - Cancel
。
- Try 指的是预留,即资源的预留和锁定,注意是预留。
- Confirm 指的是确认操作,这一步其实就是真正的执行了。
- Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
一阶段Try行为:
调用方会分别调用多个被调用方,只有当多个被调用方都返回成功的时候,才会执行confirm操作,否则就执行cancel,这个阶段其实就是一个检查资源和锁定资源的操作,在真正执行前的一个冻结操作。
二阶段Confirm行为:
使用预留的资源,完成真正的业务操作,要求Try成功Confirm一定也是要成功的。要是个别或者全部confirm失败了或者调用超时了,和try阶段不同的是在confirm阶段不会做回滚操作,而是将所有的confirm进行重试,如果超过重试次数,则必须告警通知人工进行处理。
二阶段Cancel行为:
这个阶段是释放预留资源的阶段,只有在try阶段失败或者异常的情况下才会执行cancel操作。而且这里只对try步骤中已经成功的被调用者进行cannel处理,同时这个步骤失败也会重试。
TCC是柔性事务,解决了2PC中全局资源锁定导致效率低下的问题。
相对于 2PC、3PC ,TCC 适用的范围更大,但是开发量也更大,毕竟都在业务上实现,而且有时候你会发现这三个方法还真不好写。不过也因为是在业务上实现的,所以TCC可以跨数据库、跨不同的业务系统来实现事务。