百度后端 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 是否线程安全?

需要分三层回答:

  1. 多个不同的 shared_ptr 对象共享同一个控制块时,引用计数增减通常是线程安全的;
  2. 多个线程同时修改同一个 shared_ptr 变量,不一定安全,需要同步;
  3. 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。

调用虚函数时,程序根据对象的动态类型:

  1. 获取虚表指针;
  2. 找到虚函数地址;
  3. 间接调用对应实现。

虚函数实现运行时多态,但会引入:

  • 对象中的虚表指针开销;
  • 间接调用;
  • 一定程度上影响内联优化。

基类如果会通过基类指针删除派生类对象,析构函数必须是虚函数:

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;
});

等待时条件变量会原子地:

  1. 释放 Mutex;
  2. 进入等待;
  3. 被唤醒后重新获取 Mutex;
  4. 检查条件。

必须使用循环或谓词,因为存在:

  • 虚假唤醒;
  • 唤醒后条件被其他线程抢先改变。

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_strong

weak 允许虚假失败,通常适合放在循环中;strong 不允许因实现原因虚假失败,但仍可能因值不匹配而失败。


10. 什么是 ABA 问题?

某线程读取到值 A,暂停期间其他线程把它改为 B,又改回 A。

原线程执行 CAS 时发现值仍是 A,误以为期间没有发生变化。

解决方法包括:

  • 版本号或标签指针;
  • hazard pointer;
  • epoch-based reclamation;
  • 避免立即复用节点;
  • 使用锁。

11. 什么是 false sharing?

两个线程修改不同变量,但变量位于同一个 CPU 缓存行中。

一个核心写入时会导致其他核心对应缓存行失效,产生频繁缓存一致性流量,造成性能下降。

可以通过:

  • 数据对齐;
  • 填充;
  • 将高频写变量分离到不同缓存行;
  • 减少共享写;

进行优化。


12. 死锁的四个必要条件是什么?

  1. 互斥;
  2. 持有并等待;
  3. 不可剥夺;
  4. 循环等待。

预防方法:

  • 统一锁顺序;
  • 缩小锁作用域;
  • 使用 std::lock 获取多个锁;
  • 使用超时;
  • 避免持锁调用外部代码;
  • 避免持锁执行磁盘、网络等阻塞操作。

13. 线程池怎么设计?

核心组件:

  • 工作线程集合;
  • 任务队列;
  • Mutex;
  • 条件变量;
  • 停止标志。

提交任务时:

  1. 加锁放入队列;
  2. 通知工作线程。

工作线程:

  1. 等待队列非空或停止;
  2. 取出任务;
  3. 释放锁;
  4. 执行任务。

注意:

  • 队列最好有界;
  • 关闭时明确是否执行剩余任务;
  • 捕获任务异常,避免工作线程退出;
  • 避免在线程池任务里无限等待同一线程池中的其他任务。

三、操作系统与 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。

作用:

  1. 如果最后一个 ACK 丢失,可以重新响应对端重传的 FIN;
  2. 让旧连接中的延迟报文在网络中消失,避免影响相同四元组的新连接。

大量 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 大致包括:

  1. 客户端发送支持的协议版本、密码套件和随机数;
  2. 服务端返回选择结果、证书和密钥交换信息;
  3. 客户端验证证书;
  4. 双方通过密钥交换计算会话密钥;
  5. 后续使用对称加密传输数据。

非对称密码主要用于身份认证和密钥协商,对称密码用于高效传输数据。


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) 按照:

  1. a;
  2. a 相同时按 b;
  3. 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. 四种事务隔离级别是什么?

从弱到强:

  1. READ UNCOMMITTED;
  2. READ COMMITTED;
  3. REPEATABLE READ;
  4. 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 如何排查?

  1. 查看慢查询日志和监控;
  2. 确认 SQL 频率、耗时和扫描行数;
  3. 使用 EXPLAIN 查看访问类型、使用索引和估算行数;
  4. 检查是否全表扫描、回表过多或排序落盘;
  5. 优化索引、SQL、数据模型;
  6. 必要时拆分查询、缓存、归档或分库分表;
  7. 验证优化后的执行计划和线上指标。

九、稳定性与性能排查: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 成本;
  • 错误恢复能力;
  • 人工接管率。

最终仍需结合真实业务指标和人工评测。


十一、必须手写的算法题

虽然严格来说不是”八股”,但百度后端几乎一定会考代码。

优先掌握:

  1. 反转链表;
  2. 判断链表环;
  3. 合并有序链表;
  4. 二叉树层序遍历;
  5. 前序和中序重建二叉树;
  6. 二叉树最近公共祖先;
  7. Top K;
  8. 快速排序;
  9. 二分查找及边界;
  10. 滑动窗口;
  11. LRU 缓存;
  12. 生产者消费者;
  13. 线程安全队列;
  14. 两个线程交替打印;
  15. 接雨水。

写题时主动说:

  • 输入输出;
  • 核心思路;
  • 时间复杂度;
  • 空间复杂度;
  • 空输入、重复值和溢出边界。

十二、与你项目绑定的必背 15 题

这 15 题优先级最高:

  1. 为什么 Watchdog 使用 PostMessage,不使用 SendMessage?
  2. 消息 Post 成功是否说明 UI 线程正常?
  3. ACK 为什么必须用 atomic?
  4. ACK 可以使用 memory_order_relaxed 吗?
  5. 200ms 是探测周期还是卡顿阈值?
  6. 如何避免探测消息在队列中大量积压?
  7. 暂停 UI 线程采集调用栈有什么风险?
  8. 跨进程锁的具体实现是什么?
  9. WAIT_ABANDONED 应该怎样处理?
  10. 为什么 UI 线程不能使用无限等待?
  11. 超时没有获得锁后,业务如何降级?
  12. TLS 快照为什么能减少锁竞争?
  13. 焦点未切换但配置发生变化怎么办?
  14. 怎样保持原有 ABI 兼容?
  15. 加锁次数下降 50% 为什么不等于延迟下降 50%?

你的核心叙述应稳定为:

候选框 UI 线程负责消息处理与绘制;Watchdog 异步投递探测消息,UI 线程处理后更新原子 ACK;卡顿时通过调用栈定位到 UI 热路径上的跨进程锁无限等待;随后通过超时接口和焦点驱动的 TLS 快照,把不可控阻塞改为有界等待,并降低高频路径的共享同步。


十三、复习优先级

第一优先级

  • 简历项目;
  • C++ 对象生命周期与智能指针;
  • atomic 和内存序;
  • Mutex、条件变量、死锁;
  • epoll;
  • TCP;
  • MySQL 索引和 MVCC;
  • Redis 一致性;
  • 线上 CPU、内存和延迟排查。

第二优先级

  • 分布式幂等;
  • 消息队列;
  • 限流熔断降级;
  • Raft;
  • RAG 和 Agent;
  • 医疗大模型系统设计。

第三优先级

  • C++ 模板高级细节;
  • 无锁数据结构实现;
  • 数据库内核深层实现;
  • Multi-Raft 等高级分布式内容。

面试前应优先保证第一优先级能连续口述两分钟,并能承受至少两轮追问。