虾皮(Shopee)暑期实习后端一面面经
《面试题目》
一、项目
- RAG 项目是否属于练手项目?
- RAG 项目中的关键词重写模块是如何做对比实验的?
- 关键词重写前后如何对比?
- 召回率的判断标准是看是否返回结果,还是看结果是否符合人工标准答案?
- 是通过召回结果数量判断,还是通过匹配度/相关性判断?
- 如何具体量化关键词重写对召回效果的提升?
- 如何判断重写后的整体结果是变好还是变差?
- 是否只是手动构造少量 case 做对比测试?
- 150 条评测数据是否通过脚本批量跑验证?
- 项目中的 MCP 工具调用如何防止注入恶意命令?
- 纯规则拦截存在覆盖不到的漏判场景,是否有更完善的方案?
- MCP 调用前后如何做权限校验和结果校验?
- 项目中的租约锁机制是如何实现的?
- 是否是主线程执行业务,单独开子线程续约?
- 续约线程和任务线程分离时,续约线程如何感知业务线程是否正常运行?
- 后续是否改成主线程手动续锁?
- 如果主线程卡死,无法执行续锁逻辑,任务卡住怎么处理?
- 是否是消费完成之后再续锁?
- 执行流程中具体在哪些阶段进行续锁?
- 项目中的滑动窗口 + 摘要压缩模块如何处理关键信息丢失问题?
- 如果摘要压缩丢失用户名、实体名等关键上下文,导致回答错误,如何规避?
- 如果对话中途丢失关键信息,系统如何处理?
- 摘要压缩的核心目的是什么?
- 摘要压缩主要是为了降低存储成本,还是提升推理速度?
- RAG 评测集如何标注标准答案,如何判断模型输出是否正确?
- 是否依靠大模型做语义打分,而不是简单字符串匹配?
- 领券系统中使用 RocketMQ 延迟消息时,如果组件只支持固定延时,无法自定义精确过期时间,怎么解决?
- 如果堆积大量延迟消息,RocketMQ 底层是如何实现延迟消息的?
- 如果从零实现延迟队列,核心设计思路是什么?
- 维护延迟队列后,如何定时取出到期消息?
- 到期消息如何触发推送,让消费方正常消费?
- 订单 15 分钟超时自动关闭有哪些实现方案?
- 如果不能使用成熟中间件的延迟消息功能,还有哪些方案?
- 如果不用数据库轮询,也不用中间件延迟队列,还能如何实现?
二、八股
- 微信电脑端扫码登录如何设计实现?
- 手机完成确认登录后,电脑端如何感知二维码状态变更?
- 单点登录 SSO 是什么?
- 多线程场景下,如何等待所有子线程执行完毕后,主线程再继续执行?
- 多线程并发执行 i++ 自增,如何保证线程安全?
- i++ 为什么不是线程安全的?
- 有哪些具体实现方式可以保证原子性?
- 如果加锁性能开销较大,有没有更轻量级的方案?
- CAS 场景下 Java 是否有现成工具类可以使用?
- 多线程共用同一个变量 i,但每个线程自增互不干扰,线程 A 加 5 次、线程 B 加 10 次,各自计数独立互不影响,怎么实现?
- synchronized 的锁标记存放在对象的什么位置?
- 对象头中具体是哪个区域存储锁相关信息?
- MySQL 联合索引为什么存在最左前缀原则?
- 联合索引
(A, B)下,where B = 1 and A = 2能否走索引? - 为什么 where 条件中字段顺序调换不影响索引生效?
- 建索引前如何判断索引是否有收益?
- 字段区分度的标准是什么,如何量化判断?
- 联合索引
- SQL 注入如何防护?
- PreparedStatement 的原理是什么?
- 为什么 PreparedStatement 能防止 SQL 注入?
- Redis 和 MySQL 双写数据时,除了 Cache Aside,还有哪些一致性方案?
- Redis 是多线程吗?
- Redis 过期 Key 的删除策略有哪些?
- 正则表达式中,如何匹配连续 10 位数字?
- 分库分表了解吗?
- 水平分表和垂直分表分别是什么?
- 单表数据迁移到分库分表时,如何保障迁移数据不出错?
- 原代码读写单表,如何改造适配分库分表?
- 短信短链接怎么生成?
三、算法
- 北京飞广州,支持直达和中转,求最低票价路线。
- 遍历全国所有城市机场各一次,要求总票价最低,判断属于什么问题,怎么解决。
- 全村人员按年龄排序,如何选择最快的排序算法。
- 不使用库函数计算根号 8。
- 除了牛顿迭代法,还有哪些方法可以计算平方根?
- 你觉得哪些冷门算法设计很精妙?
- int 数组求 TopK 大元素,排除全排序和暴力遍历,有哪些解法?
- 二分法能否适用于无序数组求 TopK?
四、AI Coding:无
《参考解析》
-
RAG 关键词重写的对比实验设计:核心是构造离线评测集(如题中的 150 条 query-标准答案 对),分别用重写前/后的 query 跑召回,用脚本批量对比而非手动抽查几个 case。评估维度不能只看“是否有返回结果”,更要看返回结果与标注答案的匹配度/相关性(如 Recall@K、NDCG,或用大模型做语义打分代替严格字符串匹配),才能量化重写带来的召回提升,避免召回数量涨了但相关性反而下降的假阳性。
-
RocketMQ 只支持固定延时时的精确延迟方案:RocketMQ 开源版延迟消息只有固定的 18 个等级(1s~2h),无法任意指定过期时间。常见做法是“时间轮/多级延迟队列”:用一个较小粒度的固定延时(如 1 分钟)反复投递到延迟队列做轮询,消费时判断是否已到期,未到期则重新投递并递增等待;或者维护一张按到期时间排序的表/ZSet(Redis Sorted Set,score 为到期时间戳),用定时任务或阻塞弹出(
BZPOPMIN)取出到期任务再触发业务。订单 15 分钟自动关闭同理,可选方案包括:数据库定时轮询扫描超时订单(简单但有延迟和扫表压力)、RocketMQ/RabbitMQ 延迟消息、时间轮算法、Redis 过期键通知(keyspace notification)监听 key 过期事件触发关单逻辑。 -
多线程 i++ 线程安全与 CAS:
i++实际是“读取-计算-写回”三步操作,非原子操作,多线程并发执行会互相覆盖导致结果错误。保证原子性的方式:synchronized/ReentrantLock加锁(开销较大,会有线程阻塞和上下文切换);更轻量级的方案是 CAS(Compare-And-Swap)无锁并发,Java 中对应java.util.concurrent.atomic包下的AtomicInteger/AtomicLong等工具类,底层依赖 CPU 的cmpxchg指令,配合自旋重试实现无锁的原子自增。若要求线程 A、B 各自独立计数互不干扰,可以让每个线程操作各自的AtomicInteger实例(或ThreadLocal<Integer>),而不是共享同一个变量。 -
synchronized 锁标记的存放位置:Java 对象在堆内存中的对象头(Object Header)包含 Mark Word 和类型指针(Klass Pointer)。锁状态信息(无锁、偏向锁、轻量级锁、重量级锁)就存储在 Mark Word 中,JVM 会根据竞争情况在这几种锁状态间做升级(锁膨胀),从而在无竞争或低竞争场景下尽量避免重量级锁带来的线程阻塞开销。
-
MySQL 联合索引最左前缀原则:联合索引
(A, B)底层是按 A 优先、A 相同再按 B 排序的 B+树结构,只有从最左列开始连续使用索引列,才能利用这种有序性做范围/等值查找。因此where B = 1 and A = 2依然可以走索引——MySQL 优化器会自动调整条件顺序,判断是否满足最左前缀,与 SQL 中书写顺序无关,真正起作用的是“是否用到了从最左列开始的连续列”。判断索引是否有收益,主要看字段的区分度(选择性,即distinct 值数量 / 总行数,越接近 1 越适合建索引)以及查询频率,区分度过低(如性别字段)建索引意义不大。索引设计原则通常是“等值列在前,范围列在后”。 -
int 数组求 TopK 大元素:排除全排序(O(n log n))和暴力遍历,常用解法有:① 维护一个大小为 K 的小顶堆,遍历数组,元素大于堆顶则替换并下沉调整,最终堆中即为 TopK,时间复杂度 O(n log K);② 基于快速排序 partition 思想的快速选择(Quickselect),每次 partition 后根据基准元素位置与 K 的关系决定只递归一侧,平均时间复杂度 O(n),最坏 O(n²)。二分法本身不能直接对无序数组做 TopK(二分依赖有序性),但可以结合“二分答案”思路:对值域做二分,配合一次 O(n) 遍历统计大于等于某个值的元素个数,逼近第 K 大的值,适用于对值范围有先验假设的场景。