本文介绍了关于“如何解决java中常见的分布式事务理论”的知识。很多人在实际案件操作中都会遇到这样的困难。接下来,让边肖带领大家学习如何应对这些情况!希望大家认真阅读,学点东西!
00-1010从定义开始:
00-1010所有节点都访问最新的数据副本。这是什么意思?我们知道,在分布式系统中,为了获得高可用性,一个节点通常有多个数据副本,简称为Follower节点。常见的模式是,我们的数据更新通常被写入Leader节点,然后同步发送到Follower节点。当我们读取数据时,我们可以从任何节点读取最新的数据,这就是一致性。
a、b和c可以得到相同的数据。
00-1010无故障节点可正常运行。简单来说,客户端总是可以正常访问,从系统得到正常的响应。从用户的角度来看,不会出现系统运行故障或访问超时等问题,但可能会出现系统内部网络延迟等问题。
因为节点C由于自身问题不可用,正常情况会被拒绝。节点B和节点A之间可能有同步延迟,但节点B本身没有故障,所以节点B是可用的。
00-1010网络的问题比较复杂,分布式系统必须考虑这一点。如果一个节点因为网络等问题出现数据不一致,或者数据长时间延迟无法同步,虽然会影响部分节点数据的时效性,但服务节点仍然可用,分布式系统必须能够容忍这种情况。
虽然B的对应节点与Leader失去了联系,但仍然可以提供外部服务,只是提供旧数据。
在分布式系统中,不能同时满足CAP。首先,因为节点多,网络传输需要时间,可能会有延迟。那么,我们无法保证节点之间的数据在某个时刻是完全一致的,所以应该满足P(分区容错)。当P满足了,为什么CA不能同时满足?让我们通过假设来看看,如果CA同时满足会发生什么。
现在假设需要C(一致性),也就是说某个时刻所有节点提供的数据必须一致。我们知道在P的情况下,是不可能保证的。如果要保证,只能杀光其他所有节点,比如禁止读写,这其实和A是背道而驰的(虽然有些节点延迟了,但是节点本身是可用的)。
现在假设需要A(可用性),也就是说只要节点本身没有问题,即使有一点数据延迟,也可以向外界提供服务。显然,这绝对与c相反。
在实际业务中,我们需要根据业务场景决定使用CP还是AP。比如对于一些与金钱相关的业务,原则上数据的一致性应该是最重要的,所以一般采用CP,而对于一些不影响主要功能的业务,比如新闻的阅读量,不同用户看到的阅读量不同,不会造成什么影响,所以可以采用AP。
CAP理论
因为CAP理论中的C和A不能兼得,所以易贝建筑师提出了BASE理论,主要是对CA大惊小怪,不需要很强的一致性,所以可以满足一定的可用性。让我们从定义开始:
00-1010请注意,这与不可用不是一回事。当分布式系统出现不可预测的故障时,允许失去部分可用性,保证核心功能的可用性。比如一个接口正常响应200ms,发生故障时响应时间超过1 s,虽然响应时间较长,但接口仍然可以提供外部服务。比如一个视频网站,突然流量来了,视频的弹幕服务就挂了,但是视频播放功能
C(Consistence):一致性
表示分布式系统允许一个中间状态,但这个中间状态不会严重影响服务,比如主从复制,允许节点有很短的延迟。
00-1010由于软状态的存在,系统可以容忍延迟,但经过一段时间后,延迟的数据最终需要保持一致。
g src="https://cache.yisu.com/upload/information/20211127/112/418337.png" alt="java常见分布式事务理论怎么解决">
总的来说,BASE理论适用性应该更广泛,很多时候我们并不要求数据的强一致性,只要在短暂的延时之后能达到一致性也是可以的。
一致性hash
hash这个词对我们来说并不陌生,以缓存服务器来说,一般会在线上配置好几台服务器,然后根据hash来决定请求哪台缓存服务,比如常见的就是取模方式 hash(key)%num 来获取目标机器。
假设现在有3台缓存服务器,并且当前有3个缓存的key,分别是k0,k1,k2,在经过hash以后,它们的分布情况如下:
hash(k0)%3=0 #No.0 hash(k1)%3=1 #No.1 hash(k2)%3=2 #No.2
很幸运,分布的非常均匀,每台机器一个。某天,由于线上要做个活动,预计访问量会加大,需要选择加一台服务器来分担压力,于是经过hash之后,k0,k1,k2的分布情况如下:
hash(k0)%4=0 #No.1 hash(k1)%4=1 #No.2 hash(k2)%4=2 #No.3
-
k0的目标缓存服务器由原本的No.0变成了No.1
-
k1的目标缓存服务器由原本的No.1变成了No.2
-
k2的目标缓存服务器由原本的No.2变成了No.3
可以发现因为添加了一台缓存节点,导致了k0,k1,k2原来的缓存全部失效了,这似乎有点问题,类似缓存雪崩,严重的话会对DB造成很大的压力,造成这个问题的主要原因是因为我们加了一个节点,导致hash结果发生了变动,此时的hash可以说是不稳定的。
为了解决rehash不稳定的问题,于是出现了一致性hash算法。一致性hash的原理比较简单,首先存在一个hash圆环,这个圆环可以存放 0-2^32-1 个节点。
-
第一步就是把我们的目标服务器节点通过hash映射到这个环上
-
第二步根据我们需要查找的key,它应该也对应hash环上的某个位置
也许你会问,这k0、k1、k2也没和某个缓存节点对上呀~,这就是一致性hash不同的地方,它此时查找的方式并不是 hash(key)=某个节点,而是根据key的位置,顺时针找到第一个节点,这个节点就是当下这个key的目标节点。
我们再来看看在一致性hash的情况下,新增一个节点会发生什么。
此时唯一的变动就是k0原本应该打到cache0节点的,现在却打到了我们新加的节点cache3上,而k1,k2是不变的,也就是说有且只有k0的缓存失效了,相比之前,大大降低了缓存失效的面积。
当然这样的节点分布算是比较理想的了,如果我们的节点是这样分布的:
几个cache节点分布的比较集中,由于顺时针查找法,所以最终k0,k1,k2都落在cache0节点上,也就是说cache1、cache2基本就是多余的,所以为了解决这种数据倾斜的问题,一致性hash又引入了虚拟节点的概念,每个节点可以有若干个虚拟节点,比如:
-
cache0->cache0#1
-
cache1->cache1#1
-
cache2->cache2#1
虚拟节点并不是真正的服务节点,它只是一个影子,它的目的就是站坑位,让节点更加分散,更加均匀。
这样通过映射出虚拟节点以后,k0打到cache2,k1打到cache0,k2打到cache1,虚拟节点越多,理论分布的越均匀。
Gossip协议
集群往往是由多个节点共同组成的,当一个节点加入集群或者一个节点从集群中下线的时候,都需要让集群中其他的节点知道,这样才能将数据信息分享给新节点而忽略下线节点。
A、B、C节点之间可以互相传递消息,但是D节点在下线之后会被广播告诉其他存活节点。
这样的广播协议就是今天要说Gossip协议,Gossip协议也叫Epidemic协议(流行病协议),当一个消息到来时,通过Gossip协议就可以像病毒一样感染全部集群节点,当然我们利用的是它这个极强的散播能力。
Gossip的过程是由一个种子节点发起的,当一个种子节点有信息需要同步到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,所以不能保证某个时间点所有的节点都有该条消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。
Gossip协议的特点:
-
Gossip协议是周期性散播消息,每隔一段时间传播一次
-
被感染的节点,每次可以继续散播N个节点
-
每次散播消息时,都会选择尚未发送过的节点进行散播
-
收到消息的节点,不会向发送的节点散播
-
同一个节点可能会收到重复的消息,因为可能同时多个节点正好向它散播
-
集群是去中心化的,节点之间都是平等的
-
消息的散播不用等接收节点的ack,即消息可能会丢失,但是最终应该会被感染
我们来看个例子:
-
种子节点是A
-
A节点选择B、C节点进行散播
-
C散播到D,B散播D和E,可以发现D收到两次
-
D散播到F,最终整个网络都同步到了消息
Gossip有点类似图的广度优先遍历算法,一般用于网络拓扑结构信息的分享和维护,像redis、consul都有使用到。
Raft算法
分布式协议的难点之一就是数据的一致性,当由多个节点组成的集群中只有一个节点收到数据,我们就算成功的话,风险太大,当要求所有节点都收到数据才响应成功,性能又太差,所以一般会在数据的安全和性能之间做个折中,只要保证绝大部分节点同步数据成功,我们就算成功,Raft算法作为比较知名的一致性算法,被广泛应用于许多中间件中,比如像etcd,接下来我们就看看Raft算法是如何工作的。
首先介绍下在Raft算法中,几种情况下每个节点对应的角色:
Leader
节点:同大多数分布式中的Leader节点一样,数据的变更都是通过它的
Follower
节点:Leader节点的追随者,负责复制数据并且在选举时候投票的节点
Candidate
候选节点:参与选举的节点,就是Follower节点参与选举时会切换的角色
Raft算法将一致性问题分解为两个的子问题,Leader选举和状态复制。
选举
首先我们来看看Leader的选举,系统在刚开始的时候,所有节点都为Follower节点,这时大家都有机会参与选举,也就是把自己变成Candidate,但是如果每个Follower节点都变成Candidate那么就会陷入无限的死循环,于是每个Follower都一个定时器,并且定时器的时间是随机的,当某个Follower的定时器时间走完之后,会确认当前是否存在Leader节点,如果不存在就会把自己变成Candidate,这时会投自己1票,同时告诉其它节点,让它们来投票,当拿到超过半数以上的投票时,当前的Candidate就会变成Leader节点。
-
由于A节点的定时器时间最短(10ms),所以A会成为Candidate
-
A投自己一票,同时B、C也投出自己的同意票,因此A会变成Leader节点,同时会记录是第M任。这个M是做版本校验的,比如一个编号是10的节点,收到了一个编号是9的节点的投票请求,那么就会拒绝这个请求。
在Leader节点选举出来以后,Leader节点会不断的发送心跳给其它Follower节点证明自己是活着的,其他Follower节点在收到心跳后会清空自己的定时器,并回复给Leader,因为此时没必要触发选举了。
如果Leader节点在某一刻挂了,那么Follower节点就不会收到心跳,因此在定时器到来时就会触发新一轮的选举,流程还是一样,但是如果恰巧两个Follower都变成了Candidate,并且都得到了同样的票数,那么此时就会陷入僵局,为了打破僵局,这时每个Candidate都会随机推迟一段时间再次请求投票,当然一般情况下,就是先来先得,优先跑完定时器的Candidate理论成为Leader的概率更大。
好的选举流程大致如上,接下来我们来看看数据的复制。
复制
当Leader节点收到Client的请求变更时,会把变更记录到log中,然后Leader会将这个变更随着下一次的心跳通知给Follower节点,收到消息的Follower节点把变更同样写入日志中,然后回复Leader节点,当Leader收到大多数的回复后,就把变更写入自己的存储空间,同时回复client,并告诉Follower应用此log。至此,集群就变更达成了共识。
最后,Raft算法是能够实现分布式系统强一致性的算法,每个系统节点有三种状态Leader、Follower、Candidate,实现Raft算法两个最重要的事是:主的选举和日志的复制。
分布式事务
事务相信大家不陌,事务的本质是要么一起向前冲,要么一起保持不动。对于MySQL的InnoDB来说,我们只需要执行begin、commit就行,有时候我们可能需要回滚rollback。但是这是在同一数据库的前提下,如果我们的数据表分库了或者说我们要操作的资源在不同的网络节点上该怎么办?这就得用到我们今天要说的分布式事务了,分布式事务有2PC、3PC、TCC等, 但是无论哪种都无法保证完美的ACID,我们来一起看看是怎么回事吧。
2PC
从名字可以看出它是分两个阶段的,所以它也叫做二阶段提交,即准备和提交,2PC要求有个事务的协调者,相比常规的事务,我们的请求是发给这个协调者的,然后由协调者帮我们协调各个节点资源的提交。
-
准备阶段:协调者会让各个参与事务的参与者,把除了提交之外所有的事情都干好,也就是就等着提交了
-
提交阶段:协调者收到各个参与者的准备消息后,根据准备情况通知各个参与者提交(commit)或者回滚(rollback)
可以发现整个过程非常依赖协调者,如果协调者挂了,那么整个分布式事务就不可用,所以一般建议协调者至少有个备份节点。
如果协调者在收到所有节点的ok之后,在准备发送commit消息的时候,由于网络问题,导致其中一个节点始终收不到消息,那么收不到消息的节点就会一直占着资源不释放,出现这种情况的时候,建议协调者有个重试功能,在commit失败之后,不停的重试,直至成功。2PC协议是一种强一致性协议,它是同步阻塞的,所以在高并发的场景它的性能可能还会有问题。
3PC
2PC存在一些问题,比如协调者从挂了到恢复后并不知道当前节点的状态,现在应该做什么(是该提交还是回滚等等),还有就是当发生网络问题的时候,无法通信的节点只会傻傻的等待,造成资源一直处于锁定状态。鉴于这些问题,出现了3PC。
首先3PC顾名思义,会分为3个阶段,分别是准备阶段、预提交阶段和提交阶段。
-
准备阶段:主要是询问参与者自身的状况,比如你的负载情况如何?能参与接下来的任务吧?
-
预提交阶段:除了commit之外的所有准备工作,就等着commit了
-
提交阶段:执行真正的commit或者rollback
如果在事务期间,有新的协调者顶替进来,它就可以根据一个参与者的状态来判断当前应该干嘛,比如如果一个参与者处于提交阶段,那么表明当前的事务正处于提交阶段。当因为网络问题某个节点一直收不到提交信息,那么此时也不会傻等了,会有超时时间,当超时时间过去了,节点可以自动提交,但是这里有个问题,对于参与者节点来说,当前应该是commit还是rollback呢?
其实2PC和3PC都无法保证绝对的一致性,因为某个参与者节点可能就是因为网络问题收不到消息,但是其他参与者节点已经提交了事务,一般为了预防这种问题,最好加一个报警,比如监控到事务异常的时候,通过脚本自动补偿差异的信息。
TCC
TCC事务的全程是Try、Commit、Cancel,TCC事务使用场景更贴近实际应用,因此它的使用也更广泛。
Try
:Try这个过程,一般表示锁定资源的过程,比如常见的下单,在try阶段,我们不是真正的减库存,而是把下单的库存给锁定住。
Commit
:真正的执行业务逻辑了,带提交的。
Cancel
:撤销,如果Commit失败可以把锁定的资源释放回来
TCC对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,代码改造成本高。在出现网络或者其他系统故障时,TCC要根据实际业务场景实现对应的回滚逻辑。Commit或者Cancel有可能会重试,因此对应的部分最好支持幂等。
最后其实上面3种分布式事务理论上都无法保证绝对的一致性,因为无法解决网络等带来的意外因素,要解决它,要么只能无限重试,但是这个无限重试最好通过消息队列+守护进程的方式来自动补数据,前提还是得保证消息队列不丢失数据。总之不仅仅是分布式事务会带来这些问题,分布式本身也会带来许许多多的问题,没有绝对的解决方案,只有更好的解决方案。
“java常见分布式事务理论怎么解决”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!
内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/123886.html