面灵AI→

小红书 PE 一面面经:检索链路与并发基础

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 自我介绍。
  2. 介绍一下实习项目。
  3. MQ 消息积压怎么处理?
  4. 从向量检索转为混合检索后,召回准确率提升多少?
  5. 如果某一次请求召回准确率暴跌,你们有做排查的链路吗?可能是哪一块的问题?
  6. 如果向量数据库数据量很大的话,怎么提高检索效率?
  7. HashMap 中遇到过把 id 加一个对象做成自定义 key 吗?讲一下 HashMap 吧。
  8. 如果两个线程访问同一个没有加任何限制的变量,会出现什么问题?
  9. 如何解决?除了加 synchronized 锁之外还有什么别的方法吗?
  10. Atomic 是不是可以解决?能用 CAS 或者 volatile 关键字解决吗?
  11. Java 中的垃圾回收讲一下。
  12. 如果一个服务的接口响应时间特别久,你怎么排查是哪里出了问题?
  13. 你在实习中实际遇到过这种情况吗?可以从一个服务的执行链路上讲讲。
  14. 之后是打算直接就业还是读博?
  15. 手撕:实现 LRU。
  16. 反问:针对面试以及岗位要求,我需要提升的地方有哪些?
  17. 反问:岗位是 PE,那么职责和部门的业务方向是什么?

《参考解析》

MQ 消息积压

先看是「消费能力不足」还是「消费卡住」,两者的处理方式完全不同。如果监控显示消费者还在推进、只是速率跟不上,说明是短时流量高峰,先扩容消费者实例(注意实例数不能超过分区或队列数,否则多出来的实例分不到消息),再临时提高单批拉取条数和并发线程数。如果消费位点长时间不动,多半是某条消息一直处理失败在重试:定位到那条消息,把它投到死信队列或单独的错误 topic,先让主链路恢复。长期方案要回到根因——生产者侧限流或合并消息、消费逻辑里把同步调用的外部接口改成批量或异步、分区键重新设计避免热点分区。还要注意积压恢复期的顺序问题,如果业务依赖顺序,扩容消费实例会破坏同一分区内的顺序。

召回准确率暴涨暴跌怎么排查

要有一条可观测的链路,而不是靠感觉。第一层看数据:离线评测集上的召回率、MRR 有没有同步变化,如果离线也掉了就是检索侧的问题,离线正常而线上掉就要怀疑流量分布变了。第二层拆链路:查询改写有没有改写错(把关键实体改没了)、embedding 服务有没有换模型或超时降级、向量库有没有发生分片迁移或索引重建、过滤条件(时间、类目、权限)有没有把结果筛掉、融合排序的权重有没有被人改过。第三层看口径:准确率是怎么算的,样本量够不够,是不是分母变小导致的统计波动。把每一层的输入输出都落日志并带上 traceId,才能在出问题的那一刻定位到具体哪一段。

向量库数据量很大时提高检索效率

几个方向可以叠加。索引层面用近似最近邻(HNSW、IVF-PQ)替代暴力检索,把复杂度从线性降到近似对数,代价是牺牲一点召回;HNSW 调大 efSearch 提召回、调小降延迟,IVF 通过 nprobe 控制扫描的倒排桶数量。分片层面把向量按业务维度(类目、时间、租户)水平切分,查询时只打相关的分片,避免全库扫描。存储层面做量化压缩(PQ、SQ)把内存占用降下来,或者在 SSD 上做磁盘索引。查询层面做粗排到精排的两级漏斗,先用便宜的向量粗筛出几百条,再用精排模型算;同时把热门查询的结果缓存起来。元数据过滤要尽量前置到向量检索阶段,而不是检索完再过滤,后者会白算大量无效候选。

HashMap 与自定义 key

HashMap 是数组加链表加红黑树:hash 值经过扰动函数(高 16 位异或低 16 位)后对数组长度取模定位桶,桶内先用链表,链表长度到 8 且数组长度到 64 时转红黑树,退化到 6 时转回链表。默认容量 16,负载因子 0.75,元素数超过阈值就扩容成两倍,扩容时会重新分配元素,JDK 8 用高低位拆分避免了 1.7 头插法导致的死循环。用自定义对象当 key 必须同时重写 equals 和 hashCode,否则两个「内容相同」的对象会落到不同桶里,get 拿不回来;如果 key 的字段可变,改完之后 hashCode 变了就再也找不到,所以 key 最好是不可变对象。另外要注意 key 很多时 hash 冲突严重会退化成链表,O(1) 变 O(n)。

无保护的共享变量与解决方案

两个线程读同一个变量、其中至少一个写,就会出现可见性和原子性两类问题:可见性上,一个线程改了值,另一个线程可能因为工作内存里的副本没刷新而读到旧值;原子性上,像 i++ 这种「读—改—写」三步操作会被交错执行导致丢更新;更极端的情况下还可能因为指令重排看到「半初始化」的对象。除了 synchronized,解法还有几种:用 volatile 解决可见性和禁止重排,但它不保证原子性;用 AtomicInteger 这类原子类,底层靠 CAS 自旋;用 LongAdder 在写竞争激烈时比 AtomicInteger 吞吐更高,代价是读的时候要汇总多个 cell;用 ReentrantLock 或读写锁做更细粒度的控制;再彻底一点,改成不可变对象或线程封闭,从设计上消灭共享状态。面试里如果问「CAS 能不能解决」,要答清它解决的是单个变量的原子更新,靠 Unsafe 的 compareAndSwap 指令,但会有 ABA 问题和自旋开销,ABA 用 AtomicStampedReference 加版本号来解。

Java 垃圾回收

判断对象是否存活用可达性分析,从 GC Roots(栈帧里的局部变量、静态变量、常量、JNI 引用)出发,不可达的对象才回收,而不是引用计数,因为引用计数解决不了循环引用。堆按分代组织,新生代用 Eden 加两块 Survivor 的复制算法,Minor GC 时存活对象在两个 Survivor 之间来回搬,年龄到阈值(默认 15,动态调整)晋升老年代;大对象直接进老年代避免频繁复制。老年代用标记整理或标记清除。常见回收器按停顿目标分:Serial 单线程、ParNew 配合 CMS、Parallel 追求吞吐、CMS 用并发标记清除换低停顿但有碎片和 concurrent mode failure、G1 把堆切成 Region 按回收收益排序、ZGC 和 Shenandoah 走染色指针与读屏障做到停顿与堆大小基本无关。实际发生频繁 Full GC 时,先看是不是内存泄漏、大对象分配过快、或者元空间被动态代理撑爆。

接口响应时间变长的排查

从外往里、从大到小逐层缩小范围。第一步确认范围和形态:是所有接口都慢还是某一个接口,是突然变慢还是缓慢劣化,是全部实例慢还是个别实例慢——个别实例慢优先怀疑那台机器的宿主机、网络或磁盘。第二步看指标分层:监控上的 CPU、内存、GC 次数和停顿、线程池活跃数与队列长度、数据库连接池等待、下游接口的 RT 和错误率、慢 SQL 日志。如果线程池队列堆积、CPU 不高,多半是卡在 IO 或锁上;如果 GC 停顿变长,就是内存问题。第三步做链路追踪:按 traceId 把一次请求的每段耗时打出来,找出占比最大的那一段,再进到那一段看具体是慢 SQL、锁竞争、还是下游超时。临时手段可以限流、降级、扩容或者重启,但根因要落到具体的变更上——常见的触发点是某次发布、某个索引失效、缓存大面积过期或者流量模型变了。