分布式一致性回顾
在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本(replica),这些副本会放置在不同的物理的机器上。为了对用户提供正确的增\删\改\查等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。
为了解决这种分布式一致性问题,前人在性能和数据一致性的反反复复权衡过程中总结了许多典型的协议和算法。其中比较著名的有二阶提交协议(Two-phase Commit Protocol)、三阶提交协议(Three-phase Commit Protocol)和Paxos算法。
什么是两阶段提交(Two-phase commit,2PC)
两阶段提交(Two-phase commit,2PC)是指,在计算机网络以及数据库领域内,为了使基于分布式系统架构下的所有节点在进行事务提交时,保持一致性而设计的一种算法(Algorithm)。
通常,两阶段提交也被称为是一种协议(Protocol),称为二阶提交协议(Two-phase commit Protocol)。二阶提交协议是一种原子提交协议(Atomic commitment protocol, ACP)。
在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者(Coordinator)的组件来统一管理所有节点(每个节点均是一个事务参与者(Participant))的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘)。
因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要进行提交(Commit)操作还是回滚(Rollback)操作。
两阶段提交的过程
两阶段提交包含事务协调者(Transaction Coordinator)和若干事务参与者(Transaction Participant)两种角色。
这里的事务参与者就是具体的数据库,抽象点可以说是可以控制给数据库的程序。 事务协调者可以和事务参与者在一台机器上。
所谓的两个阶段是指准备阶段(Prepare Phase)和提交阶段(Commit Phase):
准备阶段(Prepare Phase)
准备阶段(Prepare Phase),又称为投票阶段(Voting Phase)。
在这一阶段,协调者通过向所有参与者发送 query to commit
消息以询问所有参与者是否准备好提交,此后:
- 如果某个参与者在执行本地的事务的过程中没有出现异常,则向协调者回复一条
agreement
消息(其中标识了Prepared
); - 如果某个参与者在执行本地事务的过程中出现了异常,则回复一条
agreement
消息(其中标识了Abort
)。
提交阶段(Commit Phase)
提交阶段(Commit Phase)也称为完成阶段(Completion Phase)。
如果协调者在上一阶段收到所有参与者回复(的 agreement
消息中)的 Prepared
,则向所有参与者发送 commit
命令,否则协调者会向所有参与者发送 rollback
命令。
协调者向所有参与者发送 commit
命令
- 如参与者收到了
commit
命令,其会释放本地事务过程中持有的锁和其他资源,并将事务在本地提交(持久化一条commit日志)。然后向协调者发送acknowledgement
命令以标志操作成功; - 最终,如果协调者收到了所有参与者回复的
acknowledgement
消息,协调者完成该事务,并向客户端返回事务成功消息。
协调者向所有参与者发送 rollback
命令
- 在准备阶段(Prepare Phase)中,若任何一个参与者回复
No
或超时未应答,协调者会先在本地持久化事务状态,并向所有参与者发送rollback
命令; - 参与者收到
rollback
命令后,会回滚事务(有必要的情况下还要持久化一条 abort 日志),并释放本地资源和锁。最终,向协调者发送acknowledgement
消息以表示 rollback 成功; - 当协调者收到所有参与者回复的
acknowledgement
消息后,协调者取消该事务,并向客户端返回事务失败消息。
一个例子
我们设想从支付宝里转10000元到余额宝的场景……
- 首先我们的应用程序发起一个请求到事务协调者(Transaction Coordinator),然后由事务协调者来保证分布式事务。
- 准备阶段(Prepare Phase) :
- 协调者先将
prepare
消息写到本地日志。 - 向所有的参与者(事务执行器)发起
prepare
消息。以支付宝转账到余额宝为例,协调器给参与者 A 的prepare
消息,是通知支付宝数据库相应账目扣款 10000,协调者给 参与者 B 的prepare
消息是通知余额宝数据库相应账目增加 10000。 - 参与者收到
prepare
消息后,执行本机事务,如果本地事务成功则向协调者返回Prepared
,不成功则返回About
。同理,返回前,参与者将操作写入日志,当作凭证。
- 协调者先将
- 提交阶段(Commit Phase):协调者收集所有参与者返回的消息,
- 如果所有参与者都返回
Prepared
,协调者会给所有参与者发送commit
消息,参与者收到commit
后,将执行本地事务的commit
操作,并返回acknowledgement
消息给协调者; - 如果有任一个参与者返回
About
,协调者会给所有参与者发送rollback
消息,参与者收到rollback
消息后执行本地事务的rollback
操作,并返回确认消息给协调者。 - 协调者将操作写入日志,并最终返回结果给客户端。
- 如果所有参与者都返回
为什么在执行任务前需要先写本地日志,主要是为了故障后恢复用,本地日志起到现实生活中凭证的效果,如果没有本地日志(凭证),出现问题后容易发生死无对证的情况。
两阶段提交的容错方式
两阶段提交(2PC)在执行过程中,可能发生协调者或者参与者突然宕机的情况,同时,在不同时期的宕机可能会出现不同的情况。
情况一:协调者宕机,参与者正常
这种情况其实比较好解决,只要找一个协调者的替代者。当它成为新的协调者的时候,询问所有参与者本地事务的执行情况,它就能够知道当前事务处于哪个状态。
所以,这种情况不会导致数据不一致。
情况二:参与者宕机,协调者正常
这种情况其实也比较好解决。如果协调者宕机了。那么之后的事情有两种情况:
- 第一个是参与者宕机后,在一段时间后也没有被恢复。这种情况下,协调者会通知所有参与者
rollback
, 因此不会导致数据一致性问题。 - 第二个是宕机之后又恢复了,这时如果这个参与者有未执行完的事务操作,则直接取消掉,然后询问协调者目前应该进行什么操作,协调者就会比对自己的事务执行记录和该参与者的事务执行记录,并告诉这个参与者应该执行什么操作,来保持数据的一致性。
情况三:参与者宕机,协调者也宕机
这种情况比较复杂,是两阶段提交无法完美解决的情况。
我们分别进行讨论。
- 协调者和参与者在准备阶段(第一阶段)宕机了。
- 由于这时还没有执行
commit
操作,新选出来的协调者可以询问各个参与者的情况,再决定是进行commit
还是rollback
。因为还没有commit
,所以不会导致数据一致性问题。
- 由于这时还没有执行
- 协调者和参与者在提交阶段(第二阶段)宕机了,宕机了的这个参与者,在宕机之前并没有接收到协调者的指令,或者接收到指令之后,还没来的及做
commit
或者rollback
操作。- 这种情况下,当新的协调者被选出来之后,它同样是询问所有的参与者的情况。只要有参与者执行了
rollback
操作或者在第一阶段(准备阶段)返回的信息是Abort
,就直接执行rollback
操作。 - 如果没有参与者执行
rollback
操作,但是有参与者执行了commit
操作,那么就直接执行commit
操作。 - 这样,当宕机的参与者恢复之后,只要按照协调者的指示,进行事务的
commit
还是rollback
操作就可以了。因为宕机的参与者并没有做commit
或者rollback
操作,而且没有宕机的参与者们和新的协调者又执行了同样的操作,因此,这种情况不会导致数据不一致现象。
- 这种情况下,当新的协调者被选出来之后,它同样是询问所有的参与者的情况。只要有参与者执行了
- 协调者和参与者在提交阶段(第二阶段)宕机了,宕机的这个参与者在宕机之前已经执行了操作。但是由于它宕机了,因而无法知道它执行了什么操作(
commit
orrollback
)。- 这种情况下,新的协调者被选出来之后,它就只能按照之前那种情况来执行
commit
或者rollback
操作。这样新的协调者和所有没宕机的参与者就保持了数据的一致性, - 我们假定他们执行了
commit
。但是,这个时候,那个宕机的参与者恢复了怎么办,因为它之前已经执行完了之前的事务,如果他执行的是commit
那还好,因为和其他的参与者保持一致了。可是,如果它执行的是rollback
操作时,就导致数据的不一致。 - 虽然这个时候,理论上可以再通过手段让它和协调者通信,以实现数据一致性,但是,这段时间内它的数据状态已经是不一致的了!
- 这种情况下,新的协调者被选出来之后,它就只能按照之前那种情况来执行
所以,2PC协议中,如果出现协调者和参与者都宕机了的情况,有可能导致数据不一致。
两阶段提交的优缺点
优点
原理简单,易于实现。
缺点
由于两阶段协议是一个阻塞式协议(blocking protocol)。其缺点也显而易见,在参与者发送 agreement
消息给协调者后,它会被阻塞,直到协调者回复 commit
或者 rollback
。因此,系统的吞吐率(throughput)较低。
具体可归纳为以下几个:
- 单点问题
协调者在整个两阶段提交过程中扮演着举足轻重的作用,一旦协调者宕机,那么就会影响整个分布式系统的正常运行,比如在第二阶段中,如果协调者因为故障不能正常发送事务提交或回滚通知,那么参与者们将一直处于阻塞状态,因而整个系统将无法提供服务。
当然,我们可以通过 watchdog 实时监测协调者的状态。当出现协调者宕机时,立即引入一个新的协调者接管这个事务。
- 同步阻塞
两阶段提交执行过程中,所有的参与者都需要听从协调者的统一调度,期间处于阻塞状态而不能从事其他操作,这样效率较为低下。
- 数据不一致性
两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务 commit
的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 commit
操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用两阶段提交,而往往通过其他途径来解决数据一致性问题。
Reference
- 分布式理论(三) - 2PC协议 - https://juejin.im/post/5b2664446fb9a00e4a53136e
- Oracle Two-Phase Commit Mechanism - https://docs.oracle.com/cd/B28359_01/server.111/b28310/ds_txns003.htm#ADMIN12222
- Two-phase commit protocol - https://shekhargulati.com/2018/09/05/two-phase-commit-protocol/
- 关于分布式事务、两阶段提交协议、三阶提交协议 - http://www.hollischuang.com/archives/681
- 理解分布式事务的两阶段提交2pc - http://xiaorui.cc/2016/02/25/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%B8%A4%E9%98%B6%E6%AE%B5%E6%8F%90%E4%BA%A42pc/
- 如何用消息系统避免分布式事务? - http://blog.jobbole.com/89140/
- Wikipedia Two-phase commit protocol - https://en.wikipedia.org/wiki/Two-phase_commit_protocol
- 分布式事务:两阶段提交与三阶段提交 - https://my.oschina.net/wangzhenchao/blog/736909
- Wikipedia Three-phase commit protocol - https://en.wikipedia.org/wiki/Three-phase_commit_protocol
- 深入理解分布式系统的2PC和3PC - https://www.hollischuang.com/archives/1580
- 2PC和3PC一点理解 - http://jianbeike.blogspot.com/2016/04/2pc3pc.html
- 再谈2PC和3PC - https://yq.aliyun.com/articles/5854