分布式相关面试题

3,825 阅读19分钟

先来看看简单的分布式系统机架构图 初步了解一下分布式系统是个什么玩意

image.png

为什么要将系统进行拆分

  1. 要是不拆分,一个大系统几十万行代码,比如20个人维护一份代码,简直是悲剧啊。代码经常改着改着就冲突了,各种代码冲突和合并要处理,非常耗费时间;经常我改动了我的代码,你调用了我,导致你的代码也得重新测试,麻烦的要死,然后每次发布都是几十万行代码的上线,可能每次上线都要做很多的检查,很多异常的问题的处理,简直是又麻烦又痛苦,而且我现在打算把技术升级到最新的spring版本,还不行,因为这可能导致你的代码报错,我又不敢随意乱改技术。线上出现了问题,每个人都要紧张的坐在电脑前,检查日志,看自己负责的那一块儿是不是有什么问题
  2. 拆分了以后,整个世界清爽了,比如几十万行代码的系统,拆分成20个服务,平均每个服务就1~2万行代码,每个服务部署到单独的机器上,20个码农,每个人维护一个服务,是自己独立的代码,跟别人没关系,再也没有代码冲突,每次测试就测试自己的代码就可以了,上线就上线自己的工程,技术上想怎么升级就怎么升级,保持接口定义不变就可以了

如何进行系统拆分?

系统拆分分布式系统,拆成多个服务,拆成微服务的架构,拆很多轮的。上来一个构架师第一轮就给拆好勒,第一轮;团队继续扩大,拆好的某个服务,刚开始是1个人维护1万行代码,后来业务系统越来越负载,这个服务是10万行代码,5个人,第二轮,1个服务->5个服务,每个服务2万行代码,每人负责一个服务

如果是多人维护一个服务,<=3个人维护这个服务,最理想的情况下,几十个人,1个人负责1个或2~3个服务

分布式系统中接口的幂等性该如何保证?比如不能重复扣款?

面试题

分布式服务接口的幂等性如何设计?(比如不能重复扣款)

面试官心里分析

从问这个问题开始,面试官就已经进入了实际的生产问题的面试了

一个分布式系统中的某个接口,要保证幂等性,该如何保证?这个事其实是你做分布式系统的时候必须要考虑的一个生产环境的技术问题,啥意思呢?

你看,假如你有个服务提供了一个接口,结果这服务部署了5台机器,接着有个接口就是付款接口,然后人家用户在前端操作的时候,不知道为啥,总之就是一个订单不小心发起了两次支付请求,然后这两次请求分散在了不同的机器上,这个时候就尴尬了,不小心扣了两次款

或者订单系统在调用的时候,由于网络原因超时了,然后订单系统因为重试机制,重试了,支付系统有收到了两个相同的请求,因为负载均衡分别落在了不同的机器上运行,又出现了重复扣款。

面试题剖析

所谓幂等性,就是说一个接口,多次发起同一个请求,你这个接口得保证结果是准确得。比如不能多扣款。不能多插入一条数据,不能将统计值多加了1,这就是幂等性。

其实保证幂等性主要是三点:

(1)对于每个请求必须有一个唯一的标识,举个例子:订单支付请求,肯定得包含订单ID,一个订单ID最多支付一次,对吧

(2)每次处理完请求之后,必须有一个记录标识这个请求处理过了,比如说常见得方案是再mysql中记录个状态啥得,比如支付之前记录一条这个订单得支付流水,而且支付流水采用order id作为唯一键(unique key)。只有成功插入这个支付流水,才可以执行实际得支付扣款

(3)每次接收请求需要进行判断之前是否处理过得逻辑处理,比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,order id已经存在了,唯一键约束生效,报错插入不进去得。然后你就不用再扣款了。

你如果不使用数据做的话,可以用redis,order id pay 没有就支付,有就不支付了

分布式系统中的接口调用如何保证顺序性?

image.png

解决方案:可以接入MQ,如果是系统A使用多线程处理的话,可以使用内存队列,来保证顺序性,如果你要100%的顺序性,当然可以使用分布式锁来搞,会影响系统的并发性。

说说zookeeper一般都有哪些使用场景?

面试官心里分析

其实说实话,问这个问题,一般就是看看你是否了解ZK,因为zk是分布式系统中很常见的一个基础系统。而且问的话常问的就是说ZK的使用场景是什么?看你知不知道一些基本的使用场景,但是其实zk挖深了自然是可以问的很深很深。

面试题剖析

大致来说,zk的使用场景如下,我就简单举几个例子,大家能说几个就好了

(1)分布式协调:这个其实就是zk很经典的一个用法,简单来说,就好比,你系统A发送个请求到mq,然后B消费了之后处理。那A系统如何指导B系统的处理结果?用zk就可以实现分布式系统之间的协调工作。A系统发送请求之后可以在zk上对某个节点的值注册个监听器,一旦B系统处理完了就修改zk那个节点的值,A立马就可以收到通知,完美解决。

image.png

(2)分布所锁:对某一个数据联系发出两个修改操作,两台机器同时收到请求,但是只能一台机器先执行另外一个机器再执行,那么此时就可以使用zk分布式锁,一个机器接收到了请求之后先获取zk上的一把分布式锁,就是可以去创建一个znode,接着执行操作,然后另外一个机器也尝试去创建那个znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等等一个机器执行完了自己再执行。

image.png

(3)配置信息管理:zk可以用作很多系统的配置信息的管理,比如kafka,storm等等很多分布式系统都会选用zk来做一些元数据,配置信息的管理,包括dubbo注册中心不也支持zk么

image.png

(4)HA高可用性:这个应该是很常见的,比如hdfs,yarn等很多大数据系统,都选择基于zk来开发HA高可用机制,就是一个重要进程一般会主备两个,主进程挂了立马通过zk感知到切换到备份进程。

image.png

分布式锁是啥?对比下redis和zk两种分布式锁的优劣?

使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

面试官心里分析

其实一般问问题,都是这么问的,先问问你zk,然后其实是要过度的zk关联的一些问题里去,比如分布式锁,因为在分布式锁系统开发中,分布式锁的使用场景还是很常见的。

面试题剖析

(1)reids分布式锁

官方叫做RedLock算法,是redis官方支持的分布式锁算法。

这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点或者这个锁就可以加可以释放)

第一个问题,如果就是redis里创建一个key算加锁

set my:lock 随机值 NX PX 30000,这个命令就ok,这个NX的意思就是只有key不存在的时候才会设置成功,PX 30000的意思是30秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。

释放锁就是删除key,但是一般可以用lua脚本删除(保持原子性),判断value一样才删除:

if redis.call("get",KEY[1])==ARGV[1] then
    return redis.call("del",KEY[1])
else
    return 0
end

为啥要用随机值呢(保证唯一性)?因为如果某个客户端获取了锁,但是阻塞了很长的时间才执行完(可以使用守护线程给锁延时),此时可能已经自动释放锁了,此时可能别的客户端已经获取到了锁,要是你这个时候直接删除key的话会有问题,所以得用随机值加上上面得lua脚本来释放锁。

image.png

但是这样肯定是不行得,因为如果是普通的redis单实例,那就是单点故障,或者是redis普通主从,那redis主从异步复制,如果主节点挂了,key还没有同步到从节点,此时从节点切换为主节点,别人就会拿到锁。

第二个问题,RedLock算法

这个场景是假设有一个reids cluster,有5个redis master实例,然后执行如下步骤获取一把锁:

  1. 获取当前时间戳,单位是毫秒
  2. 跟上面类似,轮流尝试在每个master节点上创建锁,过期时间比较短,一般就几十毫秒
  3. 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n/2+1)
  4. 客户端计算建立好锁的时间,如果成功建立锁的时间小于超时时间,就算建立成功
  5. 要是锁建立失败了,那么就依次删除这个锁
  6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

(2)zk分布式锁

zk分布式锁,其实做的比较简单,就是某个节点尝试创建临时znode(防止死锁呗),此时创建成功了就获取了这个锁,这个时候别的客户端来创建锁会失败,只能注册监听器来监听这个锁,释放锁就是删除这个znode,一旦释放掉就会反向通知客户端,然后等待着的客户端就可以再次尝试重新加锁。

image.png

image.png

还可以通过创建临时顺序节点来实现,架构模型更易懂

(3)redis分布式锁和zk分布式锁的对比

redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能

zk分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较低

另外一点就是,如果redis获取锁的那个客户端bug了,或者挂了,那么等待超时时间之后才能释放锁,而zk的话,因为创建的是临时节点,只要客户端挂了,znode就没了,此时就会自动释放锁

个人实践认为zk的分布式锁比redis的分布式更可靠,而且模型简单易用

说说你们的分布式session方案是啥?怎么做的?

面试题

集群部署时的分布式session如何实现的?

面试题剖析

先看看没有分布式session的问题

image.png

解决方案

(1)tomcat+redis

先看一下架构原理图:

image.png

其实还挺方便的,就是使用session的代码跟以前一样,还是基于tomcat原生的session支持即可,然后就是用一个叫做tomcat RedisSessionManager的东西,让我们部署的tomcat都将session数据存储到redis即可

在tomcat的配置文件中配置如下: image.png

搞一个类似上面的配置即可,你看是不是就是用了RedisSessionManager,然后指定了redis的host和part就ok了?

image.png

还可以用上面的这种方式基于redis哨兵支持的redis高可用集群来保存session数据,都是ok的

(2)spring session+redis

分布式会话的这个东西重耦合在tomcat,如果我要将web容器迁移成jetty,难道你重新把jetty都配置一遍吗

因为上面哪种tomcat+redis的方式好用,但是严重依赖于web容器,不好将代码移植到其它web容器上去,尤其是你要换了技术栈咋整?比如缓存了springcloud或者springboot之类的。还是要好好三思

所以现在比较好用的还是基于java的一站式解决方案,使用spring session是一个很好的选择

给spring session配置基于redis来存储session数据,然后配置一个spring session的过滤器,这样的话,session相关操作都会交给spring session来管了。接着在代码中,就是用原生的session操作,就是直接基于spring session从redis中获取数据了

相关配置实现自行百度

了解分布式事务方案吗?你们都咋做的?有啥坑?

面试题

分布式事务了解吗?你们如何解决分布式事务问题的?

面试官心里分析

只要聊到你做了分布式系统,必问分布式事务,你对分布式事务一无所知的话,确实会很坑,你起码的知道有哪些方案,一般怎么来做,每个方案的缺点是什么?

面试题剖析

为什么要有分布式事务?

先来看看单机的事务:

image.png

(1)XA方案/两阶段提交方案

也叫作两阶段提交事务方案,这个举个例子,比如说咱们公司里经常tb是吧(就是团建),然后一般会有个tb主席(就是负责组织团建的那个人)

第一个阶段(先询问),一般tb主席会提前一周问一下团队里的每个人,说,大家伙,下周六我们去滑雪+烧烤,去吗?这个时候tb主席开始等待每个人的回答,如果所有人都说ok,那么就可以决定一起去这次tb,如果这个阶段里,任何一个人回答说,我有事不去,那么tb主席就会取消这次活动

第二个阶段(再执行),如果大家都ok,那下周六大家就一起去滑雪+烧烤了

所以这个就是所谓的XS事务,两阶段提交,有一个事务管理器的概念,负责协调多个数据库(资源管理器)的事务,事务管理器先问问各个数据库你准备好了吗?如果每个数据库都回复ok,那么就正式提交事务,在各个数据库上执行操作,如果任何一个数据库回答不ok,那么就回滚事务

这种分布式事务方案,比较适合单块应用里,跨多个库的分布式事务,而且因为严重依赖于数据库层面来搞定复杂的事务,效率很低,绝对不适合高并发的场景。如果要玩儿,那么基于spring+JTA就可以搞定,自己随便搜个demo看看就知道了

这个方案,我们很少用,一般来说某个系统内部如果出现跨多个库的这么一个操作,是不合规的,现在微服务,一个大的系统拆分成几百个服务,几十个服务。一般来说,我们的规定和规范是一个服务只能操作自己对应的一个库,如果你要操作别的服务的数据库,不允许直连,违反微服务架构的规范,你随便交叉胡乱访问,几百个服务,全体乱套,难以管理

你要操作别人的数据库,调用别人的接口来操作

(2)TCC方案

TCC的全程是:Try、Confirm、Cancel

这个其实是用到了补偿的概念,分为了三个阶段

Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留 Confirm阶段:这个阶段说的是在各个服务中执行实际的操作 Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经成功的业务逻辑的回滚操作

给大家举个例子吧,比如说跨银行转账的时候,要涉及到两个银行的分布式事务,如果用TCC方案来实现的话,思路是这样的:

Try阶段:先把两个银行账户中的资金给它冻结住就不操作了 Confirm阶段:执行实际的转账操作,A银行账户的资金扣减了,B银行账户的资金增加 Cancel阶段:如果任何一个银行的操作执行失败了,那么就需要回滚进行补偿,就是比如A银行账户如果已经扣减了,但是B银行账户资金增加失败了,那么就得把A银行账户资金给加回去

这种方案说实话几乎很少有人使用,因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心

比较适合的场景;这个就是除非你是真的一致性要求太高,是你系统中核心之核心的场景,比如常见的就是资金类的场景,那你就可以用TCC方案了,自己编写大量的业务逻辑,自己判断一个事务

(3)本地消息表

这个大概意思是这样的

image.png

  1. A系统在自己本地一个事务里操作的同时,插入一条数据到消息表
  2. 接着A系统将这个消息发送到MQ中去
  3. B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
  4. B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
  5. 如果系统B处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发生到MQ中去,让B处理
  6. 这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止 这个方案说实话最大的问题就在于严重依赖于数据库的消息表来管理事务啥的???这个会导致如果是高并发场景咋办呢?咋扩展呢?所以一般确实很少用

(4)可靠消息最终一致性方案

这个的意思,就是干脆不要用本地的消息表,直接基于MQ来实现事务。比如阿里的RocketMQ就支持消息事务

image.png

大概的意思就是:

  1. A系统先发送一个prepared消息到mq,如果这个prepared消息发送失败那么就直接取消操作别执行了
  2. 如果这个消息发送成功过了,那么接着执行本地事务,如果成功执行本地的事务
  3. mq会自动定时轮询所有prepared消息回调你的接口,问你,这个消息是不是本地事务处理失败了,所有没发送确认消息?那是继续重试还是回滚?一般来说这里你就可以查下数据库看之前本地事务是否执行,如果回滚了,那么这里也回滚吧。这个就是避免可能本地事务执行成功了,别确认消息发送失败了
  4. 这个方案里,要是系统B的事务失败了咋办?重试咯,自动不断重试直到成功,如果实在是不行,要么就是针对重要的资金类业务进行回滚,比如B系统本地回滚后,想办法通知系统A也回滚,或者是发送报警由人工来手工回滚和补偿

这个还是比较合适的,目前互联网公司大都是这么玩的。

(5)最大努力通知方案

image.png

这个方案的大致意思就是:

  1. 系统A本地事务执行完之后,发送个消息到MQ
  2. 这里会有个专门消费MQ的最大努力通知服务,这个服务会消费MQ然后写入数据库中记录下来,或者是放入一个内存队列也可以,接着调用系统B的接口
  3. 要是系统B执行成功就ok了,要是系统B执行失败了,那么最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃

你们公司是如何处理分布式事务的?

你找一个严格资金要求绝对不能错的场景,你可以说是用到TCC方案,如果是一般的分布式事务场景,订单插入之后要调用库存服务更新库存,库存数据没有资金那么敏感,可以用可靠消息最终一致性方案

思考一个问题?

image.png

image.png

说说一般如何设计一个高并发的系统架构

image.png

image.png