框架篇:分布式一致性解决方案
前言
上一篇架构篇:分布式理论CAP、BASE,我们了解到分布式存在的问题以及大致的解决理论,但是具体的实现协议或者方案有哪些?
- 分布式一致性
- 分布式共识算法
- paoxs、Raft、zab
- 分布式事务一致性
- 分布式事务一致性的实现方案(XA模式和AT模式)
- 两阶段提交
- 三阶段提交
- 柔性事务TCC
- AT模式
- 事件通知
关注公众号,一起交流,微信搜一搜: 潜行前行
github地址,感谢star
1 分布式一致性
- 什么是分布式一致性?分布式一致性其实更多是偏向解决多个服务间的数据副本状态的一致,而不同于关系型数据库的一致性(数据的约束)
2 分布式共识算法
paoxs算法
- Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一
- Paxos算法的通俗理解
- 假设有十个人要去旅游,目的地有成都和拉萨两个地点。为了统一目的地,简单的方法可以拉个微信群组聊天,大家投票,按少数服从多数的原则。但是在Paxos算法里,觉得微信平台不可靠,它挂了怎么办?Paxos的原则是容错性一定要很强,所以paxos采取相互发短信
- 找另外三个人当中介人(也可从十个人中选,也不局限三个中介),十个人给他们发短信,中介者之间可以不通信
- 申请阶段:每个人的短信都会带一个发送时间,中介只会和最新短信的提议者交流,而且只能和一个人交流。每个人疯狂向中介发短信,希望获得沟通权
- 沟通阶段:如果获得半数的中介者沟通权。提议者则会给这些中介提议自己希望的旅游地(例如成都)。而收到的结果有三种;
- A: 超过半数的中介者同意,收东西去成都;
- B: 至少有一个中介者决定了旅游地(不一定是成都,可能是其他提议者和中介商定的拉萨),那先看看是否超过半数的旅游地,如果没有,则下次顶最近时间选择出的旅游地
- C: 失去沟通权,再继续发短信。。。。。。
- Paxos的一致性,是为了解决冗余副本的一致性,和关系型数据库中ACID的一致性说的不是一个东西
Raft算法
- 由于Paxos难以理解,也难以实现。于是有了新的共识算法。Raft有三种角色
- Leader: 处理所有客户端交互,日志复制等,同一时刻只有一个有效的Leader
- Follower: 类似选民,完全被动
- Candidate候选人: 可以被选为一个新的领导人
选举阶段
- 一开始任何一个服务器都是Follwer,它们内置一个倒计时,当倒计时结束时变成Candidate,向其他follwers发出要求选举自己的请求
- 此时有三个状态
- A:超过半数follwers追随,成为新的leader
- B:存在竞争者,且有超过半数追随者,放弃竞选,成为其follwer
- C:存在竞争者,大家半斤八两。Candidate者则在下个竞选周期term再次发起竞选,此时也有内置一个倒计时,谁先倒计时结束快,谁则先成为抢占半数follwer的leader(注意:前一轮成为别人的follwer不能在竞选了)
日志复制阶段
- 1:Leader领导人已经选出,客户端发出增加一个日志的要求,比如日志是"hello"
- 2:Leader要求Followe遵从他的指令,都将这个新的日志内容追加到他们各自日志中
- 3:大多数follower服务器将日志写入磁盘文件后,确认追加成功,发出Commited Ok
- 4:在下一个心跳heartbeat中,Leader会通知所有Follwer更新commited 项目
- 如果在这一过程中,发生了网络分区或者网络通信故障。使得Leader不能访问大多数Follwers了,而follwers重新选举新的Leader堆外提供服务。在恢复网络时,旧的leader会成为拥有多数follwer的新Leader的follwer。故障期间的commit回滚
zab算法
ZXID
协议的事务编号 Zxid 设计中, Zxid 是一个 64位的数字
- 其中低 32 位是一个简单的单调递增的计数器, 针对客户端每一个事务请求,计数器加 1
- 而高 32 位则代表 Leader 周期 epoch 的编号,每个当选产生一个新的 Leader 服务器,就会从这个 Leader 服务器上取出其本地日志中的最大事务 ZXID ,并从中读取 epoch 值,然后加 1 ,以此作为新的 epoch。而低 32 位计数器从 0 开始重新计数
崩溃恢复模式(选举)
- 集群初始化或者Leader失去连接时,节点(任意节点)发起选主,然后集群其他节点会为发起选主的节点进行投票
- 节点B判断确定A可以成为Leader,那么节点B就投票给节点A,判断的依据是: election epoch(A) > election epoch (B) || zxid(A) > zxid(B) || sid(A) > sid(B)。并更新自己的投票为B投票
- sid是服务ID,人为配置的
消息广播模式
- Leader将客户端的request转化成一个Proposal(提议)
- Leader为每一个Follower准备了一个FIFO队列,并把Proposal发送到队列上
- Leader若收到follower的半数以上ACK反馈
- Leader向所有的follower发送commit
一些细节
- Leader在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一ID,称为事务ID(ZXID),ZAB兮协议需要保证事务的顺序,因此必须将每一个事务按照ZXID进行先后排序然后处理
- 在Leader和Follwer之间还有一个消息队列,用来解耦他们之间的耦合,解除同步阻塞
- zookeeper集群中为保证任何所有进程能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是 Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理
3 分布式事务一致性
- 对于分布式一致性和分布式事务一致性。我更愿意区分开来:
- A-分布式一致性是为了解决数据分布在多个服务的状态一致(多个副本保持一致)
- B-分布式事务一致性,更加类似关系型数据库的一致性,是约束数据在分布式服务的关系(比如数据a在服务A的状态和数据b在服务B需要保持一个固定的映射关系)
分布式共识算法和分布式一致性的区别
- 共识算法就是为了解决分布式一致性的算法,但不适合解决分布式事务一致性(可以解决只是不合适)
4 分布式事务一致性的实现方案(XA模式和AT模式)
- XA模式是预提交数据模式(预提交数据无法被其他事务访问),如果发生故障,则回滚预提交的数据
- AT模式的数据是确认提交的,只不过存在锁,使该数据无法被其他事务访问。如果发生故障,则使用冲正操作修复数据。相对XA模式,AT模式更适合解决分布式事务,减少阻塞等待时间
两阶段提交(强一致性)(XA模式)
二阶段提交协议(Two-phase Commit,即 2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段
处理流程
阶段 1:准备阶段
- 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。
- 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
- 如参与者执行成功,给协调者反馈 yes,即可以提交;如执行失败,给协调者反馈 no,即不可提交
阶段 2:提交阶段
- 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。
- 参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源
2PC 方案缺点:
- 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈
- 可靠性问题:如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态
- 数据一致性问题:在提交阶段commit时,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,会导致了节点之间数据的不一致
三阶段提交(强一致性)(XA模式)
三阶段提交协议,是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制
处理流程
阶段 1:canCommit
- 协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应(参与者不执行事务操作),否则返回 no 响应:
- 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复
- 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no
阶段 2:preCommit
- 协调者根据阶段 1 canCommit 参与者的反应情况来决定是否可以进行基于事务的 preCommit 操作。根据响应情况,有以下两种可能
- 情况 1:阶段 1 所有参与者均反馈 yes,参与者预执行事务
- 协调者向所有参与者发出 preCommit 请求,进入准备阶段
- 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)
- 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令
- 情况 2:阶段 1 任何一个参与者反馈 no,或者等待协调者超时,无法收到所有参与者的反馈,即中断事务
- 协调者向所有参与者发出 abort 请求
- 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务
阶段 3:do Commit
- 该阶段进行真正的事务提交,分为以下三种情况
- 情况 1:阶段 2 所有参与者均反馈 ack 响应,执行真正的事务提交
- 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求,参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源
- 各参与者向协调者反馈 ack 完成的消息,协调者收到所有参与者反馈的 ack 消息后,即完成事务提交
- 情况 2:阶段 2 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务
- 如果协调者处于工作状态,向所有参与者发出 abort 请求,参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源
- 各参与者向协调者反馈 ack 完成的消息,协调者收到所有参与者反馈的 ack 消息后,即完成事务中断
- 情况 3:协调者与参与者网络出现问题
- 参与者在协调者发出 do Commit 或 abort 请求等待超时,仍会继续执行事务提交
优缺点
- 优点:在第二阶段,在等待超时后协调者或参与者会中断事务
- 优点:在第三阶段,避免了协调者单点问题,在协调者出现问题时,参与者会继续提交事务(同时也是个缺点)
- 缺点:数据不一致问题依然存在,在第三阶段,如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致
柔性事务TCC (XA模式在服务级别的实现)
- Try阶段:需要做资源的检查和预留。在扣钱场景下,Try 要做的事情是就是检查账户可用余额是否充足,再冻结账户的资金。Try 方法执行之后,账号余额虽然还是100,但是其中 30 元已经被冻结了,不能被其他事务使用
- Confirm阶段: 扣减 Try 阶段冻结的资金,Confirm 方法执行之后,账号在一阶段中冻结的 30 元已经被扣除,账号 A 余额变成 70 元
- Cancel阶段:回滚的话,就需要在 Cancel 方法内释放一阶段 Try 冻结的 30 元,使账号的回到初始状态,100 元全部可用
AT模式(阿里分布式框架seata)
一阶段:提交
- 在一阶段,Seata 会拦截“业务 SQL”,首先解析SQL语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后生成行锁。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
二阶段提交或回滚
- 二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可
- 二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据
- 回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写,出现脏写就需要转人工处理
事件通知(事务消息)
同步通知
- 人的惯性思维都会考虑到同步调用,这是简单易实现的方案。但是相对第三方系统,其是不可靠的,内部处理超时,网络断开,很容易出事故。而且等待接口返回,是个阻塞过程,影响系统性能
异步回调通知
- 相对同步通知,它的处理接口是异步回调的。因此可以避免超时处理,超时返回的问题
- 考虑到回调时接口报错则需要发起重试回调,因此需要加入重试机制
消息队列
- 消息队列可以解耦服务,并且解决了错误重试的问题
- 因为调接口会出错或者重复调用,需要保证接口幂等性
- 普通消息处理存在的一致性问题:发送者业务逻辑处理成功 -> MQ存储消息成功 -> 但是MQ处理超时 -> 从而ACK确认失败 -> 导致发送者本地事务回滚,但实际MQ是处理成功
- 如果存在处理返回结果也可以通过消息队列回传
事务状态表+消息队列方案
- 基于本地消息的最终一致性方案的最核心做法就是在执行业务操作的时候,记录一条消息数据到DB,并且消息数据的记录与业务数据的记录必须在同一个事务内完成
- 在记录完成后消息数据后,可以通过一个定时任务到DB中去轮训状态为待发送的消息,然后将消息投递给MQ。这个过程中可能存在消息投递失败的可能,此时就依靠重试机制来保证,直到成功收到MQ的ACK确认之后,再将消息状态更新或者消息清除
- 同样也需要保障接口的幂等性