分布式ID,分布式锁,限流算法,微服务原则,CAP,BASE,双写一致性

分布式ID

  • UUID,缺点:页分裂占空间。

  • 数据库主键,缺点:改造复杂,多个库主键会重复。

  • 雪花算法,性能好,缺点:时钟回拨会重复。

‌雪花算法ID组成

  • 符号位,占用1位。

  • 时间戳。

  • 机器ID。

  • 序列号,12位,一毫秒生成4095个ID。

‌分布式锁应用场景

  • 系统是一个分布式系统,集群,Java的锁已经锁不住。

‌分布式锁解决方案

基于Redis

setnx key value ex 10s,Redisson的lock和unlock方法。

删除锁时,不是简单的delete key。有一种情况是,我加锁之后执行业务逻辑,设置锁的过期时间是10s,程序执行了15s,最后的5s中,有另外的程序也可以来加锁成功。我执行完逻辑后会删除其他程序的key,所以删除key之前先要get一下key。

上述情况,两个程序都拿到锁,可能造成数据不一致,所以要加一个看门狗机制,防止程序没执行完就释放锁。

基于ZooKeeper

临时节点,顺序节点。

‌Redis分布式死锁如何解决

  • 加锁需释放锁。

  • key的过期机制。

‌Redis如何做分布式锁

假设有俩服务A和B都希望获得锁,执行过程如下:

  1. 服务A为了获得锁,向Redis发起如下命令:

set productid:lock 0xx903001 nx ex 30000

其中,productid是业务主键,“0xx903001”是随机值,保证全局唯一。

  1. 服务B为了获得锁,向Redis发起同样的命令:

set productid:lock 0000111 nx ex 30000

由于Redis内已经存在同名key,服务B进入循环请求状态,直到获取锁。

  1. 服务A业务代码执行时长超过30s,key被删除。此时服务B执行,假设本次请求设置的value值为0000222。此时需要在服务A中对key进行续期。

4.服务A执行完毕,会释放锁,在删除key之前一定要判断自己的value与Redis的value是否一致。此外,删除锁涉及一系列逻辑,所以一般使用lua脚本。

‌基于Zookeeper的分布式锁

‌Zookeeper和Redis做分布式锁的区别

1.Redis只保证最终一致性,副本间的数据复制是异步进行,主从切换之后,可能有部分数据没有复制过去,会丢失锁,所以强一致性场景应该用zk。

2.使用zk集群的锁原理是使用zk的临时顺序节点,每次进行锁操作都要创建若干节点,完成后要释放节点,效率没有Redis高。

‌滑动时间窗口算法是什么?

​ 为了解决计数器算法的临界值的问题,发明了滑动窗口算法。在TCP网络通信协议中,就采用滑动时间窗口算法来解决网络拥堵问题。

​ 滑动时间窗口是将计数器算法中的实际周期切分成多个小的时间窗口,分别在每个小的时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的小时间窗口的总的请求数即可。

分布式ID,分布式锁,限流算法,微服务原则,CAP,BASE,双写一致性

在上图中,假设我们设置一分钟的请求阈值是100,我们将一分钟拆分成4个小时间窗口,这样,每个小的时间窗口只能处理25个请求,我们用虚线方框表示滑动时间窗口,当前窗口的大小是2,也就是在窗口内最多能处理50个请求。随着时间的推移,滑动窗口也随着时间往前移动,比如上图开始时,窗口是0:00到0:30的这个范围,过了15秒后,窗口是0:15到0:45的这个范围,窗口中的请求重新清零,这样就很好的解决了计数器算法的临界值问题。

​ 在滑动时间窗口算法中,我们的小窗口划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。

漏桶限流算法是什么?

​ 漏桶算法的原理就像它的名字一样,我们维持一个漏斗,它有恒定的流出速度,不管水流流入的速度有多快,漏斗出水的速度始终保持不变,类似于消息中间件,不管消息的生产者请求量有多大,消息的处理能力取决于消费者。

​ 漏桶的容量=漏桶的流出速度*可接受的等待时长。在这个容量范围内的请求可以排队等待系统的处理,超过这个容量的请求,才会被抛弃。

​ 在漏桶限流算法中,存在下面几种情况:

  1. 当请求速度大于漏桶的流出速度时,也就是请求量大于当前服务所能处理的最大极限值时,触发限流策略。

  2. 请求速度小于或等于漏桶的流出速度时,也就是服务的处理能力大于或等于请求量时,正常执行。

    漏桶算法有一个缺点:当系统在短时间内有突发的大流量时,漏桶算法处理不了。

令牌桶限流算法是什么?

​ 令牌桶算法,是增加一个大小固定的容器,也就是令牌桶,系统以恒定的速率向令牌桶中放入令牌,如果有客户端来请求,先需要从令牌桶中拿一个令牌,拿到令牌,才有资格访问系统,这时令牌桶中少一个令牌。当令牌桶满的时候,再向令牌桶生成令牌时,令牌会被抛弃。

​ 在令牌桶算法中,存在以下几种情况:

  1. 请求速度大于令牌的生成速度:那么令牌桶中的令牌会被取完,后续再进来的请求,由于拿不到令牌,会被限流。

  2. 请求速度等于令牌的生成速度:那么此时系统处于平稳状态。

  3. 请求速度小于令牌的生成速度:那么此时系统的访问量远远低于系统的并发能力,请求可以被正常处理。

令牌桶算法,由于有一个桶的存在,可以处理短时间大流量的场景。这是令牌桶和漏桶的一个区别。

你设计微服务时遵循什么原则?

  1. 单一职责原则:让每个服务能独立,有界限的工作,每个服务只关注自己的业务。做到高内聚。
  2. 服务自治原则:每个服务要能做到独立开发、独立测试、独立构建、独立部署,独立运行。与其他服务进行解耦。
  3. 轻量级通信原则:让每个服务之间的调用是轻量级,并且能够跨平台、跨语言。比如采用RESTful风格,利用消息队列进行通信等。
  4. 粒度进化原则:对每个服务的粒度把控,其实没有统一的标准,这个得结合我们解决的具体业务问题。不要过度设计。服务的粒度随着业务和用户的发展而发展。

​ 总结一句话,软件是为业务服务的,好的系统不是设计出来的,而是进化出来的。

CAP定理是什么?

​ CAP定理,又叫布鲁尔定理。指的是:在一个分布式系统中,最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • C:一致性(Consistency),数据在多个副本中保持一致,可以理解成两个用户访问两个系统A和B,当A系统数据有变化时,及时同步给B系统,让两个用户看到的数据是一致的。

  • A:可用性(Availability),系统对外提供服务必须一直处于可用状态,在任何故障下,客户端都能在合理时间内获得服务端非错误的响应。

  • P:分区容错性(Partition tolerance),在分布式系统中遇到任何网络分区故障,系统仍然能对外提供服务。网络分区,可以这样理解,在分布式系统中,不同的节点分布在不同的子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,导致整个节点环境被切分成了不同的独立区域,这就是网络分区。

    我们来详细分析一下CAP,为什么只能满足两个。看下图所示:

分布式ID,分布式锁,限流算法,微服务原则,CAP,BASE,双写一致性

  • ​ 用户1和用户2分别访问系统A和系统B,系统A和系统B通过网络进行同步数据。理想情况是:用户1访问系统A对数据进行修改,将data1改成了data2,同时用户2访问系统B,拿到的是data2数据。

    ​ 但是实际中,由于分布式系统具有八大谬论:

    • 网络相当可靠

    • 延迟为零

    • 传输带宽是无限的

    • 网络相当安全

    • 拓扑结构不会改变

    • 必须要有一名管理员

    • 传输成本为零

    • 网络同质化

    我们知道,只要有网络调用,网络总是不可靠的。我们来一一分析。

    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,因为买票必须满足数据的一致性,不然一个座位多卖了,对铁路运输都是不可以接受的。

BASE理论是什么?

由于CAP中一致性C和可用性A无法兼得,eBay的架构师,提出了BASE理论,它是通过牺牲数据的强一致性,来获得可用性。它由于如下3种特征:

  • Basically Available(基本可用):分布式系统在出现不可预知故障的时候,允许损失部分可用性,保证核心功能的可用。

  • Soft state(软状态):软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。、

  • Eventually consistent(最终一致性):最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

​ BASE理论并没有要求数据的强一致性,而是允许数据在一定的时间段内是不一致的,但在最终某个状态会达到一致。在生产环境中,很多公司,会采用BASE理论来实现数据的一致,因为产品的可用性相比强一致性来说,更加重要。比如在电商平台中,当用户对一个订单发起支付时,往往会调用第三方支付平台,比如支付宝支付或者微信支付,调用第三方成功后,第三方并不能及时通知我方系统,在第三方没有通知我方系统的这段时间内,我们给用户的订单状态显示支付中,等到第三方回调之后,我们再将状态改成已支付。虽然订单状态在短期内存在不一致,但是用户却获得了更好的产品体验。

‌双写一致性问题

先更新数据库,再删缓存,再加上一个延迟双删即可。

(3)先更新数据库,再删缓存

首先,先说一下。老外提出了一个缓存更新套路,名为《Cache-Aside pattern》。其中就指出

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

另外,知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。
这种情况不存在并发问题么?
不是的。假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
ok,如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
假设,有人非要抬杠,有强迫症,一定要解决怎么办?
如何解决上述并发问题?
首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成以后,再进行删除操作。
还有其他造成不一致的原因么?
有的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的一个问题,如果删缓存失败了怎么办,那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。这也是缓存更新策略(2)里留下的最后一个疑问。

上一篇:2021年最新大厂Java面试笔试题目,真香!


下一篇:字节跳动算法工程师总结:中高级java开发面试题