二阶段提交
二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理。
阶段
- 准备阶段
- 提交阶段
参与角色
- 协调者:事务的发起者
- 参与者:事务的执行者
A. 第一阶段(voting phase 投票阶段):
- 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复
- 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
- 如参与者执行成功,给协调者反馈同意,否则反馈中止
B. 第二阶段(commit phase 提交执行阶段):
当协调者节点从所有参与者节点获得的相应消息都为同意时:
- 协调者节点向所有参与者节点发出正式提交(
commit
)的请求。 - 参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送ack完成消息。
- 协调者节点收到所有参与者节点反馈的ack完成消息后,完成事务。
如果任一参与者节点在第一阶段返回的响应消息为中止,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
- 协调者节点向所有参与者节点发出回滚操作(
rollback
)的请求。 - 参与者节点利用阶段1写入的undo信息执行回滚,并释放在整个事务期间内占用的资源。
- 参与者节点向协调者节点发送ack回滚完成消息。
- 协调者节点受到所有参与者节点反馈的ack回滚完成消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。
两阶段案例
学校运动会上,100米决赛正准备开始,裁判对3个人分别询问
裁判:张三同学你准备好了吗?准备好了进第一赛道
张三:准备好了,随即进入第一赛道做好冲击姿势
裁判:李四同学你准备好了吗?准备好了进第二赛道
裁判:王五同学你准备好了吗?准备好了进第三赛道
王五:准备好了,.....
李四:准备好了,.....
...
如果有人没准备好,不同意,则裁判下达回滚指令
如果裁判收到了所有人的OK回复后,再次下令
裁判:跑...
...
张三、李四、执行完毕到达终点,汇报给了裁判
王五冲刺失败,汇报给了裁判
二阶段提交看起来确实能够提供原子性的操作,但是不幸的是,二阶段提交还是有几个缺点的:
- 性能问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 可靠性问题:参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。
- 数据一致性问题:二阶段无法解决的问题:协调者在发出
commit
消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
- 优点
尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致) - 缺点
实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
为此,Dale Skeen和Michael Stonebraker
在“A Formal Model of Crash Recovery in a Distributed System
”中提出了三阶段提交协议(3PC)。
三阶段提交(3PC)
三阶段提交协议,是二阶段提交协议的改进版本,三阶段提交有两个改动点。
- 在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit
、PreCommit
、DoCommit
三个阶段。处理流程如下:
小例子
班长要组织全班同学聚餐,由于大家毕业多年,所以要逐个打电话敲定时间,时间初定10.1日。然后开始逐个打电话。
班长:小A,我们想定在10.1号聚会,你有时间嘛?有时间你就说YES,没有你就说NO,然后我还会再去问其他人,具体时间地点我会再通知你,这段时间你可先去干你自己的事儿,不用一直等着我。(协调者询问事务是否可以执行,这一步不会锁定资源)
小A:好的,我有时间。(参与者反馈)
班长:小B,我们想定在10.1号聚会……不用一直等我。
班长收集完大家的时间情况了,一看大家都有时间,那么就再次通知大家。(协调者接收到所有YES指令)
班长:小A,我们确定了10.1号聚餐,你要把这一天的时间空出来,这一天你不能再安排其他的事儿了。然后我会逐个通知其他同学,通知完之后我会再来和你确认一下,还有啊,如果我没有特意给你打电话,你就10.1号那天来聚餐就行了。对了,你确定能来是吧?(协调者发送事务执行指令,这一步锁住资源。如果由于网络原因参与者在后面没有收到协调者的命令,他也会执行commit)
小A顺手在自己的日历上把10.1号这一天圈上了,然后跟班长说,我可以去。(参与者执行事务操作,反馈状态)
班长:小B,我们觉得了10.1号聚餐……你就10.1号那天来聚餐就行了。
班长通知完一圈之后。所有同学都跟他说:”我已经把10.1号这天空出来了”。于是,他在10.1号这一天又挨个打了一遍电话告诉他们:嘿,现在你们可以出门拉。。。。(协调者收到所有参与者的ACK响应,通知所有参与者执行事务的commit)
小A,小B:我已经出门拉。(执行commit操作,反馈状态)
1. 阶段一:CanCommit阶段
3PC的CanCommit
阶段其实和2PC的准备阶段很像。协调者向参与者发送commit
请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
- 事务询问
协调者向所有参与者发出包含事务内容的canCommit
请求,询问是否可以提交事务,并等待所有参与者答复。 - 响应反馈
参与者收到canCommit
请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
2. PreCommit阶段
协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit
操作。根据响应情况,有以下两种可能。
假如所有参与者均反馈 yes,协调者预执行事务。
- 发送预提交请求 :协调者向参与者发送
PreCommit
请求,并进入准备阶段 - 事务预提交 :参与者接收到
PreCommit
请求后,会执行事务操作,并将undo
和redo
信息记录到事务日志中(但不提交事务) - 响应反馈 :如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。
- 发送预提交请求 :协调者向参与者发送
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
- 发送中断请求 :协调者向所有参与者发送
abort
请求。 - 中断事务 :参与者收到来自协调者的
abort
请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。
- 发送中断请求 :协调者向所有参与者发送
3. doCommit阶段
该阶段进行真正的事务提交,也可以分为以下两种情况。
注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。
3.1 执行提交
所有参与者均反馈 ack 响应,执行真正的事务提交
- 发送提交请求
协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit
请求。 - 事务提交
参与者接收到doCommit
请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。 - 响应反馈
事务提交完之后,向协调者发送ack响应。 - 完成事务
协调者接收到所有参与者的ack响应之后,完成事务。
3.2 中断事务
任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务
- 发送中断请求
如果协调者处于工作状态,向所有参与者发出 abort 请求 - 事务回滚
参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。 - 反馈结果
参与者完成事务回滚之后,向协调者反馈ACK消息 - 中断事务
协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
- 注意
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者abort请求时,会在等待超时之后,会继续进行事务的提交。(其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。 )
优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit
请求后等待 doCommit
指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。