# 综合场景面试问题

# API设计的原则

  1. 接口版本化: 生产环境中,如果没有版本控制的程序变更会导致调用接口的相关方频繁的跟着变更,假设相关方没有及时的跟着变更,那么系统就会报错,从而影响到用户的使用及体验,使其对整个系统的运营都是不利的,接口对接的难度也会不断的加大。 如果接口能够有版本的控制,则升级系统的主动权就掌握在相关方,这样当有新版本的程序发布时旧版本的业务逻辑不会受到影响,从用户感知上也受到的影响就比较小,相关方也可以根据自身的条件是否要升级版本。
  2. 接口面向的应用场景: 在对接口进行设计时,我们还要考虑接口是面向web前端开发还是手机app开发,或者服务端开发。不同的应用场景接口总体规划是不同的(例如: 当我们的接口是提供给web前端或APP端使用时,接口安全验证我们可以采用jwt、oauth2.0等,如果我们的接口是提供给后端服务使用时,那么我可以采用token机制)。
  3. 请求参数的规范性及处理的统一性: 请求参数的规范性意思就是说,接口要以什么样的方式来接收参数。是统一使用json的方式接收呢还是xml或者form表单方式接收。在开发接口应该统一在一个地方进行对参数的接收、校验等操作。为了保证参数的完整性,我们可以考虑新增签名验证等处理。
  4. 返回数据类型、返回码及信息提示的规范性: 返回给客户端的数据类型应该要统一化(例如: 我们统一以json的形式进行返回,或者是统一以xml的形式返回)。
  5. 接口安全验证及权限的控制: 接口并不是每个操作者都能请求访问的,所以我们要为接口提供一个安全验证,就像用户登录系统一样,没有在我们系统注册的合法用户我们是不允许请求访问的。那么我们要如何对接口进行安全验证呢?其实针对不同的应用场景我们的接口安全验证也不一样(例如: 当我们的接口是提供给web前端或APP端使用时,接口安全验证我们可以采用jwt、auth2.0等,如果我们的接口是提供给后端服务使用时,那么我可以采用token机制)。
  6. 接口调用频率的控制: 对接口的调用频率进行控制从另一方面来讲也是为了接口的安全性及接口的可维护性,当然这里是否要对接口访问次数进行控制还是取决于接口针对不同的应用场景,有些接口的设计是不需要限制调用频率的,而有些接口则对接口调用是要进行严格控制的(例如: 大家熟知的微信公众号的开发就是对接口的调用频率进行限制,并且针对不同的场景及用户群体限制的频率也不一样)。(采用的线程池的思想对接口的调用的次数做一个限流操作)
  7. 请求接口日志的记录: 我们应该要为接口请求做一下日志的记录,这样我们后期维护接口时则会大大降低维护成本。而且还可以针对日志进行相关的统计处理。
  8. 接口文档的可读性: 接口文档的可读性非常重要,一个接口开发出来并不是真正的完成,如果别人不会使用你的接口,那么你的接口开发出来也是没有用的,好多程序员非常的不重视接口文档撰写及接口文档可读性。

# API怎么保证安全请求?

接口安全验证我们可以采用jwt、oauth2.0等,如果我们的接口是提供给后端服务使用时,那么我可以采用token机制。指针对不同的用户群体,我们要分配不同的权限。

  1. 主要采用设计签名的方式: 对每个客户端分别分配一个AppKey和AppSecret。需要调用API时,将AppKey加入请求参数列表,并将AppSecret和所有参数一起,根据某种签名算法生成一个签名字符串,然后调用API时把该签名字符串也一起带上。
  2. 采用HTTPS: HTTPS因为添加了SSL安全协议,自动对请求数据进行了压缩加密,在一定程序可以防止监听、防止劫持、防止重发,主要就是防止中间人攻击。因此,为了安全考虑,建议对SSL证书进行强校验,包括签名CA是否合法、域名是否匹配、是不是自签名证书、证书是否过期等。
  3. 黑白名单: 接口调用采用的IP白名单的方式来方式非法的API请求。

# 海量数据端到端的同步问题

海量数据端到端的同步问题就是一个如何保证数据传输的稳定性质。数据传输的两种方式:一种push 一种是pull。

  1. 保证发送端和接收端的高可用。如果是pull的方式 那就保证发送方的高可用。如果是push ,那就保证接受方的高可用。推荐使用一部MQ进行Push的方式。
  2. 数据分批处理进行数据传输,保证每一个小批量数据的一致性,最后整个数据的一致性,如果考虑事务,就是一个分布式事务。整个需要控制好事务的力度小。
  3. 保证数据的幂等性,如果数据中断和重试,需要保证数据的不重复性。如果没有的高并发的要求那就保证数据在数据库中唯一索引的。
  4. 数据同步状态位的记录,数据库同步到哪一个批次,同步到哪一条数据了。以便于后续做消息补偿使用,重试等。
  5. 数据传输监控,硬件,网络,接受方与发送方的服务是否正常。
  6. 增量更新的补偿,如果有旧的数据在后续有更新的。将更新记录在本地,然后使用MQ进行数据同步。
  7. 数据同步的检查,在完成一定的数据后,对两边的数据进行检查,数据量是否一直,数据的位置是否一致性。增量变化数据是否一致。

真实工作中海量数据同步采用ETL工具进行数据同步,例如采用DataX、kattle。data works与dataphingon工具。如果是大公司采用大数据平台来快速同步数据。

# 微服务的优点

  1. 可以扩展应用程序的各个区域。例如,可能需要扩展目录服务或购物篮服务,而不是订购流程。对于扩展时使用的资源而言,微服务基础结构将比单片体系结构更有效。
  2. 适合在多个团队之间划分开发工作。每个服务都可以由一个开发团队拥有。每个团队都可以独立于其他团队管理,开发,部署和扩展他们的服务。
  3. 服务独立开发部署。一个服务中存在问题,则最初只影响该服务(除非使用了错误的设计,微服务之间存在直接依赖关系),其他服务可以继续处理请求。相比之下,单片部署体系结构中的一个故障组件可能会导致整个系统崩溃,尤其是当涉及资源(如内存泄漏)时。此外,当解决微服务中的问题时,您可以仅部置受影响的微服务,不会影响应用程序的其余部分。
  4. 服务开发技术选型多样。因为您可以独立开始开发服务并并行运行,您可以方便地开始使用最新的技术和框架,而不是被困在整个应用程序的旧堆栈或框架上。

# 微服务的缺点

  1. 分布式应用。分发应用程序会增加开发人员在设计和构建服务时的复杂性。例如,开发人员必须使用HTTP或 AMPQ等协议实现服务间通信,这增加了测试和异常处理的复杂性。它还增加了系统的延迟。,
  2. 部署复杂性。具有数十种微服务类型并且需要高可伸缩性的应用程序(它需要能够为每个服务创建多个实例并在多个主机上平衡这些服务)意味着IT操作和管理的高度部署复杂性。如果您不使用面向微服条的基础架构(如协调器和调度程序),那么额外的复杂性可能需要比业务应用程序本身更多的开发工作。,
  3. 数据一致性的要求。业务需求必须包含多个微服务之间的最终一致性。。
  4. 增加全局资源需求。所有服务器或主机的总内存,驱动器和网络资源,在许多情况初全局下,当您使用微服务方法替换单一应用程序时,新的基于微服务的应用程序所需的资源量将大于原始单片应用程序的基础架构需求。这是因为更高的粒度和分布式服务需要更多的全局资源。但是,考虑到整体资源成本低,并且与单个应用程序发展过程中的长期成本相比,能够扩展应用程序的某些区域的好处,增加资源使用通常是一个很好的权衡期限申。。

# 微服务的通信手段

同步:RPC ,REST等。· 异步:消息队列,要考虑消息的可靠传输、高性能,以及编程模型的变化等。, HTTP通信:选择服务如何相互通信时,最直接的方式往往是HTTP。事实上,我们可以提出一个案例,即所有通信渠道都来自这个渠道。但是除此之外,服务之间的 HTTP 调用是服务到服务通信的可行选择。“ 消息通信:另一种通信模式是基于消息的通信。与HTTP通信不同,所涉及的服务不直接相互通信。相反,服务将消息推送到其他服务订阅的消息代理。这消除了许多与HTTP通信相关的复杂性。, 事件驱动通信:是另一种异步方法,它看起来完全消除了服务之间的耦合。与消息传递模式不同,事件驱动方法不需要服务必须知道公共消息结构。服务之间的通信通过各个服务产生的事件进行。此处仍然需要消息代理,因为各个服务会将其事件写入其中。但是与消息方法不同,消费服务不需要知道事件的细节,它们对事件的发生做出反应,而不是产生能会或可能不会传递的信息。

# 实现一个集群环境下的分布式单例模式?

单例模式,相信每个人都会。但是分布式集群环境下的单例模式,就很困难了。但大家也不用急,其实这个问题应该拆解成两个部分。如何实现跨进程级别的单例实例,如何保障在任何时刻下只有一个进程可以访问这个实例

  1. 首先,可以把单例对象序列保存到文件里面,然后再把这个文件存储到外部的共享存储组件中。
  2. 其次,各个进程在使用这个单例对象的时候,先从外部共享存储中读取到内存,并且反序列化成对象来使用。
  3. 最后,使用完成以后,再把这个对象序列化以后存储回外部共享存储组件中,并显示的把这个对象从本地内存中删除。

基于这样的操作,就能保证各个进程对单例对象的状态一致性。但是,因为多个进程可以同时访问这个单例对象,所以为了保证在任何时候只有一个进程访问单例对象。 就需要引入分布式锁的设计,也就是一个进程在获取到分布式锁以后,才能访问共享单例对象,使用完以后在释放分布式锁。分布式锁的实现,可以使用zookeeper、Etcd、Redis等,以上就是我的理解。

# 为什么阿里规范中禁止直接使用日志系统的API?

  1. 如果面试官问这样一个问题,我会认为面试官的水平可能很一般。实际上,不同的公司有自己的开发规范,开发规范的目的是为了提高代码的可维护性、稳定性、安全性等。 所以,不同规模的公司,在制定这类规范的时候,考虑的方向和规范要求会有一些差异,但这个差异不足以去验证一个候选人的技术水平。不过如果大家后续遇到这类问题,通常的回答一般可以直接说一下开发规范本身的价值就好了。

  2. 在Java生态中,涉及到的日志框架有很多,比如Log4j、Logback、Log4j2、Slf4j等。如果在开发中直接使用具体的API,在未来如果需要实现日志组件的升级和切换,会变得很困难。 基本上属于牵一发而动全身。一般情况下,我们会建议使用门面模式,也就是提供一个统一的接口去访问多个子系统的多个不同的接口。

  3. 这样的话,对于应用程序来说,无论底层的日志框架如何变,都不需要有任何感知。只要门面服务做的足够好,随意换另外一个日志框架,应用程序不需要修改任意一行代码,就可以直接上线。 即使有一天要更换代码的日志框架,只需要修改jar包,最多再改改日志输出相关的配置文件就可以了。这就是解除了应用和日志框架之间的耦合

# 会员批量过期的方案怎么实现?

  • 第一种,系统不主动轮询,而是等用户登录到系统以后,触发一次检查。如果发现会员的过期时间小于设定的阈值,就触发一次弹窗和邮件提醒。 这种方式规避了轮询问题,不会对数据库和后端应用程序造成任何压力。缺点是,如果用户一直不登陆,就一直无法实现会员过期,并且也无法提前去根据运营策略发送续期的提醒消息。

  • 第二种,我们可以使用搜索引擎,比如Solr、或者Elasticsearch。把会员表里面的会员id和会员到期时间存储一份到搜索引擎中。 搜索引擎的优势在于大数据量的快速检索,并且具有高可扩展性和高可靠性,非常适合大规模数据的处理。

  • 第三种,可以使用Redis来实现。用户开通会员以后,在Redis里面存储这个会员id,以及设置这个id的过期时间。 然后可以使用redis的过期提醒功能,把配置项 notify-keyspace-events改为notify-keyspace-events "Ex"当Redis里面的key过期以后, 会触发一个key过期事件,我们可以在应用程序中监听这个事件来处理。

  • 第四种,可以直接使用MQ提供的延迟队列,当用户开通会员以后,直接计算这个会员的过期时间,然后发送一个延迟消息到MQ上, 一旦消息达到过期时间,消费者就可以消费这个消息来触发会员过期的提醒。

# 说说你对CAP的理解?

CAP模型,在一个分布式系统里面,不可能同时满足三个点

  1. 一致性(Consistency),访问分布式系统中的每一个节点都能获得最新的数据。
  2. 可用性(Availability),每次请求都能获得一个有效的访问,但不保证数据是最新的。
  3. 分区容错性(Partition tolerance),分区相当于对通信耗时的要求,系统如果不能在时限范围内达成数据一致,就意味着发生了分区的情况。

# 服务降级的理解?

服务降级是一种提升系统稳定性和可用性的策略。简单来说,就是当服务器压力增加的情况下,根据实际业务的需求和流量的情况,不对外提供部分服务的功能。 从而释放服务器的资源去保证核心业务的正常运行。服务降级有两种方式,一种是主动降级,一种是基于特定情况的被动降级。

主动降级

这种方式在大促的时候使用比较多,比如在电商平台中,核心服务是下单、支付。所以一般会把非核心服务比如评论服务关闭掉, 这样就使得评论服务不会占用计算资源,从而保证核心服务的稳定运行。

被动降级

  • 熔断触发降级,在一个请求链路中,为了避免某个服务节点出现故障导致请求堆积,造成资源消耗是的服务崩溃的问题,一般会采取熔断策略。 当触发了熔断机制以后,如果后续再向故障节点发起请求的时候,这个请求不会发送到故障节点上,而是直接置为失败,这样就避免了请求堆积的问题。 而直接置为失败之后需要给到用户一个反馈,而这个反馈就是降级策略,就相当于给用户一个处理结果。比如返回一个“系统繁忙”之类的信息。

  • 限流触发降级,因为系统资源是有限的,为了避免高并发流量把系统压垮导致不可用问题,所以我们会采取限流的策略去保护系统。 通过限流去限制一部分用户的访问,然后保证整个系统的稳定运行,同样,触发了限流之后,需要给到用户一个反馈,这个反馈同样也称为降级策略。 比如可以反馈“当前访问人数较多,请稍候再试”,或者让这些用户排队,并显示当前排队的情况等。

因此,降级带来的结果是使得用户的体验下降,但是却保证了系统的稳定性和可用性。

# 存储MD5的值应该用VARCHAR还是CHAR?

MD5是由数字和字母组成的一个16位或者32位长度的字符串,一般在应用开发中都是使用32位。 看起来,我们用varchar(32)或者char(32)都可以存储,那用哪种更好呢?要回答这个问题,必须要了解这两个类型的功能特性和区别。

  1. 第一个,char是一个固定长度的字符串,Varchar是一个可变长度的字符串,假设声明一个char(10)的长度,如果存储字符串“abc”,虽然实际字符长度只有3,但是char还是会占10个字节长度。同样,如果用varchar存储,那它只会使用3个字符的实际长度来存储。
  2. 第二个,存储的效率不同,char类型每次修改以后存储空间的长度不变,所以效率更高,varchar每次修改数据都需要更新存储空间长度,效率较低
  3. 第三个,存储空间不同,char不管实际数据大小,存储空间是固定的,而varchar存储空间等于实际数据长度,所以varchar实际存储空间的使用要比char更小

基于他们特性的分析,可以得出一个基本的结论:

  1. char适合存储比较短的且是固定长度的字符串
  2. varchar适合存储可变长度的字符串

# CPU飙高系统反应慢怎么排查?

CPU是整个电脑的核心计算资源,对于一个应用进程来说,CPU的最小执行单元是线程。导致CPU飙高的原因有几个方面:1.线程阻塞上下文切换会占据大量CPU资源,2。CPU资源无法被释放,死循环。

  1. CPU上下文切换过多,对于CPU来说,同一时刻下每个CPU核心只能运行一个线程,如果有多个线程要执行,CPU只能通过上下文切换的方式来执行不同的线程。上下文切换需要做两个事情

    • 保存运行线程的执行状态。
    • 让处于等待中的线程执行。 这两个过程需要CPU执行内核相关指令实现状态保存,如果较多的上下文切换会占据大量CPU资源,从而使得cpu无法去执行用户进程中的指令,导致响应速度下降。在Java中,文件IO、网络IO、锁等待、线程阻塞等操作都会造成线程阻塞从而触发上下文切换
  2. CPU资源过度消耗,也就是在程序中创建了大量的线程,或者有线程一直占用CPU资源无法被释放,比如死循环! CPU利用率过高之后,导致应用中的线程无法获得CPU的调度,从而影响程序的执行效率!

  3. 既然是这两个问题导致的CPU利用率较高,于是我们可以通过top命令,找到CPU利用率较高的进程,在通过Shift+H找到进程中CPU消耗过高的线程,这里有两种情况。

    • CPU利用率过高的线程一直是同一个,说明程序中存在线程长期占用CPU没有释放的情况,这种情况直接通过jstack获得线程的Dump日志,定位到线程日志后就可以找到问题的代码。
    • CPU利用率过高的线程id不断变化,说明线程创建过多,需要挑选几个线程id,通过jstack去线程dump日志中排查。
  4. 最后有可能定位的结果是程序正常,只是在CPU飙高的那一刻,用户访问量较大,导致系统资源不够。

# 并发指标

TPS: TPS(Transaction Per Second)每秒处理的事务数。

站在宏观角度来说,一个事务是指客户端向服务端发起一个请求,并且等到请求返回之后的整个过程。从客户端发起请求开始计时, 等到收到服务器端响应结果后结束计时,在计算这个时间段内总共完成的事务个数,我们称为TPS。 站在微观角度来说,一个数据库的事务操作,从开始事务到事务提交完成,表示一个完整事务,这个是数据库层面的TPS。

QPS(Queries Per Second)每秒查询数

表示服务器端每秒能够响应的查询次数。这里的查询是指用户发出请求到服务器做出响应成功的次数,可以简单认为每秒钟的Request数量。 针对单个接口而言,TPS和QPS是相等的。如果从宏观层面来说,用户打开一个页面到页面渲染结束代表一个TPS,那这个页面中会调用服务器很多次, 比如加载静态资源、查询服务器端的渲染数据等,就会产生两个QPS,因此,一个TPS中可能会包含多个QPS。

并发数

QPS=并发数/平均响应时间 并发数是指系统同时能处理的请求数量。需要注意,并发数和QPS不要搞混了,QPS表示每秒的请求数量, 而并发数是系统同时处理的请求数量,并发数量会大于QPS,因为服务端的一个连接需要有一个处理时长,在这个请求处理结束之前,这个连接一直占用。