80%内容源于网路,致敬!
最简单的分布式系统架构
同步复制:写操作主从db同时更新成功再返回成功
异步复制:写操作主db更新成功立刻返回成功,从db异步同步主db数据
思考:数据库主从节点出现网络异常时,数据无法同步,两种同步方式各会出现什么问题?
场景:用户更新头像
同步复制:更新头像失败,因为主从db不能同时更新成功,但是主从db数据一致
异步复制:更新头像成功,用户查询头像显示旧头像,从db无法同步数据,主从db数据不一致
带着这两个问题继续阅读
备注:下文中的节点指的是db节点(包括主节点、从节点)
CAP定义
所有节点返回的数据都是一致的。
举例:用户更新了头像,当数据更新成功后,用户查看头像必须是新的头像;
说明:因为查询走的是从db,若要满足此条件就必须要求用户更新头像时主从db同时更新;
Availability(可用性)
非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)所有请求做出响应。
可用性的两个关键:一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的,这里的正确指的是比如应该返回 50,而不是返回 40。<br>
举例:主db挂了但不影响用户查看头像;<br>
说明:说白了就是某个节点坏了,不能影响其他的节点业务,如主db挂了,但不影响从db节点对外提供服务,用户还是可以读取数据的,只是不能写而已;<br>
备注:主db不能写,还算可用性吗?这里的可用性的定义是非故障节点,对每个请求做出响应,有故障的不算;
Partition tolerance(分区容错性)
当系统中有节点因网络原因无法通信时,系统依然可以继续提供服务。
举例:当主db与从db出现网络不通时,不影响用户更新头像、查看头像功能;
说明:网络永远存在不稳定的几率,这是个必须得接受的前提;
可用性和容错性的区别
可用性是针对非故障节点,如主db节点挂了,但从db没有挂,而且从db继续提供服务,说明此分布式系统具有可用性;
分区容错性是各个节点出现网络问题时,系统依然可用,如主db和从db之间没法通信时,系统可用;
总结:可用性针对节点出现故障,系统可用;分区容错性针对网络出现问题,系统可用;
CAP定理
CAP定理是表示分布式系统只能满足三项中的两项,而不可能满足全部三项。即分布式系统只能满足三种情况:CA、AP、CP。
分析一下,先看P,分区容错性,在分布式系统中,网络异常是不可避免的,所以如果不保证分区容错性,除非节点间网络不会发生异常,这个是不可能的(除非单机系统,单机系统就不是分布式系统);
分布式系统肯定要保证P,那其实CA是理论上面的,其实不存在;
结论:分布式系统必须要保证P,剩下的只能在AP、CP上取舍
再回顾一下刚才的两个问题
场景:用户更新头像
同步复制:更新头像失败,因为主从db不能同时更新成功,但是主从db数据一致
分析:网络出现问题时,主db无法访问从节点,导致更新操作一直不成功,其实就是放弃了可用性,只满足CP原则,系统只能提供读服务
疑问:系统不是能够提供读服务吗?应该系统是可用的啊,可用性的定义:非故障节点,要能够提供服务。而这里主db节点是正常的(符合非故障节点),而不能提供写请求,不符合可用性原则
异步复制:更新头像成功,用户查询头像显示旧头像,从db无法同步数据,主从db数据不一致
分析:出现网络延迟,数据没有及时同步到从db,导致了主db用户头像数据是新头像,而从db查询出来的头像数据是旧头像,导致数据不一致,也就是牺牲了数据一致性,但主从db照样可以提供服务,也就是保证了可用性A,满足AP原则,这也是mysql的CAP原则
综合来看,再满足P的前提下,是不可能同时满足C和A的。
回过头我们讨论一下最简单的情况:单数据库,可以很容易分析出来,由于单实例,所以不存在"网络分区"、"不一致", 但单点故障后会导致整个数据库瘫痪,所以可用性不能保证。
这就是CAP定理中的,保证"C"和"P",舍弃"A"。
权衡
分布式数据系统,分区容忍性是最基本的要求,否则就失去了价值,因此只能在一致性和可用性之间取一个平衡。大多数web系统并不需要强一致性,因此牺牲一致性,换取高可用性是现在多数分布式数据库产品的方向,牺牲一致性并不是完全不管数据的一致性,否则数据混乱了可用性再高,分布式再好也就没有了意义。牺牲一致性只是不再要求数据库中的强一致性,而是只要系统能达到最终一致性即可,通常通过数据的异步复制来达到系统的高可用和数据的最终一致性,也就是BASE理论的提出;
BASE理论
先看三个概念:
强一致性:
其实只有两类数据一致性,强一致性与弱一致性。除此以外,所有其他的一致性都是弱一致性的特殊情况。所谓强一致性,即复制是同步的,弱一致性,即复制是异步的;
弱一致性:系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。但经过“不一致时间窗口”这段时间后,后续对该数据的读取都是更新后的值;
最终一致性:是弱一致性的一种特例,是业务上对弱一致性的一种改善用户体验的方式;
弱一致性与最终一致性的区别:
弱一致性:用户无感知;
最终一致性:用户有感知,存在临时状态;
举例:
弱一致性:用户更新头像,提示更新成功,当网络异常时mysql主从异步复制阻塞,用户查看头像是旧头像,用户不知道更新一个头像是有可能要等会才能看到结果,用户可能认为是自己上传失败或者是服务器出了什么问题;
最终一致性:用户更新头像,提示正在更新中,用户查看头像的接口增加通过查询主从数据库对比用户头像信息是否一致,如果不一致,标记更新状态为"更新中";
弱一致性是系统知道数据等一会就一致了,但是用户不知道,也就是"用户感知不到的一致性";
最终一致性就是等会儿就一致了,早晚会一致的意思。使用最终一致性的关键就是想方设法让用户"等会儿"。这个方法叫"用户感知到的一致性",意思就是让用户自己知道数据已经不一致了,你再忍会儿。
这里的"更新中"对应的就是BASE里的软状态,中间的临时状态。
BASE原理
通过记录事务的中间的临时状态,实现最终一致性
基本可用(Basically Available)
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性 (响应时间、功能上的可用性).
通过支持局部故障而不是系统全局故障来实现的,注意,允许损失部分可用性不等价于系统不可用
响应时间上的损失:正常情况下的搜索引擎0.5 秒即可返回结果,而基本可用情况下的搜索引擎1秒才返回结果;
功能上的损失:双十一活动,电商服务器压力剧增,可能会关闭部分非核心功能保证核心业务的正常运行,如关闭查看商品评论接口;
软状态( Soft State)
软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。
最终一致性( Eventual Consistency)
最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
BASE理论本质上是对CAP理论的延伸,是基于CAP中AP方案的一种分布式系统设计实现方案,CAP理论关注粒度是数据,而不是整体系统设计的策略;
分布式事务的解决方案
XA 分布式事务协议 (todo)
一:2PC(二阶段提交)方案
二阶段提交把分布式事务分成准备阶段和提交阶段两个阶段。目的很明确,就是尽可能晚的提交事务,让事务在提交前尽可能地完成所有能完成的工作,这样,最后的提交阶段将是一个耗时极短的微小操作,这种操作在一个分布式系统中失败的概率是非常小的,也就是所谓的“网络通讯危险期”非常的短暂,这是两阶段提交确保分布式事务原子性的关键所在。
处理流程
阶段 1:准备阶段
准备阶段有如下三个步骤:
协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
如参与者执行成功,给协调者反馈 yes,即可以提交;如执行失败,给协调者反馈 no,即不可提交。
阶段 2:提交阶段
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则发送提交(commit)消息。
参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
接下来分两种情况分别讨论提交阶段的过程:
协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
参与者执行 commit 请求,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack(应答)完成的消息。
协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
协调者向所有参与者发出回滚请求(即 rollback 请求)。
参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack 完成的消息。
协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。
总结:2PC方案实现起来简单,但主要因为以下问题:
同步阻塞问题
这是2PC存在的最明显也是最大的一个问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。比如数据库事务使用锁实现隔离性,事务没有提交,就会一直占用锁资源,导致并发量很低,性能很差,所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
单点故障
如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态,直到参与者本身锁超时释放资源。
数据一致性问题
在阶段 2 中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。
二:3PC(三阶段提交)方案
与两阶段提交不同的是,三阶段提交有两个改动点。 1、引入超时机制,同时在协调者和参与者中都引入超时机制。 2、将2PC第一阶段一分为二,在CanCommit阶段中可以尽早给出事务是否可以执行的判断,占用资源很少,提高了吞吐量,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。
为什么要把准备阶段一分为二?
假设有1个协调者,9个参与者。其中有一个参与者不具备执行该事务的能力。
协调者发出prepare消息之后,其余参与者都将资源锁住,执行事务,写入undo和redo日志。
协调者收到相应之后,发现有一个参与者不能参与。所以,又出一个rollback消息。其余8个参与者,又对消息进行回滚。这样子,是不是做了很多无用功?
所以,引入can-Commit阶段,主要是为了在预执行之前,保证所有参与者都具备可执行条件,从而减少资源浪费。
快速失败机制:系统运行中,如果有错误发生,那么系统立即结束,这种设计就是快速失败;。
处理流程 阶段 1:canCommit
协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应(参与者不执行事务操作),否则返回 no 响应:
协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
阶段 2:preCommit
协调者根据阶段 1 canCommit 参与者的反应情况来决定是否可以进行基于事务的 preCommit 操作。根据响应情况,有以下两种可能。
协调者向所有参与者发出 preCommit 请求,进入准备阶段。
参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。
协调者向所有参与者发出 abort 请求。
无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。
阶段 3:do Commit 该阶段进行真正的事务提交,也可以分为以下两种情况。
如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。
参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack 完成的消息。
协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
如果协调者处于工作状态,向所有参与者发出 abort 请求。
参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
各参与者向协调者反馈 ack 完成的消息。
协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。
注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。
方案总结 优点:相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。
缺点:数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
为什么三阶段超时执行提交事务,二阶段数据库事务超时回滚事务?
从两阶段提交到三阶段提交,通过拆分了prepare后,可以在canCommit阶段就能最大概率快速确定该次事务是否能够执行。一旦所有参与者都返回yes后,才通知各个参与者锁定资源;这样就避免了两阶段提交中,一上来各自就开始锁定资源,然后发现某个参与者有问题后又通知各自在rollback操作中去释放资源的补救措施。
当然,这种方法只是一种最大概率的保障,判断的依据是一旦参与者接收到了preCommit操作之后,意味着所有的参与者事务大概率都是执行成功的。所以,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。
两阶段提交协议中所存在的长时间阻塞状态发生的几率还是非常低的,所以虽然三阶段提交协议相对于两阶段提交协议对于数据强一致性更有保障,但是因为效率问题,两阶段提交协议在实际系统中反而更加受宠。
三:TCC 事务:最终一致性
TCC 是服务化的二阶段编程模型,其 Try、Confirm、Cancel 3个方法均由业务编码实现:
Try 操作作为一阶段,负责资源的检查和预留。
Confirm 操作作为二阶段提交操作,执行真正的业务。
Cancel 是预留资源的取消。
TCC 事务的 Try、Confirm、Cancel 可以理解为SQL事务中的 Lock、Commit、Rollback。
处理流程
为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上。
①Try 阶段
从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。
TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:
完成所有业务检查( 一致性 ) 。
预留必须业务资源( 准隔离性 ) 。
Try 尝试执行业务。
TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。 因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。
②Confirm / Cancel 阶段
根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。
Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。
Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作
Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。
Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段
方案总结
TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点:
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点: TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
四: 本地消息表:最终一致性
方案简介
本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。
方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。
这样设计可以避免”业务处理成功 + 事务消息发送失败",或"业务处理失败 + 事务消息发送成功"的棘手情况出现,保证 2 个系统事务的数据一致性。
下面把分布式事务最先开始处理的事务方称为事务主动方,在事务主动方之后处理的业务内的其他事务称为事务被动方。
为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤。
库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。
事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。
整个业务处理流程如下:
步骤1:事务主动方处理本地事务。
事务主动方在本地事务中处理业务更新操作和写消息表操作。上面例子中库存服务阶段在本地事务中完成扣减库存和写消息表(图中 1、2)。
步骤 2:事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息。
消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。
上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。
步骤 3:事务被动方通过消息中间件,通知事务主动方事务已处理的消息。
上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8)。
为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。
具体保存一致性的容错处理如下:
当步骤 1 处理出错,事务回滚,相当于什么都没发生。
当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送到消息中间件进行处理。事务被动方消费事务消息重试处理。
如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。
如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。
方案总结 方案的优点如下:
从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。
方案轻量,容易实现。
缺点如下:
与具体的业务场景绑定,耦合性强,不可公用。
消息数据与业务数据同库,占用业务系统资源。
业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
五:MQ 事务:最终一致性
什么是事务消息? 通过消息的异步事务,可以保证本地事务和消息发送同时执行成功或失败,既能实现系统之间的解耦,又能保证数据的最终一致性,广泛应用于电商交易系统、支付红包等场景。
事务消息,可以认为是两阶段提交消息的实现,保证执行本地事务的执行和消息发送的原子性,确保分布式系统中的最终一致性。 <<摘自aliyun>>
方案简介
基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。
处理流程
下面主要基于 RocketMQ 介绍 MQ 的分布式事务方案。
在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相对于提供了 2PC 的提交接口,方案如下:
正常情况:事务主动方发消息
图中 1:发送方向 MQ 服务端(MQ Server)发送 half 消息。
图中 2:MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
图中 3:发送方开始执行本地事务逻辑。
图中 4:发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
图中 5:MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。
异常情况:事务主动方消息恢复
图中 5:MQ Server 对该消息发起消息回查。
图中 6:发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
图中 7:发送方根据检查得到的本地事务的最终状态再次提交二次确认。
图中 8:MQ Server基于 commit/rollback 对消息进行投递或者删除。
介绍完 RocketMQ 的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍 RocketMQ 分布式事务:
如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。
如果是事务被动方业务上的处理失败,可以通过 MQ 通知事务主动方进行补偿或者事务回滚。
方案总结
相比本地消息表方案,MQ 事务方案优点是:
消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
吞吐量优于本地消息表方案。 (todo)
缺点是:
一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
业务处理服务需要实现消息状态回查接口。
各方案使用场景
2PC/3PC:依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。
TCC:适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。
本地消息表/MQ 事务:都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。
共识算法 (todo)
我们常说的一致性问题就是:对于同一个数据的多个副本之间,如何保持其对外表现的数据一致性。
一致性往往指分布式系统中多个副本对外呈现的数据的状态。如前面提到的顺序一致性、线性一致性,描述了多个节点对数据状态的维护能力。
共识则描述了分布式系统中多个节点之间,彼此对某个提案达成一致结果的过程。因此,一致性描述的是结果,共识则是一种手段。
有的人会说一致性和共识实际上是一个问题的一体两面,某种程度上来说,共识方法确实可以看作是实现强一致性的一种方法。事实上在工业界有许多以共识算法作为核心组件的多副本状态机(Replicated State Machine)实现,本质上利用了共识算法保证了所有副本的操作日志具有完全相同的顺序,从而实现了副本的一致性。但是,即使是在这样的场景下,讨论一个共识算法的一致性也是不合适的,因为整个分布式系统最终的一致性并不单单取决于共识算法,共识算法只是解决了其中一个问题。