周末有很多校招同学跟我反馈,今天美团面试新增了 AI 面试。
也就是模拟真人的场景来面试,而你面对的面试官就是 AI 面试官,也属于技术面试,面试时长大概 30 分钟。
但是,不要觉得对面不是真人,就觉得很容易”作弊“,实际上AI面试的时候,会要求你不能离开摄像头,要看着屏幕,并且检测你切屏的操作,只要发现作弊,有可能就会被拉黑了 。
那 AI 面试具体会问什么呢?
我看了一下大家总结美团AI面试,找到了一些规律,更多的是考察基础方面的内容:
-
八股文:会问 5 个左右的八股文,其中Java/Go/C++语言基础+计算机网络+Linux 命令+数据库这些八股出现的概率比较高。
-
系统设计:每一场AI面试,必有一个系统设计(要把功能、数据库表设计、实现的逻辑流程说出来)
-
场景题:会问 2 个非技术类的场景题,主要是关于你在项目/实习/实验室中遇到的最大困难是什么怎么解决的,你平时是怎么学习这类问题
-
没有算法,不会问你项目和实习的问题
我也找了两份美团 AI 面试的面经,分别是 C++ 和 Java 的,给大家感受一下。
美团AI面试 C++
技术八股:
-
SSL/TLS的工作原理
-
Linux是怎么看内存使用情况的
-
GET和POST的区别
-
C++的多态是什么?怎么通过虚函数实现?
-
C++的函数对象是什么?跟普通函数的区别?
-
NoSQL是什么?有哪些NoSQL数据库?
系统设计:
-
设计一个用户积分系统,怎么维护积分有效性和实现提醒功能?
-
追问:怎么进行数据库设计?怎么进行积分过期提醒?
场景题 :
-
在实验室 / 项目涉及新的技术领域?是怎么学习的?举例说明
-
在实验室 / 实习遇到什么难题?是怎么解决的?举例说明
SSL/TLS的工作原理?
传统的 TLS 握手基本都是使用 RSA 算法来实现密钥交换的,在将 TLS 证书部署服务端时,证书文件其实就是服务端的公钥,会在 TLS 握手阶段传递给客户端,而服务端的私钥则一直留在服务端,一定要确保私钥不能被窃取。
在 RSA 密钥协商算法中,客户端会生成随机密钥,并使用服务端的公钥加密后再传给服务端。根据非对称加密算法,公钥加密的消息仅能通过私钥解密,这样服务端解密后,双方就得到了相同的密钥,再用它加密应用消息。
我用 Wireshark 工具抓了用 RSA 密钥交换的 TLS 握手过程,你可以从下面看到,一共经历了四次握手:
TLS 第一次握手
首先,由客户端向服务器发起加密通信请求,也就是 ClientHello 请求。在这一步,客户端主要向服务器发送以下信息:
-
(1)客户端支持的 TLS 协议版本,如 TLS 1.2 版本。
-
(2)客户端生产的随机数(Client Random),后面用于生成「会话秘钥」条件之一。
-
(3)客户端支持的密码套件列表,如 RSA 加密算法。
TLS 第二次握手
服务器收到客户端请求后,向客户端发出响应,也就是 SeverHello。服务器回应的内容有如下内容:
-
(1)确认 TLS 协议版本,如果浏览器不支持,则关闭加密通信。
-
(2)服务器生产的随机数(Server Random),也是后面用于生产「会话秘钥」条件之一。
-
(3)确认的密码套件列表,如 RSA 加密算法。(4)服务器的数字证书。
TLS 第三次握手
客户端收到服务器的回应之后,首先通过浏览器或者操作系统中的 CA 公钥,确认服务器的数字证书的真实性。如果证书没有问题,客户端会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息:
-
(1)一个随机数(pre-master key)。该随机数会被服务器公钥加密。
-
(2)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
-
(3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。
上面第一项的随机数是整个握手阶段的第三个随机数,会发给服务端,所以这个随机数客户端和服务端都是一样的。服务器和客户端有了这三个随机数(Client Random、Server Random、pre-master key),接着就用双方协商的加密算法,各自生成本次通信的「会话秘钥」。
TLS 第四次握手
服务器收到客户端的第三个随机数(pre-master key)之后,通过协商的加密算法,计算出本次通信的「会话秘钥」。然后,向客户端发送最后的信息:
-
(1)加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
-
(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。
至此,整个 TLS 的握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用「会话秘钥」加密内容。
Linux是怎么看内存使用情况的?
使用 free -m 命令可以查看内存的总体使用情况,输出结果会大致如下:
total used free shared buff/cache available
Mem: 7982 1746 2523 155 3703 5818
Swap: 2047 6 2041
关注以下几项:
-
used:已经使用的内存。
-
free:可用的空闲内存。
-
available:可用的内存,这包括了操作系统缓存,这个值更能代表实际可用内存。
如果 available 的值长期很低,可能表明内存不足。
GET和POST的区别
根据 RFC 规范,GET 的语义是从服务器获取指定的资源,这个资源可以是静态的文本、页面、图片视频等。GET 请求的参数位置一般是写在 URL 中,URL 规定只能支持 ASCII,所以 GET 请求的参数只允许 ASCII 字符 ,而且浏览器会对 URL 的长度有限制(HTTP协议本身对 URL长度并没有做任何规定)。比如,你打开我的文章,浏览器就会发送 GET 请求给服务器,服务器就会返回文章的所有文字及资源。
根据 RFC 规范,POST 的语义是根据请求负荷(报文body)对指定的资源做出处理,具体的处理方式视资源类型而不同。POST 请求携带数据的位置一般是写在报文 body 中,body 中的数据可以是任意格式的数据,只要客户端与服务端协商好即可,而且浏览器不会对 body 大小做限制。比如,你在我文章底部,敲入了留言后点击「提交」(暗示你们留言),浏览器就会执行一次 POST 请求,把你的留言文字放进了报文 body 里,然后拼接好 POST 请求头,通过 TCP 协议发送给服务器。
如果从 RFC 规范定义的语义来看:
-
GET 方法就是安全且幂等的,因为它是「只读」操作,无论操作多少次,服务器上的数据都是安全的,且每次的结果都是相同的。所以,可以对 GET 请求的数据做缓存,这个缓存可以做到浏览器本身上(彻底避免浏览器发请求),也可以做到代理上(如nginx),而且在浏览器中 GET 请求可以保存为书签。
-
POST 因为是「新增或提交数据」的操作,会修改服务器上的资源,所以是不安全的,且多次提交数据就会创建多个资源,所以不是幂等的。所以,浏览器一般不会缓存 POST 请求,也不能把 POST 请求保存为书签。
但是实际过程中,开发者不一定会按照 RFC 规范定义的语义来实现 GET 和 POST 方法。比如:
-
可以用 GET 方法实现新增或删除数据的请求,这样实现的 GET 方法自然就不是安全和幂等。
-
可以用 POST 方法实现查询数据的请求,这样实现的 POST 方法自然就是安全和幂等。
C++的多态是什么?怎么通过虚函数实现?
C++中的多态性是指的是同一个操作作用于不同的对象时,可以产生不同的行为。多态性主要通过虚函数实现,能够让你以父类的指针或引用调用子类的实现,从而在运行时决定使用哪个函数。
C++中的多态通常分为两种主要类型:
-
编译时多态(静态多态):通过函数重载和运算符重载实现,在编译时确定调用哪个函数。
-
运行时多态(动态多态):通过虚函数实现,在运行时根据对象的实际类型决定调用的函数。实现原理是,每个包含虚函数的类都有一个虚函数表,这个表记录了该类对象可以调用的虚函数指针,每个对象也有一个隐式的虚函数指针,指向其类的虚函数表。在运行时,调用虚函数时,程序通过虚函数指针查找正确的函数。
虚函数是通过在基类中声明一个函数为virtual
来实现的。这标志着这个函数可以被派生类重写(override)。当通过基类指针或引用调用虚函数时,C++会查找实际对象的类型,调用对应的子类实现,而不是基类的实现。
下面是一个简单的示例,展示了如何使用虚函数实现多态。1. 定义基类和派生类
#include <iostream>
using namespace std;
// 基类
class Shape {
public:
// 虚函数
virtual void draw() {
cout << "Drawing Shape" << endl;
}
};
// 派生类:Circle
class Circle : public Shape {
public:
void draw() override { // 重写虚函数
cout << "Drawing Circle" << endl;
}
};
// 派生类:Square
class Square : public Shape {
public:
void draw() override { // 重写虚函数
cout << "Drawing Square" << endl;
}
};
2. 使用虚函数
-
创建一个函数,接受基类指针作为参数,利用多态性调用不同的派生类方法。
void renderShape(Shape* shape) {
shape->draw(); // 调用虚函数
}
int main() {
Shape* shape1 = new Circle(); // 创建 Circle 对象
Shape* shape2 = new Square(); // 创建 Square 对象
renderShape(shape1); // 输出: Drawing Circle
renderShape(shape2); // 输出: Drawing Square
delete shape1; // 释放内存
delete shape2; // 释放内存
return 0;
}
C++的函数对象是什么?跟普通函数的区别?
函数对象是指一个重载了 operator()
的类或结构体实例。函数对象可以像普通函数一样被调用,但它们实际上是对象,具有状态和行为。
普通函数与函数对象的区别:
-
定时方式:普通函数是用
返回类型 函数名(参数)
语法定义的,而函数对象是一个类或结构体,并重载了operator()
。 -
状态:普通函数无状态,而函数对象可以有内置的状态(成员变量)。
-
调用方式:普通函数被直接调用,函数对象需要先创建实例,然后用实例调用。
-
灵活性:函数对象可以重载多个操作符或添加更多功能,普通函数则只能定义一个函数。
普通函数:
int add(int a, int b) {
return a + b;
}
函数对象:
class Add {
public:
int operator()(int a, int b) {
return a + b;
}
};
Add add;
int result = add(2, 3); // 调用函数对象
NoSQL是什么?有哪些NoSQL数据库?
NoSQL指非关系型数据库 ,主要代表:MongoDB,Redis。NoSQL 数据库逻辑上提供了不同于二维表的存储方式,存储方式可以是JSON文档、哈希表或者其他方式。选择 SQL vs NoSQL,考虑以下因素。
ACID vs BASE
关系型数据库支持 ACID 即原子性,一致性,隔离性和持续性。相对而言,NoSQL 采用更宽松的模型 BASE , 即基本可用,软状态和最终一致性。从实用的角度出发,我们需要考虑对于面对的应用场景,ACID 是否是必须的。比如银行应用就必须保证 ACID,否则一笔钱可能被使用两次;又比如社交软件不必保证 ACID,因为一条状态的更新对于所有用户读取先后时间有数秒不同并不影响使用。对于需要保证 ACID 的应用,我们可以优先考虑 SQL。反之则可以优先考虑 NoSQL。
扩展性对比
NoSQL数据之间无关系,这样就非常容易扩展,也无形之间,在架构的层面上带来了可扩展的能力。比如 redis 自带主从复制模式、哨兵模式、切片集群模式。相反关系型数据库的数据之间存在关联性,水平扩展较难 ,需要解决跨服务器 JOIN,分布式事务等问题。
美团AI面试 Java
技术八股:
-
简述 CORS 的工作原理;
-
什么是长连接和短连接?它们各有什么优缺点?
-
如何查看当前系统的内存使用情况?
-
什么是 MySQL 数据库命名规范?为什么遵守命名规范很重要?
-
解释Java中的静态变量和静态方法。
-
什么是Java里的垃圾回收?如何触发垃圾回收?
系统设计:
-
设计一个基本的用户隐私设置功能
场景题:
-
描述一次你通过重构代码或优化性能而学到新知识的经历
-
描述一次你需要在有限资源、时间、人力、技术等下解决问题的经历
简述 CORS 的工作原理
CORS(Cross-Origin Resource Sharing,跨源资源共享)是一种机制,它使用 HTTP 头允许或限制不同源之间的资源共享。CORS 解决了浏览器的同源策略限制,使得网页能够安全地请求来自不同域的资源。
所谓的同源策略,在浏览器中,同源策略限制了从不同源(协议、域名和端口均不同)加载的资源。例如,域名为 example.com
的页面无法直接请求 another-domain.com
上的资源。这是为了防止恶意网站窃取用户数据。
CORS 的工作原理
-
请求阶段:
-
当网页尝试从不同源发出请求时,浏览器会首先检查 CORS 策略。
-
对于跨源请求,浏览器会自动添加 CORS 请求头。
-
-
简单请求和预检请求:
-
简单请求:针对 GET、POST(符合特定条件,如 Content-Type 为
application/x-www-form-urlencoded
、multipart/form-data
或text/plain
)等安全请求,直接发送请求。 -
预检请求:如果请求的 HTTP 方法为 PUT、DELETE 或使用了自定义的请求头,浏览器会先发送一个 OPTIONS 请求到服务器,询问目标资源的 CORS 允许情况,这被称为预检请求。
-
-
服务器响应:
-
Access-Control-Allow-Origin
:指定允许访问的源,可以是特定源或*
(允许所有源)。 -
Access-Control-Allow-Methods
:指定允许的 HTTP 方法(如 GET、POST、PUT 等)。 -
Access-Control-Allow-Headers
:允许的请求头,主要用于自定义请求头的情况。 -
Access-Control-Max-Age
:表示预检请求的有效期。 -
服务器需要在响应中包含 CORS 相关的 HTTP 头,指示是否允许该跨源请求。主要的 CORS 响应头包括:
-
-
浏览器处理响应:
-
如果服务器响应的 CORS 头匹配请求的源和相关设置,浏览器允许请求的结果继续处理。
-
如果 CORS 配置不匹配,浏览器会阻止请求并发出错误。
-
工作流程示例
-
发起请求:
-
用户在
http://example.com
的页面中发起向http://api.example.com/data
的 AJAX 请求。
-
-
浏览器发起预检请求(如果是跨域 POST 等):
-
浏览器发送一个 OPTIONS 请求:
-
OPTIONS /data HTTP/1.1
Origin: http://example.com
-
服务器响应:
-
服务器响应包含 CORS 相关头:
-
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods: GET, POST
-
发送实际请求:
-
如果预检请求成功,浏览器将发送实际的请求(如 POST)。
-
服务器再次返回数据,并包含 CORS 头,允许浏览器处理响应。
-
什么是长连接和短连接?它们各有什么优缺点?
长连接:
-
定义:当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。
-
优点:可以省去较多的 TCP 建立和关闭的操作,减少浪费,节约时间。对于频繁请求资源的客户来说较适用。
-
缺点:存在存活功能的探测周期长的问题,遇到恶意连接时保活功能不够;随着客户端连接增多,服务器可能扛不住,需要采取一些策略来管理连接。
短连接:
-
定义:客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。
-
优点:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段。
-
缺点:如果客户请求频繁,将在 TCP 的建立和关闭操作上浪费时间和带宽。
如何查看当前系统的内存使用情况?
可以通过 top 或者 freem 命令都可以查看内存情况 top 命令的 mem 信息这一栏,就是系统内存的使用:
free 命令就是专门看系统内存的使用情况。
什么是 MySQL 数据库命名规范?为什么遵守命名规范很重要?
MySQL 数据库命名规范是指在创建和管理数据库、表、列、索引及其他对象时,遵循的一些标准化的命名规则。这些规范能够帮助开发者提高代码的可读性、可维护性及简洁性。以下是一些常见的 MySQL 数据库命名规范:
表名命名规范:
-
使用小写字母,单词之间用下划线分隔(snake_case)。
-
表名应简洁且具有描述性,例如:
user_profiles
。 -
大多数情况下,表名使用复数形式来表示集合,例如:
users
而不是user
。
列名命名规范:
-
同样使用小写字母,单词之间用下划线分隔(snake_case)。
-
列名应能清晰描述所存储的数据,例如:
created_at
、email_address
。
索引命名规范:
-
索引名称应以
idx_
开头,后接表名和列名,例如:idx_users_email
。 -
对于唯一索引,可以以
uniq_
开头,例如:uniq_users_email
。
解释Java中的静态变量和静态方法。
在Java中,静态变量和静态方法是与类本身关联的,而不是与类的实例(对象)关联。它们在内存中只存在一份,可以被类的所有实例共享。
静态变量
静态变量(也称为类变量)是在类中使用static
关键字声明的变量。它们属于类而不是任何具体的对象。主要的特点:
-
共享性:所有该类的实例共享同一个静态变量。如果一个实例修改了静态变量的值,其他实例也会看到这个更改。
-
初始化:静态变量在类被加载时初始化,只会对其进行一次分配内存。
-
访问方式:静态变量可以直接通过类名访问,也可以通过实例访问,但推荐使用类名。
示例:
public class MyClass {
static int staticVar = 0; // 静态变量
public MyClass() {
staticVar++; // 每创建一个对象,静态变量自增
}
public static void printStaticVar() {
System.out.println("Static Var: " + staticVar);
}
}
// 使用示例
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
MyClass.printStaticVar(); // 输出 Static Var: 2
静态方法
静态方法是在类中使用static
关键字声明的方法。类似于静态变量,静态方法也属于类,而不是任何具体的对象。主要的特点:
-
无实例依赖:静态方法可以在没有创建类实例的情况下调用。对于静态方法来说,不能直接访问非静态的成员变量或方法,因为静态方法没有上下文的实例。
-
访问静态成员:静态方法可以直接调用其他静态变量和静态方法,但不能直接访问非静态成员。
-
多态性:静态方法不支持重写(Override),但可以被隐藏(Hide)。
public class MyClass {
static int count = 0;
// 静态方法
public static void incrementCount() {
count++;
}
public static void displayCount() {
System.out.println("Count: " + count);
}
}
// 使用示例
MyClass.incrementCount(); // 调用静态方法
MyClass.displayCount(); // 输出 Count: 1
使用场景
-
静态变量:常用于需要在所有对象间共享的数据,如计数器、常量等。
-
静态方法:常用于助手方法(utility methods)、获取类级别的信息或者是没有依赖于实例的数据处理。
什么是Java里的垃圾回收?如何触发垃圾回收?
垃圾回收(Garbage Collection, GC)是自动管理内存的一种机制,它负责自动释放不再被程序引用的对象所占用的内存,这种机制减少了内存泄漏和内存管理错误的可能性。垃圾回收可以通过多种方式触发,具体如下:
-
内存不足时:当JVM检测到堆内存不足,无法为新的对象分配内存时,会自动触发垃圾回收。
-
手动请求:虽然垃圾回收是自动的,开发者可以通过调用
System.gc()
或Runtime.getRuntime().gc()
建议 JVM 进行垃圾回收。不过这只是一个建议,并不能保证立即执行。 -
JVM参数:启动 Java 应用时可以通过 JVM 参数来调整垃圾回收的行为,比如:
-Xmx
(最大堆大小)、-Xms
(初始堆大小)等。 -
对象数量或内存使用达到阈值:垃圾收集器内部实现了一些策略,以监控对象的创建和内存使用,达到某个阈值时触发垃圾回收。