面灵AI→

高新兴科技集团 Java 开发实习转正一面

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

《面试题目》

  1. 请做一下自我介绍。
  2. 你这两段实习都是短期,为什么没有转正?(追问离职原因)
  3. 你在第一家公司具体做了什么工作?(追问全栈开发细节)
  4. 详细说一下 Redis 加 Lua 脚本的执行步骤。
  5. 超卖问题你们是怎么解决的?(追问超卖后的补救措施)
  6. 你说的两层幂等机制分别防备什么?(接口层和消息队列层)
  7. 你们真的遇到过 Kafka 重复消费吗?
  8. 介绍一下你的项目整体架构。(前端、后端、缓存、消息队列等)
  9. 为什么使用 dify,当时应用场景主要为了解决什么问题?
  10. MCP 服务到底怎么运作?
  11. 你主要用什么语言?
  12. HashMap 的底层结构是什么?
  13. HashMap 的 put 流程是怎样的?哈希取模怎么做的?
  14. equals 和 hashCode 的关系是什么?
  15. 什么是控制反转(IOC)?
  16. 依赖注入有哪些方式?
  17. Bean 的生命周期有哪些阶段?
  18. TCP 三次握手的过程及原因是什么?
  19. TCP 四次挥手的过程及目的是什么?
  20. WebSocket 和 SSE 有什么区别?

《参考解析》

Redis 加 Lua 防超卖。核心是把「读库存—判断—扣减」这三步合成一个原子操作,避免并发下多个请求都读到同一份余量。执行步骤是:应用侧把参与判断与扣减的 key、参数打包,通过 EVAL/EVALSHA 发给 Redis;Redis 单线程执行脚本,中途不会被其他命令插入;脚本里先 GET 库存,判断 stock > 0 就 DECR(或 DECRBY)并返回扣减后的值,否则返回失败标记;应用根据返回值决定放行还是拒绝。工程上要注意几点:先把脚本 SCRIPT LOAD 拿到 sha1,之后用 EVALSHA 减少网络传输,并处理 NOSCRIPT 错误重新加载;脚本里不要写循环大 key 的操作,Redis 单线程会被阻塞;库存 key 的初始值要在活动开始前预热好(也可以带一个兜底初始化);扣减成功但后续下单失败时要走补偿(回滚库存或标记异常订单),这是「超卖后的补救措施」想听的答案。

两层幂等分别防什么。接口层幂等(通常用业务唯一键 + 数据库唯一索引,或 Redis SETNX 令牌)防的是「同一个请求被客户端重复提交」——用户连点、网络重试、超时重发都会造成这类重复,它保证一次业务动作只落一次单。消息队列层的幂等防的是「同一条消息被重复投递」——Kafka 在消费端重启、rebalance、offset 提交失败时会出现 at-least-once 语义下的重复消费,处理手段是用业务唯一键(订单号 + 事件类型)在消费侧做去重表或 Redis 记录,让重复消息的处理结果一致。两者不能互相替代:接口层拦不住 MQ 重投,MQ 层拦不住用户连点。被追问「真的遇到过重复消费吗」时,诚实回答比硬编好:可以说出你们压测或 rebalance 场景下的观测,说没有遇过也可以,但要能讲清如果发生会怎么定位(查消费日志里的同 key、比对去重表)。

HashMap 的关键点。JDK 1.8 的结构是数组 + 链表 + 红黑树:数组是哈希桶,桶内冲突少时用链表,链表长度超过 8 且数组长度达到 64 时转红黑树,把最坏查询从 O(n) 降到 O(log n)。put 流程:先算 hash(key)(1.8 用高 16 位异或低 16 位的扰动函数,让高位也参与运算),再用 (n - 1) & hash 定位桶(容量是 2 的幂,位与等价于取模但更快,这就是「哈希取模」的答案);桶为空直接 CAS 放入,不为空则遍历比较 hash 与 equals,命中就覆盖 value,没命中就尾插(1.7 是头插,扩容时可能形成环);插入后判断是否需要树化或扩容。扩容是 resize,容量翻倍、阈值翻倍,1.8 里元素要么留在原位、要么移动到「原位 + 旧容量」,不需要重新计算哈希。

equals 与 hashCode 的约定。两者必须同时重写:如果两个对象 equals 相等,它们的 hashCode 必须相等;反之 hashCode 相等不代表 equals 相等(哈希冲突)。只重写 equals 不重写 hashCode,会出现「逻辑上相等的两个对象被放进 HashMap 的不同桶里」,导致查不到、去重失效。反过来 hashCode 写得不均匀(比如返回常量)会让所有元素挤在一个桶里,退化成链表。实际写的时候用 IDE 生成或 Objects.hash,并保证参与计算的字段与 equals 用的是同一组字段。

IOC、DI 与 Bean 生命周期。控制反转是把「对象的创建与依赖装配」的控制权从业务代码交给容器:类里不再自己 new 依赖,而是声明需要什么,由 Spring 在启动时扫描、实例化、注入。依赖注入的三种方式是构造器注入、setter 注入、字段注入(@Autowired 直接标字段);现在推荐构造器注入,因为它能让依赖不可变、便于单测、也能在启动阶段就暴露循环依赖。Bean 的生命周期可以按顺序说:实例化 → 属性填充(依赖注入)→ BeanNameAware/BeanFactoryAware 等回调 → BeanPostProcessor 前置处理 → @PostConstruct / InitializingBean.afterPropertiesSet / 自定义 init-method → BeanPostProcessor 后置处理(AOP 代理常在这一步生成)→ 使用 → 容器关闭时 @PreDestroy / DisposableBean.destroy。追问常落在「循环依赖怎么解决」,答案是三级缓存,以及构造器注入的循环依赖无法解决。

TCP 三次握手与四次挥手。三次握手:客户端发 SYN(带自己的初始序号)→ 服务端回 SYN+ACK(确认客户端序号,带上自己的序号)→ 客户端回 ACK。之所以要三次,是因为要让双方都确认「自己的发送与对方的接收都正常」,并且同步双方的初始序号,防止历史连接请求造成服务端建立无效连接。四次挥手:主动方发 FIN → 被动方回 ACK(此时半关闭,被动方还能继续发数据)→ 被动方数据发完再发 FIN → 主动方回 ACK。之所以是四次而不是三次,是因为被动方收到 FIN 时可能还有数据要发,ACK 与 FIN 不能合并;主动方最后进入 TIME_WAIT,等 2MSL 是为了确保最后的 ACK 能被对端收到(丢了可以对端重发 FIN),并让本次连接的旧报文在网络中消散。

WebSocket 与 SSE 的区别。WebSocket 是双向全双工,通过 HTTP 升级握手后建立独立的长连接,双方都能主动推消息,适合聊天、协同编辑、游戏;SSE(Server-Sent Events)是单向的,服务端到客户端的文本流,基于普通 HTTP 长连接,浏览器用 EventSource 接收,自带断线重连与事件 id 续传语义,适合通知、行情、进度推送。选型上:只需要服务端推、且走标准 HTTP 更省事,用 SSE;需要客户端高频上行或二进制,用 WebSocket。两者都要考虑连接保活(心跳)、代理与网关的超时、以及断线重连后的状态补偿。顺带说一句浏览器兼容:SSE 在 HTTP/1.1 下每个域名连接数有限,HTTP/2 下没有这个限制。