分布式一致性
数据的一致性模型
- 强一致性:要求无论更新操作实在哪一个副本执行,之后所有的读操作都要能获得最新的数据。
- 弱一致性:用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。
- 最终一致性:是弱一致性的一种特例,保证用户最终能够读取到某操作对系统特定数据的更新。
数据的强一致性要求如果数据不一致,就不对外提供数据服务,保证用户读取的数据始终是一致的。数据强一致性只需要通过锁机制即可解决。
常用的锁实现算法有 Lamport bakery algorithm (俗称面包店算法), 还有 Paxos 算法以及
乐观锁。
CAP
CAP 定理是 2000 年,由 Eric Brewer 提出来的。Brewer 认为在分布式的环境下设计和部署系统时,有 3 个核心的需求,以一种特殊的关系存在。这里的分布式系统说的是在物理上分布的系统。
CAP 理论的核心是:一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求,最多只能同时较好的满足两个。
Consistency
(一致性):所有的节点在同一时间读到相同的数据。这就是数据上的一致性,当数据写入成功后,所有的节点会同时看到这个新的数据Availability
(可用性):保证无论是成功还是失败,每个请求都能够收到一个反馈。这
就是数据的可用性。重点是系统一定要有响应。Partition-Tolerance
(分区容错性):即使系统中有部分问题或者有消息的丢失,系统仍能够继续运行,这被称为分区容错性,也就是在系统的一部分出问题时,系统仍能继续工作。
因为在一个分布式系统中不可能同时满足一致性、可用性、分区容错性,最多满足两个,发展而来的有三大类系统:
CA
- 单点集群,满足一致性,可用性的系统,通常在可扩展性上不太强大。
一种较为简单的做法是将所有的数据(或者是仅仅与事务相关的数据)都放在一个分布式节点上。这样的做法虽然无法100%地保证系统不会出错,但至少不会碰到由于网络分区带来的负面影响,放弃 P 的同时也意味着放弃了系统的可扩展性。CP
- 满足一致性,分区容错性的系统。
一旦系统遇到网络分区或者其他故障时,那么受到影响的服务需要等待一定的时间,因此在等待期间系统无法对外提供正常服务,即不可用。AP
- 满足可用性,分区容错性的系统,通常可能对一致性要求低一些。
放弃一致性指的是放弃强一致性,而保留数据的最终一致性,这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态。这就引入了一个时间窗口的概念,具体多久能够达到数据一致性取决于系统的设计,主要包括数据副本在不同节点之间的复制时间长短。
对于分布式互联网应用而言,必须保证P,所以要么满足AP模型、要么满足CP模型。
很多 NoSQL 都是 AP 模型
对于CP模型,网络的问题可能会让整个系统不可用
BASE
BASE 就是为了解决关系数据库强一致性引起的问题而造成可用性降低(分布式事务的低性能)而提出的解决方案。
BASE 其实是下面三个术语的缩写:
+ 基本可用(Basically Available)
+ 软状态(Soft state)
+ 最终一致(Eventually consistent)
它的思想是通过让系统放松对某一时刻数据一致性的要求来换取系统整体伸缩性和性能上改观。为什么这么说呢,缘由就在于大型系统往往由于地域分布和极高性能的要求,不可能采用分布式事务来完成这些指标,要想获得这些指标,我们必须采用另外一种方式来完成,这里 BASE 就是解决这个问题的办法。
基本可用
分布式系统在出现不可预知故障的时候,允许损失部分可用性,比如
1. 响应时间上的损失:出现故障时响应稍慢
2. 功能上的损失:部分用户可能会被引导到一个降级页面
软状态
允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
最终一致
系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
2PC
要达成分布式一致性,发展出了许多一致性协议,其中2PC是在分布式环境下保证事务原子性和一致性设计的算法,也被认为是一种一致性协议,用来保证分布式系统数据的一致性。目前,绝大多数数据库都是采用 2PC 协议来完成分布式事务处理的。
它将事务的提交分成两个阶段:
阶段一:提交事务请求 投票阶段
由一方进行提议(propose)并收集其他节点的反馈(vote),我们将提议的节点称为协调者,其他参与决议节点称为参与者。
- 事务询问
协调者向所有参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。 - 执行事务
各参与者节点执行事务操作,并将 Undo 和 Redo 信息记入事务日志中。 - 各参与者向协调者反馈事务询问的响应
如果参与者成功执行了事务操作,那么就反馈给协调者 yes 响应,表示事务可以执行;如果参与者没有成功执行事务,那么就反馈给协调者 no 响应,表示事务不可以执行。
阶段二:执行事务提交 执行阶段
协调者根据反馈决定提交(commit)或中止(abort)事务。
正常情况下,包括以下两种可能:
执行事务提交
假如协调者从所有的参与者获得反馈都是 yes,那么就会执行事务提交。
- 发送提交请求
协调者向所有参与者节点发出 commit 请求 - 事务提交
参与者接收到 commit 请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。 - 反馈事务提交结果
参与者在完成事务提交之后,向协调者发送 ACK 消息。 - 完成事务
协调者接收到所有参与者反馈的 ACK 消息,完成事务
中断事务
假如任何一个参与者向协调者反馈了 no 响应,或者在等待超时之后,协调者无法接收到所有参与者的反馈响应,那么就会中断事务。
- 发送回滚请求
协调者向所有参与者节点发出 rollback 请求 - 事务回滚
参与者接收到 rollback 请求后,会利用其在阶段一中记录的 Undo 信息来中事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。 - 反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送 ACK 消息 - 中断事务
协调者接收到所有参与者反馈的 ACK 消息,完成事务中断。
优缺点
2PC的优点是原理简单,实现方便,缺点有
- 同步阻塞:两阶段提交中的第二阶段, 协调者需要等待所有参与者发出 yes 请求, 或者一个参与者发出 no 请求后, 才能执行提交或者中断操作. 这会造成长时间同时锁住多个资源, 造成性能瓶颈,如果参与者有一个耗时长的操作, 性能损耗会更明显。
- 单点问题:协调者的角色在整个 2PC 中起到了非常重要的作用。一旦协调者出现问题,那么整个 2PC流程将无法运转。如果协调者是在阶段二出现问题的话,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作。
- 数据不一致/脑裂:在阶段二中,即在执行事务提交的时候,当协调者向所有的参与者发送 commit 请求之后,发生了局部网络异常或者是协调者在尚未完全发送完commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了 commit 请求。于是,这部分收到了 commit 请求的参与者就会进行事务的提交,而其他没有收到 commit 请求的参与者则无法进行事务提交,于是出现数据不一致的情况。
- 太过保守:如果在协调者指示参与者进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事务,这样的策略显得比较保守。2PC 没有涉及较为完善的容错机制,任意一个节点的失败都会导致整个事务的失败。
根据2PC协议,衍生出一种分布式事务规范XA,目前许多数据库都支持。
3PC
是2PC的改进版,其将2PC的提交事务请求过程一分为二,形成了由CanCommit、PreCommit和 doCommit 三个阶段组成的事务处理协议。
在 2PC 中一个参与者的状态只有它自己和协调者知晓,假如协调者提议后自身宕机,一个参与者又宕机,其他参与者就会进入既不能回滚、又不能强制 commit 的阻塞状态,直到参与者宕机恢复。这引出两个疑问:
- 能不能去掉阻塞,使系统可以在 commit/abort 前回滚(rollback)到决议发起前的初始状态?
- 当次决议中,参与者间能不能相互知道对方的状态,又或者参与者间根本不依赖对方的状态?
阶段一:CanCommit
- 事务询问
协调者向所有的参与者发送一个包含事务内容的 canCommit 请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应 - 各参与者向协调者反馈事务询问的响应
参与者接收到来自协调者的 canCommit 请求后,正常情况下,如果其自身认为可以顺利执行事务(通常是尝试获取数据库锁),那么会反馈 yes 响应,并进入预备状态,否则反馈 no 响应。
阶段二:PreCommit
在阶段二中,协调者会根据各参与者的反馈情况来决定是否可以进行事务的 PreCommit 操
作,正确情况下,包含两种可能。
执行事务预提交
假如协调者从所有的参与者获得的反馈都是 yes,那么就会执行事务预提交。
- 发送预提交请求
协调者向所有参与者节点发出 preCommit 请求,并进入 Prepared 阶段 - 事务预提交
参与者接收到 preCommit 请求后,会执行事务操作,并将 Undo 和 Redo 信息记录到事务日志中 - 各参与者向协调者反馈事务执行的响应
如果参与者成功执行了事务操作,那么就会反馈给协调者 ACK 响应,同时等待最终的指令:提交(commit)或中止(abort)。如果失败,那么会反馈给协调者 no 响应。
中断事务
假如任何一个参与者向协调者反馈了 no 响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
- 发送中断请求
协调者向所有参与者节点发出 abort 请求 - 中断事务
无论是收到来自协调者的 abort 请求,或者是在等待协调者请求过程中出现超时,参与者都会中断事务
阶段三:doCommit
该阶段将进行真正的事务提交,会存在以下两种可能的情况
执行提交
- 发送提交请求
假设协调者处于正常工作状态,并且它接收到了来自所有参与者的 ACK 响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送 doCommit 请求。 - 事务提交
参与者接收到 doCommit 请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行阶段占用的事务资源 - 反馈事务提交结果
参与者在完成事务提交之后,向协调者发送 ACK 消息 - 完成事务
协调者接收到所有参与者反馈的 ACK 消息后,完成事务。
中断事务
进入这一阶段,假设协调者处于正常工作状态,并且有任意一个参与者向协调者反馈 no 响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
- 发送中断请求
协调者向所有的参与者发送 abort 请求 - 事务回滚
参与者接收到 abort 请求后,会利用其在阶段二记录的 Undo 信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。 - 反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送 ACK 消息 - 中断事务
协调者接收到所有参与者反馈的 ACK 消息后,中断事务。
一旦进入阶段三,可能会存在以下两种故障:
1. 协调者出现问题
2. 协调者和参与者之间的网络出现故障
无论出现哪种情况,都会导致参与者无法及时接收到来自协调者的doCommit或abort请求,针对这种异常情况,参与者会在等待超时之后,继续进行事务提交。
优缺点
优点: 相较于 2PC,引入超时机制,最大的优点是降低了参与者的阻塞范围,并且能够在出现单点故障后继续达成一致。
缺点: 在参与者接收到 preCommit 消息后,如果网络出现分区,此时协调者所在的节点和参与者无法进行正常的网络通信,此时该参与者依然会进行事务的提交,这必然会出现数据的不一致性。
由于2PC、3PC都没有解决网络分区引起的数据不一致问题,后来又出现了许多共识协议,等到下篇博客再谈。
原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/%e5%88%86%e5%b8%83%e5%bc%8f%e4%b8%80%e8%87%b4%e6%80%a7%e7%90%86%e8%ae%ba%e5%92%8c%e4%b8%80%e8%87%b4%e6%80%a7%e5%8d%8f%e8%ae%ae2pc%e3%80%813pc/