浩鲸科技 秋招一面面经:Java 基础、MySQL、Redis 与秒杀库存返还追问
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
Java 基础
- String 字面量直接赋值和直接 new 创建对象有什么区别?
- StringBuilder 跟 StringBuffer 有什么区别?
- 口述一下 HashMap 的 put 过程
- 什么时候触发扩容?(HashMap 扩容时机)
- 为什么负载因子是 0.75?
- 为什么 HashMap 不是一开始就使用红黑树?
- PECS 知道是什么吗?(泛型通配符)
- 什么是泛型擦除(类型擦除)?
线程池与序列化
- 线程池的核心参数有哪些?
- 提交任务的时候,它大概的执行流程是怎样的?
- Java 序列化里面的 serialVersionUID 是什么?它的作用是什么?
MySQL
- MySQL 常见的索引失效场景有哪些?
- MySQL 有哪些隔离级别,默认的隔离级别是什么?
- Oracle 的默认隔离级别知道吗?
- MySQL 的 redo log、undo log、binlog 分别有什么作用?
- char 跟 varchar 有什么区别?
Redis
- Redis 为什么速度很快?体现在哪几个方面?
- Redis 是单线程的,为什么还能支持高并发?
- 说一下缓存穿透、缓存击穿、缓存雪崩这三个问题
- 你对布隆过滤器了解吗?它为什么能解决穿透问题?
- Redis 的过期键是怎么删除的?
- 惰性删除和定期删除各有什么优缺点?
- 用过 Redis 实现分布式锁吗?或者说有了解过吗?
- 为什么释放锁的时候要用 Lua 脚本?
Spring Boot
- @SpringBootApplication 组合了哪些注解?这些注解各起什么作用?
- 平时开发有用过哪些 Starter(启动器)?
- 平时开发用的 ORM 框架是什么?
- 说一下 Bean 的生命周期
- BeanPostProcessor 这个扩展点有了解过吗?
- Spring AOP 的核心实现原理是什么?
- Spring Boot 支持哪些外部化配置?它们的优先级大致是什么样的?
- Spring AI 是什么?它的核心目标是什么?
- 追问:它对话交互的核心组件是哪一个?
- 追问:它相关的 API 你不是很熟是吗?
- 追问:它有一个 Advisor(增强器)机制,有了解过吗?
- 追问:简历中两段经历都有用 AI Agent,用的框架应该是 Spring AI 吧?
AI 相关问题
- 你现在是怎么看待 AI 的?
- 平时用 AI 写代码的场景多吗?使用度到了什么程度?
- 拿到一个需求用 AI 生成代码后,接下来会怎么处理?
- 除了业务方面,你觉得还有哪些方面需要着重关注?
项目相关问题
- 简历提到用 Redis 实现令牌桶限流,简单说一下令牌桶算法的实现原理
- 追问:能不能获取到令牌跟时间有关系吗?
- 追问:跟时间戳具体是什么关系?
- 简历提到自研 SDK 通过 @ReachCapability 注解声明式注册了 50 多个 Agent 业务能力,讲一下注解扫描、注册及调度总体的流程
- 追问:这个调用工具白名单维护在哪里?
- 追问:大模型识别错了(选错工具),怎么去兜底?
- 视频平台项目你承担的主要责任是什么?
- 智能商家点评平台里的秒杀是先 Redis 预减库存、再 MQ 异步下单,对吧?
- 追问:如果有一部分订单下单失败了,库存怎么返还?(因为已经预减了)
- 追问:定时任务的执行频率是多少?
- 追问:秒杀发生时间很短,如果半小时才检查一次,下单失败但库存没返还,会造成什么问题?
《参考解析》
-
HashMap 的 put 与扩容,四句话就能讲透:先算 hash(
key.hashCode()高 16 位异或低 16 位),再用(n-1) & hash定位桶;桶为空直接放,非空则比对 key,相同覆盖 value,不同挂到链表或红黑树末尾;链表长度到 8 且数组容量到 64 才树化,容量不足 64 优先扩容。扩容阈值是容量 × 负载因子(默认 16 × 0.75 = 12),超过就翻倍扩容,迁移时节点要么留在原下标、要么移动到「原下标 + 旧容量」,不需要重新计算 hash(这是容量取 2 的幂的原因)。负载因子取 0.75 是空间与冲突概率的经验平衡;不一开始就用红黑树则是因为树节点要多存父子指针和颜色、维护平衡要旋转变色,而按泊松分布链表长度到 8 的概率小于千万分之一,用链表兜底更划算。 -
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)的签名就是标准范例。 -
线程池参数与执行流程是必背,但要答出数字和顺序:七个参数是核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略。提交流程有严格顺序——先看核心线程是否已满(未满就建核心线程)、再看队列是否满(未满就入队)、再看是否到达最大线程数(未到就建临时线程)、最后才走拒绝策略(
AbortPolicy抛异常、CallerRunsPolicy由调用线程执行、DiscardPolicy丢弃、DiscardOldestPolicy丢最老的)。这里最常见的失分点是说成「先入队再建线程」的顺序错误,以及忘了核心线程默认不会被回收。serialVersionUID 的作用是反序列化时校验类版本:不显式声明时编译器会自动生成,改动类结构后 UID 变化会让旧数据反序列化抛InvalidClassException,所以实现Serializable时应显式声明。 -
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 变长(按实际长度存,适合姓名地址)。 -
Redis 为什么快、过期键怎么删、三大缓存问题,可以一起答:快的原因有四条——纯内存操作、单线程模型无线程切换与锁竞争、高效数据结构(SDS、跳表、压缩列表、渐进式 rehash)、IO 多路复用(epoll)支撑大量连接;所谓单线程指命令执行单线程,6.0 之后网络 IO 可以多线程。过期键删除是惰性删除(读写时检查,过期就删)加定期删除(每秒若干次随机抽查,过期比例高就继续抽查)的组合,再叠加内存淘汰策略兜底,惰性与定期各自的优缺点也由此而来(前者 CPU 友好但可能长期占内存,后者内存友好但有 CPU 开销与删除不及时)。缓存穿透查的是不存在的 key(布隆过滤器挡在前面、缓存空值、参数校验),击穿是单个热点 key 过期(互斥锁、逻辑过期),雪崩是大量 key 同时过期或缓存宕机(过期时间加随机、多级缓存、高可用、限流降级)。布隆过滤器用位数组加多个哈希函数,查到位为 0 就一定不存在,全为 1 则可能存在,所以有误判率且不支持删除。
-
为什么释放锁要用 Lua,要讲成一段时序问题:基础加锁是
SET key value NX EX,解锁要先判断锁是不是自己的再删。如果分两步做,中间可能发生锁自动过期、别的线程拿到锁,而当前线程接着把别人的锁删了。Lua 脚本把「判断 value 是否等于自己的标识」和「删除」合成一次原子执行——Redis 执行 Lua 期间不会插入其他命令,所以不存在中间态。进一步可以讲 Redisson 的可重入实现(hash 结构,field 是线程标识、value 是重入次数)和看门狗续期,以及加锁本身涉及多条命令时同样需要 Lua。 -
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,就如实说明并对比能力,不要硬答。 -
令牌桶与秒杀库存返还,是这场面试真正的分水岭:令牌桶的思路是按固定速率补充令牌、请求取到令牌才放行,允许一定突发;用 Redis 实现时通常不做定时补充,而是懒加载——用 hash 存当前令牌数、上次填充时间、容量与速率,请求到来时用
(当前时间 - 上次填充时间) × 速率算出应补充的令牌(上限为桶容量),再用 Lua 原子完成「补充 + 扣减 + 返回是否放行」。所以能否取到令牌既与时间戳有关(决定补多少),也与桶中现存令牌有关(决定这一下取不取得到);时间戳大的请求不一定取得到,它只能保证补得更多。秒杀用 Redis 预减库存加 MQ 异步下单,天生会出现「库存扣了但订单没建成」的问题,正确的做法是实时补偿而不是半小时一次的定时对账:消费失败进重试队列、超过次数触发库存返还;下单后发延时消息,到期检查订单状态再决定回滚;本地消息表记录未完成动作并定时补偿;定时对账只作为最后的兜底,频率要按业务量调(秒杀场景到秒级或分钟级)。面试官连问「频率是多少」「半小时会怎样」,就是在确认你有没有意识到:库存被预扣却迟迟不返还,等于少卖,超卖会投诉、少卖直接损失成交。