面灵AI→

去哪儿 AI 应用开发 AI 面试:JVM、B+ 树、订票系统设计与 RAG 落地

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

《面试题目》

一、开场与自我介绍

  1. 请你简单做个自我介绍。

二、Java 基础与并发

  1. 请介绍 JVM 运行时数据区的核心构成以及各区域的核心作用。
  2. 请描述 HashMap 的 put 元素完整流程以及触发扩容的具体条件。
  3. 两个线程同时调用同一个 ArrayList 的 add 方法,各循环 100 万次会出现什么问题?
  4. 追问:你提到了元素覆盖导致最终元素数小于 200 万的情况,那在 ArrayList 扩容的过程中,两个线程同时操作会出现什么问题?
  5. 追问:你刚才提到了数组越界的情况,那为什么 ArrayList 在多线程 add 时还可能出现元素为 null 的情况?

三、MySQL 与数据结构

  1. MySQL 的 InnoDB 引擎为什么选择 B+ 树作为索引的底层数据结构,而不是 B 树?
  2. 追问:你刚才提到非叶子节点只存索引能容纳更多索引、降低树高、减少 I/O。那如果我们用有序数组来做索引,查询单个数据的时间复杂度是 O(log n),比 B+ 树还稳定,为什么 InnoDB 不选择有序数组作为底层索引结构?
  3. 追问:你刚才提到有序数组插入删除需要移动元素、写入性能差,那 B+ 树在插入数据导致页分裂的时候也会移动数据、产生额外 I/O,InnoDB 是怎么减少这种页分裂带来的性能损耗的?

四、操作系统

  1. 请说明操作系统中进程和线程的核心区别和联系是什么?
  2. 追问:你提到线程共享进程的资源,那如果同一个进程里的两个线程同时修改同一个全局变量,会出现什么问题?

五、系统设计:订票场景

  1. 订票类场景中需要实现重复订单拦截,支持提前一年预定,允许带他人购票,你会怎么设计?
  2. 追问:你设计的这套方案里,各个模块之间的核心交互流程是怎样的?
  3. 追问:你提到代购票和本人买同一张票会判定为重复订单,那你具体会用哪些维度组合来判定一笔订单属于需要拦截的重复订单?

六、项目深挖:高并发缓存方案

  1. 请分享一个你做过的最具技术挑战、最值得深入探讨的后端开发项目,它要解决什么问题?核心难点在哪?你又是如何攻克的?
  2. 追问:你刚才提到了用布隆过滤器、分布式锁加双重检查、空值缓存的整套方案。那一开始最直接的、只靠 Redis 缓存加数据库回写的简单做法,具体是在什么场景下失效的?
  3. 追问:你刚才提到布隆过滤器存在误判率,那当时你们是怎么测量确认布隆过滤器的误判率以及整套方案实际的数据库承压情况,来验证最初的问题确实存在的?

七、AI 工具与大模型落地

  1. 请分享一段你最近半年内主动学习 AI 工具或大模型相关技术,并尝试落地到实际开发场景的真实经历,包括你做了什么、最终落地效果如何?
  2. 追问:你刚才提到做了结合精确查询和向量检索的 RAG 场景落地,当时你是在什么具体的业务痛点下,决定要做这个 RAG 方案,而不是用传统的文档检索或者直接调用大模型的方式?
  3. 追问:你刚才提到在这个 RAG 方案里调试了文档切分的块大小、重合度,还设置了精确查询和向量检索 0.3 和 0.7 的权重。你在调试这些参数的时候,具体是怎么判断参数合不合适?做了哪些实际的调整动作?

《参考解析》

  1. HashMap 的 put 与 ArrayList 多线程 add 是同一类「集合非线程安全」的考点:put 要能顺下来——算 hash 并扰动、定位桶、空桶直接放、命中则比较 key(先 == 再 equals)、链表尾插或红黑树插入、size 超过阈值(容量 × 0.75)就扩容,JDK 8 的扩容是把旧表元素按高位拆成两条链迁移。ArrayList 并发 add 的三种现象要分别给机制:两个线程读到同一个 size 后各自写同一格 → 元素覆盖、最终数量偏少;size 自增不是原子操作(读-改-写)→ 计数与实际不符;扩容时新数组刚建好还没拷贝完,另一个线程拿到的是旧数组引用,越界或读到 null 就出现了。要根治只有换 CopyOnWriteArrayList(读多写少)或外部加锁/分段写,别指望靠 size 判断来兜。

  2. B+ 树的追问链条其实在考「存储引擎的读写权衡」:不用 B 树,是因为 B+ 树非叶子节点只存键、不存数据行,一页能放下更多键,树高更低、磁盘 I/O 更少,而且叶子节点用链表串起来,范围查询和排序可以顺序扫,B 树做不到。不用有序数组,是因为索引不是只读的——插入删除要移动大量元素、还会整块重写落盘;而且有序数组没法像页一样按需加载,二分查找要随机访问,落在磁盘上就是多次寻道。至于「页分裂也有额外 I/O」,InnoDB 的对策是:自增主键让插入尽量落在最右页顺序追加,页内预留填充因子(innodb_fill_factor),页快满时才分裂并把一部分记录挪到新页,同时通过 buffer pool 把分裂摊在内存里、异步刷脏,避免每次插入都同步写盘。

  3. 订票重复订单拦截要给出一组可落地的幂等维度,而不是一句「用 Redis 去重」:先定义「同一笔订单」的业务口径——乘车人证件号 + 车次/航班 + 乘车日期 + 席别/价格档 + 出发到达站,代购场景再把下单账号与乘车人之间的关系纳入(同一乘车人同一行程只能有一张有效票,谁买的不重要)。落地分层:数据库层用上述维度的唯一索引兜底(最终一致性保证);请求层用业务幂等号 + 分布式锁防并发重复提交;状态层用「待支付/已支付/已取消/已退款」的状态机判定,因为取消后重新下单必须放行。长周期预定(提前一年)要额外处理两点:库存与价格的时效性(下单锁定与超时释放),以及索引不能用「创建时间」这类会持续膨胀的字段做主键。

  4. 高并发缓存方案的答法要顺着「缓存穿透/击穿/雪崩」把三件套串起来:只靠 Redis 缓存加数据库回写的简单做法在三种情况下失效——查不存在的 key(穿透,缓存永远不命中,请求全打到库)、热点 key 过期瞬间(击穿)、大批 key 同时失效或 Redis 抖动(雪崩)。对应的解法是布隆过滤器前置拦截 + 空值缓存(带短过期)、热点 key 的互斥重建(分布式锁 + 双重检查)、过期时间加随机抖动 + 多级缓存。真正加分的是追问里的「怎么验证」:布隆过滤器的误判率要按公式 (1 - e^(-kn/m))^k 估算,再用离线脚本灌 N 个不存在的 key 实测误判比例;数据库承压要在压测环境里对比「上方案前/后」的 QPS 与慢查询数,把「最初的问题存在」用数据坐实,而不是讲感觉。

  5. RAG 参数调优的判据要讲「评估集 + 指标 + 对照实验」:先把线上真实问题整理成一份带标注答案的评测集(按问题类型分层),再定义指标——召回率/命中率看检索质量,答案正确率与人工可接受度看最终效果。调块大小与重合度时固定其他变量做网格对照,块太小语义不完整、太大噪声多且消耗上下文;混合检索的 0.3/0.7 权重从小到大扫一遍,看评测集上的指标拐点,同时用 badcase 分析确认提升来自哪一类问题。落地之后再做线上 A/B 或灰度,看用户追问率有没有下降——这是参数调优唯一可信的收口。