面灵AI

一嗨租车 Java 二面面经

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

《面试题目》

  1. 自我介绍
  2. 在实习中你觉得有哪些是碰到了技术上的难点,你是如何解决的?
  3. 智能路由智能在哪里?
  4. 手写策略模式
  5. @Component 的作用?
  6. 这个注解注册到容器中是单例吗?
  7. 另一家公司实习碰到的难点?
  8. 面对海量数据你要如何做优化?
  9. 如果全量的数据都丢到 Redis 中,Redis 能扛住吗?
  10. 你刚才说到会将数据放到 Redis 和本地缓存,那什么样的数据会进入到本地缓存?
  11. 那你本地缓存怎么做淘汰策略?
  12. Caffeine 本地缓存是要做多实例的,如果有些用户进来查询,第一个请求打到 A 机器上缓存,第二个请求打到 B 机器缓存,那你根据刚才的淘汰策略如何做淘汰?
  13. 平常怎么学习?
  14. 对自己的职业规划有什么想法?
  15. 期望薪资多少?
  16. 能实习多久?
  17. 从上一家公司离职的原因?
  18. 智力题:有一个 9×9×9 的魔方,由 1×1×1 的小魔方组成,如果将大魔方的表面刷上颜色,那大魔方中没有被刷颜色的小魔方有多少个?
  19. 反问环节
  20. 最后写一个 update 更新语句

《参考解析》

  1. 手写策略模式:核心是把「一组可互换的算法」抽象成接口,调用方依赖接口而不是具体实现。先定义 Strategy 接口(比如 RouteStrategy#route(Request)),每个算法一个实现类;再准备一个注册/查找的地方——简单做法是用 Map<String, Strategy> 在启动时把实现按类型注册进去,配合 Spring 时可以直接让容器注入 Map<String, Strategy>(key 就是 bean 名),调用方按业务类型取出对应实现执行。这样新增一种策略只要加一个实现类,不用改调用方的判断逻辑,天然干掉一长串 if-else / switch。如果策略之间有共享逻辑,可以再抽一个抽象基类或者用组合的方式复用。

  2. @Component 与单例@Component 是 Spring 的构造型(stereotype)注解,被 @ComponentScan 扫到之后,Spring 会为它生成一个 bean 定义并注册到容器里,@Service / @Repository / @Controller 都是它的派生注解,语义上只是分层提示。默认作用域是 singleton——注意这是容器内单例,一个应用上下文里只有一个实例,不是 JVM 级别的单例(同一个类被两个上下文加载会有两个实例)。Spring 的 singleton bean 也是懒加载的(除非配了 @Lazy(false)@Configuration 里的行为差异)。如果 bean 有可变状态,要么改成 prototype,要么用 ThreadLocal / 局部变量保证线程安全。

  3. 海量数据怎么做优化:先分层看。存储层做分区/分库分表、选合适的索引、冷热分离和历史数据归档;查询层避免全表扫描、深分页改游标、批量操作合并请求、把复杂聚合下推到数据库或离线预计算;缓存层做多级缓存,热点数据挡住绝大部分读流量;架构层读写分离、异步化削峰、按业务维度做数据隔离。前提是先量化——数据量、QPS、读写比、可接受的延迟和一致性要求各是多少,脱离这些谈方案都是空话。

  4. Redis 能不能扛住全量数据:扛不住,也不该这么设计。内存是最贵的存储,全量数据的内存成本远高于磁盘;单实例 Redis 虽然是多路复用但命令执行仍是单线程,数据量越大持久化(RDB fork、AOF 重写)造成的卡顿越明显;大 key、全量 key 扫描也会拖慢整体响应;网络带宽和主从复制开销随数据量线性上升。正确的做法是按访问频次分层——热点数据放 Redis 甚至本地缓存,全量数据留在数据库/对象存储,用过期时间和主动失效控制缓存规模,同时防缓存穿透、击穿、雪崩。

  5. 什么样的数据进本地缓存:变化频率低、访问频率高、体量小、且对短暂不一致不敏感的数据最合适,比如配置项、字典表、地区/城市列表、租户元数据、开关类信息。反过来,用户私有且实时性要求高的数据(余额、订单状态)不适合放本地,多实例之间不一致会直接变成业务事故。

  6. 本地缓存的淘汰策略与多实例问题:单机层面一般用 Caffeine,底层是 W-TinyLFU——先用频率统计(Count-Min Sketch)过滤掉只访问一次的冷数据,再在窗口 LRU 和主 SLU 段之间做准入判断,命中率比纯 LRU/LFU 更高,也支持 maximumSizeexpireAfterWrite / expireAfterAccess 和自定义 Weigher。真正的难点在面试官追问的那一步:本地缓存是进程内的,A 机器和 B 机器各有一份,各自的淘汰策略独立跑,容量、过期时间、访问模式都不一样,必然出现同一份数据在 A 还在、在 B 已被淘汰的情况——这不是靠把淘汰算法写得多好能解决的,是架构层面的不一致。工程上的处理是:本地只放能容忍短时不一致的数据,TTL 设短(秒级到分钟级)做兜底;变更时通过 Redis Pub/Sub 或 MQ 广播失效消息让所有实例删掉各自的 key;用版本号/更新时间戳做校验,本地命中但版本落后就回源;或者干脆把强一致要求的读收敛到 Redis 单点,本地缓存只做降级和兜底。

  7. 魔方智力题:答案是 343 个。9×9×9 共 729 个小魔方,被刷到颜色的是「至少有一个面露在大魔方表面」的那些,只有完全被包在内部的才没被刷到。内部小魔方在每个维度上都不在最外两层,也就是每个方向的坐标都落在第 2 到第 8 层,共 7 种取值,所以是 7×7×7 = 343。这类题先想清楚「没被刷到」的判定条件,再降一个维度验证(比如 3×3×3 的答案是 1×1×1 = 1)就不容易算错。

  8. 手写 update 语句:基本形态是 UPDATE 表名 SET 列 = 新值 WHERE 条件,比如 UPDATE orders SET status = 'PAID', paid_at = NOW() WHERE order_no = 'xxx' AND status = 'UNPAID';。面试里要注意几个点:一定要带 WHERE,条件列尽量走索引避免全表加锁;WHERE 里带上状态这类前置条件可以顺带做幂等(靠受影响行数判断是否真的变更);批量更新写成一条带 IN 的语句或分批提交,别在循环里逐条 update;UPDATE ... JOIN 和子查询的写法要分清 MySQL 的语法限制。