互联网大厂高阶 Java 面试现场:从 Spring AI 到分布式事务的深度拷问
文章标题
互联网大厂高阶 Java 面试现场:从 Spring AI 到分布式事务的深度拷问
文章正文
互联网大厂高阶 Java 面试现场:从 Spring AI 到分布式事务的深度拷问
“你简历里写了 Spring AI + RAG 落地经验,还做过跨行清算的分布式事务?”
面试官推了推眼镜,语气平静,但眼神已经锁定了坤哥。
坤哥深吸一口气:“是的,去年我们上线了企业私有知识库助手,同时支撑了银联通道的实时清分系统。”
“好,那我们今天不聊 HashMap 扩容,聊聊你真正踩过的坑。”
第一轮:基础试探 —— 基本功的“温柔一刀”
面试官:Spring Boot 启动时,自动装配是怎么工作的?
坤哥:主要是靠 @EnableAutoConfiguration 和 spring.factories 文件。Spring Boot 在启动时会扫描所有 jar 包中的 META-INF/spring.factories,加载里面定义的自动配置类,然后通过 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解决定是否生效。
面试官:那如果我想禁用某个自动配置,比如 DataSourceAutoConfiguration,怎么做?
坤哥:可以在 @SpringBootApplication 上加上 exclude,比如:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
或者在 application.yml 里设置:
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
面试官点头:还行。那你说说 Spring 事务为什么有时候会失效?
坤哥:常见原因有几个:一是方法不是 public 的,二是异常被捕获没抛出去,三是异常类型不是 RuntimeException 或 Error,而 rollbackFor 没配,四是同类内方法调用导致 AOP 代理失效。
面试官:举个例子,同类内调用怎么破?
坤哥:可以用 AopContext.currentProxy() 获取代理对象再调用,或者把事务方法抽到新类里,更推荐后者,避免循环依赖和代理陷阱。
【技术分析】
这一轮看似基础,实则暗藏杀机。自动装配是 Spring Boot 的“黑盒魔法”,而事务失效是线上高频 bug 来源。候选人能准确指出同类内调用问题,说明至少看过源码或踩过坑。但面试官知道,真正的考验才刚开始。
第二轮:底层深挖 —— 源码与边界的“极限施压”
面试官:你刚提到 AOP 代理失效,那 Spring 默认用 JDK 动态代理还是 CGLIB?什么时候会切换?
坤哥:默认是 JDK 动态代理,如果目标类没有实现接口,就自动切 CGLIB。也可以通过 @EnableAspectJAutoProxy(proxyTargetClass = true) 强制使用 CGLIB。
面试官:那如果目标类实现了接口,但又想强制用 CGLIB 呢?
坤哥:同上,设置 proxyTargetClass = true 就行。
面试官:好。那你说说 CGLIB 是怎么生成代理类的?和 JDK 动态代理本质区别是什么?
坤哥:CGLIB 是通过继承目标类,重写方法,在方法前后插入切面逻辑。而 JDK 动态代理是基于接口的,只能代理接口方法。CGLIB 不需要接口,但无法代理 final 类或 final 方法。
面试官:那如果目标类有 final 方法,CGLIB 会怎样?
坤哥:……会跳过,不会代理那个方法。
面试官:对。那如果这个 final 方法里调用了另一个需要事务的方法,事务会生效吗?
坤哥:……不会,因为整个调用链都在原始对象上,没走代理。
【技术分析】
这里开始触及 AOP 的“灵魂拷问”:代理机制的本质是字节码增强,而 final 方法无法被重写,自然无法织入切面。候选人能意识到调用链断裂,说明对代理的理解已超越“注解怎么用”,进入“机制怎么跑”的层面。
面试官话锋一转:你简历里写用 ThreadLocal 做用户上下文传递,那如果用了线程池,会有什么问题?
坤哥:线程复用会导致 ThreadLocal 残留数据,造成内存泄漏或用户信息串扰。
面试官:怎么解决?
坤哥:可以用 TransmittableThreadLocal,它在提交任务时把上下文快照传到子线程,执行完再清理。
面试官:那如果不用第三方库,自己实现呢?
坤哥:……可以在任务执行前手动 set,执行后 remove,但容易漏,尤其异常时。
面试官:对。那你说说 ThreadLocalMap 的 key 为什么是弱引用?
坤哥:为了防止内存泄漏。如果 key 是强引用,即使 ThreadLocal 变量被 GC,Entry 还持有 key 的引用,导致无法回收。弱引用可以让 key 在 ThreadLocal 不再可达时被回收。
面试官:那 value 呢?为什么不是弱引用?
坤哥:因为 value 通常持有业务对象,如果设为弱引用,可能被提前回收,导致数据丢失。而且 value 的生命周期应该由业务控制,不是 GC 决定。
【技术分析】
ThreadLocal 的弱引用设计是 JVM 层与框架层协同的经典案例。候选人能区分 key 和 value 的引用策略,说明读过源码。但面试官注意到,他没提“即使 key 是弱引用,value 仍可能泄漏”,这需要主动调用 remove(),否则 Entry 虽 key 为 null,value 还在,造成“假泄漏”。
第三轮:业务实战 —— 架构与故障的“真实战场”
面试官:现在进入正题。你说做了跨行清算的分布式事务,用的是什么方案?
坤哥:我们用的是 Seata 的 AT 模式,基于全局锁和 undo log 实现。
面试官:AT 模式在什么场景下会性能很差?
坤哥:高并发热点账户更新时,比如同一个账户被多个事务同时修改,全局锁会成为瓶颈,TPS 上不去。
面试官:那你们怎么优化?
坤哥:我们把账户按 ID 分片,不同分片走不同事务分支,减少锁冲突。另外,对读多写少的查询走本地事务,只对写操作接入 Seata。
面试官:如果 Seata Server 挂了,事务还能提交吗?
坤哥:不能,TC 是核心协调者,挂了就无法完成二阶段提交。
面试官:那你们有降级方案吗?
坤哥:……暂时没有,但我们加了监控和告警,TC 挂了立刻切换备机。
面试官:备机怎么保证状态一致?
坤哥:……Seata 本身支持集群,通过 Raft 协议同步状态。
【技术分析】
Seata AT 模式虽然“开箱即用”,但在金融级场景下,全局锁和单点依赖是致命弱点。候选人意识到分片优化,但未提及“最终一致性补偿”或“TCC 兜底”,说明对分布式事务的容错设计理解还不够深。
面试官突然切换话题:你做的知识库助手,RAG 是怎么做的?
坤哥:我们把文档向量化存到向量数据库,用户提问时先检索 top-k 相关片段,再拼到 prompt 里发给大模型。
面试官:向量数据库选型是什么?为什么不用 ES?
坤哥:用的 Milvus,因为 ES 的稠密向量检索性能不如专用向量库,尤其在高维场景下。
面试官:那如果用户问“去年双十一我们卖了多少钱”,但文档里只有“2025年双十一销售额为120亿”,模型会答错年份吗?
坤哥:……有可能,因为 RAG 只返回片段,模型可能忽略时间上下文。
面试官:怎么解决?
坤哥:可以在 prompt 里显式强调时间,比如“请根据2025年的数据回答”,或者在后处理阶段做时间对齐。
面试官:那如果文档更新了,向量库怎么同步?
坤哥:我们做了定时任务,每天凌晨全量重建索引。
面试官:全量?数据量多大?
坤哥:……目前 50 万篇文档,重建要 2 小时。
面试官:那白天有新增文档怎么办?用户查不到最新内容。
坤哥:……我们计划上增量更新,但还没落地。
【技术分析】
RAG 的“幻觉”问题不仅来自模型,更来自检索粒度与上下文缺失。候选人知道 prompt 调优,但忽略了“实时性”这一企业级刚需。全量重建在高频更新场景下是不可接受的,必须引入增量索引 + 双写机制。
面试官最后问:如果让你设计一个支持千万级并发的清算系统,你会怎么分层?
坤哥:前端网关做限流和认证,接入层用 Netty 做异步通信,业务层分账户域、交易域、对账域,数据层用 ShardingSphere 分库分表,缓存用 Redis 集群,MQ 用 RocketMQ 保证消息可靠。
面试官:RocketMQ 和 Kafka 怎么选?
坤哥:Kafka 吞吐高,适合日志类场景;RocketMQ 事务消息成熟,延迟低,更适合金融交易。
面试官:那如果消息积压了,怎么办?
坤哥:可以扩容消费者,或者临时切到备用 Topic,同时查是否有慢 SQL 或死锁。
面试官:好。今天的面试就到这。
坤哥起身,手心微汗。
面试官合上笔记本,淡淡地说:“回去等通知吧。”
技术补丁包
1. Spring Boot 自动装配原理
- 核心机制:
SpringFactoriesLoader加载META-INF/spring.factories中的配置类,结合@Conditional系列注解实现条件化装配。 - 业务建议:自定义 starter 时应合理设计条件注解,避免污染全局上下文。
- 风险提示:exclude 配置错误可能导致核心功能缺失,建议通过
@ConditionalOnProperty做运行时控制。
2. Spring 事务失效场景
- 根本原因:AOP 代理未生效或异常未传播。
- 解决方案:
- 同类调用:抽离事务方法或使用
AopContext.currentProxy() - 异常处理:确保抛出
RuntimeException或配置rollbackFor
- 同类调用:抽离事务方法或使用
- 源码关键:
TransactionInterceptor在invokeWithinTransaction中判断异常类型。
3. CGLIB vs JDK 动态代理
- CGLIB:基于继承,生成子类代理,支持无接口类,但无法代理 final 方法。
- JDK Proxy:基于接口,性能略优,但只能代理接口方法。
- 选择策略:Spring 默认优先 JDK,无接口时切 CGLIB;可通过
proxyTargetClass=true强制 CGLIB。
4. ThreadLocal 内存泄漏
- 弱引用设计:key 为弱引用,防止 ThreadLocal 实例无法回收。
- value 泄漏:即使 key 被回收,value 仍强引用,需手动调用
remove()。 - 最佳实践:使用
TransmittableThreadLocal或确保 finally 块中清理。
5. Seata AT 模式局限
- 全局锁瓶颈:热点账户更新串行化,建议分片或降级为 TCC。
- TC 单点风险:必须部署集群,配合健康检查与自动故障转移。
- 补偿机制:建议补充定时对账任务,实现最终一致性兜底。
6. RAG 工程化挑战
- 检索精度:需结合元数据过滤(如时间、部门)提升相关性。
- 实时性:全量重建不可持续,应实现增量索引 + 双写同步。
- Prompt 工程:显式注入上下文(如“基于2025年数据”),减少幻觉。
7. RocketMQ vs Kafka
| 维度 | RocketMQ | Kafka | |------|--------|-------| | 事务消息 | ✅ 成熟支持 | ❌ 有限支持 | | 延迟 | 毫秒级 | 通常 10ms+ | | 吞吐 | 高 | 极高 | | 适用场景 | 金融、电商交易 | 日志、流处理 |
本次面试覆盖 Spring、并发、分布式事务、AI 工程四大领域,技术点彼此关联,体现高阶工程师需具备“全栈思维”。真正的挑战不在八股,而在业务约束下的权衡与落地。
文章标签
Spring Boot, 分布式事务, Seata, RAG, ThreadLocal, AOP, RocketMQ, 高阶面试
文章简述
本文以对话体还原互联网大厂高阶 Java 面试现场,围绕 Spring AI、分布式事务、并发编程等核心技术展开深度追问,穿插源码解析与业务落地建议,适合中高级开发者提升架构思维。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)