字节跳动 Linux C/C++ 后端 面经

今天和大家分享 字节跳动 后端 面试~

在竞争激烈的后端开发岗位中,大厂面试不仅考察知识的广度,更注重对技术原理的深度理解和实际应用能力。

这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(第一范式):列不可再分(原子性);

  • 作用:保证数据可正常查询(如按电话筛选);

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(解压)。
相关推荐
senijusene23 分钟前
Linux软件编程:IO编程,标准IO(1)
linux·运维·服务器
忧郁的橙子.31 分钟前
02-本地部署Ollama、Python
linux·运维·服务器
醇氧39 分钟前
【linux】查看发行版信息
linux·运维·服务器
No8g攻城狮1 小时前
【Linux】Windows11 安装 WSL2 并运行 Ubuntu 22.04 详细操作步骤
linux·运维·ubuntu
XiaoFan0122 小时前
免密批量抓取日志并集中输出
java·linux·服务器
souyuanzhanvip2 小时前
ServerBox v1.0.1316 跨平台 Linux 服务器管理工具
linux·运维·服务器
HalvmånEver3 小时前
Linux:线程互斥
java·linux·运维
番茄灭世神3 小时前
Linux应用编程介绍
linux·嵌入式
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [drivers][mmc][mmc_sdio]
linux·笔记·学习
Forsete4 小时前
LINUX驱动开发#9——定时器
linux·驱动开发·单片机