跳到主要内容字节跳动 Linux C/C++ 后端面试真题解析 | 极客日志C++java算法
字节跳动 Linux C/C++ 后端面试真题解析
字节跳动后端面试的核心知识点,涵盖 HTTP/HTTPS/TCP 网络协议、C++ 指针与内存管理、进程通信与多线程同步、数据库范式、Spring AOP 原理及缓存穿透击穿雪崩等问题。内容包含技术原理详解、代码示例及实战排查经验,旨在帮助候选人深入理解后端开发基础与高并发场景下的解决方案。
奶糖兔29K 浏览 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(客户端需继续发送请求体);
- 200 OK:请求成功(如查询商品返回数据);
- 204 No Content:成功但无响应体(如删除资源后无返回);
- 301 永久重定向:资源永久迁移(如旧域名跳新域名,SEO 友好);
- 302 临时重定向:临时跳转(如登录后跳首页);
- 304 Not Modified:资源未修改(客户端用本地缓存,减少服务器压力);
- 400 Bad Request:请求参数错误(如少传必填字段);
- 401 Unauthorized:未授权(如未登录访问需要权限的接口);
- 403 Forbidden:权限不足(如普通用户访问管理员接口);
- 404 Not Found:资源不存在(如访问不存在的接口路径);
- 500 Internal Server Error:服务端未知错误(如代码抛异常未捕获);
- 502 Bad Gateway:网关错误(如 Nginx 转发到的后端服务宕机);
- 503 Service Unavailable:服务暂不可用(如高峰期限流、服务重启);
- 504 Gateway Timeout:网关超时(如后端服务处理时间过长)。
Part5、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++ 内存模型理解、指针与引用的语法 / 语义差异,实际编码选择逻辑。
| 维度 | 指针 | 引用 |
|---|
| 定义与初始化 | 变量(存地址),可空 / 未初始化 | 变量别名,必须初始化且绑定对象 |
| 内存占用 | 占 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):
- 逻辑:发布者发消息到'主题',订阅者订阅主题接收消息,多对多通信;
- 解耦点:完全解耦(发布者不知订阅者,订阅者不知发布者);
- 场景:实时通知(如订单状态变更,多个服务订阅'订单主题');
- 逻辑:共享内存存数据(高吞吐),信号量负责同步(避免竞争);
- 解耦点:数据存储与同步分离(共享内存只管存,信号量只管锁);
- 场景:大数据量传输(如视频处理进程间传帧数据);
- 逻辑:进程通过注册中心(如 Nacos、Eureka)发现服务,无需硬编码对方地址;
- 解耦点:地址解耦(服务下线 / 上线不影响调用方);
- 场景:微服务调用(如订单服务调用支付服务,通过注册中心找地址);
- 逻辑:匿名管道(父子进程)、命名管道(无亲缘进程),通过管道传递数据;
- 解耦点:比直接共享内存松,但需知道管道路径(解耦程度低于消息队列);
- 场景:简单数据交互(如两个服务传递配置)。
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'讲解(常用范式),结合反例说明:
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;
void threadA() {
mutex1.lock();
std::this_thread::sleep_for(100ms);
mutex2.lock();
mutex2.unlock();
mutex1.unlock();
}
void threadB() {
mutex2.lock();
std::this_thread::sleep_for(100ms);
mutex1.lock();
mutex1.unlock();
mutex2.unlock();
}
常见场景:数据库连接池(线程 A 持有连接 1 等待连接 2,线程 B 持有连接 2 等待连接 1)、车载系统资源竞争(线程 A 持有传感器 1 等待传感器 2,线程 B 相反)。
Part14、如何避免死锁呢?
**考察重点:**针对死锁四个必要条件的解决方案,工程化的编码规范与工具应用,需结合 C++ 实践。
核心思路是'破坏死锁的任一必要条件',分 5 类具体措施,附代码示例:
**1.破坏'持有并等待'条件:**一次性申请所有资源,不部分持有;
实现:封装资源申请函数,同时申请所有需要的锁,失败则全部放弃;
void safeLock() {
std::lock(mutex1, mutex2);
std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
}
**2.破坏'循环等待'条件:**按固定顺序申请资源(如按资源 ID 升序);
规则:约定所有线程申请锁时,先申请 ID 小的锁,再申请 ID 大的锁;
const int MUTEX1_ID = 1, MUTEX2_ID = 2;
void threadA() {
if (MUTEX1_ID < MUTEX2_ID) {
mutex1.lock();
mutex2.lock();
} else {
mutex2.lock();
mutex1.lock();
}
mutex2.unlock();
mutex1.unlock();
}
void threadB() {
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);
if (!lock2.try_lock_for(100ms)) {
lock1.unlock();
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";
}
工具:C++ 用 pstack(查看线程调用栈)、valgrind --tool=helgrind(检测数据竞争与死锁);
流程:测试环境运行服务,用工具监控线程状态,若发现'循环等待'调用栈则优化代码。
Part15、C++ 是如何保证线程安全的呢?
考察重点:C++ 线程安全的实现手段,不同机制的适用场景(如原子操作 vs 锁)。
- std::mutex(互斥锁):保护临界区(如多线程读写共享变量),用 lock_guard 自动加解锁;
- std::shared_mutex(读写锁):读共享、写独占(适合读多写少,如缓存查询);
- std::spinlock(自旋锁):线程等待时不阻塞(循环查锁),适合临界区执行时间短(如传感器数据读取);
- std::atomic(如 atomic_int、atomic_bool):底层基于 CPU 原子指令(如 x86 的 cmpxchg),无需锁,效率高;
- 场景:多线程计数器(如请求量统计)、标志位(如线程停止信号);
- 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)也可能不是原子的;
- 对容器的所有操作(读 / 写)加锁(如 std::mutex),适合低并发;
- 示例:
std::mutex mtx;
std::vector<int> vec;
void push(int val) {
std::lock_guard<std::mutex> lock(mtx);
vec.push_back(val);
}
int get(int idx) {
std::lock_guard<std::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 的底层原理(动态代理)、核心概念与实际应用。
- 切面(Aspect):封装横切逻辑的类(如日志切面、事务切面),用 @Aspect 注解;
- 通知(Advice):切面的具体逻辑(@Before 前置、@After 后置、@Around 环绕、@AfterReturning 返回后、@AfterThrowing 异常后);
- 切入点(Pointcut):定义'哪些方法要被增强'(如 execution (* com.xxx.service..(..)));
- 目标对象(Target):被代理的原始对象(如 UserService);
- 代理(Proxy):Spring 生成的代理对象(增强后的对象);
动态代理:Spring AOP 基于动态代理,分两种方式,按需选择:
- 前提:目标类必须实现接口(如 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;
}
);
- 前提:目标类无需实现接口(通过继承生成子类);
- 原理:通过字节码生成工具(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 缓存未命中,测试布隆过滤器逻辑:
bloomFilter.add(1001);
bloomFilter.add(1002);
assertFalse(bloomFilter.contains(100000));
assertTrue(bloomFilter.contains(1001));
- 先对 10% 流量开启'缓存空值 + 布隆过滤器'方案,观察监控(Redis 命中率、MySQL 负载)1 小时,无异常后全量上线;
- 效果:全量后未再出现穿透,接口超时率从 5% 降到 0.1%。
Part21、你的 Linux 主要是用来干嘛的呢?
考察重点:Linux 实操能力,与后端开发的结合度(开发、部署、调试)。
- 安装工具链(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(解压)。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online