今天和大家分享 字节跳动 后端 面试~
在竞争激烈的后端开发岗位中,大厂面试不仅考察知识的广度,更注重对技术原理的深度理解和实际应用能力。
这20多个问题几乎覆盖了后端工程师必须掌握的所有核心知识点。
如果你也在准备后端面试,不妨看看下面这些问题是否都能对答如流。
Part1、Http 请求中有哪些请求方式?
考察重点:HTTP 协议的请求方法语义、适用场景区分,避免仅罗列不说明用途。
回答思路:
按 "常用 + 少见" 分类,结合后端业务场景说明:
常用方法:
- GET:查询资源(如商品详情),幂等(多次请求结果一致)、安全(不修改资源),参数拼在 URL(长度有限制);
- POST:提交资源(如用户注册),非幂等(多次提交可能重复创建),参数放请求体(支持大体积数据、二进制);
- PUT:全量更新资源(如修改用户信息),幂等(多次更新结果一致),需指定资源唯一标识(如 URL 中的用户 ID);
- DELETE:删除资源(如删除订单),幂等,需谨慎使用(建议加权限校验);
- PATCH:部分更新资源(如仅修改用户手机号),非幂等,比 PUT 更灵活(无需传完整资源字段);
少见方法:
- HEAD:仅返回响应头(无响应体),用于检查资源是否存在(如判断文件是否更新);
- OPTIONS:获取目标 URL 支持的请求方法(如跨域预检 CORS 时,浏览器自动发送)。
Part2、说一下 Https 是如何保证链接安全的?
考察重点:HTTPS 与 HTTP 的核心差异、安全机制的底层逻辑(身份认证、加密、完整性)。
回答要点:
HTTPS = HTTP + TLS/SSL 协议,通过三层机制保障安全:
身份认证(防伪装):
服务端需配置 CA 机构颁发的证书(含公钥、服务端域名、有效期),客户端请求时会验证证书合法性(如是否过期、域名是否匹配、CA 签名是否有效),避免连接 "假服务端";
数据加密(防窃听):
握手阶段用非对称加密 协商会话密钥(服务端公钥加密、私钥解密),传输阶段用对称加密(如 AES)加密数据(对称加密效率高,适合大体积数据);
完整性校验(防篡改):
用 MAC(消息认证码)或 HMAC 对数据生成 "指纹",客户端接收后验证指纹,若数据被篡改(如中间件拦截修改),指纹不匹配则拒绝接收。
Part3、Https 的加密方式是怎样的?对称还是非对称?
考察重点:HTTPS 加密体系的 "混合模式" 逻辑,避免混淆对称 / 非对称加密的作用。
回答要点:
HTTPS同时使用对称加密与非对称加密,分工不同:
非对称加密(仅用于握手阶段):
- 用途:安全传递 "会话密钥"(对称加密的密钥),避免密钥在传输中被窃取;
- 流程:服务端将 CA 证书(含公钥)发给客户端,客户端用公钥加密 "预主密钥" 并传回,服务端用私钥解密得到预主密钥,双方再基于预主密钥生成会话密钥;
对称加密(用于数据传输阶段):
- 用途:加密 HTTP 请求 / 响应数据(如表单参数、接口返回值);
- 原因:非对称加密效率低(1000 次 / 秒级),对称加密效率高(10 万次 / 秒级),适合高频数据传输;
补充:TLS1.3 优化了握手流程(仅 1 次往返),但核心加密逻辑仍为 "非对称协商密钥 + 对称传输数据"。
Part4、Http 的状态码都有哪些,代表什么意思?
考察重点:状态码的分类逻辑(1xx-5xx)、常见码的实际业务场景(如 304、403、502)。
回答要点:
按 HTTP 标准分类,结合后端场景举例:
1xx(信息性):临时响应,如 100 Continue(客户端需继续发送请求体);
2xx(成功):
- 200 OK:请求成功(如查询商品返回数据);
- 204 No Content:成功但无响应体(如删除资源后无返回);
3xx(重定向):
- 301 永久重定向:资源永久迁移(如旧域名跳新域名,SEO 友好);
- 302 临时重定向:临时跳转(如登录后跳首页);
- 304 Not Modified:资源未修改(客户端用本地缓存,减少服务器压力);
4xx(客户端错误):
- 400 Bad Request:请求参数错误(如少传必填字段);
- 401 Unauthorized:未授权(如未登录访问需要权限的接口);
- 403 Forbidden:权限不足(如普通用户访问管理员接口);
- 404 Not Found:资源不存在(如访问不存在的接口路径);
5xx(服务端错误):
- 500 Internal Server Error:服务端未知错误(如代码抛异常未捕获);
- 502 Bad Gateway:网关错误(如 Nginx 转发到的后端服务宕机);
- 503 Service Unavailable:服务暂不可用(如高峰期限流、服务重启);
- 504 Gateway Timeout:网关超时(如后端服务处理时间过长)。
Part5、TCP 是如何实现可靠传输的呢?
考察重点:TCP 可靠传输的核心机制(避免丢失、乱序、重复),底层原理与业务场景结合。
回答要点:
TCP 通过六大机制保障可靠传输,对应不同问题:
- 确认应答(ACK):客户端发送数据后,服务端需返回 ACK 确认(含确认号),未收到 ACK 则重传(解决 "数据丢失");
- 超时重传:发送方若超过超时时间未收到 ACK,自动重传数据(超时时间会动态调整,避免网络延迟导致误重传);
- 序号与确认号:每个 TCP 段都有序号,服务端按序号重组数据(解决 "数据乱序"),确认号表示 "已收到到该序号前的所有数据"(解决 "数据重复");
- 滑动窗口(流量控制):服务端告知客户端 "当前可接收的最大数据量"(窗口大小),客户端按需发送(避免服务端接收能力不足导致数据丢失);
- 拥塞控制:通过 "慢开始 - 拥塞避免 - 快重传 - 快恢复" 调整发送速率(解决 "网络拥堵",如刚开始慢发送,避免加剧拥堵);
- 数据校验:TCP 段头部含校验和,接收方校验数据完整性,校验失败则丢弃并要求重传(解决 "数据篡改")。
Part6、在浏览器中输入 url 后会发生哪些事情?
考察重点:完整网络请求链路(从 URL 解析到页面渲染),各层协议协同逻辑。
回答要点:
分 8 个核心步骤,覆盖 "网络 + 浏览器" 全流程:
- URL 解析:拆分 URL 为 "协议(http/https)、域名(如www.baidu.com)、端口(默认 80/443)、路径(/index)、参数(?id=1)";
- DNS 域名解析:将域名转为 IP(如www.baidu.com→180.101.49.12),流程:本地 DNS 缓存→路由器缓存→ISP DNS→根 DNS→顶级域 DNS→权威 DNS;
- 建立 TCP 连接:基于 IP 建立三次握手(客户端发 SYN→服务端回 SYN+ACK→客户端回 ACK),HTTPS 需额外进行 TLS 握手(协商密钥、验证证书);
- 发送 HTTP 请求:客户端构造请求(请求行:GET /index HTTP/1.1;请求头:Host、Cookie、User-Agent;请求体:POST 参数等),通过 TCP 发送到服务端;
- 服务端处理请求:服务端(如 Nginx→后端服务→数据库)处理请求(查数据、业务逻辑),构造响应(响应行、响应头、响应体);
- 关闭 TCP 连接:若 HTTP/1.1 无 keep-alive,进行四次挥手(客户端发 FIN→服务端回 ACK→服务端发 FIN→客户端回 ACK);有 keep-alive 则复用连接;
- 浏览器解析响应:
- HTML 解析为 DOM 树,CSS 解析为 CSSOM 树,两者合成 "渲染树";
- 布局(Layout):计算元素位置 / 大小;绘制(Paint):将元素画到屏幕;
- 执行 JavaScript:通过 JS 引擎(如 V8)执行脚本,操作 DOM/CSSOM(可能触发回流 / 重绘),绑定事件(如点击事件)。
Part7、C++ 指针和引用的差别是什么?
考察重点:C++ 内存模型理解、指针与引用的语法 / 语义差异,实际编码选择逻辑。
回答要点:
从 6 个核心维度对比,结合场景举例:
|-----------------------------------------------------------------------------|----------------------------------------|---------------------------------------|
| 维度 | 指针 | 引用 |
| 定义与初始化 | 变量(存地址),可空 / 未初始化 | 变量别名,必须初始化且绑定对象 |
| 内存占用 | 占 4/8 字节(依平台) | 不占独立内存(编译器优化为指针) |
| 可修改性 | 可重指向其他对象(如 int* p=&a; p=&b;) | 绑定后不可更改(如 int& r=a; r=b 是赋值,非重绑定) |
| 操作方式 | 需解引用(*p)/ 取地址(&p) | 直接使用(如 r=1 等价于 a=1) |
| 多态支持 | 可指向基类 / 派生类对象(如 Base* p=new Derived;) | 可绑定基类 / 派生类对象(如 Base& r=Derived ();) |
| 风险点 | 野指针(未初始化)、空指针(nullptr) | 无空引用,但需避免绑定局部变量(生命周期问题) |
| 场景举例:函数参数传递时,需修改实参用指针 / 引用;返回对象时,用引用避免拷贝(如 vector& getVec ()),但不可返回局部变量引用。 | | |
Part8、说一下动态链接和静态链接是什么,以及各自的优缺点
考察重点:程序编译链接机制,静态 / 动态链接的底层差异与场景适配。
回答要点:
先定义核心概念,再对比优缺点:
静态链接:
- 定义:编译时将依赖的库代码(如 libxxx.a)全部拷贝到可执行文件中;
- 优点:运行时不依赖外部库,启动快(无需加载库),部署简单;
- 缺点:可执行文件体积大(如链接 Boost 后体积翻倍),库更新需重新编译(如 libxxx.a 修复 bug,所有依赖它的程序都要重编);
动态链接:
- 定义:编译时仅记录库引用(如 libxxx.so),运行时加载共享库到内存(多个程序可共享同一份库);
- 优点:可执行文件体积小,库更新无需重编(替换.so 文件即可),内存占用低(共享库);
- 缺点:运行时依赖共享库(缺失.so 会报错 "cannot open shared object file"),启动稍慢(需加载库);
场景选择:工具类程序(如命令行工具)用静态链接(部署方便);后端服务(如 API 服务)用动态链接(便于更新库、节省内存)。
Part9、说一下深拷贝和浅拷贝的区别
考察重点:C++ 对象拷贝的内存管理,浅拷贝的风险与深拷贝的实现逻辑。
回答要点:
核心差异在 "动态内存成员的处理",结合例子说明:
浅拷贝:
- 逻辑:仅拷贝对象的成员变量值,若成员是指针(如 char*),仅拷贝指针地址(不拷贝指向的内存);
- 特点:不额外分配内存,拷贝效率高,但两个对象共享同一块动态内存;
- 风险:对象析构时会 "双重释放"(如两个对象的 char * 指向同一块内存,析构时两次 delete),导致程序崩溃;
深拷贝:
- 逻辑:拷贝成员变量时,若成员是指针,会重新分配内存并拷贝指针指向的内容;
- 特点:两个对象内存独立(修改一个不影响另一个),无双重释放风险,但拷贝效率低(需分配内存);
实现举例:
// 浅拷贝(默认拷贝构造)
class String {
public:
char* data;
String(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); }
~String() { delete[] data; } // 浅拷贝时会双重释放
};
// 深拷贝(重写拷贝构造与赋值运算符)
String::String(const String& other) {
data = new char[strlen(other.data)+1];
strcpy(data, other.data); // 拷贝指针指向的内容
}
String& String::operator=(const String& other) {
if (this != &other) {
delete[] data; // 先释放自身内存
data = new char[strlen(other.data)+1];
strcpy(data, other.data);
}
return *this;
}
Part10、进程通信的解耦机制?
考察重点:进程通信的架构设计思维,如何降低进程间依赖(避免紧耦合)。
回答要点:
解耦核心是 "减少直接交互,通过中间层 / 协议隔离",5 类常见机制:
1)、消息队列(如 Linux msgqueue、RabbitMQ):
- 逻辑:进程 A 发送消息到队列,进程 B 从队列接收,双方无需知道对方存在;
- 解耦点:异步通信(发送方无需等待接收方处理)、数据格式统一(队列定义消息结构);
- 场景:日志收集(业务进程发日志到队列,日志进程消费);
2)、发布 - 订阅模式(如 Redis Pub/Sub、Kafka):
- 逻辑:发布者发消息到 "主题",订阅者订阅主题接收消息,多对多通信;
- 解耦点:完全解耦(发布者不知订阅者,订阅者不知发布者);
- 场景:实时通知(如订单状态变更,多个服务订阅 "订单主题");
3)、共享内存 + 信号量:
- 逻辑:共享内存存数据(高吞吐),信号量负责同步(避免竞争);
- 解耦点:数据存储与同步分离(共享内存只管存,信号量只管锁);
- 场景:大数据量传输(如视频处理进程间传帧数据);
4)、中间件代理(如 RPC 服务发现):
- 逻辑:进程通过注册中心(如 Nacos、Eureka)发现服务,无需硬编码对方地址;
- 解耦点:地址解耦(服务下线 / 上线不影响调用方);
- 场景:微服务调用(如订单服务调用支付服务,通过注册中心找地址);
5)、管道 / 命名管道(半解耦):
- 逻辑:匿名管道(父子进程)、命名管道(无亲缘进程),通过管道传递数据;
- 解耦点:比直接共享内存松,但需知道管道路径(解耦程度低于消息队列);
- 场景:简单数据交互(如两个服务传递配置)。
Part11、Linux 进程通信的几种方式以及各自的应用场景
考察重点:Linux IPC 机制的分类与场景适配,避免仅罗列不说明用途。
回答要点:
7 类核心 IPC 方式,按 "数据传递 / 同步" 分类:
|-------------|------------------------|---------------------------------------------------|
| 方式 | 核心逻辑 | 适用场景 |
| 匿名管道 | 父子 / 兄弟进程,半双工,基于文件描述符 | shell 命令管道(如 ls |
| 命名管道(FIFO) | 无亲缘进程,半双工,文件系统可见 | 不同服务间简单通信(如服务 A→服务 B 传状态) |
| 消息队列 | 无亲缘进程,异步,按类型接收消息 | 高并发异步通信(如日志收集、订单通知) |
| 共享内存 | 进程直接访问同一块内存,最快 IPC | 大数据量传输(如视频帧、传感器数据) |
| 信号量 | 同步互斥,不传递数据,控制并发数 | 保护共享资源(如限制 3 个进程读写共享内存) |
| 信号 | 异步通知,传递简单信号(如 SIGKILL) | 异常处理(如 kill -9 终止进程)、事件触发(子进程退出通知父进程) |
| Socket(套接字) | 跨主机 / 本地进程,基于 TCP/UDP | 网络服务(如客户端 - 服务端通信,Nginx 接收请求)、本地进程通信(如 Unix 域套接字) |
Part12、说一下数据库的范式
考察重点:数据库设计规范,范式的作用(减少冗余、避免异常)与实际权衡。
回答要点:
按 "1NF→3NF→BCNF" 讲解(常用范式),结合反例说明:
1NF(第一范式):列不可再分(原子性);
- 反例:"联系方式" 列存 "13800138000,user@xxx.com"(拆分为 "电话""邮箱");
- 作用:保证数据可正常查询(如按电话筛选);
2NF(第二范式):满足 1NF,且非主键列 "完全依赖" 主键(消除部分依赖);
- 反例:订单表(订单 ID,商品 ID,商品名称),商品名称依赖商品 ID(部分依赖主键 "订单 ID + 商品 ID");
- 优化:拆为 "订单表(订单 ID,商品 ID)""商品表(商品 ID,商品名称)";
- 作用:避免插入异常(新增商品无需先有订单);
3NF(第三范式):满足 2NF,且非主键列 "不传递依赖" 主键(消除传递依赖);
- 反例:用户表(用户 ID,用户名,地区 ID,地区名称),地区名称依赖地区 ID(传递依赖主键);
- 优化:拆为 "用户表(用户 ID,用户名,地区 ID)""地区表(地区 ID,地区名称)";
- 作用:避免更新异常(修改地区名称只需改地区表,无需改所有用户);
BCNF(巴斯 - 科德范式):满足 3NF,且主键列 "不传递依赖" 非主键列(解决主属性传递依赖);
- 反例:学生选课表(学生 ID,课程 ID,教师 ID),若 "一门课对应一个教师",教师 ID 依赖课程 ID(主属性传递依赖);
- 优化:拆为 "选课表(学生 ID,课程 ID)""课程教师表(课程 ID,教师 ID)";
- 作用:避免删除异常(删除学生不会丢失课程 - 教师关系);
关键提醒:范式不是越高越好,过度范式会增加表连接(如 5 张表 join,查询效率低),实际设计需 "反范式" 权衡(如热点数据冗余存储,减少 join)。
Part13、说一下多线程死锁的原因吧
考察重点:死锁的底层逻辑(必要条件)、实际开发中的资源竞争场景,避免仅罗列理论不结合代码。
回答要点:
多线程死锁是 "多个线程互相等待对方持有的资源,无法继续执行" 的状态,需同时满足四个必要条件,结合 C++ 场景举例说明:
互斥条件:资源只能被一个线程占用(如 std::mutex 加锁后,其他线程需等待解锁);
- 示例:线程 A 加锁 mutex1,线程 B 尝试加锁 mutex1 时会阻塞;
持有并等待条件:线程持有已获得的资源,同时等待其他线程的资源;
- 示例:线程 A 持有 mutex1,等待线程 B 的 mutex2;线程 B 持有 mutex2,等待线程 A 的 mutex1;
不可剥夺条件:线程持有的资源不能被强制剥夺(如已加锁的 mutex,只能由持有线程主动解锁);
- 示例:线程 A 不释放 mutex1,线程 B 无法强制获取 mutex1,只能一直等待;
循环等待条件:多个线程形成 "资源等待循环链"(线程 A→线程 B→线程 C→线程 A);
示例(典型死锁代码):
std::mutex mutex1, mutex2;
// 线程A
void threadA() {
mutex1.lock(); // 持有mutex1
std::this_thread::sleep_for(100ms); // 让线程B先持有mutex2
mutex2.lock(); // 等待mutex2,此时线程B已持有mutex2并等待mutex1,形成死锁
// 业务逻辑...
mutex2.unlock();
mutex1.unlock();
}
// 线程B
void threadB() {
mutex2.lock(); // 持有mutex2
std::this_thread::sleep_for(100ms); // 让线程A先持有mutex1
mutex1.lock(); // 等待mutex1,死锁发生
// 业务逻辑...
mutex1.unlock();
mutex2.unlock();
}
常见场景:数据库连接池(线程 A 持有连接 1 等待连接 2,线程 B 持有连接 2 等待连接 1)、车载系统资源竞争(线程 A 持有传感器 1 等待传感器 2,线程 B 相反)。
Part14、如何避免死锁呢?
**考察重点:**针对死锁四个必要条件的解决方案,工程化的编码规范与工具应用,需结合 C++ 实践。
回答要点:
核心思路是 "破坏死锁的任一必要条件",分 5 类具体措施,附代码示例:
**1.破坏 "持有并等待" 条件:**一次性申请所有资源,不部分持有;
实现:封装资源申请函数,同时申请所有需要的锁,失败则全部放弃;
示例:
// 一次性申请mutex1和mutex2,用std::lock避免部分持有
void safeLock() {
std::lock(mutex1, mutex2); // 原子操作,要么同时获取,要么同时等待
std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock); // 接管已加锁的mutex1
std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock); // 接管已加锁的mutex2
// 业务逻辑...
}
**2.破坏 "循环等待" 条件:**按固定顺序申请资源(如按资源 ID 升序);
规则:约定所有线程申请锁时,先申请 ID 小的锁,再申请 ID 大的锁;
示例:
const int MUTEX1_ID = 1, MUTEX2_ID = 2;
// 线程A、B都按"小ID→大ID"申请
void threadA() {
if (MUTEX1_ID < MUTEX2_ID) { // 先申请小ID的mutex1
mutex1.lock();
mutex2.lock();
} else {
mutex2.lock();
mutex1.lock();
}
// 业务逻辑...
mutex2.unlock();
mutex1.unlock();
}
void threadB() {
// 与线程A申请顺序一致,避免循环等待
if (MUTEX1_ID < MUTEX2_ID) {
mutex1.lock();
mutex2.lock();
} else {
mutex2.lock();
mutex1.lock();
}
// 业务逻辑...
mutex2.unlock();
mutex1.unlock();
}
**3.破坏 "不可剥夺" 条件:**允许超时释放资源(用带超时的锁申请);
实现:用std::unique_lock的try_lock_for,超时未获取资源则释放已持有资源;
示例:
void threadA() {
std::unique_lock<std::mutex> lock1(mutex1);
// 尝试获取mutex2,超时100ms未成功则放弃
if (!lock2.try_lock_for(100ms)) {
lock1.unlock(); // 释放已持有的mutex1
return; // 退出或重试
}
// 业务逻辑...
lock2.unlock();
lock1.unlock();
}
**4.破坏 "互斥" 条件:**用无锁数据结构(如std::atomic)或共享资源(如读写锁的读共享);
场景:读多写少的场景,用std::shared_mutex让多线程同时读,避免互斥;
示例:
std::shared_mutex rwMutex;
std::string data;
// 读线程(共享访问,不互斥)
void readThread() {
std::shared_lock<std::shared_mutex> lock(rwMutex);
std::cout << data << std::endl;
}
// 写线程(独占访问,互斥)
void writeThread() {
std::unique_lock<std::shared_mutex> lock(rwMutex);
data = "new data";
}
**5.工程化工具检测:**提前发现死锁风险; 工具:C++ 用pstack(查看线程调用栈)、valgrind --tool=helgrind(检测数据竞争与死锁);
流程:测试环境运行服务,用工具监控线程状态,若发现 "循环等待" 调用栈则优化代码。
Part15、C++ 是如何保证线程安全的呢?
考察重点:C++ 线程安全的实现手段,不同机制的适用场景(如原子操作 vs 锁)。
回答要点:
分 6 类核心手段,结合后端场景举例:
互斥同步(阻塞式):
- std::mutex(互斥锁):保护临界区(如多线程读写共享变量),用 lock_guard 自动加解锁;
- std::shared_mutex(读写锁):读共享、写独占(适合读多写少,如缓存查询);
- std::spinlock(自旋锁):线程等待时不阻塞(循环查锁),适合临界区执行时间短(如传感器数据读取);
原子操作(无锁式):
- std::atomic(如 atomic、atomic):底层基于 CPU 原子指令(如 x86 的 cmpxchg),无需锁,效率高;
- 场景:多线程计数器(如请求量统计)、标志位(如线程停止信号);
线程局部存储(TLS):
- thread_local 关键字:每个线程有独立的变量副本,避免共享;
- 场景:线程私有缓存、日志 ID(如每个线程记录自己的请求 ID);
避免共享状态:
- 设计无状态函数(不依赖全局 / 静态变量),如纯函数(输入决定输出);
- 场景:工具函数(如字符串处理函数);
内存屏障:
- std::memory_order(如 memory_order_acquire/release):解决指令重排序与内存可见性问题;
- 场景:多线程下变量修改后,确保其他线程能立即看到(如线程 A 改 flag=true,线程 B 能及时读取);
同步工具:
- std::condition_variable(条件变量):线程间通信(如生产者 - 消费者模型,生产者唤醒消费者);
- std::semaphore(信号量):限制并发数(如限制 5 个线程同时访问数据库)。
Part16、说一下 C++ 里面的容器是如何保证线程安全的呢?
考察重点:C++ 标准容器的线程安全特性,实际使用中的同步策略(避免踩坑)。
回答要点:
先明确核心前提,再讲保障手段:
前提 :C++ 标准库容器(vector、map、queue 等)本身不保证线程安全(标准未强制,避免性能开销),单个操作(如 push_back)也可能不是原子的;
保障线程安全的 3 种方式:
外部全局锁:
- 对容器的所有操作(读 / 写)加锁(如 std::mutex),适合低并发;
-
示例:
std::mutex mtx;
std::vector<int> vec;
// 写操作
void push(int val) {
std::lock_guardstd::mutex lock(mtx);
vec.push_back(val);
}
// 读操作
int get(int idx) {
std::lock_guardstd::mutex lock(mtx);
return vec[idx];
}
细粒度锁(分段锁):
- 对容器分段加锁(如 hash_map 按桶加锁),不同段的操作可并发,提高效率;
- 场景:高并发哈希表(如自定义线程安全 hash_map);
使用线程安全容器(第三方):
- 标准库无,需依赖第三方库:如 Boost 的 boost::thread_safe_queue、Intel TBB 的 tbb::concurrent_vector;
- 优势:内部实现同步,无需外部加锁(如 tbb::concurrent_vector 支持多线程 push_back);
关键注意事项:
即使单个操作(如 vec.push_back)是原子的,复合操作仍需同步(如 "判断非空→取值":if (!vec.empty ()) { val=vec [0]; },empty () 和 [] 之间可能被其他线程打断,导致取到空值)。
Part17、AOP 在 Spring 中是怎么实现的呢?
考察重点:Spring AOP 的底层原理(动态代理)、核心概念与实际应用。
回答要点:
先讲核心概念,再拆实现流程,最后讲场景:
AOP 核心概念:
- 切面(Aspect):封装横切逻辑的类(如日志切面、事务切面),用 @Aspect 注解;
- 通知(Advice):切面的具体逻辑(@Before 前置、@After 后置、@Around 环绕、@AfterReturning 返回后、@AfterThrowing 异常后);
- 切入点(Pointcut):定义 "哪些方法要被增强"(如 execution (* com.xxx.service..(..)));
- 目标对象(Target):被代理的原始对象(如 UserService);
- 代理(Proxy):Spring 生成的代理对象(增强后的对象);
底层实现:
动态代理:Spring AOP 基于动态代理,分两种方式,按需选择:
JDK 动态代理:
- 前提:目标类必须实现接口(如 UserService implements IUserService);
- 原理:通过 java.lang.reflect.Proxy 生成代理对象,InvocationHandler 接口处理增强逻辑;
-
示例:
IUserService proxy = (IUserService) Proxy.newProxyInstance(
classLoader,
new Class[]{IUserService.class},
(proxy, method, args) -> {
// 前置通知:日志记录
System.out.println("method " + method.getName() + " start");
Object result = method.invoke(target, args); // 执行目标方法
// 后置通知
System.out.println("method " + method.getName() + " end");
return result;
}
);
CGLIB 动态代理:
- 前提:目标类无需实现接口(通过继承生成子类);
- 原理:通过字节码生成工具(ASM)生成目标类的子类,重写目标方法实现增强;
- 限制:不能代理 final 类 / 方法(无法继承);
- Spring 选择逻辑:Spring Boot 2.x 后默认 CGLIB(无论是否有接口),可通过配置切换为 JDK 代理;
执行流程:
- 扫描 @Aspect 类,解析切入点与通知;
- 对目标类创建代理对象(JDK/CGLIB);
- 调用代理对象方法时,先执行通知逻辑,再执行目标方法(@Around 可完全控制方法执行,如修改参数、捕获异常);
应用场景:日志记录、事务管理(Spring 声明式事务基于 AOP)、权限校验、性能监控。
Part18、说一下缓存穿透、击穿、雪崩
考察重点:缓存常见问题的根因与解决方案,结合后端高并发场景(如电商)。
回答要点:
分三类问题,每类讲 "原因 + 解决方案 + 示例":
|--------|---------------------------------------------|----------------------------------------------------------------------------------|------------------------------------------------|
| 问题 | 原因 | 解决方案 | 示例(Redis 缓存) |
| 缓存穿透 | 请求不存在的数据(如恶意查 item:999999),缓存 / 数据库都无,直击数据库 | 1. 缓存空值(设短期过期,如 5 分钟);2. 布隆过滤器(存所有存在的 key);3. 接口限流 | 缓存 item:999999 为 null,expire 300 |
| 缓存击穿 | 热点 key 过期(如 item:1001,百万用户同时查),缓存失效后直击数据库 | 1. 互斥锁(查缓存失效后加锁查库,其他线程等);2. 热点 key 永不过期(后台更新);3. 熔断降级 | 用 SETNX lock:item:1001 加锁,查库后 del 锁 |
| 缓存雪崩 | 大量 key 同时过期 / 缓存服务宕机,所有请求直击数据库 | 1. 过期时间随机化(如 1h+0-30min);2. 多级缓存(本地缓存 + Caffeine+Redis);3. 缓存集群(主从 + 哨兵);4. 服务熔断 | 给 key 加随机过期:expire item:1001 3600+rand ()%1800 |
Part19、写的项目有没有上线过,有没有用户大规模使用,缓存穿透这些问题是怎么遇到的?
考察重点:项目落地经验、问题排查能力,避免空泛回答(需具体场景 + 数据)。
回答要点:
按 "项目背景→问题现象→排查过程" 结构回答,举例参考:
"我参与开发的'电商商品详情页'项目(后端用 Java+Redis+MySQL)已上线,峰值 QPS 5000+,日活用户 10 万 +。缓存穿透是上线后第 2 周遇到的:
现象:监控显示 Redis 命中率从 95% 骤降到 12%,MySQL CPU 使用率飙升到 90%,接口响应时间从 50ms 涨到 500ms,部分请求超时;
排查过程:
- 查 Redis 监控:发现大量 key(如 item:999999、item:1000000)的 "未命中" 记录,且这些 key 在数据库中无对应商品;
- 查 Nginx 访问日志:有 IP 批量发送 "itemId=999999" 的请求(每秒 100 + 次),判断是恶意攻击;
- 定位根因:恶意请求查不存在的商品,Redis 未缓存空值,所有请求直击 MySQL,导致数据库过载;
解决措施:给不存在的商品 key 缓存空值(expire 300 秒),同时用布隆过滤器预加载所有存在的商品 ID,拦截无效请求,1 小时后 Redis 命中率回升到 93%,MySQL CPU 降到 35%。"
Part20、你是怎么模拟这些过程的呢?
考察重点:测试能力(本地模拟、压测、灰度验证),验证解决方案有效性的逻辑。
回答要点:
结合题 19 的缓存穿透场景,分 4 步讲模拟流程:
本地环境模拟:
- 工具:JMeter 构造请求,参数设为不存在的商品 ID(如 item:100000-200000),并发数 1000;
- 观察:Redis 命中率从 95% 降到 10%,MySQL CPU 从 20% 升到 80%,复现穿透场景;
压测环境验证方案:
- 搭建与生产一致的集群(Redis 3 主 3 从、MySQL 主从),用 Gatling 模拟 5000 QPS 混合请求(80% 正常 ID,20% 无效 ID);
- 验证 "缓存空值" 方案:给无效 ID 缓存 5 分钟空值,模拟后 Redis 命中率回升到 92%,MySQL CPU 降到 30%;
单元测试覆盖:
-
用 JUnit+Mockito 模拟 Redis 缓存未命中,测试布隆过滤器逻辑:
// 预加载存在的商品ID到布隆过滤器
bloomFilter.add(1001); bloomFilter.add(1002);
// 测试无效ID(100000)被拦截
assertFalse(bloomFilter.contains(100000));
// 测试有效ID(1001)通过
assertTrue(bloomFilter.contains(1001));
线上灰度验证:
- 先对 10% 流量开启 "缓存空值 + 布隆过滤器" 方案,观察监控(Redis 命中率、MySQL 负载)1 小时,无异常后全量上线;
- 效果:全量后未再出现穿透,接口超时率从 5% 降到 0.1%。
Part21、你的 Linux 主要是用来干嘛的呢?
考察重点:Linux 实操能力,与后端开发的结合度(开发、部署、调试)。
回答要点:
分 4 类核心场景,结合后端实际操作举例:
开发环境搭建:
- 安装工具链(gcc、g++、jdk、cmake)、版本控制(git clone/commit/push)、编辑器(vim/VS Code);
- 举例:用 cmake 编译 C++ 后端服务,git 管理代码分支(feature 分支开发,merge 到 main);
服务部署与运维:
- 部署中间件(Nginx、Redis、MySQL),用 systemd 管理服务(systemctl start/stop redis);
- 编写 shell 脚本(如服务启动脚本、日志切割脚本:每天 0 点压缩 Nginx 日志);
- Docker 容器化(docker run Redis,docker-compose 编排多服务:Redis+MySQL + 后端服务);
问题排查与监控:
- 日志查看:tail -f app.log(实时日志)、grep "error" app.log(查错误);
- 进程管理:ps aux | grep java(找后端服务 PID)、top/htop(看 CPU / 内存占用);
- 网络调试:netstat -tulpn | grep 8080(查端口占用)、telnet 127.0.0.1 8080(测端口通断);
- 性能分析:vmstat(系统资源)、perf top -p PID(分析 CPU 高占用进程的函数);
日常操作:
- 文件管理:ls、cd、cp、rm(如 rm -rf 清理日志)、chmod 755 设权限;
- 远程操作:ssh 登录服务器、scp 传文件(如 scp local.log user@ip:/home/log/);
- 压缩解压:tar -zcvf app.tar.gz app/(压缩)、tar -zxvf app.tar.gz(解压)。