携程 Java 开发 AI 面面经(10.08,全程 50 多分钟)
- 轮次
- AI面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
开场与动机
- 请简单做个自我介绍,包含基本信息、毕业院校、毕业时间和所学专业;如果有实习或兼职经历,可以重点介绍。
- 分享一段在实习过程中需要和团队成员协作完成任务的具体案例。
- 在这段协作过程中,有没有遇到过和对接的同学意见不一致的情况?你是怎么处理的?
- 你是如何理解携程这个岗位的?你选择应聘携程的原因是什么?
Java 与并发
- ConcurrentHashMap 是如何实现线程安全的?它的
size计算是如何实现的? - JDK8 中 HashMap 会锁链表头节点,那当桶内的链表已经转化为红黑树时,它是通过什么方式保证写操作线程安全的?
- 什么是 CAS?它的实现原理是什么?
- 高并发场景下,大量 CAS 操作持续重试会带来什么问题?
- Java 中通常是怎么解决 ABA 问题的?
MySQL 与网络
- MySQL 执行一次查询的完整流程是怎样的?针对查询过程可以做哪些优化?
- 如果一条 SQL 因为优化器选错了索引导致查询变慢,你会怎么排查和处理?
- MySQL 的统计信息里具体存储了哪些和索引选择相关的核心数据?
- 从浏览器输入 URL 到页面完整展示,中间经历了哪些完整过程?如果页面加载超时,你会怎么排查?
- 如果 HTTP 站点在建立连接阶段出现加载超时,TLS 握手环节的常见排查点有哪些?
系统设计:千万级积分排行榜
- 需要设计一个千万级用户的每日更新积分排行榜,要求支持高效的排名查询和积分更新,你会如何设计?
- 为什么要选用 Redis 的 ZSet?好处是什么?
- 你提到会按用户 ID 做分片来降低单节点压力,那分片之后用户的全局排名具体是怎么计算得到的?
- ZSet 分片预计算积分区间快照的方案,和直接使用单 key 全量承载千万级数据的方案,你是基于哪些因素做权衡和选型的?
- 如果遇到突发的积分批量更新,比如短时间内百万级用户同时完成任务获得积分,你会怎么调整方案来保障排行榜的查询和更新性能?
项目与 AI 学习
- 聊聊你的智能销售数据分析 Agent 项目,过程中有没有遇到过特别有挑战性的技术问题?难点在哪?怎么排查原因、敲定方案并解决?
- 你提到通过引入 Query 改写和多路召回优化了 RAG 的召回质量,落地过程中有没有遇到具体阻碍?比如多路召回带来的结果冗余,你怎么权衡和处理?
- 这个优化上线稳定运行之后,你有没有对这个链路做过后续迭代或优化?比如发现新的改进点,或基于线上运行数据做过调整?
- 你平时通过哪些路径学习 AI 相关技术?说说最近学习的一项 AI 技术的具体内容。
- 你提到最近在学习 AI Coding 的 Brainstorming Skill、开源人机协同项目 Multica、Loop Engineering 等,为什么先选这些而不是其他同类的 AI 技术?
- 学习这些技术内容时,你有没有摸索出适合自己的、能更快吃透技术思路的学习方法?
- 你把这些学到的 AI 技术落地到项目里时,做成了什么具体的事情?拿到了怎样的实际效果?
《参考解析》
ConcurrentHashMap 的线程安全要分三个时期讲,JDK8 之后是「CAS 加桶内加锁」。 JDK7 用 Segment 分段锁,把哈希表切成若干段、每段一把锁,并发度等于段数,缺点是并发度固定、内存开销大。JDK8 改成「数组 + 链表 / 红黑树」,锁的粒度细到单个桶:插入时先用 CAS 无锁地尝试把值放到空桶(casTabAt),只有桶非空时才用 synchronized 锁住该桶的头节点再插入;扩容(transfer)时用 ForwardingNode 标记迁移过的桶,其他线程遇到标记就协助扩容。这里正好接上面试官的追问:链表转成红黑树之后,锁的对象从链表头节点变成了树的根节点,插入或删除前对根节点加 synchronized,红黑树的旋转与重平衡都在锁内完成,所以并发安全不依赖链表结构本身。至于 size,JDK8 不再用带锁的段计数,而是用 baseCount 加一个 CounterCell[] 数组:无竞争时 CAS 更新 baseCount,一旦 CAS 失败说明有并发冲突,就按线程哈希分散到不同的 CounterCell 上各自累加,最终把 baseCount 与各 cell 求和得到近似值——这也是为什么 size() 返回的是「估计值」,并发下不保证精确,需要强一致时要靠 mappingCount() 或额外同步手段。
CAS 的原理、代价与 ABA 是一次成串的追问,要答出因果。 CAS 是「比较并交换」:compareAndSwap(期望值, 新值) 在硬件层面用一条原子指令实现(x86 上是 lock cmpxchg),JVM 通过 Unsafe 的本地方法暴露给上层,AtomicInteger、ReentrantLock 的 AQS 队列、ConcurrentHashMap 的空桶插入都建立在它之上。它的代价是自旋:如果竞争激烈,CAS 反复失败就变成忙等,白烧 CPU,而且在高争用下失败重试会加剧缓存行在核心之间来回同步(伪共享与总线风暴),此时分段累加(LongAdder)或直接加锁反而更快——所以「无锁一定优于加锁」是错的,要看竞争程度。CAS 还有三个边界:只能保证单个变量的原子性,多变量要打包成对象或上锁;存在 ABA 问题——值从 A 改成 B 又改回 A,CAS 会误判「没变过」。解决 ABA 的标准做法是加版本号,AtomicStampedReference 每次修改同时递增一个 stamp,比较时值和版本一起比;只关心「是否被改过」而不关心改了几次时可以用 AtomicMarkableReference 的布尔标记。另一条路是用不可变对象整体替换,AtomicReference 里存指向新对象的引用,从根上避免中间态。
MySQL 的查询流程与优化要连成一条链路来讲。 一条查询进来,依次经过连接器(鉴权、连接管理)、查询缓存(8.0 已移除)、解析器(词法与语法分析、生成解析树)、预处理器(校验表与列是否存在、展开视图)、优化器(决定连接顺序、索引选择、是否下推条件下沉,产出执行计划)、执行器(按计划调用存储引擎接口逐行取数并做过滤、排序、聚合),最后经存储引擎(InnoDB 通过聚簇索引和缓冲池读取页)返回结果。可优化的点对应这条链路:网络与连接层用连接池减少握手;SQL 层避免 select *、把过滤条件写成能走索引的形式(不在索引列上做函数运算、注意隐式类型转换、like 不以 % 开头);索引层做覆盖索引减少回表、用联合索引的最左前缀、把排序列纳入索引以省掉 filesort;架构层加缓存、读写分离、冷热分离。出现慢查询的标准排查路径是:先看慢查询日志确认耗时与扫描行数,再用 EXPLAIN(或 EXPLAIN ANALYZE)看 type、key、rows、filtered 和 Extra,定位是没走索引、走了错的索引、还是回表太多。
「优化器选错索引」这道题在考你是否知道统计信息是估算出来的。 MySQL 的优化器不看真实数据,而是基于统计信息估算每条执行路径的成本(I/O 成本加 CPU 成本)后选最低的那个,估算失准就会选错。最常见的原因是索引基数(cardinality)不准——它是采样估计出来的,InnoDB 默认采样 20 个数据页;当数据分布倾斜、或表刚做过大批量增删改而统计信息还没更新时,基数会严重偏离真实值。处理手段有几步:用 ANALYZE TABLE 重新采样刷新统计信息;用 EXPLAIN 加 SHOW INDEX FROM 表 对比估算行数与实际行数;如果确实要强制走某个索引,短期可以用 FORCE INDEX 或 STRAIGHT_JOIN 兜底,但那是止血不是治病;根因层面要考虑是否该补一个更贴合查询的联合索引、或者用 WHERE 条件把选择度提上去。统计信息具体包含:每个索引的基数(不同值的估计个数)、索引占用的页数、表的总行数与数据页数、以及 8.0 引入的直方图(对列的值分布分桶统计,帮助优化器估计非索引列或倾斜数据的选择率);采样页数由 innodb_stats_persistent_sample_pages 控制,调大能提高准确度但会增加 ANALYZE 的开销,这是一个典型的准确性与成本权衡点。
「从输入 URL 到页面展示」是背了无数遍的题,高分点在超时排查那一段。 完整链路:浏览器解析 URL 并查缓存(浏览器缓存、Service Worker、系统 DNS 缓存)→ DNS 解析(本地 hosts、递归查询到权威服务器)→ 建立 TCP 连接(三次握手)→ TLS 握手(协商版本与密码套件、验证证书、交换密钥,TLS 1.3 只需一个往返)→ 发送 HTTP 请求并等待响应(可能经过 CDN、负载均衡、网关、应用服务)→ 浏览器解析 HTML 构建 DOM,遇到 CSS、JS、图片等资源继续发起请求 → 合成 CSSOM、执行 JS、构建渲染树、布局、绘制、合成上屏。排查超时要按阶段切分而不是笼统地看「打不开」:先用 curl -w 或浏览器 DevTools 的 Network 看耗时分布(DNS、连接、TLS、首字节 TTFB、内容下载各自多少毫秒),再看 ping/traceroute 判断网络可达性,看 nslookup 判断解析是否正常,服务端侧看网关与应用的访问日志、线程池与连接池是否打满。TLS 阶段的常见问题有几类:证书链不完整或过期(浏览器报错,但一些客户端只在部分环境失败)、SNI 未正确配置导致 CDN 或网关回落到默认证书、TLS 版本或密码套件不匹配(老旧客户端协商失败)、中间设备做深度包检测或需要额外握手往返导致超时、系统时间偏差导致证书有效期校验失败。把「TLS 握手失败」和「TLS 握手慢」分开处理,前者看证书与协商,后者常是网络往返次数或 OCSP 校验被墙导致。
千万级每日积分排行榜,主线是「Redis ZSet 承载热榜 + 分片解决容量与热点 + 快照解决跨片排名」。 ZSet 的底层是跳表加哈希表,ZINCRBY 更新分值、ZREVRANGE 按排名取区间、ZREVRANK 取某人的名次,都是 O(log N) 级别,天然贴合排行榜的三种访问模式(更新、取 TopN、取名次),这是选它的核心理由;同时分值相同的情况下会按成员字典序排序,需要处理同分并列的名次语义。千万级用户全量放一个 ZSet,内存与单节点写入都会成为瓶颈,所以按用户 ID 分片成若干个 ZSet(例如按 uid % 128,或者按积分区间分流)。分片带来的直接问题是全局排名:常规做法是「片内名次 + 其他片的人数贡献」——全局排名 = 本片内 ZREVRANK 得到的名次 + 所有比自己分数高的片内元素个数,而后者可以用每个分片维护的分数直方图 / 有序分数快照快速算出:每个片子定期把「分数 → 该分数的人数」做成分桶统计,查询时对所有分片的桶做二分累加即可得到「分数高于我的总人数」,再叠加同分内的次级排序规则。查询 TopN 则更简单,从每个分片各取 N 个候选做一次归并即可。选型权衡要摆出三组矛盾:单 key 方案的实现最简单、排名天然准确,但受单节点内存与网络带宽限制、扩容需要 resharding、并且是明显的热点;分片加预计算方案的容量与吞吐可线性扩展,代价是排名变成了近似或需要额外维护快照、以及跨片归并带来的延迟;维护成本上,快照要定期刷新,刷新频率决定排名的时效性。百万级突发更新的应对要在写入侧做削峰:把积分变更先写消息队列或 Redis 的缓冲结构(如按用户哈希的待更新列表),由后台按批 ZINCRBY(pipeline 批量提交减少往返),必要时对同一用户的多次变更先本地合并再落 ZSet;读取侧则用「短 TTL 的 TopN 结果缓存 + 名次惰性计算」,并对非关键的名次查询降级到上一次快照,把写放大与读放大的压力错峰。另外要记得榜单是「每日更新」的,可以按天建 key 并设置过期时间,用定时任务做快照与归档,避免单个 key 无限增长。
RAG 多路召回的冗余问题,标准解法是「融合排序 + 去重 + 重排」三层。 多路召回(向量召回、关键词 BM25、Query 改写后的多次召回、按元数据过滤召回)各自带不同的偏置,简单拼接会出现同一文档被多次命中、或者高度相似的片段挤占名额。工程上的做法是:先在合并阶段按文档 ID 或内容指纹去重、同一文档的多个片段只保留最高分并做聚合;再用融合算法统一各路的分数尺度——RRF(Reciprocal Rank Fusion)只用排名不用原始分数,score = Σ 1/(k + rank_i),天然规避了不同召回通道分数不可比的问题,实现也简单;最后进重排(cross-encoder 或 LLM 精排)对 TopK 做细粒度打分,只把最优的少数片段送进生成。要判断有没有改进,不能只看「召回率涨了」,而要建一套离线评测集,跟踪最终答案的正确率、引用命中率与冗余率,同时盯线上指标(首答采纳率、追问率、延时);上线后还能继续做的迭代包括 Query 改写的策略收敛(哪类查询值得改写)、召回通道的权重调参、片段切分粒度调整,以及把线上 badcase 回流进评测集。回答这题时主动提到「多路召回一定会带来冗余,关键是让排序阶段而不是召回阶段来做取舍」,会显得真的落地过。
AI 面试的最后一段是行为面,准备方式和八股完全不同。 AI 面试官会顺着你上一句的答案追问,所以回答里不要主动引入自己讲不深的名词——提到 Multica、Loop Engineering 这类具体项目,就要准备好被追问「它解决什么问题、和你做的事有什么关系、你实际用了哪一部分」。学习路径的问题可以按「信息源 + 实践方式」组织:论文与技术博客、开源仓库与源码、社区讨论,再配合最小闭环——挑一个小场景把它跑通、复现一遍问题、写下失败与修正。落地效果要给可验证的结果而不是「感觉变好了」:比如某个内部流程的处理时间、某类问题的准确率、或者省掉的人工步骤。整场 AI 面 50 多分钟的体验也值得记下来:机器面试官会连续追问,答得越具体、被追问的空间越大,所以每段回答收尾时最好主动给一个可继续的抓手(做了什么、结果如何),而不是留下含糊的说法让对方无从下手。