百度后端 J100737 八股复习手册
结合你的简历,面试官最容易沿着以下技术栈展开:C++17、STL、RAII、多线程、Linux、Socket、Epoll、TCP/IP、HTTP,以及 Windows 输入法中的 UI 卡顿、跨进程锁、TLS 快照;此外还会验证你是否具备 MySQL、Redis 和分布式后端基础。
回答八股时建议固定采用:
定义是什么 -> 底层原理 -> 优缺点 -> 适用场景 -> 结合项目举例
一、C++ 基础:P0
1. 指针和引用有什么区别?
参考回答:
指针本身是一个对象,保存地址,可以为空,也可以在生命周期中指向不同对象;引用是一个对象的别名,定义时必须初始化,之后不能改为引用另一个对象。
主要区别:
- 指针可以是
nullptr,引用原则上应绑定有效对象; - 指针可以重新赋值,引用不能重新绑定;
- 指针有自身存储空间,引用在语言层面是别名;
- 指针需要显式解引用,引用使用方式和普通变量类似;
- 多级指针存在,但没有普通意义上的引用数组。
工程中,可空或需要改变指向时使用指针;参数必须存在且不转移所有权时,可以使用引用。
2. new/delete 与 malloc/free 有什么区别?
malloc 只分配指定大小的原始内存,不调用构造函数,返回 void*。
new 分配内存后还会调用对象构造函数,并返回对应类型的指针。
类似地:
delete会调用析构函数并释放内存;free只释放内存;new失败默认抛出std::bad_alloc;malloc失败返回nullptr;new[]必须和delete[]匹配。
C++ 工程中应优先使用自动对象、标准容器和智能指针,避免直接手动管理内存。
3. 栈和堆有什么区别?
栈一般由编译器和运行时自动管理,用于局部变量、函数参数和调用信息。分配释放速度快,**生命周期与作用域绑定,**但容量有限。
堆用于动态分配,生命周期由程序控制,空间较大,但会产生分配器开销、内存碎片和泄漏风险。
需要注意:
栈和堆是常用的实现概念,C++ 标准更关注对象的存储期,而不是强制对象必须放在某种具体物理区域。
4. 什么是 RAII?
RAII 是”资源获取即初始化”,把资源生命周期绑定到对象生命周期:
- 构造函数获取资源;
- 析构函数释放资源。
资源不仅包括内存,也包括:
- Mutex;
- 文件描述符;
- Socket;
- 数据库连接;
- 线程句柄。
例如 std::lock_guard 构造时加锁,析构时自动解锁,可以保证提前返回或异常时也不会忘记释放锁。
5. unique_ptr、shared_ptr、weak_ptr 有什么区别?
unique_ptr
表示独占所有权:
- 不能复制;
- 可以移动;
- 开销较小;
- 应作为动态对象所有权的默认选择。
shared_ptr
表示共享所有权:
- 通过控制块维护强引用计数;
- 最后一个强引用销毁时释放对象;
- 存在引用计数和原子操作开销;
- 可能产生循环引用。
weak_ptr
表示非拥有观察关系:
- 不增加强引用计数;
- 通过
lock()尝试获得shared_ptr; - 常用于打破循环引用。
一句话:
优先
unique_ptr,确实需要共享生命周期时才使用shared_ptr,非拥有关系使用引用、裸指针或weak_ptr。
6. shared_ptr 是否线程安全?
需要分三层回答:
- 多个不同的
shared_ptr对象共享同一个控制块时,引用计数增减通常是线程安全的; - 多个线程同时修改同一个
shared_ptr变量,不一定安全,需要同步; shared_ptr所指向的业务对象不会自动变成线程安全。
所以:
引用计数线程安全,不等于对象内容线程安全。
7. 什么是拷贝语义和移动语义?
拷贝通常创建资源的独立副本。
移动通常把资源所有权从源对象转移到目标对象,避免昂贵的深拷贝。移动后源对象必须仍然处于合法但状态未指定的状态。
典型移动构造:
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}标准容器扩容时通常更愿意使用 noexcept 的移动构造,否则为了异常安全,可能退回拷贝。
8. 什么是右值引用和完美转发?
右值引用使用 T&&,主要用于:
- 移动语义;
- 转发临时对象;
- 避免不必要拷贝。
完美转发是模板函数通过 std::forward<T> 保留参数原来的左值或右值属性。
template <typename T>
void wrapper(T&& value) {
target(std::forward<T>(value));
}这里的 T&& 在类型推导环境中属于转发引用,不一定总是普通右值引用。
9. 虚函数是怎么实现的?
常见实现中,包含虚函数的对象内部会保存一个虚表指针 vptr,指向类对应的虚函数表 vtable。
调用虚函数时,程序根据对象的动态类型:
- 获取虚表指针;
- 找到虚函数地址;
- 间接调用对应实现。
虚函数实现运行时多态,但会引入:
- 对象中的虚表指针开销;
- 间接调用;
- 一定程度上影响内联优化。
基类如果会通过基类指针删除派生类对象,析构函数必须是虚函数:
virtual ~Base() = default;10. 构造函数和析构函数中能否调用虚函数?
可以调用,但不会表现出完整动态多态。
在基类构造期间,派生部分尚未构造完成;在基类析构期间,派生部分已经销毁。因此调用虚函数时,只会调用当前正在构造或析构层级的实现。
不要依赖构造函数中的虚调用执行派生类逻辑。
构造函数期间,对象还没有,何来的虚表指针呢。
11. vector、deque、list 怎么选?
vector
- 连续存储;
- 随机访问 O(1);
- 缓存局部性好;
- 尾部插入均摊 O(1);
- 中间插入需要移动元素;
- 扩容可能导致迭代器失效。
默认优先选择。
deque
- 分段连续存储;
- 支持首尾高效插入;
- 支持随机访问;
- 缓存局部性一般弱于 vector。
list
- 双向链表;
- 已知位置插入删除 O(1);
- 不支持随机访问;
- 节点开销大;
- 缓存局部性差。
LRU 通常使用 list + unordered_map,因为需要 O(1) 定位和 O(1) 移动节点。
12. map 和 unordered_map 有什么区别?
map 通常基于红黑树:
- 查询、插入、删除 O(log n);
- Key 有序;
- 支持范围查询;
- 性能比较稳定。
unordered_map 基于哈希表:
- 平均查询、插入 O(1);
- 最坏 O(n);
- 无序;
- 需要处理哈希冲突和扩容。
需要有序遍历、范围查询或稳定复杂度时用 map;只要求快速查找时优先考虑 unordered_map。
13. 哈希冲突怎么解决?
常见方法:
链地址法
同一桶中的元素通过链表、数组或树结构存放。
开放寻址法
发生冲突后,在哈希表中寻找其他空位置,例如:
- 线性探测;
- 二次探测;
- 双重哈希。
负载因子过高会增加冲突,因此哈希表需要扩容和重新散列。
14. vector 迭代器什么时候失效?
典型情况:
- 扩容后,所有指针、引用和迭代器失效;
- 中间插入或删除后,操作位置及其后面的迭代器通常失效;
- 尾部
pop_back会使被删除元素的迭代器失效。
不要在遍历期间无条件调用 push_back,除非已经保证容量不会变化或重新获取迭代器。
二、并发与 C++ 内存模型:P0
这是你最需要重点准备的部分,因为你的项目中直接使用了原子 ACK、多线程 Watchdog 和跨进程锁。
1. 进程和线程有什么区别?
进程是资源分配和隔离的基本单位,通常拥有独立的虚拟地址空间、文件描述符表和其他资源。
线程是 CPU 调度和执行的基本单位。同一进程中的线程共享:
- 地址空间;
- 堆;
- 全局变量;
- 文件描述符。
但线程各自拥有:
- 栈;
- 寄存器上下文;
- 线程局部存储;
- 调度状态。
进程隔离更强,但通信和切换成本更高;线程共享数据方便,但更容易产生数据竞争。
2. Mutex、自旋锁、读写锁如何选择?
Mutex
竞争时线程进入阻塞状态,适合临界区较长或竞争较明显的情况。
自旋锁
竞争时线程持续循环等待,不发生睡眠切换。适合:
- 临界区非常短;
- 不允许睡眠;
- CPU 核心充足;
- 锁持有时间可控。
持锁时间长时,自旋会浪费 CPU。
读写锁
允许多个读者并发,但写者独占。适合读多写少且读临界区不太短的场景。
读写锁不一定比普通 Mutex 快,还可能产生写者饥饿或更高管理开销。
3. 条件变量为什么必须配合 Mutex?
条件变量负责等待”某个状态发生变化”,但状态本身仍需要 Mutex 保护。
标准写法:
std::unique_lock<std::mutex> lock(mutex);
condition.wait(lock, [&] {
return !queue.empty() || stopped;
});等待时条件变量会原子地:
- 释放 Mutex;
- 进入等待;
- 被唤醒后重新获取 Mutex;
- 检查条件。
必须使用循环或谓词,因为存在:
- 虚假唤醒;
- 唤醒后条件被其他线程抢先改变。
4. atomic 与 Mutex 怎么选?
适合 atomic:
- 单个计数器;
- 独立状态位;
- 简单的读、写、CAS。
适合 Mutex:
- 多个变量需要保持不变量;
- 临界区包含复杂逻辑;
- 操作无法由单个原子指令完成;
- 需要条件变量等待。
无锁不一定更快。高竞争时 CAS 可能持续失败,复杂无锁结构还需要处理 ABA 和内存回收。
5. 什么是数据竞争?
如果两个线程并发访问同一内存位置:
- 至少一个是写;
- 且没有原子操作或同步关系;
就构成数据竞争。
C++ 中的数据竞争会导致未定义行为,不只是”可能读到旧数据”。
你项目里的 ACK 由 UI 线程写、Watchdog 线程读,因此需要 atomic 或锁。
6. volatile 能保证线程安全吗?
不能。
C++ 中的 volatile 主要告诉编译器该内存可能被程序之外的因素修改,不应随意省略相关访问。
它不保证:
- 操作原子性;
- 多线程可见性;
- 内存顺序;
- happens-before。
线程间共享状态应使用 atomic、Mutex 或其他同步工具。
9. 什么是 CAS?
CAS 比较内存中的当前值和预期值:
- 相等则写入新值;
- 不相等则返回失败及当前值。
C++ 中常用:
compare_exchange_weak
compare_exchange_strongweak 允许虚假失败,通常适合放在循环中;strong 不允许因实现原因虚假失败,但仍可能因值不匹配而失败。
10. 什么是 ABA 问题?
某线程读取到值 A,暂停期间其他线程把它改为 B,又改回 A。
原线程执行 CAS 时发现值仍是 A,误以为期间没有发生变化。
解决方法包括:
- 版本号或标签指针;
- hazard pointer;
- epoch-based reclamation;
- 避免立即复用节点;
- 使用锁。
11. 什么是 false sharing?
两个线程修改不同变量,但变量位于同一个 CPU 缓存行中。
一个核心写入时会导致其他核心对应缓存行失效,产生频繁缓存一致性流量,造成性能下降。
可以通过:
- 数据对齐;
- 填充;
- 将高频写变量分离到不同缓存行;
- 减少共享写;
进行优化。
12. 死锁的四个必要条件是什么?
- 互斥;
- 持有并等待;
- 不可剥夺;
- 循环等待。
预防方法:
- 统一锁顺序;
- 缩小锁作用域;
- 使用
std::lock获取多个锁; - 使用超时;
- 避免持锁调用外部代码;
- 避免持锁执行磁盘、网络等阻塞操作。
13. 线程池怎么设计?
核心组件:
- 工作线程集合;
- 任务队列;
- Mutex;
- 条件变量;
- 停止标志。
提交任务时:
- 加锁放入队列;
- 通知工作线程。
工作线程:
- 等待队列非空或停止;
- 取出任务;
- 释放锁;
- 执行任务。
注意:
- 队列最好有界;
- 关闭时明确是否执行剩余任务;
- 捕获任务异常,避免工作线程退出;
- 避免在线程池任务里无限等待同一线程池中的其他任务。
三、操作系统与 Linux:P0
1. 用户态和内核态有什么区别?
用户态权限较低,不能直接访问硬件和内核数据。
内核态可以执行特权指令,管理:
- 内存;
- 进程;
- 文件系统;
- 网络;
- 设备。
用户程序通过系统调用进入内核,例如:
read
write
open
socket
epoll_wait
系统调用涉及权限级别切换和上下文保存恢复,因此不是零成本。
2. 什么是虚拟内存?
每个进程看到的是独立的虚拟地址空间。操作系统通过页表将虚拟地址映射到物理内存或磁盘页面。
优势:
- 进程隔离;
- 地址空间统一;
- 按需分配;
- 共享内存;
- 文件映射;
- 支持超过物理内存的地址空间。
3. 什么是缺页异常?
CPU 访问虚拟页时,如果页表显示页面未驻留或访问不合法,就会触发缺页异常。
可能情况:
- 页面尚未分配;
- 文件映射页尚未加载;
- 页面被换出;
- 写时复制;
- 非法地址访问。
缺页异常不一定是错误。操作系统可能完成分配或加载后继续执行;非法访问则可能终止进程。
4. 什么是 TLB?
TLB 是页表项的高速缓存,用来减少每次虚拟地址转换都访问多级页表的开销。
TLB miss 时需要进行页表遍历。上下文切换和大量随机内存访问可能增加 TLB miss。
大页可以减少页表项数量和 TLB 压力,但也可能增加内存浪费。
5. 什么是上下文切换?
线程或进程被调度切换时,需要保存当前执行状态并恢复另一个执行单元的状态,包括:
- 寄存器;
- 程序计数器;
- 栈指针;
- 调度信息。
进程切换通常还涉及地址空间切换,可能影响 TLB 和缓存,因此一般比同进程线程切换成本更高。
6. 常见进程间通信方式有哪些?
- 匿名管道;
- 命名管道;
- 消息队列;
- 共享内存;
- 信号;
- Socket;
- RPC;
- Windows 命名 Mutex、Event 等同步对象。
共享内存速度快,但必须额外设计同步、一致性和异常恢复。
7. 阻塞、非阻塞、同步、异步有什么区别?
阻塞和非阻塞描述调用线程是否等待。
同步和异步描述结果完成时由谁负责推进和通知。
例如:
- 非阻塞
read:没有数据立即返回EAGAIN; - epoll 通知后再调用
read,通常仍属于同步 I/O; - 真正异步 I/O 是内核完成数据传输后通知应用。
8. select、poll、epoll 有什么区别?
select
- 使用位图;
- 描述符数量通常受限;
- 每次调用需要传入集合;
- 返回后遍历全部描述符。
poll
- 使用数组;
- 没有同样的固定位图限制;
- 仍需线性扫描。
epoll
- 内核维护关注集合;
epoll_ctl增删监听对象;epoll_wait返回就绪事件;- 更适合大量连接、少量活跃场景。
epoll 并不是任何情况下都更快,连接少或大部分连接都活跃时优势会降低。
9. epoll 的 LT 和 ET 有什么区别?
LT:水平触发
只要描述符仍然可读或可写,每次 epoll_wait 都可能继续通知。
ET:边缘触发
通常只在状态从未就绪变为就绪时通知。
ET 模式收到可读事件后,应使用非阻塞 I/O 循环读到 EAGAIN,否则剩余数据可能不会再次通知。
10. 什么是零拷贝?
传统文件发送可能经历:
磁盘 -> 内核页缓存 -> 用户缓冲区 -> Socket 缓冲区 -> 网卡
零拷贝技术尝试减少用户态与内核态之间的数据复制,例如:
sendfile;splice;mmap配合write。
“零拷贝”通常不是绝对没有任何硬件复制,而是减少 CPU 参与的数据复制和上下文切换。
四、Windows 与输入法专项:P0
这部分与你的实习项目强相关。
1. Windows 消息循环是什么?
UI 线程通常不断取消息并分发:
while (GetMessage(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}如果 UI 线程长时间计算、等待锁或执行同步 I/O,就无法继续处理:
- 绘制;
- 键盘;
- 鼠标;
- 窗口移动;
- 自定义消息。
于是表现为窗口卡顿。
2. PostMessage 和 SendMessage 有什么区别?
PostMessage 异步投递消息到目标窗口所属线程的消息队列,通常立即返回。
SendMessage 具有同步语义,尤其跨线程时,发送方可能等待目标窗口过程处理完成。
Watchdog 应使用 PostMessage:
如果 UI 线程已经卡住,Watchdog 不能同步等待 UI 线程,否则监控线程也会一起卡住。
3. 消息 Post 成功是否说明 UI 线程正常?
不说明。
Post 成功只表示消息成功进入队列。只有 UI 线程真正取出消息、执行窗口过程并更新 ACK,才能说明消息循环有响应。
4. 输入法候选框 UI 线程在哪里?
需要以真实架构为准。较常见的是:
- 候选框有自己的 Windows 窗口;
- 由搜狗自己的 UI 线程负责;
- UI 线程可能属于输入法独立进程;
- 宿主 UI 线程是另一条执行链。
你明天需要核实:
- Watchdog 在哪个进程;
- 候选框窗口由哪个线程创建;
- ACK 在哪个窗口过程中更新;
- 跨进程锁的另一端是谁。
5. Windows Mutex 与 Critical Section 有什么区别?
Critical Section 主要用于同一进程内线程同步,用户态快速路径开销相对小。
Windows Mutex 属于内核同步对象,可以跨进程使用,但开销通常更高。
等待 Mutex 时可能返回:
WAIT_OBJECT_0;WAIT_TIMEOUT;WAIT_ABANDONED;WAIT_FAILED。
6. WAIT_ABANDONED 是什么意思?
原持锁线程在没有释放 Mutex 的情况下退出。
当前线程会获得 Mutex 所有权,但受保护的数据可能只更新了一半。因此不能简单当作正常成功,应该:
- 校验共享状态;
- 恢复或重建;
- 必要时放弃当前数据;
- 记录异常。
7. 为什么 UI 线程不能无限等待 Mutex?
UI 线程等待期间无法返回消息循环,窗口绘制、候选更新和鼠标交互都会停止。
正确思路:
- 使用短超时;
- 超时后返回旧快照或默认值;
- 把阻塞操作异步化;
- 使用本地缓存或 TLS 快照。
8. TLS 是什么?
TLS 是线程局部存储。每个线程拥有独立的数据副本。
C++ 中可以使用:
thread_local UserEnvSnapshot snapshot;优点:
- 热路径读取不竞争共享锁;
- 读取成本稳定;
- 缓存局部性好。
缺点:
- 多线程可能看到不同版本;
- 需要更新和失效机制;
- 每个线程占用一份内存;
- 需要保证对象生命周期。
9. 为什么焦点切换适合更新 TLS 快照?
输入焦点变化通常意味着:
- 宿主窗口变化;
- 输入上下文变化;
- 中英文状态可能变化;
- 当前 UserEnv 作用域变化。
而焦点切换频率远低于按键频率。
因此可以:
焦点切换时同步一次共享配置,按键热路径只读 TLS 副本。
这是用短暂一致性换取低延迟和可用性。
10. 什么是 ABI 兼容?
ABI 是编译后二进制层面的接口约定,包括:
- 函数调用约定;
- 参数和返回值布局;
- 类对象布局;
- 虚表;
- 符号名称;
- 异常和运行库边界。
保持 ABI 兼容的常见方式:
- 不修改旧导出函数签名;
- 不随意调整公开类成员;
- 新增接口而不是修改原接口;
- 结构体增加版本号和大小字段;
- 明确内存由哪一侧分配和释放。
你的项目新增超时锁接口,而不是直接破坏旧接口,是典型兼容思路。
五、计算机网络:P0
1. TCP 为什么三次握手?
三次握手主要用于:
- 双方确认发送和接收能力;
- 同步初始序列号;
- 协商 TCP 参数。
两次握手无法让服务端确认客户端是否已经收到自己的初始序列号,也更容易受到历史失效连接请求影响。
2. TCP 为什么四次挥手?
TCP 是全双工连接,两个方向需要分别关闭。
一方发送 FIN,只表示自己没有更多数据发送,但仍可以接收数据。另一方确认后,等自己的数据发送完,再发送 FIN,因此通常需要四个报文。
3. TIME_WAIT 有什么作用?
主动关闭方通常进入 TIME_WAIT。
作用:
- 如果最后一个 ACK 丢失,可以重新响应对端重传的 FIN;
- 让旧连接中的延迟报文在网络中消失,避免影响相同四元组的新连接。
大量 TIME_WAIT 通常说明存在大量主动关闭的短连接,优先考虑连接池、长连接和复用,而不是直接粗暴修改系统参数。
4. CLOSE_WAIT 很多说明什么?
说明对端已经关闭连接,但本地应用还没有调用 close。
常见原因:
- 异常路径遗漏关闭;
- 业务线程长期阻塞;
- 连接对象泄漏;
- 处理函数没有返回。
排查:
ss / netstat
lsof
pstack / gdb
日志和连接ID
5. TCP 如何保证可靠性?
主要机制:
- 序列号;
- 确认应答;
- 超时重传;
- 快速重传;
- 校验和;
- 滑动窗口;
- 流量控制;
- 拥塞控制;
- 有序重组;
- 重复报文丢弃。
6. 流量控制与拥塞控制有什么区别?
流量控制保护接收方,主要依据接收窗口,防止发送方把接收方缓冲区打满。
拥塞控制保护网络,依据拥塞窗口调整发送速率,常见阶段包括:
- 慢启动;
- 拥塞避免;
- 快速重传;
- 快速恢复。
7. 什么是 TCP 粘包?
TCP 是字节流,没有消息边界。
发送方连续写入多条消息,接收方一次 read 可能得到:
- 半条消息;
- 一条消息;
- 多条消息。
应用层需要自行定义协议边界,例如:
- 固定长度;
- 分隔符;
- 长度字段;
- TLV 格式。
“粘包”不是 TCP 错误,而是应用误以为一次写对应一次读。
8. HTTP/1.1、HTTP/2、HTTP/3 有什么区别?
HTTP/1.1
- 长连接;
- 文本协议;
- 一个连接上的请求处理容易受队头阻塞影响。
HTTP/2
- 二进制分帧;
- 多路复用;
- Header 压缩;
- 服务端推送机制。
但多个流仍共享一个 TCP 连接,TCP 丢包时可能影响全部流。
HTTP/3
基于 QUIC 和 UDP:
- 连接建立更快;
- 每个流独立处理丢包;
- 支持连接迁移;
- 默认集成 TLS。
9. HTTPS 建立连接的大致过程是什么?
现代 TLS 大致包括:
- 客户端发送支持的协议版本、密码套件和随机数;
- 服务端返回选择结果、证书和密钥交换信息;
- 客户端验证证书;
- 双方通过密钥交换计算会话密钥;
- 后续使用对称加密传输数据。
非对称密码主要用于身份认证和密钥协商,对称密码用于高效传输数据。
10. RPC 和 HTTP 有什么区别?
RPC 是远程调用抽象,关注像调用本地函数一样调用远端服务。
HTTP 是应用层协议,可以作为 RPC 的传输载体。
常见 RPC 框架通常提供:
- 接口定义;
- 序列化;
- 服务发现;
- 连接管理;
- 超时和重试;
- 负载均衡;
- 代码生成。
REST HTTP 通用性和可调试性较好;二进制 RPC 通常体积更小、性能更高、接口约束更强。
六、MySQL:P0
1. 为什么数据库索引使用 B+ 树?
B+ 树非叶子节点通常只存 Key 和子指针,因此一个页可以容纳更多索引项,分支因子大、树高低。
叶子节点按 Key 有序连接,适合:
- 等值查询;
- 范围查询;
- 排序;
- 顺序扫描。
与哈希相比,B+ 树支持范围查询;与二叉树相比,能减少磁盘页访问次数。
2. 什么是聚簇索引和二级索引?
InnoDB 聚簇索引的叶子节点存完整行数据,通常按主键组织。
二级索引叶子节点一般存:
- 索引列;
- 主键值。
通过二级索引找到主键后,如果查询字段不在索引中,需要再访问聚簇索引,这就是回表。
3. 什么是覆盖索引?
查询需要的所有字段都可以从某个索引直接获得,不需要回表。
例如联合索引:
INDEX idx_user_status(user_id, status)查询只返回 user_id 和 status 时,可能使用覆盖索引。
覆盖索引可以减少随机 I/O,但索引字段过多会增加存储和写入开销。
4. 什么是最左前缀原则?
联合索引 (a,b,c) 按照:
a;a相同时按b;a、b相同时按c;
排序。
因此通常可以支持:
WHERE a = ?
WHERE a = ? AND b = ?
WHERE a = ? AND b = ? AND c = ?直接只使用 b 或 c 通常无法利用完整有序前缀。
范围条件之后的列可能无法继续有效缩小扫描范围,最终以 EXPLAIN 为准。
5. ACID 是什么?
- Atomicity 原子性:事务操作要么全部成功,要么全部失败;
- Consistency 一致性:事务使数据库从一个合法状态转换到另一个合法状态;
- Isolation 隔离性:并发事务之间按照规定的隔离级别相互影响;
- Durability 持久性:事务提交后结果不会因普通故障丢失。
6. 四种事务隔离级别是什么?
从弱到强:
- READ UNCOMMITTED;
- READ COMMITTED;
- REPEATABLE READ;
- SERIALIZABLE。
可能问题:
- 脏读;
- 不可重复读;
- 幻读。
InnoDB 默认一般是 REPEATABLE READ,并通过 MVCC 与锁机制处理并发读取和写入。
幻读:满足条件的行变多或变少了
7. MVCC 是什么?
MVCC 通过维护数据的多个版本,使读操作根据事务可见性规则读取合适版本,减少读写阻塞。
核心包括:
- 隐藏事务信息;
- undo log 版本链;
- Read View;
- 可见性判断。
READ COMMITTED 通常每次一致性读创建新的 Read View;REPEATABLE READ 一般在事务第一次一致性读时创建并复用。
8. redo log、undo log、binlog 分别做什么?
redo log
InnoDB 层,用于崩溃恢复,保证已提交修改的持久性。
undo log
保存修改前的信息,用于:
- 事务回滚;
- MVCC 历史版本。
binlog
MySQL Server 层,记录逻辑变更,常用于:
- 主从复制;
- 数据恢复;
- 订阅数据变化。
9. 什么情况下索引可能失效?
常见情况:
- 联合索引没有满足有效前缀;
- 对索引列进行函数计算;
- 隐式类型转换;
- 前导模糊匹配:
LIKE '%abc'; - 选择性过低;
- OR 条件无法合理使用索引;
- 优化器判断全表扫描成本更低。
不能机械地说”某语法一定失效”,最终应看执行计划。
10. 慢 SQL 如何排查?
- 查看慢查询日志和监控;
- 确认 SQL 频率、耗时和扫描行数;
- 使用
EXPLAIN查看访问类型、使用索引和估算行数; - 检查是否全表扫描、回表过多或排序落盘;
- 优化索引、SQL、数据模型;
- 必要时拆分查询、缓存、归档或分库分表;
- 验证优化后的执行计划和线上指标。
九、稳定性与性能排查:P0
1. 服务 CPU 突然升高怎么排查?
先看:
- 用户态还是内核态;
- 单核还是多核;
- 单进程还是整机;
- 是否伴随 QPS、延迟、错误率变化。
工具:
top
pidstat
perf top
perf record
火焰图
pstack / gdb
常见原因:
- 死循环;
- 锁自旋;
- 重试风暴;
- 频繁序列化;
- 正则或算法退化;
- 内存分配;
- 系统调用;
- 流量突增。
先止损:限流、降级、扩容、回滚,再做根因分析。
2. 内存持续上涨怎么排查?
先区分:
- RSS;
- 虚拟内存;
- 堆;
- mmap;
- 页缓存;
- 业务缓存。
判断泄漏还是缓存:
- 是否随请求持续单调增长;
- 流量下降后是否回落;
- 对象数量是否有上限;
- 缓存是否有淘汰;
- 线程、连接、文件描述符是否持续增加。
工具:
- ASan/LSan;
- Valgrind;
- heap profiler;
- 堆快照;
- 对象计数。
3. 请求延迟突然升高怎么排查?
看分位数:
- P50 高:整体普遍变慢;
- P99 高但 P50 正常:长尾问题;
- 只有部分接口高:局部链路问题。
按链路拆分:
排队时间
业务计算
数据库
Redis
RPC
模型服务
网络
序列化
结合 Trace 定位最慢 Span,再检查:
- 线程池队列;
- 连接池;
- 锁等待;
- 下游超时;
- GC;
- 磁盘 I/O;
- CPU 饱和;
- 热Key。
4. 为什么平均值不够?
平均值容易掩盖长尾。
例如 99 个请求耗时 10ms,一个请求耗时 10s,平均值会明显变大,但仍不能清晰描述用户分布。
常见指标:
- P50:典型请求;
- P95:较慢用户;
- P99:长尾;
- 最大值:极端异常,但容易受偶发值影响。
你的版本数使用 P99,也是为了观察长尾设备,而不是只看平均水平。
5. 可观察性包括什么?
三大基础:
- Metrics:指标;
- Logs:日志;
- Traces:链路追踪。
关键原则:
- 每个请求有 Trace ID;
- 日志包含关键上下文;
- 指标能看容量、错误率和延迟;
- 告警可行动,避免噪声;
- 能关联版本、配置和发布批次;
- 故障能够复现和回滚。
十、AI、RAG 与 Agent:P1
你简历中有 LangGraph、RAG、MCP、多 Agent 和长期记忆经历,这部分很可能作为岗位加分项被追问。
1. RAG 是什么?
RAG 通过检索外部知识,再把检索结果加入模型上下文生成答案。
典型流程:
文档解析
-> 切分 Chunk
-> Embedding
-> 建立索引
-> Query 改写
-> 检索
-> Rerank
-> 构造 Prompt
-> 模型生成
-> 引用与评估
优势:
- 更新知识不一定需要重新训练;
- 可以提供来源;
- 降低部分知识型幻觉。
但 RAG 不能完全消除幻觉。
2. Chunk 太大或太小有什么问题?
Chunk 太小:
- 上下文信息不完整;
- 语义被切断;
- 检索结果碎片化。
Chunk 太大:
- 检索精度下降;
- 无关内容多;
- Token 成本增加;
- 模型更难定位关键信息。
需要结合文档结构、模型上下文长度和评测结果选择。
3. 什么是混合检索和 Rerank?
混合检索结合:
- 关键词检索;
- 向量语义检索。
关键词适合专业术语、编号和精确匹配;向量检索适合语义相似。
Rerank 使用更精确但更昂贵的模型,对初步召回结果重新排序,提高最终上下文质量。
4. Agent 与固定工作流有什么区别?
固定工作流的步骤和分支由程序明确规定,稳定、可预测、容易测试。
Agent 会根据模型判断动态选择工具和下一步操作,适合开放性任务,但:
- 延迟更高;
- 行为更不稳定;
- 成本更高;
- 需要终止和安全控制。
你的 Game Agent 中,能由游戏状态明确判断的场景使用确定性路由,这是比无条件模型路由更稳妥的设计。
5. Agent 工具调用要注意什么?
- 参数 Schema 校验;
- 权限控制;
- 超时;
- 重试;
- 幂等;
- 结果长度限制;
- 敏感操作确认;
- 工具返回值结构化;
- 防止无限循环;
- 全链路 Trace。
不能直接信任模型生成的工具参数。
6. Agent Memory 和普通对话历史有什么区别?
普通对话历史通常是当前会话的上下文。
Memory 更强调:
- 跨步骤或跨会话保存;
- 结构化;
- 可检索;
- 有来源;
- 有作用域;
- 有更新、失效和删除机制。
你的 SG Flow Memory 更偏向代码库特定的工程经验,包括实现约束、历史故障和验证经验。
7. 如何评估 RAG 或 Agent?
RAG 离线指标:
- Recall@K;
- 检索命中率;
- 上下文相关性;
- 答案正确性;
- 引用准确性;
- 拒答准确性。
Agent 指标:
- 任务完成率;
- 工具调用成功率;
- 平均步骤数;
- 重试率;
- 延迟;
- Token 成本;
- 错误恢复能力;
- 人工接管率。
最终仍需结合真实业务指标和人工评测。
十一、必须手写的算法题
虽然严格来说不是”八股”,但百度后端几乎一定会考代码。
优先掌握:
- 反转链表;
- 判断链表环;
- 合并有序链表;
- 二叉树层序遍历;
- 前序和中序重建二叉树;
- 二叉树最近公共祖先;
- Top K;
- 快速排序;
- 二分查找及边界;
- 滑动窗口;
- LRU 缓存;
- 生产者消费者;
- 线程安全队列;
- 两个线程交替打印;
- 接雨水。
写题时主动说:
- 输入输出;
- 核心思路;
- 时间复杂度;
- 空间复杂度;
- 空输入、重复值和溢出边界。
十二、与你项目绑定的必背 15 题
这 15 题优先级最高:
- 为什么 Watchdog 使用
PostMessage,不使用SendMessage? - 消息 Post 成功是否说明 UI 线程正常?
- ACK 为什么必须用 atomic?
- ACK 可以使用
memory_order_relaxed吗? - 200ms 是探测周期还是卡顿阈值?
- 如何避免探测消息在队列中大量积压?
- 暂停 UI 线程采集调用栈有什么风险?
- 跨进程锁的具体实现是什么?
WAIT_ABANDONED应该怎样处理?- 为什么 UI 线程不能使用无限等待?
- 超时没有获得锁后,业务如何降级?
- TLS 快照为什么能减少锁竞争?
- 焦点未切换但配置发生变化怎么办?
- 怎样保持原有 ABI 兼容?
- 加锁次数下降 50% 为什么不等于延迟下降 50%?
你的核心叙述应稳定为:
候选框 UI 线程负责消息处理与绘制;Watchdog 异步投递探测消息,UI 线程处理后更新原子 ACK;卡顿时通过调用栈定位到 UI 热路径上的跨进程锁无限等待;随后通过超时接口和焦点驱动的 TLS 快照,把不可控阻塞改为有界等待,并降低高频路径的共享同步。
十三、复习优先级
第一优先级
- 简历项目;
- C++ 对象生命周期与智能指针;
- atomic 和内存序;
- Mutex、条件变量、死锁;
- epoll;
- TCP;
- MySQL 索引和 MVCC;
- Redis 一致性;
- 线上 CPU、内存和延迟排查。
第二优先级
- 分布式幂等;
- 消息队列;
- 限流熔断降级;
- Raft;
- RAG 和 Agent;
- 医疗大模型系统设计。
第三优先级
- C++ 模板高级细节;
- 无锁数据结构实现;
- 数据库内核深层实现;
- Multi-Raft 等高级分布式内容。
面试前应优先保证第一优先级能连续口述两分钟,并能承受至少两轮追问。