一、分布式架构介绍
什么是分布式系统
分布式系统指一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。
通俗的理解,分布式系统就是一个业务拆分成多个子业务,分布在不同的服务器节点,共同构成的系统称为分布式系统,同一个分布式系统中的服务器节点在空间部署上是可以随意分布的,这些服务器可能放在不同的机柜中,也可能在不同的机房中,甚至分布在不同的城市。
分布式与集群的区别
集群指多个服务器做同一个事件
分布式指多个服务器做不同事情
分布式系统特性
- 分布性
空间中随机分布。这些计算机可以分布在不同机房,不同城市,甚至不同的国家
- 对等性
分布式系统中的计算机没有主从之分,组成分布式系统的所有节点都是对等的;
- 并发性
同一个分布式系统的多个节点,可能会并发地操作一些共享资源,诸如数据库或分布式存储;
- 缺乏全局时钟性
各个计算机之间是依赖于交互信息来进行相互通信,很难定义两件事情的先后顺序,缺乏全局始终控制序列;
- 故障总会发生
组成分布式的计算机,都有可能在某一时刻突然间崩掉。分的计算机越多,可能崩掉一个的机率就越大。如果再考虑到设计程序的异常故障,也会加大故障的概率。
- 处理单点故障
单点SPoF(Single Point Of Failure):某个角色或功能只有某一台计算机在支撑,在这台计算机上出现的故障是单点故障;
分布式系统面临问题
- 通信异常
网络本身的不可靠性,因此每次网络通信都会伴随着网络不可用的风险(光纤、路由、DNS等硬件设备或系统的不可用),都会导致最终分布式系统无法顺利进行一次网络通信,另外,即使分布式系统各节点之间的网络通信能够正常运行,其延时也会大于单机操作,存在巨大的延时差别,也会影响消息的收发过程,因此消息丢失和 消息延迟变的非常普遍;
- 网络分布
网络之间出现了网络不连通,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成若干个孤立的区域,分布式系统就会出现局部小集群,在极端情况下,这些小集群会独立完成原本要整个分布式系统才能完成的功能,包括数据的事务处理,这就对分布式一致性提出非常大的挑战;
- 节点故障
节点故障是分布式系统下另一个比较常见的问题,指的是组成分布式系统的服务器节点出现的宕机或“僵死”现象,根据经验来说,每个节点都有可能出现故障,并且经常发生;
- 三态
分布式系统每一次请求与响应存在特有“三态”概念,即成功、失败和超时;
- 重发
分布式系统在发生调用的时候可能会出现失败、超时的情况,这个时候需要重新发起调用
- 幂等
一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
二、分布式理论
分布式数据一致性,指的是数据在多份副本中存储时,各副本中的数据是一致的;
分布式系统当中,数据往往会有多个副本。多个副本就需要保证数据的一致性。这就带来了同步的问题,因为网络延迟问题等因素,我们几乎没有办法保证可以同时更新所有机器当中的包括备份所有数据,就会有数据不一致的情况。
一致性分为:
- 强一致性
在分布式系统中,如果每次读取都能保证返回最近一次写入的值,则称系统具有强一致性。即所有节点上的数据都是实时同步的
- 弱一致性
系统在写入成功后,会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。
- 最终一致性
最终一致性也是弱一致性的一种,它无法保证数据更新后,所有后续的访问都能看到最新数值,而是需要一个时间,在这个时间之后可以保证这一点(就是一段时间后,节点间的数据会最终达到一致状态),而在这个时间内,数据也许是不一致的,这个系统无法保证强一致性的时间片段被称为“不一致窗口”。不一致窗口的时间长短取决于很多因素,比如备份数据的个数、网络传输延迟速度、系统负载等。
最终一致性在实际应用中又有多种变种:
- 因果一致性
如果进程A通知进程B它已更新了一个数据项,那么进程B的后续访问将返回更新后的值。与进程A无因果关系的进程C的访问遵守一般的最终一致性规则
- 读己之所写一致性
当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例
- 会话一致性
它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证“读己之所写”一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统保证不会延续到新的会话。
- 单调读一致性
如果一个进程已经读取到特定值,那么该进程不会读取到该值以前的任何值
- 单调写一致性
系统保证对同一个进程的写操作串行化
CAP定理
CAP定理(CAP theorem),又被称作布鲁尔定理(Brewer's theorem),它指出对于一个分布式计算系统来说,不可能同时满足以下三点:
一致性:所有节点访问时都是同一份最新的数据副本
可用性:每次请求都能获取到不错的响应,但是不保证获取的数据为最新数据
分区容错性:分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障
因此,我们只能二选一:
CA(Consistency+Availability):关注一致性和可用性,它需要非常严格的全体一致的协议;
CP(Consistency+partition tolerance):关注一致性和分区容忍性。它关注的是系统里大多数人的一致性协议。这样的系统只需要保证大多数结点数据一致,而少数的结点会在没有同步到最新版本的数据时变成不可用的状态。这样能够提供一部分的可用性。
AP(availability + partition tolerance):这样的系统关心可用性和分区容忍性。因此,这样的系统不能达成一致性,需要给出数据冲突,给出数据冲突就需要维护数据版本。
放弃一致性,满足分区容错,那么节点之间就有可能失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会容易导致全局数据不一致性。对于互联网应用来说,机器数量庞大,节点分散,网络故障正常不过了,那么此时就是保障AP,放弃C的场景,而从实际中理解,像网站这种偶尔没有一致性是能接受的,但不能访问问题就大了。
对于银行来说,就是必须保证强一致性,也就是说C必须存在,那么就只有CA和CP两种情况,当保障一致性和可用性(CA),那么一旦出现通信故障,系统将完全不可用。另一方面,如果保障了强一致性和分区容错(CP),那么就具备了部分可用性。实际应该选择什么,是需要通过业务场景进行权衡的(并不是所有情况都是CP好于CA,只能查看信息但不能更新信息有时候还不如直接拒绝服务)
BASE理论
BASE:全称 Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写,BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大型互联网分布式实践的总结,是基于CAP定理逐步演化而来的。其核心思想是:既是无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
1、Basically Available(基本可用)
假设系统,出现 了不可预知的故障,但还是能用,相比较正常的系统而言:
- 响应时间上的损失:正常情况下的搜索引擎0.5秒即返回给用户结果,而基本可用的搜索引擎可以在1秒返回结果;
- 功能上的损失:在一个电商网站上,正常情况下,用户可以顺利完成每一笔订单,但是到了大促期间,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面;
2、Soft state(软状态)
相对于原子性而言,要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态指的是:允许系统中的数据存在中间状态,并认为该状态不会影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
3、Eventually consistent(最终一致性)
上面说的软状态,不可能一直是软状态,必须有个时间期限。在期限过后,应当保证所有副本保持数据一致性。从而达到数据的最终一致性。这个时间期限取决于网络延时,系统负载,数据复制方案设计等等因素。
Base 理论是在 CAP 上发展的,CAP 理论描述了分布式系统中数据一致性、可用性、分区容错性之间的制约关系,当你选择了其中的两个时,就不得不对剩下的一个做一定程度的牺牲。
Base 理论则是对 CAP 理论的实际应用,也就是在分区和副本存在的前提下,通过一定的系统设计方案,放弃强一致性,实现基本可用,这是大部分分布式系统的选择,比如 NoSQL 系统、微服务架构。
三、分布式事务
两阶段提交协议(2PC)
两阶段提交协议,简称2PC(2 Prepare Commit)是比较常用的解决分布式事务问题的方式,要么所有参与进程都提交事务,要么都取消事务,即实现ACID中的原子性的常用手段。
分布式事务:事务提供一种操作本地数据库的不可分割的一系列操作“要么什么都不做,要么做全套”的机制,而分布式事务就是为了操作不同数据库的不可分割的一系列操作“要么什么都不做,要么做全套”的机制;
1、成功执行事务提交流程
阶段一:
- 事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应;
- 执行事务
- 各参与者向协调者反馈事务询问的响应
阶段二:
- 发送提交请求
协调者向所有参与者发出commit请求;
- 事务提交
参与者收到commit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源;
- 反馈事务提交结果
参与者在完成事务提交之后,向协调者发送ACK信息;
- 完成事务
协调者接收到所有参与者反馈的ACK信息后,完成事务;
2、中断事务流程
假如任何一个参与者向协调者反馈了NO响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务
阶段一:
- 事务询问
协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应;
- 执行事务(写本地的Undo/Redo日志)
- 各参与者向协调者反馈事务询问的响应
阶段二:
- 发送回滚请求
协调者向所有参与者发出Rollback请求
- 事务回滚
参与者接收到Rollback请求后,会利用其在阶段一中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源;
- 反馈事务回滚结果
参与者在完成事务回滚之后,向协调者发送ACK信息;
- 中断事务
协调者接收到所有参与者反馈的ACK信息后,完成事务中断;
3、缺点
2PC原理简单,但存在以下缺点:
- 同步阻塞
在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,即当参与者占有公共资源时,其他节点访问公共资源会处于阻塞状态;
- 单点问题
若协调器出现问题,那么整个二阶段提交流程将无法运转,若协调者是在阶段二中出现问题时,那么其他参与者将会一直处于锁定事务资源的状态中,而无法继续完成事务操作;
- 数据不一致
在阶段二中,执行事务提交的时候,当协调者向所有的参与者发送Commit请求之后,发生了局网络异常或者是协调者在尚未发送完Commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了Commit请求,于是会话出现数据不一致的现象;
- 太过保守
在进行事务提交询问的过程中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,此时协调者只能依靠自身的超时机制来判断是否需要中断事务,这样的策略过于保守,即没有完善的容错机制,任意一个结点的失败都会导致整个事务的失败。
MySQL Cluster 内部数据的同步就是用的 2PC 协议。
三阶段提交协议(3PC)
3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求” 过程一分为二,共形成了由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。
三阶段提交升级点(基于二阶段):
- 三阶段提交协议引入 了超时机制;
- 在第一阶段和第二阶段中,引入了一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的;
1、第一阶段(CanCommit 阶段):
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应;
- 事务询问
协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应;
- 响应反馈
参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则,反馈No
2、第二阶段(PreCommit 阶段):
协调者根据参与者的反应情况来决定是否可以执行事务的PreCommit操作。根据响应情况,有以下两种可能
- Yes
-
- 发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段;
- 事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将undo或redo信息记录到事务日志中;
- 响应反馈:如果参与者成功地执行了事务操作,则返回ACK响应,同时开始等待最终指令;
- No
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。则有:
- 发送中断请求:协调者向所有参与者发送abort请求
- 中断事务:参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断
3、第三阶段(doCommit 阶段):
该阶段进入真正的事物提交,也可以分为执行提交和中断事务两种情况。
- 执行成功
-
- 发送提交请求:协调者接收到参与者发送的ACK响应,那么它将从预提交状态进入提交状态。并向所有参与者发送doCommit请求;
- 事务提交:参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源;
- 响应反馈:事务提交完之后,向协调者发送ACK响应;
- 完成事务:协调者接收到所有参与者的ACK响应之后,完成事务;
- 中断事务
-
- 发送中断请求:协调者向所有参与者发送abort请求;
- 事务回滚:参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源;
- 反馈结果:参与者完成事物回滚后,向协调者发送ACK信息;
- 中断事务:协调者接收到所有参与者反馈的ACK消息之后,执行事务的中断;
注意:一旦进入阶段三,可能会出现2 种故障:协调者出现问题、协调者和参与者之间的网络故障
如果出现了任一一种情况,最终都会导致参与者无法收到doCommit请求或者abort请求,针对这种情况,参与者都会在等待超时之后,继续进行事务提交,也就出现数据的不一致
4、2PC对比3PC
- 首先对于协调者和参与者都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
- 通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的;
- PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的;
3PC协议并没有完全解决数据一致性问题
TCC事务
TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作即回滚操作。TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel操作若执行失败,TM会进行重试。
分支事务失败的情况:
TCC分为三个阶段:
1.Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能真正构成一个完整的业务逻辑。
2.Confirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。
3.Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
4.TM事务管理器
TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公
用组件,是为了考虑系统结构和软件复用。
TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文,追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。
Seata
整体机制
两阶段提交协议的演变:
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
-
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
一阶段加载
在一阶段,Seata会拦截“业务SQL”
解析SQL语义,找到“业务SQL" 要更新的业务数据,在业务数据被更新前,将其保存成"before image”
执行“业务SQL" 更新业务数据,在业务数据更新之后,
其保存成"after image”,最后生成行锁。
以上操作全部在一个数据库事务内完成, 这样保证了一阶段操作的原子性。
- 二阶段提交
二阶段如果顺利提交的话,因为"业务SQL"在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的 “业务SQL",还原业务数据。
回滚方式便是用"before image"还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和"after image"。
如果两份数据完全一致就说明没有脏写, 可以还原业务数据,如果不一致就说明有脏写, 出现脏写就需要转人工处理。
四、分布式算法
NWR协议
NWR是一种在分布式存储系统中用于控制一致性级别的一种策略。在亚马逊的云存储系统中,就应用NWR来控制一致性。
- N:在分布式存储系统中,有多少备份数据
- W:代表一次成功的更新操作要求至少有W份数据写入成功
- R:代表一次成功的读数据操作要求至少有R份数据成功读取
NWR值的不同组合会产生不同的一致性效果,当W+R>N的时候,整个系统对于客户端来讲能保证强一致性。
以常见的N=3、W=2、R=2为例:
- N=3,表示,任何一个对象都必须有三个副本
- W=2表示,对数据的修改操作只需要在3个副本中的2个上面完成就返回
- R=2表示,从三个对象中要读取到2个数据对象,才能返回
在分布式系统中,数据的单点是不允许存在的。所以,N必须大于2,N越高,系统的维护和整体成本就越高。工业界通常把N设置为3。
当W是2、R是2的时候,W+R>N,这种情况对于客户端就是强一致性的。
当R+W<=N,无法保证数据的强一致性
Gossip协议
Gossip 协议也叫 Epidemic 协议 (流行病协议)。原本用于分布式数据库中节点同步数据使用,后被广泛用于数据库复制、信息扩散、集群成员身份确认、故障探测等。
Gossip协议利用一种随机的方式将信息传播到整个网络中,并在一定时间内使得系统内的所有节点数据一致。Gossip其实是一种去中心化思路的分布式协议,解决状态在集群中的传播和状态一致性的保证两个问题。
1、原理
Gossip 协议的消息传播方式有两种:反熵传播和谣言传播
1)反熵传播
以固定的概率传播所有的数据。所有参与节点只有两个状态:Suspective(病原)、Infective(感染)。过程是种子节点会把所有的数据都跟其他节点共享,以便消息节点之间数据的任何不一致,它可以保证最终、完全的一致。缺点是消息数量非常庞大,且无限制;通常只用于新加入节点的数据初始化。
2)谣言传播
是以固定的概率仅传播新到达的数据。所有参与节点有三种状态:Suspective(病原)、Infective(感染)、Remove(愈除)。过程是消息只包含最新update,谣言消息在某个时间点之后会被标记为removed,并且不再被传播。缺点是系统有一定的概率会不一致,通常用于节点间数据增量同步。
2、通信方式
Gossip 协议最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:推送模式、拉取模式、推/拉模式
- Push
节点A将数据及对应的版本号推送给B节点,B节点更新A中比自己新的数据
- Pull
A仅将数据key,version推送给B,B将本地比A新的数据(key、value、version)推送给A,A更新本地
- Push/Pull
与Pull类似,只是多了一步,A再将本地比B新的数据推送给B,B则更新本地
3、优缺点
Gossip是一种去中心化的分布式协议,数据通过节点像病毒一样逐个传播。因为是指数级传播,整体传播速度非常快。
1)优点
- 扩展性:允许节点的任意增加和减少,新增节点的状态最终会与其他节点一致;
- 容错:任意节点的宕机和重启都不会影响Gossip消息的传播,具有天然的分布式系统容错特性;
- 去中心化:无需中心节点,所有节点都是对等的,任意节点无需知道整个网络状况,只要网络连通,任意节点可把消息散播到全网;
- 最终一致性:Gossip协议实现信息指数级的快速传播,因此在新信息需要传播时,消息可以快速地发送到全局节点,在有限的时间人能够做到所有节点都拥有最新的数据;
2)缺点
- 消息延迟:节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网;不可避免的造成消息延迟;
- 消息冗余:节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤;不可避免的引起同一节点消息多次接收,增加消息处理压力;
Gossip协议由于以上优缺点,所以适合于AP场景的数据 一致性处理,常见应用有:P2P网络通信、Redis Cluster、Consul
Paxos算法
Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
Paxos的最大特点就是难,不仅难以理解,更难以实现。Google Chubby的作者Mike Burrows说过这个世界上只有一种一致性算法,那就是Paxos,其它的算法都是残次品。
1、解决什么问题
在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序、还有网络分区)等情况。Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。
这里某个数据的值并不只是狭义上的某个数,它可以是一条日志,也可以是一条命令(command)。。。根据应用场景不同,某个数据的值有不同的含义。
2PC和3PC的时候在一定程度上是可以解决数据一致性问题的。但是并没有完全解决就是协调者宕机的情况。
如何解决2PC和3PC的存在的问题呢?
其实在引入多个协调者之后又引入主协调者。那么这个就是最简单的一种Paxos算法。
Paxos的版本有: Basic Paxos , Multi Paxos, Fast-Paxos, 具体落地有Raft 和zk的ZAB协议
2、Basic Paxos相关概念
1)角色介绍
- Client::客户端
客户端向分布式系统发起请求,并等待响应。例如,对分布式文件服务器中文件的写请求。
- Proposer:提案发起者
提案者提倡客户端请求,试图说服Acceptor达成一致,并在发生冲突时充当协调者推动协议向前发展
- Acceptor:决策者,可以批准提案
Acceptor可以接受提案,并进行投票,投票结果是否通过以多数派为准,以如果某个提案被选定,那么该提案里的value就被选定了
- Learner:最终决策的学习者
学习者充当该协议的复制因素(不参与投票)
2)决策模型
3)basic paxos流程
basic paxos流程一共分为4个步骤:
- Prepare
Proposer提出一个提案,编号为N,此N大于这个Proposer之前所有提出的编号,请求Acceptor的多数人接受这个提案
- Promise
如果编号N大于此Acceptor之前接收的提案编号则接收,否则拒绝
- Accept
如果达到多数派,Proposer会发出accept请求,此请求包含提案编号和对应的内容
- Accepted
如果此Acceptor在此期间没有接受到任何大于N的提案,则接收此提案内容,否则忽略
3、Basic Paxos流程图
1)无故障的basic Paxos
2)Acceptor失败时的basic Paxos
在下图中,多数派中的一个Acceptor发生故障,因此多数派大小变为2。在这种情况下,BasicPaxos协议仍然成功。
3)Proposer失败时的basic Paxos
Proposer在提出提案之后但在达成协议之前失败。具体来说,传递到Acceptor的时候失败了,这个时候需要选出新的Proposer(提案人),那么 Basic Paxos协议仍然成功
4)当多个提议者发生冲突时的basic Paxos
最复杂的情况是多个Proposer都进行提案,导致Paxos的活锁问题.
针对活锁问题解决起来非常简单: 只需要在每个Proposer再去提案的时候随机加上一个等待时间即可.
4、Multi-Paxos流程图
针对basic Paxos是存在一定得问题,首先就是流程复杂,实现及其困难, 其次效率低(达成一致性需要2轮RPC调用),针对basic Paxos流程进行拆分为选举和复制的过程.
1)第一次流程-确定Leader
选举1:在三个Acceptor选择一个Leader即a,有一票通过和拒绝权利
复制2:复制到其他Acceptor和Learners,a+1代表一直使用同一个Leader
2)第二次流程-直接由Leader确认
5、Multi-Paxos角色重叠流程图
Multi-Paxos在实施的时候会将Proposer,Acceptor和Learner的角色合并统称为“服务器”。因此,最后只有“客户端”和“服务器”。
开源的ZooKeeper(zab协议),以及MySQL 5.7推出的用来取代传统的主从复制的MySQL GroupReplication等纷纷采用Paxos算法解决分布式一致性问题。
Raft协议
斯坦福大学RamCloud项目中提出了易实现,易理解的分布式一致性复制协议 Raft。Java,
C++,Go 等都有其对应的实现之后出现的Raft相对要简洁很多。引入主节点,通过竞选确定主节点。节 点类型:Follower、Candidate 和 Leader
Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一 般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段, 通过竞选阶段的投票多的人成为Leader 。
1、相关概念
节点状态
- Leader(主节点):接受 client 更新请求,写入本地后,然后同步到其他副本中
- Follower(从节点):从 Leader 中接受更新请求,然后写入本地日志文件。对客户端提供读
- Candidate(候选节点):如果 follower 在一段时间内未收到 leader 心跳。则判断 leader
可能故障,发起选主提议。节点状态从 Follower 变为 Candidate 状态,直到选主结束
请求
TermId:任期号,时间被划分成一个个任期,每次选举后都会产生一个新的 termId,一个任期内只有一个 leader
RequestVot:请求投票,candidate 在选举过程中发起,收到多数派响应后,成为 leader。
2、竞选阶段流程
1)分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随
机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
2)此时 A 发送投票请求给其它所有节点。
3)其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
4)之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
3、Leader节点宕机
Follower在规定超时时间内没有收到Leader心跳包就会变成 Candidate,进入竞选阶段, 通过竞选阶段的投票多的人成为Leader 。
4、多个Candidate竞选
1) 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票
2)当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低
5、日志复制
1)、来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
2)、Leader 会把修改复制到所有 Follower。
3)、Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
4)、 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
6、网络分区
1)最初始正常情况下状态,B节点会对其他4个节点发送心跳
2) 当出现网络分区情况, 但是出现网络分区的请求后,只能对A发送心跳,同时其他三个节点会再次选出 一个leader节点
7、网络分区情况日志复制
1)不同分区写入数据不同
2)最终E节点Termid最大成为Leader节点,同步节点数据,达成数据一致性
Consul、sentinel采用Raft协议解决一致性问题
Lease机制
Lease机制,翻译过来即是租约机制,是一种在分布式系统常用的协议,是维护分布式系统数据一致性的一种常用工具。
Lease机制有以下几个特点:
- Lease是颁发者对一段时间内数据一致性的承诺;
- 颁发者发出Lease后,不管是否被接收,只要Lease不过期,颁发者都会按照协议遵守承诺;
- Lease的持有者只能在Lease的有效期内使用承诺,一旦Lease超时,持有者需要放弃执行,重新申请Lease。
在分布式中最直观的处理方法是在每个副本与主副本维护一个心跳,期望通过心跳是否存在而判断对方是否依旧存活。 由于在某个时刻Node1主节点突然出现网络抖动或者网络中断情况(注意:不是宕机),导致从节点无法接受到心跳
会在剩下的副节点中选取一个当主节点
主要解决思路有四种:
- 设计给容忍双主的分布式协议
- Raft协议——通过Term版本高的同步低的
- 用lease机制
- 涉及去中心化Gossip协议
1、原理
1)引入中心节点负责下发Lease
2)出现网络问题
在01:05期间如果出现网络抖动导致其他节点申请Lease会申请失败, 因为中心节点在01:10之前都会承认有主节点,不允许其他节点在申请Lease
3)如果网络恢复
4)如果到01:10时间,主节点会进行续约操作,然后在下发新的Lease
5)如果主节点宕机,其他节点申请Lease也会失败,承认主节点存在
6)副节点申请Lease,申请成功. 因为Lease过期
2、容错
1)主节点宕机
lease机制天生即可容忍网络、lease接收方的出错,时间即Lease剩余过期时长
2)中心节点异常
颁发者宕机可能使得全部节点没有lease,系统处于不可用状态,解决的方法就是使用一个小集群而不是单一节点作为颁发者。
3)时差问题
中心节点与主节点之间的时钟可能也存在误差,只需要中心节点考虑时钟误差即可。
lease时间长短一般取经验值1-10秒即可。太短网络压力大,太长则收回承诺时间过长影响可用性。
3、应用
1)GFS(Google 文件系统)中,Master通过lease机制决定哪个是主副本,lease在给各节点的心跳响应消息中携带。收不到心跳时,则等待lease过期,再颁发给其他节点。
2)chubby中,paxos选主后,从节点会给主颁发lease,在期限内不选其他节点为主。另一方面,主节点给每个client节点发送lease,用于判断client死活。
五、分布式锁
基于数据库的分布式锁
数据库表锁:通过在数据库中创建一个表,表中包含锁的信息(如锁标识、锁持有者、过期时间等)。当需要加锁时,向表中插入一条记录,如果插入成功则表示获取锁成功;释放锁时,删除相应的记录。这种方法依赖于数据库的唯一索引来保证互斥性。
乐观锁:在数据库表中增加版本号或时间戳字段,在更新数据时检查版本号或时间戳是否发生变化,如果未变则更新数据并增加版本号或时间戳,否则表示数据已被其他事务修改,当前操作需重新尝试。
基于Redis的分布式锁
SETNX
Redis的SETNX(SET if Not eXists)命令是实现分布式锁的一种简单方式。当SETNX命令执行时,如果指定的key不存在,则将该key的值设为value,并返回1;如果key已存在,则不做任何操作,返回0。这样就可以通过SETNX命令的返回值来判断是否获取锁成功。但需要注意的是,SETNX命令本身不具备设置过期时间的功能,因此需要额外使用EXPIRE命令来设置锁的过期时间,以避免死锁。不过,在Redis 2.6.12及以后的版本中,可以使用SET命令的NX(Not Exists)选项和PX(设置key的过期时间,单位为毫秒)选项来同时完成这两个操作,实现原子性。
Redisson
Redisson是一个在Redis的基础上实现的一个Java驻内存数据网格(In-Memory Data Grid)。它不仅实现了分布式锁,还提供了许多其他分布式服务和数据结构。Redisson的分布式锁是基于Redis的发布/订阅(Pub/Sub)功能和Lua脚本来实现的。当一个客户端需要获取锁时,它会向Redis发送一个Lua脚本,该脚本会尝试设置一个key的value(通常为客户端的UUID或线程ID),并设置过期时间。如果设置成功,则表示获取锁成功;如果设置失败(因为其他客户端已经设置了该key),则客户端会订阅一个与锁相关的频道,并等待其他客户端释放锁的通知。这种方式的好处是可以避免轮询Redis服务器来检查锁的状态,从而减少网络开销和CPU使用率。
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config; public class RedissonConfig { public static RedissonClient createClient() { Config config = new Config(); // 假设你使用的是单节点Redis config.useSingleServer().setAddress("redis://127.0.0.1:6379"); // 如果使用哨兵、集群等其他模式,请相应配置 return Redisson.create(config); }
}import org.redisson.api.RLock;
import org.redisson.api.RedissonClient; public class DistributedLockExample { public static void main(String[] args) { // 获取Redisson客户端 RedissonClient redisson = RedissonConfig.createClient(); // 获取分布式锁 RLock lock = redisson.getLock("myLock"); try { // 尝试获取锁,最多等待100秒,上锁以后10秒自动解锁 // 注意:lock()方法在没有设置leaseTime的情况下,默认是永久持有的,需要手动释放 // 这里为了演示,我们设置了leaseTime为10秒 boolean isLocked = lock.tryLock(100, 10, TimeUnit.SECONDS); if (isLocked) { try { // 执行需要同步的代码 System.out.println("Lock acquired, executing critical section"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 释放锁 lock.unlock(); System.out.println("释放锁"); } } else { System.out.println("未获取到锁"); } // 5. 关闭 Redisson 客户端 redisson.shutdown(); }
}
基于ZooKeeper的分布式锁
ZooKeeper是一个开源的分布式协调服务,它使用Zab协议来保证数据的一致性。ZooKeeper的分布式锁是通过创建临时顺序节点来实现的。当一个客户端需要获取锁时,它会在ZooKeeper中创建一个临时顺序节点(该节点的名称会包含一个序列号,表示创建的顺序)。然后,客户端会检查自己创建的节点是否是所有节点中序列号最小的那个。如果是,则表示获取锁成功;如果不是,则客户端会监听前一个节点的删除事件(因为ZooKeeper的临时节点在客户端断开连接时会自动删除)。一旦前一个节点被删除,客户端就会再次检查自己是否是当前序列号最小的节点,并重复上述过程直到获取锁成功。这种方式的好处是它可以保证锁的公平性和可重入性,并且ZooKeeper的集群特性还可以保证分布式锁的高可用性。
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.framework.recipes.locks.InterProcessMutex; @Service
public class DistributedLockService { private static final String LOCK_PATH = "/your/lock/path"; private final CuratorFramework curatorFramework; public DistributedLockService() { RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); this.curatorFramework = CuratorFrameworkFactory.newClient("localhost:2181", retryPolicy); this.curatorFramework.start(); } public void withLock(Runnable runnable) throws Exception { InterProcessMutex lock = new InterProcessMutex(curatorFramework, LOCK_PATH); try { if (lock.acquire(10, TimeUnit.SECONDS)) { try { runnable.run(); } finally { lock.release(); } } } catch (Exception e) { // 处理异常 } } // 确保在Spring容器关闭时关闭Curator客户端 @PreDestroy public void close() { CuratorFrameworkUtils.closeQuietly(curatorFramework); }
}
基于ETCD的分布式锁
ETCD是一个高可用的键值存储系统,它内部采用Raft协议来保证数据的一致性。ETCD的分布式锁是通过其Lease机制和Revision机制来实现的。当一个客户端需要获取锁时,它会在ETCD中创建一个以某个前缀开头的key,并设置该key的Lease(租约)。Lease是一个可以自动续期的租约,如果客户端在租约到期前没有续期,则ETCD会自动删除该key。同时,ETCD还会为每个key分配一个Revision号,该号在每次事务后递增,以保证key的版本一致性。客户端在创建key时会获取当前的Revision号,并监听比自己小的Revision号的key的删除事件。一旦监听到前一个key的删除事件,客户端就会尝试获取锁。这种方式的好处是它可以利用ETCD的集群特性和一致性算法来保证分布式锁的高可用性和一致性。
综上所述,分布式锁有多种实现方式,每种方式都有其独特的底层原理和适用场景。在实际应用中,可以根据具体的需求和场景选择合适的分布式锁实现方式。
六、分布式id生成方案
UUID(Universally Unique Identifier)
工作方式:UUID是通过一系列算法生成的128位数字,通常基于时间戳、计算机硬件标识符、随机数等元素。
全局唯一性:算法设计确保了即使在分布式系统中也能生成全局唯一的ID。
优点:
实现简单,无需网络交互。
保证了ID的全球唯一性。
缺点:
通常不能保证顺序性。
ID较长(36个字符),可能导致存储和索引效率低下。
网络依赖性:无网络依赖。
数据库自增ID
工作方式:基于数据库的auto_increment自增ID机制。
顺序性:保证了生成ID的顺序性和唯一性。
优点:
简单可靠,保证顺序性。
缺点:
可能成为系统的单点故障,对数据库有较高的依赖。
无法扛住高并发场景,有性能瓶颈。
网络依赖性:高度依赖网络,所有ID生成请求都需要访问数据库。
雪花算法(SnowFlake)
工作方式:Twitter开发的一种生成64位ID的服务,基于时间戳、节点ID和序列号。
时间戳:确保ID按时间顺序增长。
优点:
ID有时间顺序,长度适中(64位)。
生成速度快。
缺点:
对系统时钟有依赖,时钟回拨会导致ID冲突。
网络依赖性:通常无需网络交互,除非在多机器环境中同步机器ID。
Redis生成
工作方式:利用Redis的原子操作(如INCR和INCRBY命令)生成唯一的递增数值。
分布式环境中的应用:在分布式环境中,可以部署多个Redis实例,每个实例可以独立生成ID,或者通过配置不同的起始值和步长来确保ID的全局唯一性。
优点:
快速、简单且易于扩展。
支持高并发环境。
缺点:
依赖于外部服务(Redis),需要管理和维护额外的基础设施。
网络依赖性:高度依赖网络。