分布式系统的CAP理论是由Eric Brewer于1999年首先提出的,又被称作布鲁尔定理(Brewer’s theorem),CAP是对Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容忍性)的一种简称,如下图所示:
CAP定理告诉我们,一个分布式系统不可能同时满足一致性(Consistency),可用性(Availability)和分区容错性(Partition tolerance)这三个基本需求,最多只能同时满足其中两个。
一致性
在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应保证系统的数据仍然处于一致性状态。
分布式系统中典型的数据不一致情况如,对第一个节点数据进行更新却没有更新第二个节点的数据,此时读取第二个节点数据为老数据(即为脏读)。若能做到针对一个数据项的更新操作执行成功后,所有的用户都可以读到最新的值,这样的系统被认为具有强一致性。
可用性
可用性指的是系统提供的服务必须一直处于可用的状态,对用户的每一个操作总能在有限的时间内返回结果。
分区容错性
分区容错性即分布式系统在遇到任何网络分区故障的情况下仍然需要保证对外提供满足一致性和可用性的服务。除非是整个网络环境都发生了故障。
网络分区是指在分布系统中,不同的节点分布在不同的子网络中,由于特殊的原因导致这些子网络之间出现网络不连通的状况,但各个子网络的内部网络是正常的。
取舍策略
CAP三个特性只能满足其中两个,那么取舍的策略就共有三种:
CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。
CP without A:如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis、HBase等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。
AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是现在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。
BASE理论是基于CAP理论逐步演化而来的,其核心思想是即使不能达到强一致性,也可以根据应用特点采用适当的方法来达到最终一致性的效果。BASE是Basically Available(基本可用)、Soft state(软状态/柔性状态)、Eventually consistent(最终一致性)三个词组的简写,是对CAP中C和A的延伸。
前提
二阶段提交算法的成立基于以下假设:
1/ 该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
2/ 所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏也不会导致日志数据的消失。
3/ 所有节点不会永久性损坏,即使损坏后仍然可以恢复。
基本算法
二阶段提交协议主要分为两个阶段:准备阶段和提交阶段。
第一阶段(提交请求阶段)
1.协调者节点向所有参与者节点询问是否可以执行提交操作,并开始等待各参与者节点的响应。
2.参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。
3.各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个"同意"消息;如果参与者节点的事务操作实际执行失败,则它返回一个"中止"消息。
有时候,第一阶段也被称作投票阶段,即各参与者投票是否要继续接下来的提交操作。
第二阶段(提交执行阶段)
成功
当协调者节点从所有参与者节点获得的相应消息都为"同意"时:
1.协调者节点向所有参与者节点发出"正式提交"的请求。
2.参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
3.参与者节点向协调者节点发送"完成"的消息。
4.协调者节点收到所有参与者节点反馈的"完成"消息后,完成事务。
失败
如果任一参与者节点在第一阶段返回的相应消息为"终止",或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:
1.协调者节点向所有参与者节点发出"回滚操作"的请求。
2.参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
3.参与者节点向协调者节点发送"回滚完成"的消息。
4.协调者节点收到所有参与者节点反馈的"回滚完成"消息后,取消事务。
有时候,第二阶段也被称作完成阶段,因为无论结果怎样,协调者都必须在此阶段结束当前事务。
缺点
1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
4、二阶段无法解决的问题:协调者在发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
优缺点
优点:降低参与者阻塞范围,并能够在出现单点故障后继续达成一致
缺点:引入preCommit阶段,在这个阶段如果出现网络分区,协调者无法与参与者正常通信,参与者依然会进行事务提交,造成数据不一致。
2PC 要求参与者实现了 XA 协议,通常用来解决多个数据库之间的事务问题,比较局限。在多个系统服务器利用 api 接口相互调用的时候,就不遵守 XA 协议了,这时候 2PC 就不适用了。现代企业多采用分布式的微服务,因此更多的是要解决多个微服务之间的分布式事务问题。
TCC 就是一种解决多个微服务之间的分布式事务问题的方案。TCC 是 Try、Confirm、Cancel 三个词的缩写,其本质是一个应用层面上的 2PC,同样分为两个阶段:
1)阶段一:准备阶段。协调者调用所有的每个微服务提供的 try 接口,将整个全局事务涉及到的资源锁定住,若锁定成功 try 接口向协调者返回 yes。
2)阶段二:提交阶段。若所有的服务的 try 接口在阶段一都返回 yes,则进入提交阶段,协调者调用所有服务的 confirm 接口,各个服务进行事务提交。如果有任何一个服务的 try 接口在阶段一返回 no 或者超时,则协调者调用所有服务的 cancel 接口。
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性。
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
以阿里的 RocketMQ 中间件为例,其思路大致为:
第一阶段Prepared消息,会拿到消息的地址。 第二阶段执行本地事务,第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持。