面试背景
这是一次面向 1-3 年 Java 后端候选人的大厂一面模拟,时长约 30 分钟。候选人背景为电商订单系统开发,熟悉 Spring Boot、MySQL、Redis、Kafka,有基础并发编程经验。面试官风格偏实战,注重原理理解与线上落地能力,提问节奏紧凑,强调"为什么这么设计"和"边界场景怎么处理"。
整场面试围绕 Java 并发、Spring 事务、MySQL 锁机制、线程池调优和分布式场景展开,重点考察候选人对技术点的理解深度、边界判断能力以及线上问题应对思路。
主问题部分
问题 1:你项目中线程池是怎么配置的?核心参数怎么定的?
候选人回答 : 我们用的是 ThreadPoolExecutor,核心线程数设为 CPU 核数的 2 倍,最大线程数设为 4 倍,队列用 LinkedBlockingQueue,容量 1000,拒绝策略是 CallerRunsPolicy。
面试官追问:为什么核心线程数是 CPU 核数的 2 倍?有没有压测数据支撑?如果任务主要是 IO 密集型,这个设置合理吗?
候选人回答:我们任务主要是查 DB 和调 RPC,IO 占比高,所以设成 2 倍。压测时 QPS 能到 3000,CPU 使用率在 60% 左右,没出现明显瓶颈。
面试官追问:那如果突然流量翻倍,队列积压到 800,你怎么办?有没有监控和报警?
候选人回答:我们有 Prometheus + Grafana 监控队列大小和活跃线程数,超过阈值会发告警。如果积压严重,会临时扩容机器或降级非核心功能。
问题 2:Spring 事务传播机制中,REQUIRES_NEW 和 NESTED 有什么区别?
候选人回答 :REQUIRES_NEW 会挂起当前事务,开启一个新事务;NESTED 是在当前事务内创建一个保存点,子事务回滚不会影响外层事务。
面试官追问 :那如果外层事务回滚了,NESTED 的子事务会回滚吗?
候选人回答 :会的,因为 NESTED 本质上还是同一个物理事务,只是通过保存点实现部分回滚。
面试官追问:那在 MySQL 中,保存点是怎么实现的?会不会有性能开销?
候选人回答 :保存点是通过 SAVEPOINT 语句实现的,MySQL 会记录事务状态。频繁使用保存点可能会增加 undo log 的大小,影响性能。
问题 3:你在项目中遇到过死锁吗?怎么排查的?
候选人回答 :遇到过一次,是两张表互相更新导致的。我们用 SHOW ENGINE INNODB STATUS 查到了死锁日志,发现是事务 A 先锁了表 A,再锁表 B;事务 B 先锁表 B,再锁表 A,形成了循环等待。
面试官追问:那你怎么解决的?有没有考虑过用乐观锁替代?
候选人回答:我们调整了 SQL 执行顺序,保证所有事务都按相同顺序加锁。也评估过乐观锁,但因为并发高,版本冲突频繁,最终没采用。
面试官追问:那如果必须用乐观锁,你怎么设计重试机制?
候选人回答:可以在应用层做重试,比如最多重试 3 次,每次加随机延迟,避免雪崩。
问题 4:Redis 做分布式锁,你用的是什么方案?
候选人回答 :用的是 Redisson 的 RLock,支持可重入、自动续期、看门狗机制。
面试官追问:看门狗是怎么工作的?如果主节点挂了,从节点还没同步锁信息,会不会出现多个客户端同时持有锁?
候选人回答:看门狗会定期续期锁的 TTL,防止业务执行时间过长导致锁过期。Redis 主从异步复制确实可能导致锁失效,所以我们要求业务尽量短,或者用 RedLock 算法。
面试官追问:RedLock 真的安全吗?有没有可能因为时钟漂移导致锁失效?
候选人回答:RedLock 依赖多个独立 Redis 实例,要求大多数节点成功才算加锁成功。但确实存在时钟漂移风险,所以我们更倾向于用 ZooKeeper 或 etcd 做强一致锁。
问题 5:MySQL 的 SELECT ... FOR UPDATE 会加什么锁?
候选人回答:会对查询到的行加排他锁(X 锁),如果走了索引,只锁索引记录;如果没走索引,会锁全表。
面试官追问 :那如果 where 条件是 id > 100,会锁哪些范围?
候选人回答 :会锁满足 id > 100 的所有记录,以及它们之间的间隙(gap lock),防止幻读。
面试官追问:那在 RR 隔离级别下,这个锁会持续到什么时候?
候选人回答:会持续到事务结束,包括 commit 或 rollback。
问题 6:你项目中怎么保证消息消费的幂等性?
候选人回答:我们在消费端做了幂等校验,比如用 Redis 记录消息 ID,消费前先查是否已处理。
面试官追问:如果 Redis 挂了,怎么办?有没有考虑过用数据库唯一索引?
候选人回答:我们也有数据库兜底,比如订单状态变更表有唯一键,重复消费会抛异常,我们捕获后直接返回成功。
面试官追问:那如果消息乱序到达,比如"支付成功"消息比"创建订单"还早,你怎么处理?
候选人回答:我们会在消费端做状态机校验,比如只有"已创建"状态才能进入"已支付",否则拒绝或延迟处理。
问题 7:Spring Bean 的生命周期中,AOP 代理是在哪个阶段创建的?
候选人回答 :是在 BeanPostProcessor 的 postProcessAfterInitialization 阶段,通过 AbstractAutoProxyCreator 创建代理对象。
面试官追问:那如果 Bean A 依赖 Bean B,而 Bean B 被 AOP 代理了,Spring 怎么保证注入的是代理对象?
候选人回答:Spring 在依赖注入时,会从三级缓存中获取早期暴露的对象,如果该对象需要代理,会提前创建代理并放入缓存。
面试官追问:那三级缓存具体是哪三级?为什么需要三级而不是两级?
候选人回答:一级是成品 Bean,二级是早期暴露的 Bean,三级是 ObjectFactory。三级是为了处理循环依赖时,能延迟生成代理对象,避免重复创建。
问题 8:你项目中怎么处理慢 SQL?
候选人回答 :我们开启了慢查询日志,阈值设成 1 秒,定期分析。也用了 EXPLAIN 看执行计划,优化索引。
面试官追问 :那如果 EXPLAIN 显示 type=ALL,你怎么办?
候选人回答:说明是全表扫描,我们会检查 where 条件字段是否有索引,或者是否走了最左前缀。
面试官追问 :那如果字段有索引,但 EXPLAIN 还是显示 type=ALL,可能是什么原因?
候选人回答:可能是索引选择性差,或者统计信息过期,或者查询条件用了函数导致索引失效。
追问部分
重点追问 1:线程池的拒绝策略,除了 CallerRunsPolicy,你还用过哪些?为什么选这个?
候选人回答 :还用过 AbortPolicy,直接抛异常。CallerRunsPolicy 的好处是让调用线程自己执行任务,能起到限流作用,避免任务丢失。
面试官追问:那如果调用线程是 Tomcat 的请求线程,会不会影响接口响应?
候选人回答 :会的,所以我们只在非核心路径用,比如日志上报、统计等。核心业务我们会用 DiscardPolicy 或自定义策略,记录日志后丢弃。
面试官追问:那有没有考虑过用有界队列 + 动态线程池?比如根据负载自动调整核心线程数?
候选人回答 :有调研过,比如用 ThreadPoolTaskExecutor 配合监控,动态调用 setCorePoolSize,但实现复杂,目前还是固定配置。
重点追问 2:Spring 事务中,如果方法 A 调用方法 B,B 抛异常,A 捕获了,事务会回滚吗?
候选人回答 :如果 B 是 REQUIRED 传播,异常会传播到 A,即使 A 捕获了,事务也会回滚,除非用 @Transactional(noRollbackFor=...)。
面试官追问 :那如果 B 是 REQUIRES_NEW,A 捕获了异常,B 的事务会回滚吗?
候选人回答:会的,B 的事务独立,异常会导致 B 回滚,但 A 的事务不受影响。
面试官追问:那如果 B 的事务回滚了,但 A 继续执行并 commit,会不会导致数据不一致?
候选人回答:会的,所以我们通常会在 A 中检查 B 的执行结果,或者用补偿机制。
重点追问 3:Redis 分布式锁的自动续期,如果业务执行时间超过锁的 TTL,会发生什么?
候选人回答:看门狗会续期,但如果业务线程阻塞(比如 Full GC),看门狗可能无法续期,锁会过期,其他客户端可能获取到锁。
面试官追问:那你怎么避免这种情况?
候选人回答:我们会设置合理的 TTL,比如 30 秒,并监控 GC 情况。如果业务复杂,会拆分成多个小任务。
面试官追问:那如果看门狗续期失败,但业务还在执行,怎么办?
候选人回答:我们会在业务逻辑中加状态检查,比如用 Redis 记录执行状态,避免重复执行。
面试点评
本场面试主要考察候选人在高并发、分布式场景下的技术深度与实战能力。重点包括:
- 线程池调优:不仅要知道参数含义,还要理解 IO/CPU 密集型任务的区别,以及监控与动态调整。
- Spring 事务传播 :要求理解
REQUIRES_NEW与NESTED的底层差异,以及 MySQL 保存点的实现。 - 死锁排查:强调从现象到根因的分析能力,以及解决方案的权衡。
- 分布式锁:考察对 Redis 主从复制、RedLock、时钟漂移等边界问题的理解。
- 消息幂等:要求设计完整的防重机制,包括 Redis、数据库、状态机等多层保障。
候选人容易卡在"原理与线上落地的衔接"上,比如知道 NESTED 是保存点,但不知道 MySQL 如何实现;知道看门狗续期,但没考虑 GC 影响。
技术补丁包
-
线程池核心参数调优 原理:核心线程数、最大线程数、队列容量、拒绝策略共同决定线程池行为。 设计动机:平衡资源利用与任务吞吐量,避免 OOM 或线程过多。 边界条件:IO 密集型任务可设更高线程数,CPU 密集型建议接近核数。 落地建议:结合压测数据调整,监控队列积压与线程活跃度。
-
Spring 事务传播机制 原理:通过
TransactionInterceptor和PlatformTransactionManager实现事务边界控制。 设计动机:支持嵌套事务、独立事务等复杂业务场景。 边界条件:NESTED依赖数据库保存点,REQUIRES_NEW会挂起当前事务。 落地建议:避免在循环中开启新事务,防止连接耗尽。 -
MySQL 死锁排查 原理:InnoDB 通过等待图检测死锁,自动回滚代价最小的事务。 设计动机:保证事务并发执行时的数据一致性。 边界条件:死锁日志只保留最近一次,需及时采集。 落地建议:统一 SQL 执行顺序,使用
SHOW ENGINE INNODB STATUS分析。 -
Redis 分布式锁实现 原理:基于
SET key value NX PX timeout实现原子加锁。 设计动机:解决多实例部署下的资源竞争问题。 边界条件:主从异步复制可能导致锁失效,时钟漂移影响 RedLock。 落地建议:业务尽量短,结合看门狗续期,或改用 ZooKeeper。 -
消息消费幂等性设计 原理:通过唯一标识(消息 ID、业务 ID)判断是否已处理。 设计动机:防止重复消费导致数据错误。 边界条件:Redis 宕机、消息乱序、网络分区等场景。 落地建议:多级防重(Redis + 数据库唯一键 + 状态机)。
-
Spring AOP 代理创建时机 原理:在
BeanPostProcessor的postProcessAfterInitialization阶段创建。 设计动机:支持对 Bean 的方法进行增强。 边界条件:循环依赖时需提前暴露代理对象。 落地建议:避免在@PostConstruct中调用被代理方法。 -
MySQL 行锁与间隙锁 原理:
FOR UPDATE加排他锁,RR 隔离级别下加间隙锁防幻读。 设计动机:保证事务隔离性。 边界条件:未走索引会锁全表,范围查询会锁间隙。 落地建议:确保 where 条件走索引,避免大事务。 -
慢 SQL 排查与优化 原理:通过慢查询日志、
EXPLAIN、SHOW PROFILES分析执行计划。 设计动机:提升数据库查询性能。 边界条件:索引失效、统计信息过期、隐式类型转换。 落地建议:定期分析慢日志,使用覆盖索引,避免SELECT *。 -
线程池拒绝策略选择 原理:
AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。 设计动机:应对任务提交速度超过处理能力的情况。 边界条件:CallerRunsPolicy可能阻塞调用线程。 落地建议:核心业务用AbortPolicy,非核心用CallerRunsPolicy。 -
分布式锁的自动续期机制 原理:看门狗线程定期续期锁的 TTL。 设计动机:防止业务执行时间过长导致锁过期。 边界条件:GC 暂停可能导致续期失败。 落地建议:设置合理 TTL,监控 GC,业务拆分。
-
Spring 三级缓存解决循环依赖 原理:一级存成品 Bean,二级存早期暴露 Bean,三级存 ObjectFactory。 设计动机:支持 AOP 代理与循环依赖共存。 边界条件:只能解决单例、非构造器注入的循环依赖。 落地建议:尽量避免循环依赖,使用
@Lazy延迟注入。 -
MySQL 保存点实现机制 原理:通过
SAVEPOINT记录事务状态,ROLLBACK TO回滚到指定点。 设计动机:支持部分回滚,减少事务回滚代价。 边界条件:保存点过多会增加 undo log 大小。 落地建议:谨慎使用,避免深层嵌套。 -
消息乱序处理策略 原理:通过状态机校验消息顺序,延迟处理或拒绝非法状态。 设计动机:保证业务逻辑的正确性。 边界条件:消息延迟、重复、丢失等网络问题。 落地建议:设计幂等接口,使用消息版本号或时间戳。
-
线程池动态调整策略 原理:通过监控指标动态调用
setCorePoolSize调整线程数。 设计动机:适应流量波动,提升资源利用率。 边界条件:频繁调整可能导致线程频繁创建销毁。 落地建议:结合 Prometheus + 自定义 Controller 实现。 -
分布式事务的最终一致性保障 原理:通过消息队列 + 本地事务表 + 补偿机制实现。 设计动机:解决跨服务事务一致性问题。 边界条件:消息丢失、重复消费、网络分区。 落地建议:使用可靠消息服务,设计幂等消费与对账机制。