被快手追着项目问,我晕了...

程序员内点事

共 13617字,需浏览 28分钟

 · 2024-04-11

大家好,我是小富~

最近不是春招开始了嘛,我多给大家分享一些互联网公司的后端校招面经,给同学们学习,根据面经去复习,效率 upupup!

今天分享一位同学Java后端快手实习面经,主要是针对项目涉及的技术栈去问了,同学项目用到了组件比较多,比如微服务组件、mysql、redis、es、kafaka。

所以针对每个组件都稍微问一点,所以大家简历所涉及的技术栈一定得掌握,不是说会使用就行。

面试考察的内容:

  • 网络:DNS、HTTP、UDP、Cookie
  • 数据结构与算法:数组、链表、栈、队列
  • 后端:mysql 日志、es 倒排索引、kafaka 消息可靠+消息不重复消息
  • 微服务:微服务组件、负载均衡算法、服务熔断、服务降级
  • spring:ioc、aop、循环依赖、事务、spring mvc 流程。

网络

Dns基于什么协议实现?udp 还是 tcp?

9f3d67459b9665f340e2047bae74fa96.webp域名解析的工作流程

DNS 基于UDP协议实现,DNS使用UDP协议进行域名解析和数据传输。

为什么是udp?

因为基于UDP实现DNS能够提供低延迟、简单快速、轻量级的特性,更适合DNS这种需要快速响应的域名解析服务。

  • 低延迟: UDP是一种无连接的协议,不需要在数据传输前建立连接,因此可以减少传输时延,适合DNS这种需要快速响应的应用场景。
  • 简单快速: UDP相比于TCP更简单,没有TCP的连接管理和流量控制机制,传输效率更高,适合DNS这种需要快速传输数据的场景。
  • 轻量级:UDP头部较小,占用较少的网络资源,对于小型请求和响应来说更加轻量级,适合DNS这种频繁且短小的数据交换。

尽管 UDP 存在丢包和数据包损坏的风险,但在 DNS 的设计中,这些风险是可以被容忍的。DNS 使用了一些机制来提高可靠性,例如查询超时重传、请求重试、缓存等,以确保数据传输的可靠性和正确性。

http的特点是什么?

HTTP具有简单、灵活、易用、通用等特点,是一种广泛应用于Web通信的协议。

  • 基于文本: HTTP的消息是以文本形式传输,易于阅读和调试,但相比二进制协议效率较低。
  • 可扩展性:HTTP协议本身不限制数据的内容和格式,可以通过扩展头部、方法等来支持新的功能。
  • 灵活性: HTTP支持不同的数据格式(如HTML、JSON、XML等),适用于多种应用场景。
  • 无状态: 每个请求之间相互独立,服务器不会保留之前请求的状态信息,需要通过其他手段(如Cookies、Session)来维护状态。

http无状态体现在哪?

HTTP的无状态体现在每个请求之间相互独立,服务器不会保留之前请求的状态信息。每次客户端向服务器发送请求时,服务器都会独立处理该请求,不会记住之前的请求信息或状态。

这意味着服务器无法知道两次请求是否来自同一个客户端,也无法知道客户端的历史状态,需要通过其他机制(如Cookies、Session)来维护和管理状态信息。

Cookie 通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。

相当于,在客户端第一次请求后,服务器会下发一个装有客户信息的「小贴纸」,后续客户端请求服务器的时候,带上「小贴纸」,服务器就能认得了了

856094d728df8ed745e55ad86a66295e.webpCookie 技术

Cookie和session的区别是什么?

  • 存储位置:Cookie存储在客户端(浏览器)中,而Session存储在服务器端。
  • 安全性:由于Cookie存储在客户端,因此容易受到安全攻击,如跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。而Session存储在服务器端,对客户端不可见,相对来说更安全。
  • 存储容量:Cookie的存储容量有限,通常为4KB左右,而Session的存储容量较大,受限于服务器的配置。

数据结构与算法

链表和数组有什么区别?

  • 访问效率:数组可以通过索引直接访问任何位置的元素,访问效率高,时间复杂度为O(1),而链表需要从头节点开始遍历到目标位置,访问效率较低,时间复杂度为O(n)。

  • 插入和删除操作效率:数组插入和删除操作可能需要移动其他元素,时间复杂度为O(n),而链表只需要修改指针指向,时间复杂度为O(1)。

  • 缓存命中率:由于数组元素在内存中连续存储,可以提高CPU缓存的命中率,而链表节点不连续存储,可能导致CPU缓存的命中率较低,频繁的缓存失效会影响性能。

  • 应用场景:数组适合静态大小、频繁访问元素的场景,而链表适合动态大小、频繁插入、删除操作的场景

如何使用两个栈实现队列?

使用两个栈实现队列的方法如下:

  1. 准备两个栈,分别称为stackPushstackPop
  2. 当需要入队时,将元素压入stackPush栈。
  3. 当需要出队时,先判断stackPop是否为空,如果不为空,则直接弹出栈顶元素;如果为空,则将stackPush中的所有元素依次弹出并压入stackPop中,然后再从stackPop中弹出栈顶元素作为出队元素。
  4. 当需要查询队首元素时,同样需要先将stackPush中的元素转移到stackPop中,然后取出stackPop的栈顶元素但不弹出。
  5. 通过上述方法,可以实现用两个栈来模拟队列的先进先出(FIFO)特性。

这种方法的时间复杂度为O(1)的入队操作,均摊时间复杂度为O(1)的出队和查询队首元素操作。

以下是使用两个栈实现队列的Java代码示例:

      
      import java.util.Stack;

class MyQueue {
    private Stack<Integer> stackPush;
    private Stack<Integer> stackPop;

    public MyQueue() {
        stackPush = new Stack<>();
        stackPop = new Stack<>();
    }

    public void push(int x) {
        stackPush.push(x);
    }

    public int pop() {
        if (stackPop.isEmpty()) {
            while (!stackPush.isEmpty()) {
                stackPop.push(stackPush.pop());
            }
        }
        return stackPop.pop();
    }

    public int peek() {
        if (stackPop.isEmpty()) {
            while (!stackPush.isEmpty()) {
                stackPop.push(stackPush.pop());
            }
        }
        return stackPop.peek();
    }

    public boolean empty() {
        return stackPush.isEmpty() && stackPop.isEmpty();
    }
}

// 测试代码
public class Main {
    public static void main(String[] args) {
        MyQueue queue = new MyQueue();
        queue.push(1);
        queue.push(2);
        System.out.println(queue.peek());  // 输出 1
        System.out.println(queue.pop());   // 输出 1
        System.out.println(queue.empty()); // 输出 false
    }
}

后端组件

MySQL的三大日志说一下,分别应用场景是什么?

MySQL的三大日志包括:redologbinlogundolog

  • redolog:主要用于保证事务的持久性(ACID特性中的D:持久性)。当数据库发生故障时,通过重做日志可以将未提交的事务重新执行,确保数据的一致性。
  • binlog:用于主从复制、数据恢复和数据备份。二进制日志记录了所有对数据库的更改操作,包括数据更新、插入、删除等,以便在主从复制时同步数据或进行数据恢复和备份。
  • undolog:主要用于事务的回滚操作。当事务执行过程中发生异常或需要回滚时,回滚日志记录了事务的操作信息,可以用于撤销事务对数据库的修改,实现事务的原子性。

ElasticSearch如何进行全文检索的?

主要是利用了倒排索引的查询结构,倒排索引是一种用于快速搜索的数据结构,它将文档中的每个单词与包含该单词的文档进行关联。通常,倒排索引由单词(terms)和包含这些单词的文档(document)列表组成。

如何理解倒排索引呢?假如现有三份数据文档,文档的内容如下分别是:

dd1c6b4119e85df67e95633233cb4d6e.webp

通过分词器将每个文档的内容域拆分成单独的「词词汇」:

44baa61a3a02aad35f64d5679fee61ea.webp

然后再构建从词汇到文档ID的映射,就形成了倒排索引。

5ce250691e1dce1795aa826a17953101.webp

当进行搜索时,系统只需查找倒排索引中包含搜索关键词的文档列表,比如用户输入"秋水",通过倒排索引,可以快速的找到含有"秋水"的文档是id为 1,2 的文档,从而达到快速的全文检索的目的。

了解过 es 分词器有哪些?

常见的分词器如下:

  • standard 默认分词器,对单个字符进行切分,查全率高,准确度较低

  • IK 分词器 ik_max_word:查全率与准确度较高,性能也高,是业务中普遍采用的中文分词器

  • IK 分词器 ik_smart:切分力度较大,准确度与查全率不高,但是查询性能较高

  • Smart Chinese 分词器:查全率与准确率性能较高

  • hanlp 中文分词器:切分力度较大,准确度与查全率不高,但是查询性能较高

  • Pinyin 分词器:针对汉字拼音进行的分词器,与上面介绍的分词器稍有不同,在用拼音进行查询时查全率准确度较高

66bebfcea901763bc0eecf7b0cfd1d99.webp分词器比较

Kafka如何保证消息不丢失?

使用一个消息队列,其实就分为三大块:生产者、中间件、消费者,所以要保证消息就是保证三个环节都不能丢失数据。

af549bb7140216420a5f3fa06a1ae798.webp图片
  • 消息生产阶段:生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 ( MQ 中间件) 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,如果返回异常则进行消息重发,那么这个阶段是不会出现消息丢失的。
  • 消息存储阶段:Kafka 在使用时是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,也就是有多个副本,这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。
  • 消息消费阶段:消费者接收消息+消息处理之后,才回复 ack 的话,那么消息阶段的消息不会丢失。不能收到消息就回 ack,否则可能消息处理中途挂掉了,消息就丢失了。

Kafka如何保证消息不重复消费?

导致重复消费的原因可能出现在生产者,也可能出现在 MQ 或 消费者。

这里说的重复消费问题是指同一个数据被执行了两次,不单单指 MQ 中一条消息被消费了两次,也可能是 MQ 中存在两条一模一样的消费。

  • 生产者:生产者可能会重复推送一条数据到 MQ 中,为什么会出现这种情况呢?也许是一个 Controller 接口被重复调用了 2 次,没有做接口幂等性导致的;也可能是推送消息到 MQ 时响应比较慢,生产者的重试机制导致再次推送了一次消息。
  • MQ:在消费者消费完一条数据响应 ack 信号消费成功时,MQ 突然挂了,导致 MQ 以为消费者还未消费该条数据,MQ 恢复后再次推送了该条消息,导致了重复消费。
  • 消费者:消费者已经消费完了一条消息,正准备但是还未给 MQ 发送 ack 信号时,此时消费者挂了,服务重启后 MQ 以为消费者还没有消费该消息,再次推送了该条消息。

消费者怎么解决重复消费问题呢?这里提供两种方法:

  • 状态判断法:消费者消费数据后把消费数据记录在 redis 中,下次消费时先到 redis 中查看是否存在该消息,存在则表示消息已经消费过,直接丢弃消息。
  • 业务判断法:通常数据消费后都需要插入到数据库中,使用数据库的唯一性约束防止重复消费。每次消费直接尝试插入数据,如果提示唯一性字段重复,则直接丢失消息。一般都是通过这个业务判断的方法就可以简单高效地避免消息的重复处理了。

微服务

你的项目用到了哪些微服务组件?

  • Eureka:服务注册与发现组件,用于实现微服务架构中的服务注册和发现。

  • Ribbon:负载均衡组件,用于在客户端实现负载均衡,提高系统的可用性和性能。

  • Feign:声明式的 HTTP 客户端组件,简化了服务之间的调用和通信。

  • Hystrix:熔断器组件,用于防止微服务间的故障蔓延,提高系统的容错能力。

  • Zuul:API 网关组件,用于统一访问入口、路由请求和过滤请求,提高系统的安全性和可维护性。

  • Config:配置中心组件,用于集中管理微服务的配置信息,实现配置的动态刷新。

负载均衡有哪些算法?

  • 简单轮询:将请求按顺序分发给后端服务器上,不关心服务器当前的状态,比如后端服务器的性能、当前的负载。

  • 加权轮询:根据服务器自身的性能给服务器设置不同的权重,将请求按顺序和权重分发给后端服务器,可以让性能高的机器处理更多的请求

  • 简单随机:将请求随机分发给后端服务器上,请求越多,各个服务器接收到的请求越平均

  • 加权随机:根据服务器自身的性能给服务器设置不同的权重,将请求按各个服务器的权重随机分发给后端服务器

  • 一致性哈希:根据请求的客户端 ip、或请求参数通过哈希算法得到一个数值,利用该数值取模映射出对应的后端服务器,这样能保证同一个客户端或相同参数的请求每次都使用同一台服务器

  • 最小活跃数:统计每台服务器上当前正在处理的请求数,也就是请求活跃数,将请求分发给活跃数最少的后台服务器

如何实现一直均衡给一个用户?

可以通过「一致性哈希算法」来实现,根据请求的客户端 ip、或请求参数通过哈希算法得到一个数值,利用该数值取模映射出对应的后端服务器,这样能保证同一个客户端或相同参数的请求每次都使用同一台服务器。

介绍一下服务熔断

服务熔断是应对微服务雪崩效应的一种链路保护机制,类似股市、保险丝

比如说,微服务之间的数据交互是通过远程调用来完成的。服务A调用服务,服务B调用服务c,某一时间链路上对服务C的调用响应时间过长或者服务C不可用,随着时间的增长,对服务C的调用也越来越多,然后服务C崩溃了,但是链路调用还在,对服务B的调用也在持续增多,然后服务B崩溃,随之A也崩溃,导致雪崩效应。

服务熔断是应对雪崩效应的一种微服务链路保护机制。例如在高压电路中,如果某个地方的电压过高,熔断器就会熔断,对电路进行保护。同样,在微服务架构中,熔断机制也是起着类似的作用。当调用链路的某个微服务不可用或者响应时间太长时,会进行服务熔断,不再有该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。

所以,服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。

在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制。

介绍一下服务降级

服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行。

服务器的资源是有限的,而请求是无限的。在用户使用即并发高峰期,会影响整体服务的性能,严重的话会导致宕机,以至于某些重要服务不可用。故高峰期为了保证核心功能服务的可用性,就需要对某些服务降级处理。可以理解为舍小保大

服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。

Spring

Spring的IOC介绍一下

IOC:Inversion Of Control,即控制反转,是一种设计思想。在传统的 Java SE 程序设计中,我们直接在对象内部通过 new 的方式来创建对象,是程序主动创建依赖对象;

090e73144f5700d5e8d1b535f3e7018d.webp

而在Spring程序设计中,IOC 是有专门的容器去控制对象。

a27bea7451bfdf605d8529db6b8b43f5.webp

所谓控制就是对象的创建、初始化、销毁。

  • 创建对象:原来是 new 一个,现在是由 Spring 容器创建。
  • 初始化对象:原来是对象自己通过构造器或者 setter 方法给依赖的对象赋值,现在是由 Spring 容器自动注入。
  • 销毁对象:原来是直接给对象赋值 null 或做一些销毁操作,现在是 Spring 容器管理生命周期负责销毁对象。

总结:IOC 解决了繁琐的对象生命周期的操作,解耦了我们的代码。

所谓反转:其实是反转的控制权,前面提到是由 Spring 来控制对象的生命周期,那么对象的控制就完全脱离了我们的控制,控制权交给了 Spring 。这个反转是指:我们由对象的控制者变成了 IOC 的被动控制者。

为什么依赖注入不适合使用字段注入?

字段注入可能引起的三个问题:

  • 对象的外部可见性
  • 可能导致循环依赖
  • 无法设置注入的对象为final,也无法注入静态变量

首先来看字段注入

      
      @RestController
public class TestHandleController {

    @Autowired
    TestHandleService testHandleService;

    public void helloTestService(){
        testHandleService.hello();
    }
}

字段注入的非常的简便,通过以上代码我们就可以轻松的使用TestHandleService类,但是如果变成下面这样呢:

      
      TestHandleController testHandle = new TestHandleController();
testHandle.helloTestService();

这样执行结果为空指针异常,这就是字段注入的第一个问题:对象的外部可见性,无法在容器外部实例化TestHandleService(例如在测试类中无法注入该组件),类和容器的耦合度过高,无法脱离容器访问目标对象。

接下来看第二段代码:

      
      public class TestA(){

    @Autowired
    private TestB testB;

}

public class TestB(){

    @Autowired
    private TestA testA;

}

这段代码在idea中不会报任何错误,但是当你启动项目时会发现报错,大致意思是:创建Bean失败,原因是当前Bean已经作为循环引用的一部分注入到了其他Bean中。

这就是字段注入的第二个问题:可能导致循环依赖

字段注入还有第三个问题:无法设置注入的对象为final,也无法注入静态变量,原因是变量必须在类实例化进行初始化。

Spring的aop介绍一下

Spring AOP是Spring框架中的一个重要模块,用于实现面向切面编程。

我们知道,Java 就是一门面向对象编程的语言,在 OOP 中最小的单元就是“Class 对象”,但是在 AOP 中最小的单元是“切面”。一个“切面”可以包含很多种类型和对象,对它们进行模块化管理,例如事务管理。

在面向切面编程的思想里面,把功能分为两种

  • 核心业务:登陆、注册、增、删、改、查、都叫核心业务
  • 周边功能:日志、事务管理这些次要的为周边业务

在面向切面编程中,核心业务功能和周边功能是分别独立进行开发,两者不是耦合的,然后把切面功能和核心业务功能 "编织" 在一起,这就叫AOP。

AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码降低模块间的耦合度,并有利于未来的可拓展性和可维护性

在 AOP 中有以下几个概念:

  • AspectJ:切面,只是一个概念,没有具体的接口或类与之对应,是 Join point,Advice 和 Pointcut 的一个统称。

  • Join point 连接点,指程序执行过程中的一个点,例如方法调用、异常处理等。 在 Spring AOP 中,仅支持方法级别的连接点。

  • Advice:通知,即我们定义的一个切面中的横切逻辑,有“around”,“before”和“after”三种类型。在很多的 AOP 实现框架中,Advice 通常作为一个拦截器,也可以包含许多个拦截器作为一条链路围绕着 Join point 进行处理。

  • Pointcut:切点,用于匹配连接点,一个 AspectJ 中包含哪些 Join point 需要由 Pointcut 进行筛选。

  • Introduction:引介,让一个切面可以声明被通知的对象实现任何他们没有真正实现的额外的接口。例如可以让一个代理对象代理两个目标类。

  • Weaving:织入,在有了连接点、切点、通知以及切面,如何将它们应用到程序中呢?没错,就是织入,在切点的引导下,将通知逻辑插入到目标方法上,使得我们的通知逻辑在方法调用时得以执行。

  • AOP proxy:AOP 代理,指在 AOP 实现框架中实现切面协议的对象。在 Spring AOP 中有两种代理,分别是 JDK 动态代理和 CGLIB 动态代理。

  • Target object:目标对象,就是被代理的对象。

Spring AOP 是基于 JDK 动态代理和 Cglib 提升实现的,两种代理方式都属于运行时的一个方式,所以它没有编译时的一个处理,那么因此 Spring 是通过 Java 代码实现的。

Spring的事务,使用this调用是否生效?

不能生效。

因为Spring事务是通过代理对象来控制的,只有通过代理对象的方法调用才会应用事务管理的相关规则。当使用this直接调用时,是绕过了Spring的代理机制,因此不会应用事务设置。

Spring 如何解决循环依赖问题?

循环依赖指的是两个类中的属性相互依赖对方:例如 A 类中有 B 属性,B 类中有 A属性,从而形成了一个依赖闭环,如下图。

54cb9870c365fa388c48bc25b3d5b8b0.webp

循环依赖问题在Spring中主要有三种情况:

  • 第一种:通过构造方法进行依赖注入时产生的循环依赖问题。
  • 第二种:通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
  • 第三种:通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。

只有【第三种方式】的循环依赖问题被 Spring 解决了,其他两种方式在遇到循环依赖问题时,Spring都会产生异常。

Spring 解决单例模式下的setter循环依赖问题的主要方式是通过三级缓存解决循环依赖。三级缓存指的是 Spring 在创建 Bean 的过程中,通过三级缓存来缓存正在创建的 Bean,以及已经创建完成的 Bean 实例。具体步骤如下:

  1. 实例化 Bean:Spring 在实例化 Bean 时,会先创建一个空的 Bean 对象,并将其放入一级缓存中。

  2. 属性赋值:Spring 开始对 Bean 进行属性赋值,如果发现循环依赖,会将当前 Bean 对象提前暴露给后续需要依赖的 Bean(通过提前暴露的方式解决循环依赖)。

  3. 初始化 Bean:完成属性赋值后,Spring 将 Bean 进行初始化,并将其放入二级缓存中。

  4. 注入依赖:Spring 继续对 Bean 进行依赖注入,如果发现循环依赖,会从二级缓存中获取已经完成初始化的 Bean 实例。

通过三级缓存的机制,Spring 能够在处理循环依赖时,确保及时暴露正在创建的 Bean 对象,并能够正确地注入已经初始化的 Bean 实例,从而解决循环依赖问题,保证应用程序的正常运行。

Spring MVC的工作流程描述一下

f78994a51b574333c10ced1eacf42152.webp




Spring MVC的工作流程如下:

  1. 用户发送请求至前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用处理器映射器HandlerMapping。
  3. 处理器映射器根据请求url找到具体的处理器,生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。
  4. DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作,如:参数封装,数据格式转换,数据验证等操作
  5. 执行处理器Handler(Controller,也叫页面控制器)。
  6. Handler执行完成返回ModelAndView
  7. HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet
  8. DispatcherServlet将ModelAndView传给ViewReslover视图解析器
  9. ViewReslover解析后返回具体View
  10. DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。
  11. DispatcherServlet响应用户。

我是小富~ 下期见


··········  END  ··············


            

在看 点赞 转发 ,是对我最大的鼓励


技术书籍公众号内回复[  pdf  ] Get


面试笔记、springcloud进阶实战PDF,公众号内回复[  1222  ] Get


浏览 7
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报