面灵AI→

浩鲸科技 秋招一面面经:Java 基础、MySQL、Redis 与秒杀库存返还追问

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

《面试题目》

Java 基础

  1. String 字面量直接赋值和直接 new 创建对象有什么区别?
  2. StringBuilder 跟 StringBuffer 有什么区别?
  3. 口述一下 HashMap 的 put 过程
  4. 什么时候触发扩容?(HashMap 扩容时机)
  5. 为什么负载因子是 0.75?
  6. 为什么 HashMap 不是一开始就使用红黑树?
  7. PECS 知道是什么吗?(泛型通配符)
  8. 什么是泛型擦除(类型擦除)?

线程池与序列化

  1. 线程池的核心参数有哪些?
  2. 提交任务的时候,它大概的执行流程是怎样的?
  3. Java 序列化里面的 serialVersionUID 是什么?它的作用是什么?

MySQL

  1. MySQL 常见的索引失效场景有哪些?
  2. MySQL 有哪些隔离级别,默认的隔离级别是什么?
  3. Oracle 的默认隔离级别知道吗?
  4. MySQL 的 redo log、undo log、binlog 分别有什么作用?
  5. char 跟 varchar 有什么区别?

Redis

  1. Redis 为什么速度很快?体现在哪几个方面?
  2. Redis 是单线程的,为什么还能支持高并发?
  3. 说一下缓存穿透、缓存击穿、缓存雪崩这三个问题
  4. 你对布隆过滤器了解吗?它为什么能解决穿透问题?
  5. Redis 的过期键是怎么删除的?
  6. 惰性删除和定期删除各有什么优缺点?
  7. 用过 Redis 实现分布式锁吗?或者说有了解过吗?
  8. 为什么释放锁的时候要用 Lua 脚本?

Spring Boot

  1. @SpringBootApplication 组合了哪些注解?这些注解各起什么作用?
  2. 平时开发有用过哪些 Starter(启动器)?
  3. 平时开发用的 ORM 框架是什么?
  4. 说一下 Bean 的生命周期
  5. BeanPostProcessor 这个扩展点有了解过吗?
  6. Spring AOP 的核心实现原理是什么?
  7. Spring Boot 支持哪些外部化配置?它们的优先级大致是什么样的?
  8. Spring AI 是什么?它的核心目标是什么?
    • 追问:它对话交互的核心组件是哪一个?
    • 追问:它相关的 API 你不是很熟是吗?
    • 追问:它有一个 Advisor(增强器)机制,有了解过吗?
    • 追问:简历中两段经历都有用 AI Agent,用的框架应该是 Spring AI 吧?

AI 相关问题

  1. 你现在是怎么看待 AI 的?
  2. 平时用 AI 写代码的场景多吗?使用度到了什么程度?
  3. 拿到一个需求用 AI 生成代码后,接下来会怎么处理?
  4. 除了业务方面,你觉得还有哪些方面需要着重关注?

项目相关问题

  1. 简历提到用 Redis 实现令牌桶限流,简单说一下令牌桶算法的实现原理
    • 追问:能不能获取到令牌跟时间有关系吗?
    • 追问:跟时间戳具体是什么关系?
  2. 简历提到自研 SDK 通过 @ReachCapability 注解声明式注册了 50 多个 Agent 业务能力,讲一下注解扫描、注册及调度总体的流程
    • 追问:这个调用工具白名单维护在哪里?
    • 追问:大模型识别错了(选错工具),怎么去兜底?
  3. 视频平台项目你承担的主要责任是什么?
  4. 智能商家点评平台里的秒杀是先 Redis 预减库存、再 MQ 异步下单,对吧?
    • 追问:如果有一部分订单下单失败了,库存怎么返还?(因为已经预减了)
    • 追问:定时任务的执行频率是多少?
    • 追问:秒杀发生时间很短,如果半小时才检查一次,下单失败但库存没返还,会造成什么问题?

《参考解析》

  1. HashMap 的 put 与扩容,四句话就能讲透:先算 hash(key.hashCode() 高 16 位异或低 16 位),再用 (n-1) & hash 定位桶;桶为空直接放,非空则比对 key,相同覆盖 value,不同挂到链表或红黑树末尾;链表长度到 8 且数组容量到 64 才树化,容量不足 64 优先扩容。扩容阈值是容量 × 负载因子(默认 16 × 0.75 = 12),超过就翻倍扩容,迁移时节点要么留在原下标、要么移动到「原下标 + 旧容量」,不需要重新计算 hash(这是容量取 2 的幂的原因)。负载因子取 0.75 是空间与冲突概率的经验平衡;不一开始就用红黑树则是因为树节点要多存父子指针和颜色、维护平衡要旋转变色,而按泊松分布链表长度到 8 的概率小于千万分之一,用链表兜底更划算。

  2. String 与泛型这几道基础题,答到「为什么」比答结论重要:字面量走字符串常量池(相同内容复用同一对象),new String() 在堆上新建对象,所以 == 为 false、equals 为 true;常量池在 JDK 7 之后从方法区移到堆。StringBuilder 线程不安全但无锁开销、StringBuffer 方法加 synchronized 因而线程安全,方法内拼接用前者、共享可变状态才用后者。泛型擦除指编译后泛型信息被替换为限定类型或 Object(无界擦除为 Object),因此运行时不能 new T[]、不能 instanceof T、静态成员不能用类型参数;PECS 是通配符使用原则——生产者(只读)用 ? extends T,消费者(只写)用 ? super T,Collections.copy(dest, src) 的签名就是标准范例。

  3. 线程池参数与执行流程是必背,但要答出数字和顺序:七个参数是核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略。提交流程有严格顺序——先看核心线程是否已满(未满就建核心线程)、再看队列是否满(未满就入队)、再看是否到达最大线程数(未到就建临时线程)、最后才走拒绝策略(AbortPolicy 抛异常、CallerRunsPolicy 由调用线程执行、DiscardPolicy 丢弃、DiscardOldestPolicy 丢最老的)。这里最常见的失分点是说成「先入队再建线程」的顺序错误,以及忘了核心线程默认不会被回收。serialVersionUID 的作用是反序列化时校验类版本:不显式声明时编译器会自动生成,改动类结构后 UID 变化会让旧数据反序列化抛 InvalidClassException,所以实现 Serializable 时应显式声明。

  4. MySQL 的索引失效、隔离级别与三种日志,按「为什么」串起来记:索引失效的常见写法——索引列上用函数或参与运算、隐式类型转换(字符串列传数字)、like '%x' 前缀模糊、or 两侧有一侧无索引、联合索引不满足最左前缀、!=/not in/is not null 导致优化器判定选择性差;出现问题时用 EXPLAIN 看 type、key、rows 才能确诊,因为这通常是优化器基于代价的选择。隔离级别四种,MySQL 默认可重复读(通过 MVCC 加 Next-Key Lock 处理幻读),Oracle 默认读已提交(靠多版本读一致性,并且不长时间持锁)。三种日志的分工:undo log 保证原子性并支撑 MVCC 版本链,redo log 保证持久性(记录数据页物理修改、顺序写),binlog 用于主从复制与备份(逻辑日志,推荐 ROW 格式);提交时两阶段提交——先写 redo 的 prepare,再写 binlog,最后提交 redo,崩溃恢复时靠 binlog 是否完整决定提交还是回滚。char 定长(不足补空格、适合手机号身份证这类定长字段),varchar 变长(按实际长度存,适合姓名地址)。

  5. Redis 为什么快、过期键怎么删、三大缓存问题,可以一起答:快的原因有四条——纯内存操作、单线程模型无线程切换与锁竞争、高效数据结构(SDS、跳表、压缩列表、渐进式 rehash)、IO 多路复用(epoll)支撑大量连接;所谓单线程指命令执行单线程,6.0 之后网络 IO 可以多线程。过期键删除是惰性删除(读写时检查,过期就删)加定期删除(每秒若干次随机抽查,过期比例高就继续抽查)的组合,再叠加内存淘汰策略兜底,惰性与定期各自的优缺点也由此而来(前者 CPU 友好但可能长期占内存,后者内存友好但有 CPU 开销与删除不及时)。缓存穿透查的是不存在的 key(布隆过滤器挡在前面、缓存空值、参数校验),击穿是单个热点 key 过期(互斥锁、逻辑过期),雪崩是大量 key 同时过期或缓存宕机(过期时间加随机、多级缓存、高可用、限流降级)。布隆过滤器用位数组加多个哈希函数,查到位为 0 就一定不存在,全为 1 则可能存在,所以有误判率且不支持删除。

  6. 为什么释放锁要用 Lua,要讲成一段时序问题:基础加锁是 SET key value NX EX,解锁要先判断锁是不是自己的再删。如果分两步做,中间可能发生锁自动过期、别的线程拿到锁,而当前线程接着把别人的锁删了。Lua 脚本把「判断 value 是否等于自己的标识」和「删除」合成一次原子执行——Redis 执行 Lua 期间不会插入其他命令,所以不存在中间态。进一步可以讲 Redisson 的可重入实现(hash 结构,field 是线程标识、value 是重入次数)和看门狗续期,以及加锁本身涉及多条命令时同样需要 Lua。

  7. Spring 这条线从注解到 Bean 生命周期再到 AOP:@SpringBootApplication 组合了 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan,自动配置的原理是读取 META-INF 下的配置类清单,再用 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 这些条件注解决定是否生效。Bean 的生命周期分实例化、属性填充、初始化(各种 Aware 回调、BeanPostProcessor 前置、@PostConstruct、afterPropertiesSet、init-method、BeanPostProcessor 后置)、销毁四个阶段,AOP 代理就是在后置处理里生成的,@Autowired 也由 BeanPostProcessor 的实现类处理。AOP 用动态代理实现,有接口走 JDK 动态代理、无接口走 CGLIB。被问到 Spring AI 时,可以答它的目标是统一 ChatModel 等抽象、对话入口是 ChatClient,Advisor 相当于拦截器链(QuestionAnswerAdvisor 做 RAG、MessageChatMemoryAdvisor 注入记忆);如果项目实际用的是 LangChain4j,就如实说明并对比能力,不要硬答。

  8. 令牌桶与秒杀库存返还,是这场面试真正的分水岭:令牌桶的思路是按固定速率补充令牌、请求取到令牌才放行,允许一定突发;用 Redis 实现时通常不做定时补充,而是懒加载——用 hash 存当前令牌数、上次填充时间、容量与速率,请求到来时用 (当前时间 - 上次填充时间) × 速率 算出应补充的令牌(上限为桶容量),再用 Lua 原子完成「补充 + 扣减 + 返回是否放行」。所以能否取到令牌既与时间戳有关(决定补多少),也与桶中现存令牌有关(决定这一下取不取得到);时间戳大的请求不一定取得到,它只能保证补得更多。秒杀用 Redis 预减库存加 MQ 异步下单,天生会出现「库存扣了但订单没建成」的问题,正确的做法是实时补偿而不是半小时一次的定时对账:消费失败进重试队列、超过次数触发库存返还;下单后发延时消息,到期检查订单状态再决定回滚;本地消息表记录未完成动作并定时补偿;定时对账只作为最后的兜底,频率要按业务量调(秒杀场景到秒级或分钟级)。面试官连问「频率是多少」「半小时会怎样」,就是在确认你有没有意识到:库存被预扣却迟迟不返还,等于少卖,超卖会投诉、少卖直接损失成交。