赞
踩
2023-06-19 18:50:07
出自B站 灰灰的Java面试
在高并发场景的架构里,幂等性是必须得保证的。比如说支付功能,用户发起支付,如果后台没有
做幂等校验,刚好用户手抖多点了几下,于是后台就可能多次受到同一个订单请求,不做幂等很容
易就让用户重复支付了,这样用户是肯定不能忍的。
解决方案:。
1,查询和删除不在幂等讨论范围,查询肯定没有幂等的说,删除:第一次删除成功后,后面来删
除直接返回0,也是返回成功。
2,建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存在唯一索引,并发
时新增异常时,再查询一次就可以了,数据应该已经存在了,返回结果即可)。
3,token机制:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交。前端
在数据提交前要向后端服务的申请token,token放到 Redis 或 JVM 内存,token有效时间。提交后
后台校验token,同时删除token,生成新的token返回。redis要用删除操作来判断token,删除成
功代表token校验通过,如果用select+delete来校验token,存在并发问题,不建议使用。
4,悲观锁悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用(另外还要考
虑id是否为主键,如果id不是主键或者不是 InnoDB 存储引擎,那么就会出现锁全表)。for update
5,乐观锁,给数据库表增加一个version字段,可以通过这个字段来判断是否已经被修改了
6,分布式锁,比如 Redis 、 Zookeeper 的分布式锁。单号为key,然后给Key设置有效期(防止支
付失败后,锁一直不释放),来一个请求使用订单号生成一把锁,业务代码执行完成后再释放锁。
7,保底方案,先查询是否存在此单,不存在进行支付,存在就直接返回支付结果。
场景:多个服务或者多个库,要保持在一个事务中。
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,
特别是在微服务架构中,几乎可以说是无法避免。
理论:ACID、CAP、BASE。
ACID
指数据库事务正确执行的四个基本要素:
CAP:cp,ap。
CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性
(Availability)、分区容忍性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同
时实现两点,不可能三者兼顾。一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。
可用性:在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。
分区容忍性:以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数
据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
BASE理论
BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到
强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
解决方案:seata,消息队列+本地事件表,事务消息,最大努力通知方案,tcc
两阶段提交2PC是分布式事务中最强大的事务类型之一,
流程:
两段提交就是分两个阶段提交:
第一阶段询问各个事务数据源是否准备好,投票阶段。
第二阶段才真正将数据提交给事务数据源。
为了保证该事务可以满足ACID,就要引入一个协调者(Cooradinator)。其他的节点被称为参与者
(Participant)。协调者负责调度参与者的行为,并最终决定这些参与者是否要把事务进行提交。
处理流程如下:
阶段一
a) 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
b) 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
c) 如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,
发送提交(commit)消息。两种情况处理如下:
情况1:当所有参与者均反馈 yes,提交事务
a) 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
b) 参与者执行 commit 请求,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack(应答)完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。
情况2:当有一个参与者反馈 no,回滚事务
a) 协调者向所有参与者发出回滚请求(即 rollback 请求)。
b) 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
c) 各参与者向协调者反馈 ack 完成的消息。
d) 协调者收到所有参与者反馈的 ack 消息后,即完成事务。
问题:
致。
优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证
强一致)。
缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
优化:
举例子。
案例:seata,lcn,tcc。
补偿性事务是什么:
TCC (Try Confifirm Cancel)是服务化的二阶段编程模型,采用的补偿机制:
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补
偿(撤销)操作。
它分为三个步骤:
1。Try 阶段主要是对业务系统做检测及资源预留。
2。Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默
认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
3。Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
使用场景:
业务需要:
举个例子,假入你要向A 转账,思路大概是:
我们有一个本地方法,里面依次调用步骤:
1、首先在 Try 阶段,要先调用远程接口把 你 和 A 的钱给冻结起来。
2、在 Confifirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
3、如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。
技术需要:
不同组件,无法在一个事务中完成。
优点:
性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据
的一致性。
可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动
管理器也变成多点,引入集群。
缺点:
TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。
唯一性:确保生成的ID是全网唯一的。
有序递增性:确保生成的ID是对于某个用户或者业务是按一定的数字有序递增的。
高可用性:确保任何时候都能正确的生成ID。
带时间:ID里面包含时间,不容易重复。
算法的核心思想是结合机器的网卡、当地时间、一个随记数来生成UUID。
优点:本地生成,生成简单,性能好,没有高可用风险
缺点:长度过长,存储冗余,且无序不可读,查询效率低
使用数据库的id自增策略,如 MySQL 的 auto_increment。并且可以使用两台数据库分别设置不同
步长,生成不重复ID的策略来实现高可用。
优点:数据库生成的ID绝对有序,高可用实现方式简单
缺点:需要独立部署数据库实例,成本高,有性能瓶颈
一次按需批量生成多个ID,每次生成都需要访问数据库,将数据库修改为最大的ID值,并在内存中
记录当前值及最大值。
优点:避免了每次生成ID都要访问数据库并带来压力,提高性能
缺点:属于本地生成策略,存在单点故障,服务重启造成ID不连续
Redis的所有命令操作都是单线程的,本身提供像 incr 和 increby 这样的自增原子命令,所以能保
证生成的 ID 肯定是唯一有序的。
优点:不依赖于数据库,灵活方便,且性能优于数据库;数字ID天然排序,对分页或者需要排
序的结果很有帮助。
缺点:如果系统中没有Redis,还需要引入新的组件,增加系统复杂度;需要编码和配置的工作
量比较大。
考虑到单节点的性能瓶颈,可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有5台
Redis。可以初始化每台 Redis 的值分别是1, 2, 3, 4, 5,然后步长都是 5。
Twitter 利用 zookeeper 实现了一个全局ID生成的服务 Snowflflake
如上图的所示,Twitter 的 Snowflflake 算法由下面几部分组成:
1位符号位:
由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用
的ID一般都是正数,所以最高位为 0。
41位时间戳(毫秒级):
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间
戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41
位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年 。
10位数据机器位:包括5位数据标识位和5位机器标识位,这10位决定了分布式系统中最多可以部署 1 << 10 = 1024个节点。超过这个数量,生成的ID就有可能会冲突。
12位毫秒内的序列:
这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096个ID
加起来刚好64位,为一个Long型。
优点:高性能,低延迟,按时间有序,一般不会造成ID碰撞
缺点:需要独立的开发和部署,依赖于机器的时钟
UidGenerator是百度开源的分布式ID生成器,基于于snowflflake算法的实现,看起来感觉还行。不
过,国内开源的项目维护性真是担忧。
Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,但也需要依赖关系数据库、Zookeeper等中间件。
轮询负载均衡算法:RR ,Round Robin,挨个发,适合于所有服务器硬件都相同的场景。一个一个来,如果有两个服务器,第一次是你,第二次就是我。
代码实现:用i 保存 取 服务器的 下标。第一次来 取0,第二次来取1 ,第三次来 取 0。
**加权轮询算法:**weighted round robin ,wrr,按照权重不同来分发,基本上是基于配置。
代码实现:
比如:两个服务权重分别是6和4,我们的方法,在1-10之间取 随机数,比如取到 1-6,就走6的权重,取到7-10,就走4权重的服务。
随机轮询算法:Random
代码实现:这个就随意了。
**最少链接:**Least connections,记录每个服务器正在处理的 连接数 (请求数),将新的请求 分发到最少连接的服务器上,这是最符合负载均衡的算法
代码实现:放到redis里,做个hash。
可以用:incr。
(redis> SET key 20
OK
redis> INCR key(integer) 21
redis> GET key # 数字值在 Redis 中以字符串的形式保存"21"
)
每次调用一次,服务次数+1。
原地址散列:Source Hashing,根据请求来源的ip地址 进行hash计算,只要原地址不变,每次请求映射来的后面提供服务的节点也不变。这有利于进行session信息的维护。
计数器算法,是指在指定的时间周期内,累加访问的次数,达到设定的阈值时,触发限流策略。下一个时间周期进行访问时,访问次数清零。此算法无论在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性,再结合key的过期时间,即可轻松实现,计算器算法如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WQqkF6ds-1687171674753)(file:///C:\Users\Administrator\AppData\Local\Temp\ksohtml25964\wps1.png)]
从上图看出,设置一分钟的阈值是100,在0:00到1:00内请求数是60,当到1:00时,请求数清零,从0开始计算,这时在1:00到2:00之间能处理的最大的请求为100,超过100个的请求,系统都拒绝。
这个算法有一个临界问题,例如在图中,在0:00到1:00内,只在0:50有60个请求,而在1:00到2:00之间,只在1:10有60个请求,虽然在两个一分钟的时间内,都没有超过100个请求,但是在0:50到1:10这20秒内,确有120个请求,虽然在每个周期内,都没超过阈值,但是在这20秒内,已经远远超过了原来设置的1分钟内100个请求的阈值。
为了解决计数器算法的临界值的问题,发明了滑动窗口算法。在TCP网络通信协议中,就采用滑动时间窗口算法来解决网络拥堵问题。
滑动时间窗口是将计数器算法中的时间周期切分成多个小的时间窗口,分别在每个小的时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的小时间窗口的总的请求数即可,如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dqd8egeV-1687171674754)(file:///C:\Users\Administrator\AppData\Local\Temp\ksohtml25964\wps2.png)]
在图中,假设设置一分钟的请求阈值是100,将一分钟拆分成4个小时间窗口,这样,在每个小的时间窗口内,只能处理25个请求,用虚线方框表示滑动时间窗口,当前窗口的大小是2,也就是在窗口内最多能处理50个请求。随着时间的推移,滑动窗口也随着时间往前移动,例如图开始时,窗口是0:00到0:30的这个范围,过了15秒后,窗口是0:15到0:45的这个范围,窗口中的请求重新清零,这样就很好的解决了计数器算法的临界值问题。
在滑动时间窗口算法中,小窗口划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。
漏桶算法的原理就像它的名字一样,维持一个漏斗,它有恒定的流出速度,不管水流流入的速度有多快,漏斗出水的速度始终保持不变,类似于消息中间件,不管消息的生产者请求量有多大,消息的处理能力取决于消费者。
漏桶的容量=漏桶的流出速度*可接受的等待时长。在这个容量范围内的请求可以排队等待系统的处理,超过这个容量的请求,才会被抛弃。
在漏桶限流算法中,存在下面几种情况。
(1)当请求速度大于漏桶的流出速度时,也就是请求量大于当前服务所能处理的最大极限值时,触发限流策略。
(2)请求速度小于或等于漏桶的流出速度时,也就是服务的处理能力大于或等于请求量时,正常执行。
漏桶算法有一个缺点,当系统在短时间内有突发的大流量时,漏桶算法处理不了。
令牌桶算法,是增加一个大小固定的容器,也就是令牌桶,系统以恒定的速率向令牌桶中放入令牌,如果有客户端来请求,先需要从令牌桶中拿一个令牌,拿到令牌,才有资格访问系统,这时令牌桶中少一个令牌。当令牌桶满的时候,再向令牌桶生成令牌时,令牌会被抛弃。
在令牌桶算法中,存在以下几种情况。
(1)请求速度大于令牌的生成速度:那么令牌桶中的令牌会被取完,后续再进来的请求,由于拿不到令牌,会被限流。
(2)请求速度等于令牌的生成速度:那么此时系统处于平稳状态。
(3)请求速度小于令牌的生成速度:那么此时系统的访问量远远低于系统的并发能力,请求可以被正常处理。
令牌桶算法,由于有一个桶的存在,可以处理短时间大流量的场景。
对数据库进行:分区、分库分表,主从架构(读写分离)。
分区:隔离数据访问。
水平分库/表,各个库和表的结构一模一样,数据量不一样。
垂直分库/表,各个库和表的结构不一样,数据量一样。
读写分离:主机负责写,从机负责读。
CAP定理,又叫布鲁尔定理。指的是在一个分布式系统中,最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。
l C:一致性(Consistency),数据在多个副本中保持一致,可以理解成两个用户访问两个系统A和B,当A系统数据有变化时,及时同步给B系统,让两个用户看到的数据是一致的。
l A:可用性(Availability),系统对外提供服务必须一直处于可用状态,在任何故障下,客户端都能在合理时间内获得服务端非错误的响应。
l P:分区容错性(Partition tolerance),在分布式系统中遇到任何网络分区故障,系统仍然能对外提供服务。网络分区,可以这样理解,在分布式系统中,不同的节点分布在不同的子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,导致整个节点环境被切分成了不同的独立区域,这就是网络分区。
来详细分析一下CAP,为什么只能满足两个,如图所示。
用户1和用户2分别访问系统A和系统B,系统A和系统B通过网络进行同步数据。理想情况是用户1访问系统A对数据进行修改,将data1改成了data2,同时用户2访问系统B,拿到的是data2数据。
但是实际中,由于分布式系统具有八大谬论。
(1)网络相当可靠。
(2)延迟为零。
(3)传输带宽是无限的。
(4)网络相当安全。
(5)拓扑结构不会改变。
(6)必须要有一名管理员。
(7)传输成本为零。
(8)网络同质化。
只要有网络调用,网络总是不可靠的,来一一分析。
(1)当网络发生故障时,系统A和系统B没法进行数据同步,也就是不满足P,同时两个系统依然可以访问,那么此时其实相当于是两个单机系统,就不是分布式系统了,所以既然是分布式系统,P必须满足。
(2)当P满足时,如果用户1通过系统A对数据进行了修改将data1改成了data2,也要让用户2通过系统B正确的拿到data2,那么此时是满足C,就必须等待网络将系统A和系统B的数据同步好,并且在同步期间,任何人不能访问系统B(让系统不可用),否则数据就不是一致的。此时满足的是CP,牺牲的是可用性。
(3)当P满足时,如果用户1通过系统A对数据进行了修改将data1改成了data2,也要让系统B能继续提供服务,那么此时,只能接受系统A没有将data2同步给系统B(牺牲了一致性)。此时满足的就是AP,牺牲了数据的一致性。
注册中心Eureka就是满足 的AP,它并不保证C。而Zookeeper是保证CP,它不保证A。在生产中,A和C的选择,没有标准的规定,是取决于自己的业务的。例如12306,是满足CP,因为买票必须满足数据的一致性,不然一个座位多卖了,对铁路运输都是不可以接受的。
由于CAP中一致性C和可用性A无法兼得,eBay的架构师,提出了BASE理论,它是通过牺牲数据的强一致性,来获得可用性。它由于如下3种特征。
l Basically Available(基本可用):分布式系统在出现不可预知故障的时候,允许损失部分可用性,保证核心功能的可用。
l Soft state(软状态):软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
l Eventually consistent(最终一致性):最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
BASE理论并没有要求数据的强一致性,而是允许数据在一定的时间段内是不一致的,但在最终某个状态会达到一致。在生产环境中,很多公司,会采用BASE理论来实现数据的一致,因为产品的可用性相比强一致性来说,更加重要。例如在电商平台中,当用户对一个订单发起支付时,往往会调用第三方支付平台,例如支付宝支付或者微信支付,调用第三方成功后,第三方并不能及时通知我方系统,在第三方没有通知我方系统的这段时间内,给用户的订单状态显示支付中,等到第三方回调之后,再将状态改成已支付。虽然订单状态在短期内存在不一致,但是用户却获得了更好的产品体验。
可靠消息最终一致性方案。它是保证事务最终一致性的一种方案,允许数据在业务中出现短暂的不一致状态。
可靠消息最终一致性方案是指,当事务的发起方(事务参与者,也就是消息发送者)执行完本地事务后,同时发出一条消息,事务参与方(事务参与者,也就是消息的消费者)一定能够接收消息并可以成功处理自己的事务,如图所示。
这里面强调两点。
l 可靠消息:发起方一定得把消息传递到消费者。
l 最终一致性:最终发起方的业务处理和消费方的业务处理得完成,达成最终一致。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T5wSMulp-1687171674760)(file:///C:\Users\Administrator\AppData\Local\Temp\ksohtml25964\wps4.png)]
上图是一个可靠消息服务最终一致性方案的流程图,从图中可以看出事务发起方将消息发送给消息中间件,事务消费方从消息中间件接收消息,事务发起方和消息中间件之间,事务消费方和消息中间件之间,都有网络通信,由于网络通信的不确定性,这块会导致数据的问题。下面针对导致的问题来分别进行解决。
(1)事务发起方本地事务和消息发送之间的原子性问题。
此问题是本地事务执行成功,消息必须发出去,否则丢弃消息,即本地事务执行成功和消息的发送成功,要么都成功,要么都失败。
来一段伪代码参考一下。
begin transaction;
发送消息;
操作数据库;
commit transaction;
这种情况下,如果发送消息成功,数据库操作失败,则无法保证原子性。调换一下顺序。
begin transaction;
操作数据库;
发送消息;
commit transaction;
这种情况下,如果操作数据库出错,回滚,不影响数据;如果发送消息出错,也回滚,不影响数据。这么一看似乎可以保证原子性,但是会有一种情况,发送消息响应超时,导致数据库回滚,但是消息已经发送成功了。这时原子性还是无法保证的,这个时候就需要人工补偿了。
(2)事务消费方和消息消费的原子性问题。
此时要保证事务消费方必须能接受到消息,如果由于程序故障,导致事务消费方重启,那么需要消息中间件要有消息重发机制;由于网络延时的存在,当事务消费方消费消息成功,没有向消息中间件响应时,而消息中间件由于重发机制,会再次投递消息,就导致了消息重复消费的问题。此时在消费方要有幂等性解决方案。
在此方案中,需要借助一个消息中间件RocketMQ,它是阿里巴巴的一个开源消息中间件,阿里集团内部的消息都运行在RocketMQ之上,可见它的性能之强。它在4.3之后的版本支持了事务消息,为解决分布式事务提供了便利 。
RocketMQ的事务消息,主要为了解决事务生产方执行业务和消息发送的原子性问题。事务消息流程如图所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EuOf1sti-1687171674761)(file:///C:\Users\Administrator\AppData\Local\Temp\ksohtml25964\wps5.png)]
图事务消息流程
介绍流程之前,先介绍一下事务消息的几种状态:
l TransactionStatus.CommitTransaction: 提交状态,它允许消费者消费此消息。
l TransactionStatus.RollbackTransaction: 回滚状态,它代表该消息将被删除,不允许被消费者消费。
l TransactionStatus.Unknown: 中间状态,它代表需要检查消息队列来确定状态。
具体流程如下。
(1)发送half message。在执行本地业务之前,先向消息队列发送一条事务消息,此时叫做half message,此时消息被标记为(Prepared预备状态),此时的消息是无法被消费者消费的,需要生产者对消息进行二次确认后,消费者才能去消费它。
(2)消息队列回应half message发送成功。
(3)当事务发起方收到消息队列的成功响应之后,开始执行本地业务。
(4)如果本地事务执行成功,则向消息队列发送half message的确认,这样事务消费方就可以消费消息了。
(5)如果本地事务执行失败,则向消息队列发送half message的回滚,删除half message。事务消费方就无法消费消息。
(6)回查机制。当第4步无论是提交还是回滚,由于网络闪断,生产者应用重启等原因,导致生产者无法对消息队列中的half message进行二次确认(即上面的第4步骤,发送提交或者回滚消息)时,消息队列中的half message就不知道应该怎么办了。此时消息队列会定时扫描长期处于half message的消息,并发起一个回查机制,来确认此时的half message应该是提交还是回滚。此时,消息队列主动询问生产者该消息的最终状态(提交还是回滚),即为消息的回查机制。
服务太多,无法管理。
还是类比一下通讯录。
生活中的通讯录,存储的信息如表所示。
表通讯录示例
姓 名 | 电话 |
---|---|
张三 | 139xxxxxxxx |
同理,注册中心是不是也应该这么存?
注册中心示例
服务名 | 服务信息 |
---|---|
order-service(订单服务) | 服务的IP地址,服务端口,服务对外提供的URL等 |
那在程序中如何存储呢?从上面的存储内容和存储方式,是不是能想到Java中常用的数据结构Map?
打开Eureka的源码,印证一下,在AbstractInstanceRegistry.java 中,有一个属性。
private final ConcurrentHashMap<String, Map<String, Lease>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
它的存储结构是用的ConcurrentHashMap,它的键值是服务名,值是个Map。而值的Map中键是服务实例ID,值是租约(租约里面包括服务信息)。
key: value
<服务名:<服务实例:ip+port,服务配置等等。>>
大家想一下,服务启动后,是否需要向注册中心进行注册?所以注册中心需要提供一个接口,让服务调用它来进行服务的登记注册,这就是注册中心的第一个功能, 接受服务注册 。
服务注册完成后,注册中心得知道这个服务是否还是有效服务?所以此时需要服务定期地告诉注册中心,自己的工作状态(是否可用)。此时需要注册中心提供第二个功能, 接受服务心跳 。
当服务下线时,要通知注册中心自己要下线,注册中心需要提供对应的接口,来让服务调用。此时需要注册中心的第三个功能, 接受服务下线 。
如果服务挂了,没有及时通知注册中心,此时注册中心也发现服务,最近没有发送心跳。注册中心要主动剔除挂了的服务。此时需要注册中心第四个功能, 服务剔除 。
注册表中存储的信息,是要供其他服务查询的,就像通讯录一样,是要供主人查阅的,所以注册中心还需要第五个功能, 查询注册表中的服务信息 。
一般微服务中,每个服务都要避免单点故障,注册中心也要做集群,所以还要涉及到注册中心间,注册信息的同步。这就是注册中心的第六个功能, 注册中心集群间注册表的同步 。
如果让你开发,是不是也能开发出一个注册中心呢?其实本质就是一个web服务,提供上面分析的5个接口,供服务调用。这就是平时所说的 注册中心服务端 。那对应的调用注册中心的服务(业务服务),一般称之为 注册中心客户端 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iWR4sKqC-1687171674762)(file:///C:\Users\Administrator\AppData\Local\Temp\ksohtml25964\wps6.jpg)]
很多求职者对RESTful风格有误解,认为它是一个新的协议,其实他就是普通的http请求。其中REST表示Resource Representational State Transfer,直接翻译即“资源表现层状态转移”。
Resource代表互联网资源。所谓“资源”是网络上的一个实体,或者说网上的一个具体信息。它可以是一段文本、一首歌曲、一种服务,可以使用一个URI指向它,每种“资源”对应一个URI(统一资源标识符Uniform Resource Identifier)。
Representational是“表现层”意思。“资源”是一种消息实体,它可以有多种外在的表现形式,把“资源”具体呈现出来的形式叫作它的“表现层”。例如说文本可以用TXT格式进行表现,也可以使用XML格式、JSON格式和二进制格式;视频可以用MP4格式表现,也可以用AVI格式表现。URI只代表资源的实体,不代表它的形式。它的具体表现形式,应该由HTTP请求的头信息Accept和Content-Type字段指定,这两个字段是对“表现层”的描述。
State Transfer是指“状态转移”。客户端访问服务的过程中必然涉及数据和状态的转化。如果客户端想要操作服务端资源,必须通过某种手段,让服务器端资源发生“状态转移”。而这种转化是建立在表现层之上的,所以被称为“表现层状态转移”。客户端通过使用HTTP协议中的常用的四个动词来实现上述操作,它们分别是获取资源的GET、新建或更新资源的POST、更新资源的PUT和删除资源的DELETE。
RestTemplate是由Spring提供,用于封装HTTP调用,它可以简化客户端与HTTP服务器之间的交互,并且它强制使用RESTful风格。它会处理HTTP连接和关闭,只需要使用者提供服务器的地址(URL)和模板参数。
RESTful其实是一种风格并不是一种协议。http协议。
/user
post
put
delete
get
大家看一下下面的服务调用场景。C服务和D服务调用B服务,B服务调用A服务。在下面情况1中,服务正常调用。服务在运行过程中,A服务发生故障(网络延时,服务异常,负载过大无法及时响应),系统变成了情况2。由于B服务调用A服务,A服务出故障,导致B服务调用A的代码处也出故障,此时B服务也出故障了,系统变成了情况3。以此类推,系统最终发展成A、B、C、D所有的服务都出错了,整个系统崩塌了。这就是雪崩,如图所示。
雪崩效应
微服务系统之间通过互相调用来实现业务功能,但每个系统都无法百分之百保证自身运行不出问题。在服务调用中,很可能面临依赖服务失效的问题(网络延时,服务异常,负载过大无法及时响应),导致服务雪崩,这对于一个系统来说是灾难性的。因此需要一个组件,能提供强大的容错能力,当服务发生异常时,能提供保护和控制,把影响控制在较小范围内,不要造成所有服务的雪崩。
什么时候恢复系统?
熔断开关状态:开(走降级的方法),关闭(正常的调用),半开()
从两方面来阐述。
(1)相似性:
①目的一致:都是从可用性和可靠性着想,为防止系统的整体响应缓慢甚至崩溃,而采用的技术手段。
②最终表现类似:对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用。
③粒度一致:都是服务级别的。
④自治性要求很高:熔断模式一般都是服务基于策略的自动触发,降级虽说可人工干预,但在微服务架构下,完全靠人显然不可能,开关预置、配置中心都是必要手段。
(2)区别:
①触发原因不一样:服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑。
②管理目标的层次不一样:熔断时一个框架级的处理,每个服务都需要,而降级一般有业务层级之分,例如,降级一般在服务调用的上层处理。
分流:负载均衡,消息队列,数据库拆分
导流:缓存,cdn
并行/发:看具体业务。
进行微服务设计时,服务的数量相对于单体应用来说,会比较多(你在公司中,有多少个服务)。考虑的重点就是如何准确识别系统的隔离点,也就是系统的边界。只有每个服务的边界确定了,才能在以后的开发中做到更好的协作。识别系统的隔离点。
结合具体业务。电商。负载均衡。不得不拆。
问的抽象的问题,一定举例子。
(1)单一职责原则。让每个服务能独立,有界限的工作,每个服务只关注自己的业务。做到高内聚,服务和服务之间做到低耦合。
(2)服务自治原则。每个服务要能做到独立开发、独立测试、独立构建、独立部署,独立运行,与其他服务进行解耦。
(3)轻量级通信原则。让每个服务之间的调用是轻量级,并且能够跨平台、跨语言。例如采用RESTful风格,利用消息队列进行通信等。
(4)粒度进化原则。对每个服务的粒度把控,其实没有统一的标准,这个得结合解决的具体业务问题。不要过度设计。服务的粒度随着业务和用户的发展而发展。
还有一句话别忘了:软件是为业务服务的,好的系统不是设计出来的,而是进化出来的。
如果接入过支付宝或者微信的支付接口,会遇到这样一种流程。例如,APP调用支付宝或微信的SDK(Software Development Kit软件开发工具包)进行了支付,钱已经从用户的支付宝或微信账户,转到了公司(开发APP的公司)的支付宝或微信账户上,但是支付系统,并不知道钱是否已经支付成功,需要支付宝或者微信回调公司的支付系统,才能进行后续的业务,如图所示。
这其实就是一个最大努力通知的解决方案。在方案中主要保证两点:
(1)有一定的消息重复通知机制。因为接收通知方(图中的我方支付系统)可能没有接收到通知,此时要有一定的机制对消息进行重复通知。
(2)消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。
落地:
这种解决方案,其实针对内部系统和外部系统,有不同的做法。
(1)公司内部系统。针对公司内部系统来做的话,可以通过系统直接订阅消息队列来完成。因为都是自己的系统,直接订阅就可以。
(2)公司外部系统。针对公司外部系统来做的话,直接让消费方订阅消息队列就有点不合适了,毕竟不能让两家公司同时对一个消息队列进行操作,所以此时,可以在内部写一个程序来订阅消息队列,通过RPC的方式,调用消费方,使其被动的的接受通知消息。在接支付宝和微信时,一般都是采用这种方式。
采用微服务会带来更清晰的业务划分和更好的可扩展性,在很多企业中十分流行。支持微服务的技术栈也是多种多样。当前主流的是Spring Cloud和Dubbo。简单做一下对比,如表所示。
Spring Cloud和Dubbo对比
Spring Cloud | Dubbo | |
---|---|---|
注册中心 | Eureka,Nacos,Consul,ETCD,Zookeeper | Zookeeper |
服务间调用方式 | RESTful API 基于 http协议 | RPC基于dubbo协议 |
服务监控 | Spring Boot Admin | Dubbo-Monitor |
熔断器 | Spring Cloud Circuit Breaker | 不完善 |
网关 | Zuul,Gateway | 无 |
配置中心 | Config,Nacos | 无 |
服务追踪 | Sleuth+Zipkin | 无 |
数据流 | Spring Cloud Stream | 无 |
批量任务 | Spring Cloud Task | 无 |
消息总线 | Spring Cloud Bus | 无 |
从上面的比较可以看出,Spring Cloud的功能比Dubbo更全面,更完善,并且作为Spring 的旗舰项目,它可以与Spring其他项目无缝结合,完美对接,整个软件生态环境比较好。
Spring Cloud就像品牌机,整合在Spring的大家庭中,并做了大量的兼容性测试,保证了机器各部件的稳定。
Dubbo就像组装机,每个组件的选择自由度很高,但是如果你不是高手,如果你选择的某个组件出问题,就会导致整个机器的宕机,造成整体服务的可不用。
@LoadBalanced
RestTemplate
重点答以下三点。
(1)通过拦截器对被注解@LoadBalanced修饰的RestTemplate进行拦截。
(2)将RestTemplate中调用的服务名,解析成具体的IP地址,由于一个服务名会对应多个地址,那么在选择具体服务地址的时候,需要做负载均衡。
(3)确定目标服务的IP和PORT后,通过Httpclient进行http的调用。
(1)Authentication(认证) 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。
(2)Authorization(授权) 发生在 Authentication(认证) 之后。授权,它主要掌管访问系统的权限。例如有些特定资源,只能是具有特定权限的人才能访问例如admin,有些对系统资源操作例如删除、添加、更新只能特定人才具有。
双写一致性。db,cache。
首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出
失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从cache中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。
会有问题吗?
假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生。
(1)缓存没有。
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存。
ok,如果发生上述情况,确实是会发生脏数据。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pzoIzcE3-1687171674765)(file:///C:\Users\Administrator\AppData\Local\Temp\ksohtml25964\wps8.jpg)]
这件事情会发生,那么发生的概率多大呢?
发生上述情况有一个先天性条件:
就一条,读操作正常情况下,比写操作快很多,所以上述4条同时满足的概率是极低的。
(PS:就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。
可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
)
真出现了怎么办?延时双删。
Cache aside 机制是一种 简单有效的缓存更新机制,应用非常广泛,所以叫做cache aside,缓存在边上。就是说以数据库为主,写完有空再处理边上的 缓存。
在Cache Aside中,有概率虽然很低出现数据不一致的情况,我们也用了延迟双删,但还是比较复杂。但要想避免缓存不一致的出现也很简单即进行写入操作时,直接将结果写入缓存,而再从缓存同步写入到数据提供方。等写入数据提供方操作结束后,写入操作才被返回。
这就是Read/Write Through写入机制。
在这种机制下,调用方只需要和缓存打交道,而不需要关心缓存后方的数据提供方。而由缓存来保证自身数据和数据提供方的一致性。
结论:读操作只和缓存打交道,直接读取缓存的结果;写操作的话,调用方写入缓存,再由缓存同步写入数据提供方。
和Cache-Aside的区别?
在Cache Aside机制中,数据写入缓存的操作,是由调用方的查询结果触发的,
而在Read/write through 机制中,则需要缓存在启动时,自身完成将所有数据从数据提供方读入缓存的过程(在项目启动的时候,其实初始化什么也没有,也没有什么需要读取的,一会有修改,缓存就是新的数据,也不用读)。
比较一下Cache Aside和Read/write through机制。在Cache aside中,缓存只是一个辅助的存在,即使缓存不工作,调用方也可以通过数据提供方完成所有的读写操作,正如其名,缓存在边上,像胯子。
而在Read/write through中,缓存直接对接了调用方,屏蔽了数据提供方,这就意味着缓存系统不可或缺,要求十分可靠。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。