6.1什么是CAP原则?

CAP原则,指的是在一个分布式系统中,一致性、可用性、分区容错性,最多只能同时满足三个特性中的两个,三者不可以见得。
(1)一致性(consistency):
客户访问集群中的任一节点都能访问到同一份最新的数据副本。
(2)可用性(availability):
系统提供的服务一直处于可用的状态,对于用户的每一个请求都能给出正常响应时间的响应。
(3)分区容错性(partition Tolerance):
分布式系统在遇到某节点或网络分区故障的时候,依然能够对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。网络分区指的是,在分布式系统中,不同的节点分布在不同的自网络(机房或异地网络)中,由于一些特殊的原因导致这些子网络之间出现网络不连通的状态,但是各个子网络的内部网络是正常的,从而导致整个系统的网络环境被分割成若干个孤立的区域。
【如何取舍】
对于分布式系统,分区容错性可以说是一个最基本的要求。因为既然是一个分布式系统,那分布式系统中的组件必然需要被部署到不同的节点,因此必然出现子网络。而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。
当一个数据项只在一个节点中保存,那么分区以后,和这个节点连通不了的部分就无法访问到了,这是分区不能容忍的。提供分区容忍性的办法就是把一个数据项复制到多个节点上,出现网络分区以后,这个数据项就可能分布到各个区里,容忍性就提高了。但是要把数据复制到多个节点,就必然会带来一致性的问题,多个节点的数据可能会出现不一致的情况。要保证写操作等待全部节点都写成功的话,这又会带来可用性的问题。所以架构师往往在分布式系统中只需要对可用性和一致性进行取舍。
【解决方案】:BASE理论
就是BasicallyAvailable(基本可用)、Sort state(软状态)和Eventually consistent(最终一致性)三个单词的简写,BASE理论是对CAP中一致性和可用性权衡的结果。基本思想是:即使无法做到强一致性,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
基本可用:指分布式系统在出现不可预知故障的时候,允许损失部分可用性,但不等价于系统不可用。基本可以指的是响应时间上的损失:正常情况下,一个在线搜索引擎需要0.5s内返回用户相应的查询结果,但由于出现了异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间延长了;功能上的损失:正常情况下,在一个电商平台上购物,消费者几乎能够顺利地完成每一笔订单,但是在高峰期的时候,由于消费者的购物行为激增,为了保护系统的稳定性,部分消费者可能被引导到一个降级页面。
软状态:指的是允许系统中的数据存在中间状态,并认为这个中间状态的存在不会影响系统的整体可用性,简单来说就是允许系统在不同节点的数据副本之间进行数据同步的过程存在延迟。
最终一致性:指的是系统中所有的数据副本,在经过一段时间的同步后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

6.2介绍一下Raft算法

Raft算法就是分布式共识算法,Redis的哨兵集群机制就是基于Raft算法。
【1】领导者选举
(1)成员身份
一个Raft集群包括若干服务器,以典型的3台服务器集群举例。在任意的时间,每个服务器一定会处于以下三个状态中的一个:
Leader:负责发起心跳,证明还活着;处理响应客户端;创建日志;同步日志;
Candidate:候选人会向其他节点发送请求投票RPC消息,通知其他节点来投票,如果赢得了大多数选票,就晋升成领导者
Follower:接收Leader的心跳和日志同步数据,投票给Candidate;
在正常的情况下,只有一个服务器是Leader,剩下的服务器是Follower。Follower是被动的,它们不会发起任何请求,只是响应来自Leader和Candidate的请求。
image.png
(2)选举领导者的过程
6.分布式 - 图2
在初始状态下,集中群所有节点都是跟随者状态。Raft算法实现了随机超时时间的特征,每个节点等待领导者心跳信息的超时时间间隔是随机的。上图中,集群中没有领导者,而节点A的等待超时时间最小,它会最先因为没有等到领导者的心跳信息发生超时。这时节点A增加自己的任期编号,并推举自己成为候选人,先给自己投上一票,然后向其他节点发送请求投票RPC消息,请他们选举自己为领导者。
image.png
如果其他节点接收到候选人A的请求投票RPC消息,在编号为1的这届任期内,也还没有进行过投票,那么它将投票给节点A,并增加自己的任期编号。
image.png
如果候选人在选举超时时间内赢得了大多数人的选票,那么它就会成为本届任期内新的领导者。
image.png
节点A当选领导者后,它会周期性地发送心跳消息,通知其他服务器我是领导者,阻塞跟随者发起新的选举。
(3)节点间如何通信?
在Raft算法中,服务器节点间的通信采用远程过程调用RPC,在领导者选举中,需要用到这两类RPC;
请求投票RPC:是由候选人在选举期间发起的,通知各节点进行投票
日志复制RPC:是由领导人发起,用来复制日志和提供心跳消息
(4)任期
Raft算法中每个任期由单调递增的数字(任期编号)来标识,任期编号是随着选举的举行而变化的。

  1. 跟随者在等待领导者心跳消息超时后,推举自己为候选人时,会增加自己的任期编号,比如节点A的任期编号是0,那么在选举自己为候选人时,会将自己的任期编号增加1.
  2. 如果一个服务器节点,发现自己的任期编号比其他节点小,那么它会更新自己的任期编号来获得较大值,比如节点B的任期编号是0,当收到来自节点A的请求投票RPC消息时,因为消息中包含了A的任期编号,且编号为1,那么节点B会把自己的任期编号更新为1
  3. 如果一个候选人或者领导者,发现自己的任期编号比其他节点小,那么它会立即恢复成跟随者状态。比如分区问题恢复后,任期编号为3的领导者节点B收到新领导者的包含任期编号为4的心跳消息,那么节点B将立即恢复成跟随者状态。
  4. 如果一个节点收到了一个包含较小的任期编号的请求,那么它会直接拒绝这个请求。比如节点C的任期编号是4,收到包含任期编号为3的请求投票RPC消息,那么它会拒绝这个消息。

(5)选举有哪些规则?

  1. 领导者周期性得向所有跟随者发送心跳消息。
  2. 如果在指定时间内,跟随者没有收到来自领导者的消息,那么它就认为当前没有领导者,推举自己为候选人,发起领导者选举
  3. 在一次选举中,赢得大多数选票的候选人将晋升为领导者
  4. 在一个任期内,领导者一直都会是领导者,直到它自身出现问题比如宕机,或者网络延迟没有及时发送心跳,其他节点发起一轮新的选举
  5. 在一次选举中,每一个服务器节点最多会多一个任期编号投出一张选票,并且按照先来先服务的原则进行投票。比如节点C的任期编号为3,先收到了一个包含任期编号为4的投票请求(来自节点A),然后又收到了一个包含任期编号为4的投票请求(来自节点B)。那么节点C会把唯一一张选票投给节点A,当再接收节点B的投票请求RPC消息时,对于编号为4的任期,已经没有选票可投了。
  6. 日志完整性高的跟随者拒绝投票给日志完整性低的候选人。比如节点C的任期编号为3,节点B的任期编号为4,节点C的最后一条日志项对应的任期编号为3,而节点,B为2,节点C就会拒绝给节点B投票。

(6)随机超时时间是什么?
就是把超时时间都分散开,在大多数情况下只有一个服务器节点先发给选举成为候选人,而不是同时发起选举,这样就能减少因选票瓜分导致选举失败的情况。
在Raft算法中,随机超时时间又两种含义:

  1. 跟随者等待领导者心跳信息超时的时间间隔是随机的;
  2. 如果候选人在一个随机的时间间隔内,没有赢得半数以上的选票,那么选举就无效了,然后候选人发起新一轮的选举。也就是说,等待选举的超时时间间隔是随机的

(7)缺点:

  1. 读写请求和数据转发压力都落在领导点节点上,相当于单机,性能和吞吐量也会受到限制
  2. 大规模跟随者的集群,领导者需要承担大量元数据维护和心跳通知的成本
  3. 领导者单点问题,故障后直到新领导者选举出来期间集群不可用
  4. 随着候选人规模增大,收集半数以上的投票成本更大

【2】日志复制
(1)如何理解日志
数据副本是以日志的形式存在的,日志由日志项组成,它主要包括用户的指令,还包含一些附加信息,比如索引值、任期编号
image.png
索引值:日志项对应的整数索引值,用来表示一个日志项,是一个连续、单调递增的证书编号;
任期编号:创建这条日志项的领导者的任期编号
(2)如何复制日志?
领导者通过日志复制RPC消息,将日志项复制到集群其他节点上;
接着如果领导者接收到大多数的复制成功响应后,它将日志项应用到它的状态机,并返回成功给客户端。如果领导者没有接收到大多数的复制成功响应,那么就会返回错误给客户端
当跟随者接收到心跳消息,或者新的日志复制RPC消息后,如果跟随者发现领导者已经提交了某条日志项,而它还没应用,那么跟随者就将这条日志项应用到本地的状态机上。
(3)如何实现日志的一致性?
在Raft算法中,以领导者的日志为准,来实现各节点日志的一致性。
首先,领导者通过日志复制RPC的一致性检查,找到跟随者节点上与自己相同日志项的最大索引值,然后复制并更新覆盖该索引值之后的日志项,实现各节点日志的一致。跟随者中不一致日志项会被领导者的日志覆盖,而且领导者不会删除自己的日志。

6.3介绍一下网关?

在微服务背景下,一个系统被拆分成多个服务,但是像鉴权、限流、路由、日志、监控等功能是每个微服务都需要的,没有网关的话,就需要在每个服务中单独实现,这使得我们做了很多重复的事情并且没有一个全局的视图来统一管理这些功能。
所以一般情况下,网关都会提供请求转发(动态路由)、权限认证、流量控制、日志、监控这些功能。后面做的事情,可以统一为过滤请求。
【SpringCloud Gateway】
SpringCloud Gateway属于Spring Cloud生态系统中的网关,它诞生的目的就是为了替代老牌网关Zuul,准确来说是Zuul 1.x。
为了提升网关的性能,SpringCloud Gateway使用Reactor库来实现响应式编程模型,底层基于Netty实现异步IO。
SpringCloud Gateway的目标是不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能。

6.4介绍一下分布式ID?

ID对应的是数据库表中一条数据记录的唯一标识。
分布式ID是分布式系统下的ID,举个例子,如果一个项目使用的是单机Mysql。但是随着项目上线以后,使用的人数越来越多,整个系统的数据量将越来越大。单机Mysql已经没有办法支撑了,需要进行分库分表。在分库分表以后,数据遍布在不同服务器上的数据库,数据库的自增主键已经没有办法满足生成主键唯一了。那我们这时候应该如何为不同的数据节点生成全局唯一主键呢?
image.png
image.png
高性能:分布式ID的生成速度要快,对本地资源消费要小;
高可用:生成分布式ID的服务要保证可用性接近100%;
方便易用:拿来即用,使用方便,快速接入
除了这些以外,我们的分布式ID还应该保证:
安全:ID中不包含敏感信息;
有序递增:如果要把ID存放在数据库的话,ID的有序性可以提升数据库的写入速度。
独立部署:分布式系统单独有一个发号器服务,专门用来生成分布式ID。生成ID的服务可以和业务相关的服务解耦,不过这样会增加网络调用消费的问题。此外这个id分发器的服务必须要保证高可用和高并发(搭建主从集群)。
(1)数据库自增主键
利用数据库的auto_increment和auto_increment_offset实现自增ID,当需要生成一个ID的时候,向id分发器的数据库表中插入一条数据库记录,返回主键id。
优点:实现起来比较简单、ID有序递增、存储消费空间也小;
缺点:
1.支持的并发量不大、每次获取ID都要访问一次数据库(获取速度很慢,因为有auto-incr锁并且会增加数据库的压力)
2.安全问题(比如根据订单的ID递增规律就能推算出每天的订单量,这可是商业机密)
3.需要在业务层对id进行判断然后将请求分发到不同的数据库节点上
4.为了保证id分发器的高可用性,需要搭建集群,要不然这个id分发的服务挂掉了。整个系统都不可用了。
搭建Mysql集群可以搭建主从集群或者主主集群,如果是主从集群的话,在主库挂掉以后从库可以顶上,看上去似乎是高可用的,但实际上主从同步有延迟,打个比方主库上生成的下一个id的最大值是4,而从库上的最大值是3,在还没来得及数据同步之前,主库就挂掉了,从库顶上这时候就会带来id不是全局唯一的问题。
搭建主主集群的话,我们可以这样做,通过Mysql的auto_increment_increment(步长)和auto_increment_offset(起始值)的方式来解决主从同步延迟的问题,这样即使一台节点挂掉,也不会影响整个系统的可用性。这样带来的缺点就是可扩展性太低了,比如两台机器,一台生成奇数,一台生成偶数;但是如果增加一台机器的话就需要重新去修改步长和起始值的配置,每增加一台机器就需要修改一次,也太麻烦了。如果要想生成全局id,就必须把整个Mysql集群都给停掉来修改这个配置;因为如果是动态修改配置的话,依然会存在非全局Id的问题
(2)Nosql方案
优点:通过Redis的incr命令可以实现id原子顺序递增,redis的读写命令是基于内存的,比访问数据库要快得多,可以减轻数据库的压力。
缺点:虽然没有唯一性冲突,但是安全问题以及如何访问数据库节点的问题依然存在。
(3)UUID
因为UUID的生成规则包括时间戳、时序、名字空间、MAC地址、随机数等元素,计算机基于这些规则是肯定不会重复的。虽然UUID可以做到全局唯一,但是我们一般很少使用它。
优点:生成速度比较快、简单易用
缺点:
1.UUID由32个16进制数组成,每一个16进制数占4位也就是半个字节,UUID占16字节,存储消耗空间相当大;
2.不安全,基于MAC地址生成的UUID的算法会造成MAC地址泄漏;
3.无序非自增,这样对数据库的插入删除非常不友好;
4.如果机器的系统时间不对的情况下,也可能会产生重复ID
(4)雪花算法
雪花算法是推特公司开源的分布式ID生成算法,由64位的二进制数组成,占8字节;
第0位:符号位,始终为0,没有用,不用管;
第1-41位:一共41位用来表示时间戳,单位是毫秒,可以支持2^41毫秒(大约是64年)
第42-51位:一共10位,一般来说,前5位表示机房ID,后5位表示机器ID。这样就可以区分不同集群的节点。
第52位-64位:一共12位,用来表示序列号。序列号为自增键,代表单台机器每毫秒能够产生的最大ID数(2^12=4096),也就是说单台机器每毫秒最多可以生成4096个唯一ID,QPS可以达到400W/S。
优点:生成速度比较快、生成的ID有序递增、比较灵活(可以对雪花算法进行简单改造比如加入业务ID)
缺点:需要解决重复ID的问题(强烈依赖机器时钟,如果机器时钟回拨,可能会导致产生重复ID)。
时钟回拨:硬件时钟可能会因为各种原因发生不准的情况,网络中提供了ntp服务来做时间校准,做校准的时候就会发生时钟的跳跃或者回拨的问题。比如12:00:05 128 由于机器原因导致时间回拨到了12:00:00: 500 这个时间,可能就会生成重复id
(5)双Buffer DB发号器(美团和滴滴采用这种方式) 号段模式
在说双Buffer之前先说一下单Buffer保证分布式场景下id唯一的策略。本质上还是使用数据库递增的方式,通过减少写DB的次数来提升性能,每个服务器上缓存一个号段,发号时,先存缓存中取,等缓存中去完了,就更新DB中号段的最大值,同时更新缓存的号段。打个比方:三台机器分别缓存了号段A:1-1000,B:1001-2000,C:2001-3000,这时候DB中存储的号段最大值为3000,这个时候A机器的号段发完了,需要更新DB中号段最大值4000,并更新A机器的号段为3001-4000。这样就可以将读写DB的次数降低为1/step,step为号段的长度。如果在发号的过程中服务挂了,缓存的号段丢失了,当服务再拉起时,就更新DB的最大值,重新取号段,对外表现为一段号码被跳过了,没有什么太大的影响。单Buffer的方案,当号码分配完了再去更新DB重新分配号段,在高并发场景下必然会造成服务的短暂不可用,因为读写DB的IO延迟是比较大的,所以可以在号段还未发完时就预先分配号段,但不能直接覆盖原有的号段,因为还没分配完,这就需要另一个Buffer了。
【双Buffer】
服务内部有两个号段缓存区segment。当前号段已下发10%时,每次请求都判断下下一个号段是否更新,如果未更新,就另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复,这样就可以消除持久化DB造成的服务抖动。
(6)雪花算法优化
解决两个问题:1.机器(workerid)的分配管理 2.机器可能存在时钟回拨
1.借助中心化的节点
通过中心化的节点,比如ZooKeeper来维护每台机器的状态和时钟信息。以美团的leaf框架为例
image.png

  1. 启动Leaf-snowflake服务,连接Zookeeper,在leaf_foever父节点下检查自己是否已经注册过。
  2. 如果有注册过直接取回自己的workerID(Zookeeper顺序节点生成int类型的id号),启动服务
  3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。
  4. 缓存workID,减少对第三方组件的依赖

2.解决时钟问题,分为两种类型解决

  1. 服务启动回拨:通过定时上报本机的时钟信息,在第一次启动服务时,如果发现自己的时钟信息与NTP时钟同步服务器相差过大,则认为有问题,启动失败。
  2. 服务运行中回拨:运行中回拨,会导致发号过程时间戳变小,这个时候等待一定时间再提供服务,若多次回拨则剔除机器并告警,本质上是重试并等待时钟追赶。

【历史时钟方式】
将序列号的分配调整为如图,
image.png
将集群id+机器id的高位移至低位,从10位变更成13位,将递增序列号从12位修改成9位;其中将机器id调整到最后是为了避免序列号增1导致的整体数据增1的问题,这样可以在一定程度上规避外部数据对id的猜测,以防止恶意爬取。核心:不采用实际时间,而采用历史时间,在服务启动后,我们将当前时间作为该业务进程的时间戳的起始时间段。后续的自增是在序列号自增到最大值的时候时间戳增1,而序列号重新归为0,算是将时间戳和序列号作为一个大值进行自增,只是初始化不同。

6.5说说RPC的实现原理

(1)建立通信
首先要解决通讯的问题:即A机器想要调用B机器,首先得建立起通信连接。
主要是通过在客户端和服务器之间建立TCP连接,过程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉。也可以是长连接,客户端与服务器建立连接之后保持长期持有,不管此时有无数据包的发送,可以配合心跳检测机制定期确认连接存活就行,多个远程过程调用共享同一个连接。
(2)服务寻址
要解决寻址的问题,也就是说,A服务器上的应用怎么告诉底层的RPC框架, 如何连接到B服务器(如主机、Ip地址、端口号以及特定的接口,方法名称是什么)。通常情况下我们需要提供B机器(主机名或IP地址)以及特定的端口,然后指定调用的方法或者函数的名称以及入参出参等信息,这样才能完成服务的一个调用。注册中心是RPC的实现基石,比如可以采用Redis或者ZooKeeper来注册服务等等。
【从服务提供者的角度看】

  1. 当服务提供者启动的时候,需要将自己提供的服务注册到指定的注册中心,比便消费者能够通过服务注册中心进行查找;
  2. 当服务提供者由于某些原因致使提供的服务停止时,需要向注册中心注销停止的服务;
  3. 服务的提供者需要定期向服务注册中心发送心跳检测,服务注册中心如果一段时间未收到来自服务提供者的心跳后,就可以认定该服务提供者已经停止服务,则将该服务从注册中心上去掉。

【从服务的调用者角度看】

  1. 服务的调用者启动的时候根据自己订阅的服务向服务注册中心查找服务提供者的地址等信息;
  2. 当服务调用者消费的服务上线或下线的时候,注册中心会告知该服务的调用者;
  3. 服务调用者下线的时候,取消订阅

(3)网络传输
序列化:
当A机器上的应用发起一个RPC调用时,调用方法和入参信息都需要通过底层的TCP网络协议传输到B机器,由于网络协议是基于二进制的,所有我们传输的参数数据都需要先进行序列化成二进制的形式才能在网络中进行传输,然后通过寻址和网络传输将序列化之后的二进制数据发送给B机器
反序列化:
当B机器接收到A机器发送的请求之后,需要对接收到的参数信息进行反序列化,将二进制信息恢复成内存中的表达方式,然后再找到对应的方法进行本地调用(一般是通过生成代理Proxy去调用),之后得到调用的返回值。
(4)服务调用
B机制进行本地调用(通过代理Proxy和反射调用)之后得到了返回值,此时还需要再把返回值发送回A机器,同样也需要经过序列化操作,然后再经过网络传输将二进制数据发送回A机器,而当A机器接收到这些返回值之后,则再次进行反序列化操作,恢复成内存中的表达方式,最后再交给A机器上的应用进行相关处理。

6.6说一下什么是QPS、TPS、并发数、吞吐量

(1)QPS:每秒查询率,就是一台服务每秒的响应请求数。
(2)TPS:客户端向服务器发送请求(含事务的操作),然后服务器做出反应的过程。客户端在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数。
(3)并发数:系统能够同时处理的请求数量,同样反应了系统的负载能力,这个数值可以通过分析1s内的访问日志数量来得到。高并发就是接口的请求数量多。
(4)吞吐量:系统在单位时间内处理请求的数量,TPS、QPS就是吞吐量的量化指标。
影响吞吐量的因素:1.一个系统的吞吐量(承载能力)与request(请求)对cpu的消耗、外部接口、IO等等紧密相关;2.单个request对cpu消耗越高、IO影响速度越慢,系统吞吐能力越低。

6.7接口的幂等性如何设计?

接口的幂等性:多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单词调用的结果一致。
使用幂等的场景:
(1)前端重复提交
用户注册,用户创建商品等操作,前端都会提交一些数据给后端服务,后台需要根据用户提交的数据在数据库创建记录。如果用户不小于多点了几次,后端收到了好几次提交,这时就会在数据库中重复创建多条记录,这就是接口没有幂等性带来的bug。
(2)接口超时重试
对于给第三方调用的接口,有可能因为网络原因而调用失败,这时一般在设计的时候会对接口调用加上失败重试的机制。如果第一次调用已经执行了一半时,发生了调用异常。这时候再次调用就会因为脏数据的存在而出现调用异常。
(3)消息重复消费
在使用消息中间件来处理消息的时候,手动ack确认消息被正常消费时,如果消费者突然断开连接,那么一句执行了一半的消息会重新放回队列。当消息被其他消费者重新消费时。当消息被其他消费者重新消费时,如果没有幂等性,就会导致消息重复消费时结果异常。比如数据库重复数据,资源重复等等。
解决方案:
(1)通过Redis的token机制实现,全局唯一id可以用百度的uid-generator生成
image.png

(2)基于redis的setnx命令来实现
image.png

6.8介绍一下分布式事务

就是当系统的体量很小的时候,单体结构完全可以满足现有业务需求,所有的业务共用同一个数据库,整个下单流程都是在同一个事务下完成能够做到所有操作要么全部提交要么全部回滚,在单体架构下这是很容易实现的。
image.png
随着业务量的不断增加,单体架构逐渐扛不住巨大的流量,这时候就需要对数据库、表做分库分表处理,将应用进行服务化拆分。这样就产生了用户中心、库存中心、订单中心等,由此带来的问题就是业务间相互隔离,每个业务都维护这自己的数据库,数据的交换通过RPC调用。当用户再次下单时,需要同时对订单库、库存库、用户库进行操作,可此时我们只能保证自己本地的数据一致性,无法保证其他服务调用的操作是否成功,所以为了保证整个下单流程的数据一致性,就需要分布式事务介入。
image.png
方案(1):基于XA协议的2PC
XA协议中主要分为两部分:事务协调者和本地资源管理器
【第一阶段】:precommit
1.事务协调者给每个本地资源管理器发送Prepare消息,询问每个参与者是否可以执行提交操作,并开始等待各参与者节点的响应。
2.各个参与者执行所有的事务操作,并将Undo信息和Redo信息写入日志。这里如果所有节点都执行成功其实已经执行了事务的主体部分,只不过还没有提交。
3.各个参与者节点响应协调者节点发起的询问,如果在各自的事务操作部分都执行成功只差提交了就返回yes,否则返回no。
image.png
【第二阶段】:提交阶段
1.如果协调者收到了参与者的失败消息或者超时了,直接给每个参与者发送回滚消息;否则,发送提交消息。参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。
2.如果协调者向各个节点发出的指令是rollbal,所有节点需要利用undo信息进行回滚,并释放整个事务期间占用的资源,然后再向节点协调者发送”回滚成功”的消息。协调者节点收到所有参与者节点反馈的”回滚完成”消息后,取消事务。如果是commit,也会返回成功的消息给协调者。
image.png
【缺点】:
1.同步阻塞问题。在执行过程中,所有参与者节点都是事务阻塞型的。当参与者占有公共资源的时候,其他第三方节点访问公共资源也不得不处于阻塞状态,各个参与者在等待协调者发出提交或中断的请求的等待过程会一直阻塞。而协调者发出的时间依赖于各个参与者的响应时间,如果协调者宕机了的话,那么各个参与者所占用的资源就一直无法得到释放。
2.单点故障问题。由于协调者的重要性,一旦协调者发生故障,参与者会一直阻塞下去。
3.数据不一致问题。如果出现分区或者是网络故障,当协调者向参与者发送commit请求之后,发生了局部网络异常或者协调者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
4.太过保守。2pc没有设计相应的容错机制。当任意一个参与者节点宕机,协调者因为超时没有收到响应就会导致整个事务回滚。
5.事务状态未知问题。协调者在第二阶段发出commit消息之后,唯一接收到这条消息的参与者以及协调者同时宕机。那么即使协调者通过选举协议产生了新的协调者,也没人知道这个事务是否已被提交。
方案(2):基于XA协议的3PC,将准备阶段一分为二,形成了cancommit、precommit、docommit三个阶段。主要是为了解决参与者阻塞的问题,当协调者崩溃时,参与者不能做出最后的选择,就会一直保持阻塞状态锁定资源。在2PC中只有协调者有超时机制,3PC中协调者和参与者都引入了超时机制,协调者出现故障后,参与者就不会一直阻塞,而且在第一阶段和第二阶段中间插入了一个准备阶段,保证了在最后提交阶段之前各参与者节点的状态都是一致的。
【第一阶段】CanCommit阶段
1.事务询问:协调者向所有参与者发送CanCommit请求
2.响应反馈:参与者接收到canCommit请求之后,正常情况下,如果认为自身可以顺利执行事务,就返回Yes响应,并进入预备状态。否则反馈No。
3.如果协调者从所有的参与者中获得的反馈都是Yes响应,那么就会执行事务的预执行阶段
【第二阶段】PreCommit阶段
1.发送预提交请求:协调者向所有的参与者发送PreCommit命令,并进入Prepared阶段。
2.事务预提交:参与者在接收到PreCommit的请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。
3.响应反馈:如果参与者成功的执行了事务操作,则反馈ACK响应,同时开始等待最终指令。
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
【第三阶段】DoCommit阶段
1.发送提交请求:协调者收到参与者发送的ACK响应后,将预提交状态进入为提交状态,并向所有的参与者发送doCommit请求。
2.事务提交:参与者接收到doCommit请求之后,执行正式的事务提交,并在完成事务提交之后释放所有资源。
3.响应反馈:事务提交完之后,向协调者发送ACK响应。
4.完成事务:协调者接收到所有参与者的ack响应之后,完成事务。
【中断事务】:协调者没有接收到二阶段所有参与者发送的ACK响应,就会执行中断事务。
1.发送中断请求:协调者向所有参与者发送abort请求
2.事务回滚:参与者接收到abort请求之后,利用二阶段的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。
3.反馈结果:参与者完成事务回滚以后,向协调者发送ACK消息。
4.中断事务:协调者接收到参与者反馈的ACK消息之后,执行事务的中断。
注意:
在doCommit阶段,如果参与者无法及时接收到来自协调者的doCommit或者rebort请求时,会在等待超时之后,会继续进行事务的提交。
引入超时提交的依据:
其实这个应该是基于概率来决定的,当进入第三阶段时,说明参与者在第二阶段已经收到了PreCommit请求,那么协调者产生PreCommit请求的前提条件是他在第二阶段开始之前,收到所有参与者的CanCommit响应都是Yes。(一旦参与者收到了PreCommit,意味他知道大家其实都同意修改了)所以,一句话概括就是,当进入第三阶段时,由于网络超时等原因,虽然参与者没有收到commit或者abort响应,但是他有理由相信:成功提交的几率很大。
3PC无法解决:数据不一致以及太过保守问题。但是这种机制也会导致数据一致性问题,因为,由于网络原因,协调者发送的abort响应没有及时被参与者接收到,那么参与者在等待超时之后执行了commit操作。这样就和其他接到abort命令并执行回滚的参与者之间存在数据不一致的情况。
(3)方案三:TCC(补偿事务)
TCC编程模式,其实也是两阶段提交的一个变种,不同的是TCC是在业务层编写代码的两阶段提交。TCC分别表示的是:Try、Confirm、Cancel,一个业务要对应得写这三个方法。
以扣库存为例,Try阶段去占库存,Confirm阶段实际扣除库存,如果库存扣减失败Cancel阶段进行回滚,释放库存。
TCC不存在资源阻塞的问题,因为每个方法都直接进行事务的提交,一旦出现异常通过Cancel来进行回滚补偿。
但缺点是:原本一个方式现在需要三个方法的支持,并且TCC对业务的侵入性很强。
image.png
(4)方案四:消息事务+最终一致性
image.png
1.服务A向消息中间件发送一条预备消息。
2.消息中间件保存预备消息并返回成功。
3.服务A执行本次事务,服务A会实现MQ的回调接口,检查自己的本地事务是否执行成功,如果失败就回滚预备消息,成功则对消息进行最终commit。
4.服务A发送提交消息给消息中间件,服务B接收到消息之后执行本地事务。如果服务B的本地事务执行失败或者超时未给出响应,消息会重新投放,一旦超出重试次数,则持久化失败消息,并启动定时任务做补偿。
(4)方案五:分布式框架seata的AT模式
image.png
角色:
RM(资源管理器):管理执行分支事务的资源,向TC注册分支事务、上报分支事务状态、控制分支事务的提交以及回滚。
TC(事务协调者):维护全局和分支事务的状态,指示全局提交或回滚。他是一个独立运行的服务进程。
TM(事务管理者):开启、提交、回滚一个全局事务。
执行流程:
1.Business是一个业务入口,在程序中会通过注解来说明这是一个全局事务,他的角色是TM;Business会请求TC,说明自己要开启一个全局事务,TC会生成一个全局事务XID返回给TM。TM得到XID以后,开始进行微服务调用。调用storage、order、account微服务。
2.随着微服务调用链的传播。Storage会收到一个xid,知道自己的事务属于全局事务。Storage执行自己的业务逻辑。
【1】解析sql,解析sql的类型(insert、update、delete),表(table_name)、条件(where xxx=’xxx’)等相关信息。
【2】查询前镜像:根据解析得到的条件,生成查询语句,定位数据,得到前镜像数据。
【3】开始执行本地业务sql。
【4】查询后镜像数据,更新以后的数据记录。
【5】插入回滚日志,把前后镜像数据以及业务sql相关的信息组成一条回滚日志记录,插入到undo_log表中。
【6】提交前,向TC注册分支事务,并申请锁。
【7】本地事务提交:把业务数据的更新以及生成的undo log一起提交。
【8】将本地事务的提交结果上报给TC。
TC拿到所有分支事务的本地执行情况并告知TM,这个执行情况;由TM决定是发起全局提交还是全局回滚。
二阶段-回滚
【1】收到TC的分支回滚请求,开启一个本地事务。
【2】通过xid和BranchID查找到相应的undo log记录。
【3】数据校验:通过undolog数据记录中的前镜像数据进行回滚。
【4】生成并执行回滚语句
【5】把本地事务的执行结果上报给TC。
二阶段-提交
【1】收到TC的分支请求以后,把请求放入一个异步任务队列中,马上返回提交成功的结果给TC。
【2】异步任务队列的请求,将异步得批量删除undo log记录。