Java大厂面试实录:Spring Boot、Redis、Kafka、Elasticsearch、分布式事务与云原生高并发系统
"咚、咚、咚。"
谢飞机推开玻璃门,带着三分紧张七分自信走进了面试间。面试官抬眼看了看,面前摆着简历和一台ThinkPad,屏幕上一堆代码。
面试官(下称"官"):坐。先说下你这个项目吧。你负责什么?用了哪个Java版本?构建工具是什么?
谢飞机(下称"飞"):我这个项目是一个UGC内容社区,用户可以发短视频、图文,还能评论、点赞、搜索。我主要做内容发布和审核这一条链路。Java从8升级到11,构建工具用得最多是Maven,Gradle也看过。后端是Spring Boot + Spring Cloud,数据库MySQL,ORM用MyBatis和JPA,缓存用Redis,消息队列用Kafka,搜索用Elasticsearch。
官:这个技术栈还比较主流。你们这种读多写少的社区,高并发下怎么扛?
飞:我们分了好几层:CDN抗图片和视频,Nginx做负载均衡,应用层用Caffeine本地缓存 + Redis分布式缓存。缓存过期处理上,用布隆过滤器挡穿透,用分布式锁防止热点key击穿,给缓存加随机过期时间防止雪崩。数据库这边做了主从分离,主库负责写,从库扛读,流量再大还能在K8s里扩Pod。
官:不错,这一套是标准答案了。那缓存和数据库一致性怎么做?
飞:Cache Aside。我们更新时先写数据库,再删缓存。如果删失败,就依赖过期时间;后来订阅了MySQL的binlog,用Canal去删Redis,保证最终一致。
官:你还知道Canal?可以。那Spring Boot的自动配置原理呢?
飞:这个我背过,不是,我理解过。@SpringBootApplication里有@EnableAutoConfiguration,它会读classpath下的要导出的自动配置类列表,比如spring.factories。每个自动配置类都有@ConditionalOnClass、@ConditionalOnMissingBean这些条件注解,满足条件才会创建Bean。像RedisAutoConfiguration,只要引入了Redis Starter并且没有自定义RedisTemplate,它就会自动给你配一个。
官:嗯,看来平时看过源码。那日志和监控呢?
飞:日志用SLF4J + Logback,链路追踪用Jaeger和Zipkin;监控用Micrometer + Prometheus + Grafana。我们还会把日志输出到ELK里做错误分析。
官:好,第一轮差不多了。接下来聊聊你们的内容审核模块。
谢飞机心说:终于到了我背得最多的部分。
官:用户上传内容后,审核流程怎么设计?现在AIGC内容这么多,你们怎么做?
飞:我们采用"先审后发"。上传后先把文件和元数据存到OSS和MySQL,状态是"待审核";然后发一条消息到Kafka。审核服务消费后并行做图片/视频审核、OCR、敏感词检测、重复度和AI生成识别。审核通过就把状态改成"已发布",同时把内容写入ES;不通过就进人工复审。AIGC内容会打上标签。
官:那你说说,为什么用消息队列?以及Kafka如何保证消息不丢失?把ISR、HW、LEO这些细节也说说。
飞:主要是削峰、异步、解耦。特别是像用户集中发布的时候,审核服务不会被打爆。Kafka消息不丢失...生产者要设acks=all和重试;Broker要有多副本和min.insync.replicas;消费者要手动提交offset,处理成功后再提交。ISR就是同步副本集合,HW是高水位,LEO是日志末端偏移量。具体怎么推进我有点忘了,我只记得Kafka通过副本机制保证高可用。
官:那最终一致性呢?如果审核服务消费失败或者重复消费怎么办?
飞:我们消费逻辑做了幂等,用Redis的setnx,用contentId作为key,保证同一个内容不会处理两次。处理失败的话,try-catch重试三次,还不行就发到死信队列,人工捞出来处理。至于分布式事务,我们没用到强事务,最终一致就行。
官:好,搜索呢?你们用ES,说说倒排索引。
飞:倒排索引就是"词语 -> 文档ID"的映射。比如一篇文章里有"苹果",那么"苹果"就会指向这篇文档。查询的时候直接查"苹果"就能拿到所有文章,不用扫全表。我们中文用IK分词器,ES里用MatchQuery。
官:那ES深分页问题怎么解决?
飞:我们用search_after。因为from+size超过一万会报错。MySQL那边如果要分页很深,我会用where id > 上一页最大id limit 10,或者用延迟关联。
官:嗯,这轮也算过关。再说说安全和运维。
谢飞机擦了擦汗。
官:用户登录鉴权怎么做的?JWT和OAuth2有什么区别?
飞:我们的登录是账号密码+短信验证码,成功后签发JWT,里面带userId和过期时间。Spring Security的过滤器会解析JWT,验证签名,把用户身份放到上下文里。第三方登录用OAuth2授权码模式,授权服务器用的Keycloak。区别的话,JWT是一种token格式,OAuth2是一个授权框架。OAuth2可以用JWT当access_token,也可以不用。
官:如何防止接口被刷和恶意攻击?
飞:首先Nginx加限流,每个IP限制QPS。然后应用层用Spring Security做权限控制,对敏感接口加验证码。我们还用Redis的令牌桶算法做了一个限流组件。另外防SQL注入用预编译,防XSS做输入过滤,HTTPS是必须的,WAF也上了。
官:项目怎么部署的?Docker和K8s里Java服务的JVM参数怎么设置?
飞:用Jenkins构建镜像,推到镜像仓库,然后K8s的Deployment拉取镜像滚动更新。JVM参数会放在环境变量JAVA_OPTS里,比如-Xms2g -Xmx2g -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump。注意Java 11默认开了容器识别,所以-Xmx会按容器内存比例来算。
官:线上OOM了,你怎么排查?
飞:先看监控,如果OOM时自动导出了Heap Dump,就用MAT分析,看看是不是有大对象或者内存泄漏。有时候dump文件没有生成,就只能重启服务。如果是线程创建太多导致的OOM,那要查线程池和代码里的new Thread。嗯...大概是这样。
官:那压测时CPU飙到了100%,怎么定位?
飞:用top -Hp找到CPU高的线程,再用jstack把线程栈转出来,看十六进制线程ID对应的栈帧。如果看到GC线程,就调JVM;如果是业务线程,就看是不是死循环。很多时候是因为正则回溯、日志打印太多。这个我实际练过一次,但不太熟练。
官:好,今天聊得差不多了。你回去等通知吧。
谢飞机站起来,走出门口,长出一口气:"完了,我还没说我的测试框架是JUnit和Mockito呢......算了,反正也是背的。"
问题答案详解(小白学习版)
下面我们把面试中的问题逐一展开,用更完整的描述讲清楚。每一个问题都对应一个实际业务场景,小白可以对照着查漏补缺。
第一轮:项目架构与基础
1. 项目介绍、负责模块、Java版本、构建工具
业务场景:一个UGC内容社区,用户上传短视频/图文,其他用户浏览、点赞、评论、搜索。这是典型的互联网高并发场景:读多写少、热点集中、内容产生后有审核和索引需求。
技术点:
- Java 8 / 11 / 17:Java 8是长期支持版本,主流企业大量使用;Java 11带来的ZGC(实验性)、
UseContainerSupport(容器内存自动感知)很实用;Java 17是LTS,Spring Boot 3强制要求。 - Maven:依赖管理和构建生命周期,通过
pom.xml统一管理第三方库。Gradle更灵活、构建更快,但Maven更通用。 - Spring Boot + Spring Cloud:微服务基础框架。Spring Boot负责快速构建单服务,Spring Cloud提供注册中心、配置中心、网关、负载均衡等微服务组件。
- ORM:MyBatis适合复杂SQL和动态SQL,Hibernate/JPA适合对象关系映射和标准CRUD。
- 中间件:Redis(缓存)、Kafka(消息队列)、Elasticsearch(搜索)是互联网后端高频组件。
2. 高并发读如何扛?缓存设计
业务场景:用户刷首页视频流和详情页,流量高峰每秒几十万次读。如果全部打到MySQL,数据库会瞬间被击穿。
技术点:
- CDN / OSS:图片、视频等静态资源放对象存储,通过CDN加速,用户就近获取,不经过应用服务器。
- Nginx:反向代理和负载均衡,分发流量到多个后端实例。
- 应用层多级缓存:Caffeine本地缓存(服务进程内,最快) + Redis分布式缓存(多实例共享)。
- 缓存穿透:查询一个一定不存在的key,导致每次都直穿数据库。解决:布隆过滤器(
BloomFilter)先判断key是否存在;也可以将空值短时间缓存。 - 缓存击穿:热点key正好失效,大量请求同时回源。解决:用Redis
setnx做互斥锁,只有一个线程回源,其他线程等待后从缓存读。 - 缓存雪崩:大量key同时失效,请求全部打到数据库。解决:TTL加随机值;使用多级缓存;Redis高可用 + 持久化。
- 数据库主从分离:主库写,从库读;进一步提升读能力。
3. 缓存和数据库一致性
业务场景:用户修改昵称后,立刻读到的可能还是旧昵称,这不可接受。需要保证缓存与MySQL最终一致。
技术点:
- Cache Aside Pattern(旁路缓存模式):
- 读:先读缓存,命中则返回;未命中则读数据库,再回填缓存。
- 写:先更新数据库,再删除缓存。
- 为什么不更新缓存而是删除缓存?因为更新缓存需要重新生成数据,在高并发下容易产生覆盖旧值的问题;删除后下次读自然回填,更简单。
- 删除缓存失败怎么办?引入重试机制(MQ异步重试),或者订阅MySQL Binlog(Canal),数据库变更后自动协调缓存删除。
- 强一致性 vs 最终一致性:互联网业务大多接受最终一致(秒级)。如果要强一致,需要分布式事务或加锁,但会牺牲性能和可用性。
4. Spring Boot自动配置原理
核心 :@SpringBootApplication 是一个组合注解,包含:
@SpringBootConfiguration:标明是配置类。@EnableAutoConfiguration:开启自动配置。@ComponentScan:扫描当前包及其子包,装配Bean。
@EnableAutoConfiguration 通过 AutoConfigurationImportSelector 加载 META-INF/spring.factories(Spring Boot 2.7之前)或 META-INF/spring/...AutoConfiguration.imports(Spring Boot 2.7+)里的自动配置类。
每个自动配置类上都有一组条件注解:
@ConditionalOnClass:classpath中有某个类才生效。@ConditionalOnMissingBean:用户没有自定义某个Bean时才生效。@ConditionalOnProperty:配置项满足条件才生效。
例如 RedisAutoConfiguration,当你引入 spring-boot-starter-data-redis 后,classpath中有了 RedisTemplate 相关类,且你没有自己定义 RedisTemplate,Spring Boot就会自动创建一个。
5. 日志和监控
技术点:
- SLF4J:日志门面(接口),统一不同日志实现。
- Logback:SLF4J的最好实现之一,性能高,支持滚动文件和异步输出。
- ELK Stack:Elasticsearch + Logstash + Kibana,收集、存储和展示日志。
- 链路追踪:微服务跨服务调用时,用
TraceId串联一次完整请求;Jaeger / Zipkin 是主流工具。 - Micrometer:监控门面,像SLF4J一样统一指标输出。
- Prometheus:定时从应用拉取监控指标。
- Grafana:将Prometheus数据可视化,并配置告警规则。
第二轮:内容审核与异步处理
6. 内容审核流程设计
业务场景:UGC内容必须"先审后发",否则会出现违规信息传播。同时AIGC大量生成内容,也需要识别和打标。
流程:
- 用户上传内容 -> 文件存到对象存储(OSS),MySQL插入一条"待审核"记录。
- 发送MQ消息到
content.audittopic,消息体为contentId。 - 审核服务消费消息,执行以下审核:
- 图片/视频:抽帧,调用第三方AI识别(色情、暴恐、政治敏感)、OCR文字识别。
- 音频:转写为文本,再做文本审核。
- 文本:敏感词库(DFA算法)、涉政/广告/引流内容检测。
- 重复内容:对文本提取SimHash、对图片提取pHash,计算相似度,查重。
- AIGC识别:用模型判断内容是否AI生成,并打上"AI生成"标签。
- 审核结果:
- 通过:更新MySQL状态为"已发布",写入ES索引,通知feed服务。
- 疑似违规:进入人工复审队列。
- 确认违规:删除内容,按规则处罚用户。
7. 为什么用消息队列?Kafka如何保证消息不丢失?
消息队列三大作用:
- 异步:用户发布后立即返回"已提交",审核在后台执行。
- 削峰:高峰期的任务先堆积在MQ,消费者按自身能力消费。
- 解耦:内容服务和审核服务互不依赖,通过MQ通信。
Kafka保证不丢消息,需要从三个端分别保证:
- 生产者端:
acks=all(或-1):必须等待所有ISR副本都写入成功。retries设置大于0:网络抖动/Leader选举时自动重试。enable.idempotence=true:开启幂等,避免重试导致消息重复。
- Broker端:
- Topic
replication.factor >= 3,即多个副本。 min.insync.replicas >= 2:至少两个副本写在ISR中才返回成功。- Leader挂了之后,从ISR中选举新Leader,避免消息丢失。
- Topic
- 消费者端:
- 关闭自动提交:
enable.auto.commit=false。 - 业务处理成功后再手动提交offset。
- 消费逻辑必须幂等,防止重复消费。
- 关闭自动提交:
ISR、HW、LEO:
- LEO(Log End Offset):每个分区日志最后一条消息的偏移量。
- HW(High Watermark):高水位,表示可被消费者看见的最大偏移量。
- ISR(In-Sync Replicas):与Leader保持同步的副本集合。HW和LEO的推进涉及副本同步机制,是Kafka可靠性、一致性最核心的细节。
8. 最终一致性、失败重试、重复消费
业务场景:用户发的内容进入审核队列后,审核服务可能宕机、消息可能重复。
技术点:
- 幂等:用
contentId作为Redissetnx的key,处理前先加锁,处理完释放,保证同一个消息只处理一次。 - 本地消息表:业务表和消息表在同一数据库事务中写入,然后定时任务把未发送的消息发给MQ。
- 事务消息:RocketMQ支持半消息(half message),先发送半消息,本地事务成功后再提交确认;Kafka事务API也可以实现,但模型不同。
- 重试机制:消费失败捕获异常,最多重试3次,若仍然失败则写入死信队列(DLQ),由人工或补偿系统处理。
- 分布式事务:内容审核这种场景不需要强事务,最终一致即可。如果跨库/跨服务需要强一致,可用Seata AT模式、TCC、Saga等。
9. Elasticsearch搜索和倒排索引
场景:用户搜索"Java面试",需要毫秒级返回相关视频、文章。
技术点:
- 正排索引:文档ID -> 文档内容。MySQL的
LIKE '%关键词%'就是顺序扫描所有文档。 - 倒排索引:词项 -> 文档ID列表。建立过程:
- 对文档进行分词。
- 过滤停用词、转小写。
- 建立词典和倒排列表。
- 查询时在词典中找到词项,直接获取文档列表。
- ES的文档写入:文档被分词后生成倒排索引;查询时从分片并行检索,汇总后按相关性打分排序(BM25算法)。
- 中文分词:默认分词器对中文不友好,一般集成 IK 分词器。
10. 深分页问题
技术点:
- ES的
from + size深度分页:例如from=1000000, size=10,每个分片都要取前1000010条再排序,协调节点进行全局排序,非常耗CPU和内存。默认超过10000条会报错。scroll:适合全量导出,可以游标方式拉取,但不适合实时用户翻页。search_after:必须指定唯一排序值(如_id+ 时间戳),只能下一页,不能随机跳页;性能稳定。
- MySQL深分页:
LIMIT 100000, 10会扫描前100010行再丢弃,性能很差。- 方式一:延迟关联,先查
id,再回表:SELECT * FROM t INNER JOIN (SELECT id FROM t ORDER BY id LIMIT 100000, 10) tmp ON t.id = tmp.id。 - 方式二:记住上一页最大ID:
WHERE id > last_max_id ORDER BY id LIMIT 10。
- 方式一:延迟关联,先查
第三轮:安全、部署与排障
11. 登录鉴权,JWT和OAuth2的区别
业务场景:用户登录后,需要证明"我是我",并且访问资源时服务端能校验身份。
技术点:
- JWT(JSON Web Token):一种Token格式,由三部分组成:
- Header:算法和类型。
- Payload:携带用户ID、角色、过期时间等非敏感信息。
- Signature:签名,防止篡改。
- JWT特点:服务端无状态,不保存Token;校验签名即可。但无法主动吊销,泄露后到期前都有效,所以需要刷新令牌(refresh_token)。
- OAuth2:一种授权框架,解决"第三方应用如何访问用户数据"的问题。
- 核心角色:资源所有者(用户)、客户端(应用)、授权服务器、资源服务器。
- 常见流程:授权码模式(Code)、客户端凭证模式(Client Credentials)、密码模式(已不推荐)。
- OAuth2提供
access_token和refresh_token,支持撤销,也能限制权限范围。
- 关系:OAuth2是一种框架,JWT是一种Token载体。OAuth2授权服务器可以签发JWT作为
access_token,也可以签发不透明Token(opaque token)。
12. 如何防止接口被刷和恶意攻击
技术点:
- 网络层:
- HTTPS加密传输,防止中间人篡改。
- WAF(Web防火墙)拦截SQL注入、XSS、恶意扫描。
- Nginx限流:
limit_req_zone按IP或用户维度限制QPS。
- 应用层:
- Spring Security:认证 + 授权。
- 参数校验:Bean Validation,防止非法参数。
- 防SQL注入:使用预编译
PreparedStatement,MyBatis中使用#{}而不是${}。 - 防XSS:用户输入转义、富文本内容白名单过滤。
- 防CSRF:敏感请求校验CSRF Token,或使用
SameSiteCookie。 - 接口幂等:前端生成唯一请求ID,后端用Redis
setnx防止重复提交。 - 限流:令牌桶算法(Guava RateLimiter、Resilience4j)或滑动窗口(Redis Lua)。网关层可用Sentinel。
13. Docker/K8s中Java服务的JVM参数设置
部署流程:
- Jenkins / GitLab CI 拉取代码,执行Maven构建,生成jar包。
- 编写Dockerfile,基础镜像使用
eclipse-temurin:17-jre-alpine或openjdk:11-jre-slim。 - 将jar包和JVM启动参数放入镜像。
- 推送镜像到私有仓库,K8s Deployment拉取镜像,设置CPU/内存requests和limits。
- 滚动更新和弹性扩容由K8s管理。
JVM参数注意点:
- Java 8u191之前,JVM不感知容器内存限制;Java 10+默认开启
UseContainerSupport。 - 常见参数:
-Xms4g -Xmx4g:堆大小,通常设为容器内存的75%左右。-XX:+UseG1GC:G1垃圾收集器,适合大堆和低停顿。-XX:MaxGCPauseMillis=200:G1目标停顿时间。-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/dumps:OOM时自动生成堆转储。-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m:元空间限制。-Djava.security.egd=file:/dev/./urandom:加快启动。
- 启动命令:
java $JAVA_OPTS -jar app.jar,JAVA_OPTS通过K8s环境变量传入。
14. 线上OOM排查
排查步骤:
- 如果配置了
-XX:+HeapDumpOnOutOfMemoryError,去HeapDumpPath下找.hprof文件。 - 使用MAT(Memory Analyzer)打开
.hprof:- "Leak Suspects" 一眼看出可疑大对象。
- "Dominator Tree" 查看对象持有关系。
- 如果没有dump,可以手动用
jmap -dump:live,format=b,file=app.hprof <pid>导出(线上慎用,会STW)。 - 分析GC日志:
- Java 8:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log - Java 11+:
-Xlog:gc*:file=gc.log
- Java 8:
- 常见OOM类型:
Java heap space:堆内存不足(大对象、内存泄漏、加载数据过多)。Metaspace:元空间不足,反射/动态类太多。GC overhead limit exceeded:GC几乎一直在工作但收效甚微。unable to create new native thread:线程数达到系统限制,比如无限创建线程。Direct buffer memory:堆外内存NIO buffer未释放。
15. 压测时CPU飙升定位
定位步骤:
top查看哪个进程CPU高,记录PID。top -Hp <pid>查看进程中所有线程,找到CPU占用最高的线程TID。printf "%x\n" <TID>转换成十六进制。jstack <pid> > jstack.log,然后在日志中搜索nid=0x十六进制。- 观察线程栈顶部:
- 如果显示
GC threads/VM Thread,说明GC压力大,需要看GC日志,调整堆大小、GC算法,检查代码中是否频繁创建大对象。 - 如果显示业务代码,比如
dead loop、无限递归、正则回溯、密集日志输出,就优化对应代码。
- 如果显示
- 也可以使用Arthas:
thread -n 3一键查看CPU最忙的线程。
压测时CPU飙升并不可怕,怕的是没有现场分析。所以线上环境务必提前开启JVM自动dump、保留GC日志、接入监控告警。
以上所有问题的答案都是"面试官真正想听到的关键点"。小白学习时不要只背概念,要放到业务场景中去理解。谢飞机虽然背了不少东西,但细节不扎实,所以只能回家等通知。希望正在准备面试的你,能真正把每个技术点吃透,拿下Offer。