跟字节注重社交媒体营销不同,在小红书、脉脉、知乎上你能经常看到字节员工晒免费三餐、各种福利等,但快手在社交媒体上则声名不显,不重营销,主管闷头做业务!就连应届生offer开奖这种事也是闷声开大奖!
有同学拿到腾讯和快手的 offer,本以为腾讯会比快手开的高,结果最后快手开的总包接近 50w,超过了腾讯给他开的薪资,真是没想到快手也开这么高。。。
不过,这个同学开奖的情况,是属于快手的 ssp offer 档了,属于最高档位的薪资了,这种一般是学历优秀+有大厂实习的同学才能拿到的offer,并不是每个拿到快手的同学都能开到这个级别的 offer。
朋友小林整理了25 届快手开发岗位的开奖情况:
30k x 16 + 2k x 12,同学 bg 985 本科,base 北京 27k x 16 + 2k x 12 ,同学 bg 本科 211,base 北京 26.5k x 16 + 2k x 12,同学 bg 硕士双一流,base 北京 25k x 16 + 2k x 12,同学 bg 硕士 211,base 北京 23k x 16 + 2k x 12,同学 bg 本科,base 北京
其中 2k x 12 是房补,整体看下来,快手的薪资确实还是比较高的,校招拿到快手 offer 同学的年薪平均有 40w+了。
翻了快手去年 24 届的校招薪资,去年白菜档是 21k,今年是 23k,跟今年的美团一样,相比去年都多了 2k。
比较有意思的,之前有流传过一个图片,快手内部校招宣讲的PPT被拍照流出了,上面讲述了24届快手校招的情况,收到了 16 万简历,录取了 1400 位应届生,录取率 0.875%,从这个比例来看,进大厂的难度真不比考 985 低。
回归正传,这次我们来看看快手校招后端开发的面经。主要是针对项目涉及的技术栈去问了,同学项目用到了组件比较多,比如微服务组件、mysql、redis、es、kafka。
所以针对每个组件都稍微问一点,所以大家简历所涉及的技术栈一定得掌握,不是说会使用就行。
面试考察的内容:
微服务:微服务组件、负载均衡算法、服务熔断、服务降级 网络:DNS、HTTP、UDP、Cookie 数据结构与算法:数组、链表、栈、队列 后端:mysql 日志、es 倒排索引、kafaka 消息可靠+消息不重复消息 spring:ioc、aop、循环依赖、事务、spring mvc 流程。
微服务
你的项目用到了哪些微服务组件?
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(退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
计算机网络
Dns基于什么协议实现?udp 还是 tcp?
域名解析的工作流
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 信息来控制客户端的状态。
相当于,在客户端第一次请求后,服务器会下发一个装有客户信息的「小贴纸」,后续客户端请求服务器的时候,带上「小贴纸」,服务器就能认得了了,
Cookie和session的区别是什么?
存储位置:Cookie存储在客户端(浏览器)中,而Session存储在服务器端。 安全性:由于Cookie存储在客户端,因此容易受到安全攻击,如跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。而Session存储在服务器端,对客户端不可见,相对来说更安全。 存储容量:Cookie的存储容量有限,通常为4KB左右,而Session的存储容量较大,受限于服务器的配置。
数据结构与算法
链表和数组有什么区别?
访问效率:数组可以通过索引直接访问任何位置的元素,访问效率高,时间复杂度为O(1),而链表需要从头节点开始遍历到目标位置,访问效率较低,时间复杂度为O(n)。 插入和删除操作效率:数组插入和删除操作可能需要移动其他元素,时间复杂度为O(n),而链表只需要修改指针指向,时间复杂度为O(1)。 缓存命中率:由于数组元素在内存中连续存储,可以提高CPU缓存的命中率,而链表节点不连续存储,可能导致CPU缓存的命中率较低,频繁的缓存失效会影响性能。 应用场景:数组适合静态大小、频繁访问元素的场景,而链表适合动态大小、频繁插入、删除操作的场景
如何使用两个栈实现队列?
使用两个栈实现队列的方法如下:
准备两个栈,分别称为 stackPush
和stackPop
。当需要入队时,将元素压入 stackPush
栈。当需要出队时,先判断 stackPop
是否为空,如果不为空,则直接弹出栈顶元素;如果为空,则将stackPush
中的所有元素依次弹出并压入stackPop
中,然后再从stackPop
中弹出栈顶元素作为出队元素。当需要查询队首元素时,同样需要先将 stackPush
中的元素转移到stackPop
中,然后取出stackPop
的栈顶元素但不弹出。通过上述方法,可以实现用两个栈来模拟队列的先进先出(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的三大日志包括:redolog、binlog和undolog。
redolog:主要用于保证事务的持久性(ACID特性中的D:持久性)。当数据库发生故障时,通过重做日志可以将未提交的事务重新执行,确保数据的一致性。 binlog:用于主从复制、数据恢复和数据备份。二进制日志记录了所有对数据库的更改操作,包括数据更新、插入、删除等,以便在主从复制时同步数据或进行数据恢复和备份。 undolog:主要用于事务的回滚操作。当事务执行过程中发生异常或需要回滚时,回滚日志记录了事务的操作信息,可以用于撤销事务对数据库的修改,实现事务的原子性。
ElasticSearch如何进行全文检索的?
主要是利用了倒排索引的查询结构,倒排索引是一种用于快速搜索的数据结构,它将文档中的每个单词与包含该单词的文档进行关联。通常,倒排索引由单词(terms)和包含这些单词的文档(document)列表组成。
如何理解倒排索引呢?假如现有三份数据文档,文档的内容如下分别是:
通过分词器将每个文档的内容域拆分成单独的「词词汇」:
然后再构建从词汇到文档ID的映射,就形成了倒排索引。
当进行搜索时,系统只需查找倒排索引中包含搜索关键词的文档列表,比如用户输入"秋水",通过倒排索引,可以快速的找到含有"秋水"的文档是id为 1,2 的文档,从而达到快速的全文检索的目的。
了解过 es 分词器有哪些?
常见的分词器如下:
standard 默认分词器,对单个字符进行切分,查全率高,准确度较低 IK 分词器 ik_max_word:查全率与准确度较高,性能也高,是业务中普遍采用的中文分词器 IK 分词器 ik_smart:切分力度较大,准确度与查全率不高,但是查询性能较高 Smart Chinese 分词器:查全率与准确率性能较高 hanlp 中文分词器:切分力度较大,准确度与查全率不高,但是查询性能较高 Pinyin 分词器:针对汉字拼音进行的分词器,与上面介绍的分词器稍有不同,在用拼音进行查询时查全率准确度较高
分词器比较
Kafka如何保证消息不丢失?
使用一个消息队列,其实就分为三大块:生产者、中间件、消费者,所以要保证消息就是保证三个环节都不能丢失数据。
图片
消息生产阶段:生产者会不会丢消息,取决于生产者对于异常情况的处理是否合理。从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 ( MQ 中间件) 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,如果返回异常则进行消息重发,那么这个阶段是不会出现消息丢失的。 消息存储阶段:Kafka 在使用时是部署一个集群,生产者在发布消息时,队列中间件通常会写「多个节点」,也就是有多个副本,这样一来,即便其中一个节点挂了,也能保证集群的数据不丢失。 消息消费阶段:消费者接收消息+消息处理之后,才回复 ack 的话,那么消息阶段的消息不会丢失。不能收到消息就回 ack,否则可能消息处理中途挂掉了,消息就丢失了。
Kafka如何保证消息不重复消费?
导致重复消费的原因可能出现在生产者,也可能出现在 MQ 或 消费者。
这里说的重复消费问题是指同一个数据被执行了两次,不单单指 MQ 中一条消息被消费了两次,也可能是 MQ 中存在两条一模一样的消费。
生产者:生产者可能会重复推送一条数据到 MQ 中,为什么会出现这种情况呢?也许是一个 Controller 接口被重复调用了 2 次,没有做接口幂等性导致的;也可能是推送消息到 MQ 时响应比较慢,生产者的重试机制导致再次推送了一次消息。 MQ:在消费者消费完一条数据响应 ack 信号消费成功时,MQ 突然挂了,导致 MQ 以为消费者还未消费该条数据,MQ 恢复后再次推送了该条消息,导致了重复消费。 消费者:消费者已经消费完了一条消息,正准备但是还未给 MQ 发送 ack 信号时,此时消费者挂了,服务重启后 MQ 以为消费者还没有消费该消息,再次推送了该条消息。
消费者怎么解决重复消费问题呢?这里提供两种方法:
状态判断法:消费者消费数据后把消费数据记录在 redis 中,下次消费时先到 redis 中查看是否存在该消息,存在则表示消息已经消费过,直接丢弃消息。 业务判断法:通常数据消费后都需要插入到数据库中,使用数据库的唯一性约束防止重复消费。每次消费直接尝试插入数据,如果提示唯一性字段重复,则直接丢失消息。一般都是通过这个业务判断的方法就可以简单高效地避免消息的重复处理了。
Spring
Spring的IOC介绍一下
IOC:Inversion Of Control,即控制反转,是一种设计思想。在传统的 Java SE 程序设计中,我们直接在对象内部通过 new 的方式来创建对象,是程序主动创建依赖对象;
而在Spring程序设计中,IOC 是有专门的容器去控制对象。
所谓控制就是对象的创建、初始化、销毁。
创建对象:原来是 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属性,从而形成了一个依赖闭环,如下图。
循环依赖问题在Spring中主要有三种情况:
第一种:通过构造方法进行依赖注入时产生的循环依赖问题。 第二种:通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。 第三种:通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。
只有【第三种方式】的循环依赖问题被 Spring 解决了,其他两种方式在遇到循环依赖问题时,Spring都会产生异常。
Spring 解决单例模式下的setter循环依赖问题的主要方式是通过三级缓存解决循环依赖。三级缓存指的是 Spring 在创建 Bean 的过程中,通过三级缓存来缓存正在创建的 Bean,以及已经创建完成的 Bean 实例。具体步骤如下:
实例化 Bean:Spring 在实例化 Bean 时,会先创建一个空的 Bean 对象,并将其放入一级缓存中。 属性赋值:Spring 开始对 Bean 进行属性赋值,如果发现循环依赖,会将当前 Bean 对象提前暴露给后续需要依赖的 Bean(通过提前暴露的方式解决循环依赖)。 初始化 Bean:完成属性赋值后,Spring 将 Bean 进行初始化,并将其放入二级缓存中。 注入依赖:Spring 继续对 Bean 进行依赖注入,如果发现循环依赖,会从二级缓存中获取已经完成初始化的 Bean 实例。
通过三级缓存的机制,Spring 能够在处理循环依赖时,确保及时暴露正在创建的 Bean 对象,并能够正确地注入已经初始化的 Bean 实例,从而解决循环依赖问题,保证应用程序的正常运行。
Spring MVC的工作流程描述一下
Spring MVC的工作流程如下:
用户发送请求至前端控制器DispatcherServlet DispatcherServlet收到请求调用处理器映射器HandlerMapping。 处理器映射器根据请求url找到具体的处理器,生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。 DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作,如:参数封装,数据格式转换,数据验证等操作 执行处理器Handler(Controller,也叫页面控制器)。 Handler执行完成返回ModelAndView HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet DispatcherServlet将ModelAndView传给ViewReslover视图解析器 ViewReslover解析后返回具体View DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。 DispatcherServlet响应用户。
好文推荐
你好,我是阿秀,普通学校毕业,校招时拿到字节跳动SP、百度、华为、农业银行等6个互联网中大厂offer,这是我在校期间的编程学习之路,详细记录了我是如何自学技术以应对第二年的校招秋招的。
毕业后我先于抖音部门担任全栈开发工程师,目前在上海某外企带领团队继续从事全栈开发,负责的项目已经顺利盈利300w+。在研三那年就组建了一个阿秀的学习圈,一直持续分享校招/社招跳槽找工作的经验,都是自己一路走过来的经验,目前已经累计服务超过 4000 +人,欢迎点此了解一二。