谢飞机面试大厂:Spring Boot、JVM、Redis、Kafka、微服务与音视频搜索场景求生实录
谢飞机面试大厂Spring Boot、JVM、Redis、Kafka、微服务与音视频搜索场景求生实录开场某互联网大厂面试间。面试官王工四十岁目光如炬面前摆放着一杯冷掉的咖啡和一台笔记本电脑。他看了一眼简历又看了一眼对面坐着的谢飞机——一个穿着格子衬衫、眼神涣散但透着清澈愚蠢的年轻人。王工“谢飞机同学你好。我们开始吧。”谢飞机“好嘞王工。我准备好了随便问我都能接住。”王工笑了笑那笑容里既有期待也有一丝不祥的预感。第一轮基础热身JVM 与 Spring Boot 的底裤王工敲了敲电脑“先热个身。你简历上写熟悉 Java SE 8/11/17那我问你Java 8 和 Java 11 相比有哪些你实际用过的区别”谢飞机眼睛一亮“这个我会Java 8 有 Lambda 表达式还有 Stream 流集合处理起来特别爽。还有 Optional 防空指针。Java 11 的话……嗯有个 var 类型推断就是局部变量那个但当时项目里怕老代码看不懂基本没用。对了有个 HttpClient 可以同步异步发请求我试过比老 HttpURLConnection 好用。”王工点头“不错能用得上。那 JVM 运行时数据区域有哪些每个区域干什么的”谢飞机“这个也简单。堆存对象方法区存类信息和常量不过 Java 8 好像改成元空间了不再用永久代。虚拟机栈每个线程一个里面都是栈帧装局部变量、操作数栈。本地方法栈给 native 方法用。程序计数器就是记录下一步执行哪条字节码指令的。大概就这些吧。”王工“嗯元空间为什么要改成本地内存”谢飞机愣住挠头“因为……永久代有大小限制容易 OOM改成元空间就……不用管最大限制了反正能省点永久代的空间吧。嗯……具体原因我有点记不清了。”王工没深究换个问题“Spring Boot 的自动配置原理你大概说说。”谢飞机“这个我熟Spring Boot 启动类上有 SpringBootApplication里面组合了 EnableAutoConfiguration。然后会加载 META-INF/spring.factories 里配的自动配置类。这些类上有 ConditionalOnClass、ConditionalOnMissingBean 一堆条件注解比如你引入了 redis 的 client 依赖才会去配置 RedisTemplate。整个过程就是基于条件装配你要是没导入对应依赖它就不会瞎配置。最后配置完还会放到容器里。我当时是这么理解的。”王工难得地露出一丝赞许“理解得不错。那 Maven 里依赖冲突怎么办”谢飞机“依赖冲突太常见了。两个 jar 包里面有同一个类或者版本不一样。我一般用 Maven 依赖树看mvn dependency:tree把重复的找出来然后在 pom 里用 exclusions 排除掉不需要的传递依赖。或者直接在最外层 pom 里定义依赖的 version强制统一版本。但有一次搞了半天最后发现是本地仓库的 jar 坏了clean 一下仓库才好。挺傻的。”王工点点头“第一轮不错看来基础还是有的。我们进入第二轮场景题。”第二轮电商高并发Redis 与 Kafka 的考验王工“假设你在做一个电商秒杀系统。高峰期有大量用户同时抢购一件商品。你怎么用 Redis 防止缓存穿透、击穿和雪崩”谢飞机深吸一口气像是背过武林秘籍“这个我会穿透就是用户疯狂查一个根本不存在的数据每次都要打到数据库。我可以用布隆过滤器把所有可能存在的 key 先放进去查之前先判断如果过滤器说没有那就直接返回不打数据库了。还有一招是即使查到空也把空值缓存起来设一个很短的过期时间。”王工“那击穿呢”谢飞机“击穿就是有一个热点 key突然过期了这时候大量请求同时涌向数据库。解决办法是加互斥锁只有拿到锁的线程可以去查数据库其他线程就等着或者先返回旧值。还有一种方法是把热点 key 的过期时间设长一点或者用逻辑过期。”王工“雪崩”谢飞机“雪崩就是大量 key 同时过期导致一堆请求全部打到数据库。解决办法很简单给过期时间加上一个随机数让过期时间分散开。而且可以做永久 key或者后台线程去更新。”王工“不错。那 Redis 分布式锁呢怎么实现”谢飞机“简单setnx key value设置成功就拿到锁用完删除。为了防止死锁锁要加过期时间。为了防止删错value 要用唯一的随机字符串删除前判断一下是自己的值。还有……如果要实现高可用可以用 Redisson 或者 RedLock。但我没在生产用过 RedLock感觉有点复杂。对了Redis 主从切换的时候锁可能会丢失但我没说……”王工笑了笑“行。那 Kafka 怎么保证消息顺序性”谢飞机“这题我懂一点点。Kafka 一个 topic 可以有多个分区消息在同一个分区内是有序的。要保证全局有序就只用一个分区但这样吞吐量就低了。一般业务场景只需要保证同一个 key 的消息有序比如同一个订单号的消息用 key 做分区器让同一个订单的消息都进同一个分区。这样消费者在分区内按顺序消费基本就满足需求了。”王工“那如果消费者是多线程消费还会乱序怎么办”谢飞机“啊多线程就会把原来的顺序打乱。可以……可以建多个内存队列按 key 哈希到不同队列然后每个队列对应一个单线程线程池。对就这么干。不过我当时没实现过是看博客学的。”王工“你在微服务里咋做服务熔断降级”谢飞机“我用过 Resilience4j在方法上加 CircuitBreaker 注解然后配置熔断阈值。当失败率达到一定比例熔断器打开后续请求直接走 fallback 方法不调用下游服务。还有 RateLimiter 限流Retry 重试。但我就用了最简单的fallback 里返回一个兜底数据比如返回一个空对象或者提示语。不过具体参数怎么写我记不全都是复制同事的……”王工听完若有所思“第二轮也算沾边。最后来一轮综合场景结合音视频和内容社区。”第三轮音视频社区ES 与容器化的终极考验王工“我们做一个视频内容社区用户会上传视频。你要设计一个大文件分片上传和断点续传的方案你会怎么做”谢飞机“这个我熟前端把视频文件切成很多片比如每片 5MB然后一片一片上传到后端。后端每收到一片就保存一块临时文件。全部传完后再把这些分片合并成一个完整文件。断点续传就是前端通过一个接口查询已经上传了哪些分片然后从没传的继续传。合并的时候用 FileInputStream 或者 RandomAccessFile 挨个写。嗯……但最后合并的顺序不能乱每片要带一个序号。”王工“如果分片上传到一半服务器重启了还没合并怎么办”谢飞机“那就……那就得在服务器上把临时分片存到磁盘重启后还能根据 session 或者 uploadId 找到它们。但如果服务器是多实例部署文件存到本地是不是就找不到所以应该把分片上传到 OSS 对象存储比如阿里云 OSS分片上传本身就是断点续传而且不依赖本地磁盘。哎这么一说我想起来了应该用 OSS 的 MultipartUpload而不是自己写……但是如果是公司内部私有云呢我就有点犹豫了。”王工看他开始冒虚汗换了一题“那用户上传的视频需要在社区里搜索标题和简介你会用 Elasticsearch 吗简单说说原理。”谢飞机“会用视频元数据存到 MySQL然后通过同步工具监听 binlog 或者用定时任务扫表把数据同步到 Elasticsearch。搜索的时候直接查 ES不要查 MySQL。ES 核心是倒排索引就是术语到文档的映射。比如用户搜索‘搞笑猫’, ES 会先分词拆成‘搞笑’和‘猫’然后去倒排索引里匹配文档 ID再打分排序。分词器可以用 IK 中文分词比标准分词器更适合中文。”王工“那 ES 的搜索结果默认是按相关度打分这个你会调整吗”谢飞机“评分是 TF-IDF 或者 BM25 算法新版默认 BM25。要调整……可以添加 function score手动加权重新算分。比如视频的浏览量、点赞数作为权重加到分数上。但我当时改过一次经常调不出想要的效果最后还是靠 copy_to 和 keyword 字段硬搞的。哎。”王工“微服务里服务之间是怎么发现彼此的说说过程。”谢飞机“我用过 Spring Cloud 和 Eureka。服务启动的时候向注册中心注册自己把自己的 IP 和端口告诉 Eureka然后周期性地发心跳。调用方从注册中心拿到服务列表缓存到本地。默认的话Ribbon 会轮询选择一个实例来调。不过现在都换 Nacos 或者 Consul 了Consul 我用过一点点有健康检查比 Eureka 更灵活支持配置中心。但 gRPC 和 Thrift 我没实际调过只写过 demo……”王工“最后问一个你用 Docker 部署过 Spring Boot 应用吗写一下 Dockerfile 的大致步骤”谢飞机“写过先 FROM openjdk:11-jre然后把 jar 包 copy 进去再 CMD java -jar app.jar。为了减小镜像有时候用多层构建先 maven 打包第二个 FROM 才把 jar 拷过来。但之前有个坑如果 jar 包很大用 ADD 或 COPY 会触发缓存失效所以一般先复制 pom.xml然后 run mvn dependency:go-offline 把依赖缓存好再复制源码打包。不过我这都是照搞资料来的自己对镜像底层原理不咋通。”王工喝了口冷咖啡站起身伸出手“谢飞机同学今天面试到这里。你的表现非常有特点也展示了一些基础能力但在深度和实际落地方面还需要加强。你先回去等通知吧。”谢飞机也站起身握住王工的手“好的王工那大概什么时候有消息我手机二十四小时开机没接到也会回拨的。”王工抽出手微微一僵“我们会尽快。你回去多看看 JVM 调优和分布式系统的设计祝你顺利。”谢飞机走出面试间顺手把桌上的企业宣传册塞进包里心想“等通知……那就是没戏了吧不过面试官还挺好教我回去看什么。回去赶紧补课”面试问题详解让小白也能看懂的干货第一轮问题详解1. Java 8 与 Java 11 的区别Java 8 引入了 Lambda 表达式、Stream API、Optional、新的日期时间 APIjava.time、接口默认方法、方法引用等。它是后续 Java 版本的基石。Java 11 是 LTS长期支持版本引入了var局部变量类型推断var list new ArrayListString();但只能用于局部变量不能用于成员变量、方法参数或返回值。一套标准化后的 HTTP Client API支持 HTTP/2 和 WebSocket替代了老旧的HttpURLConnection。基于嵌套的访问控制nest-based access controls让嵌套类之间访问私有成员不需要编译器生成合成方法。在集合、Stream 和 Optional 上新增一些方法如String.repeat()、String.strip()、List.of()Java 9 引入等。面试官问“你用过的区别”回答核心目的是体现你对版本演进的关注。仅答出 lambda 和 var 也可以但最好说出 LTS 版本的选择对项目稳定性有影响。2. JVM 运行时数据区域程序计数器每个线程私有存储当前线程执行字节码的行号是唯一不会 OOM 的区域。Java 虚拟机栈线程私有每个方法从调用到执行产生一个栈帧栈帧里包含局部变量表、操作数栈、动态链接、方法返回地址。栈深度不足抛 StackOverflowError。本地方法栈为 native 方法服务在 HotSpot 中和虚拟机栈合并了。堆所有线程共享存放对象实例和数组是 GC 的主要区域。可通过 -Xms / -Xmx 指定大小。方法区存储类信息、常量、静态变量、JIT 编译后的代码。Java 8 之前叫永久代Java 8 之后改为元空间Metaspace使用本地内存默认无上限但可以通过 -XX:MaxMetaspaceSize 限制。元空间替代永久代的原因字符串常量池和类元数据从永久代移到堆/元空间永久代的大小难以预测受类加载数量影响为 FullGC 带来压力改为本地内存可以减少 OOM 概率。3. Spring Boot 自动配置原理核心注解SpringBootApplicationSpringBootConfigurationEnableAutoConfigurationComponentScan。ComponentScan扫描主类所在包及子包的 Component。EnableAutoConfiguration通过AutoConfigurationImportSelector导入自动配置类。自动配置类在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsSpring Boot 2.7 中或META-INF/spring.factories旧版本中列出。每个自动配置类使用Conditional系列注解例如ConditionalOnClass如果没有对应的类则不配置。ConditionalOnMissingBean用户已经定义了自己的 Bean不再自动配置。ConditionalOnProperty配置文件中没有指定值时默认启用。比如RedisAutoConfiguration上有ConditionalOnClass(RedisOperations.class)你导入了 Spring Data Redis 依赖它才会生效并创建RedisTemplate/StringRedisTemplate等 Bean。通过这个机制应用可以“拿来即用”且不会抢走用户自定义 Bean 的优先权。4. Maven 依赖冲突解决冲突原因多个依赖传递引入了同一个类库的不同版本导致编译错误或运行时方法不存在 / NoSuchMethodError。解决办法mvn dependency:tree查看依赖树定位冲突来源。在声明依赖时使用exclusions排除某个传递依赖。在父 pom 中使用dependencyManagement统一版本这样子模块的传递依赖会被强制覆盖到指定版本。如果本地仓库有损坏的 jar可以删除对应目录或使用mvn -U强制更新。对于多模块项目推荐使用 BOMBill of Materials方式管理版本如 Spring Cloud 的spring-cloud-dependencies。第二轮问题详解5. Redis 缓存穿透、击穿、雪崩穿透查询一个不存在的 key缓存无数据导致每次请求都打到数据库甚至被恶意利用打垮数据库。解决方法把空值也缓存起来过期时间设短使用布隆过滤器预先判断 key 是否存在对非法请求做参数校验。击穿一个热点 key 刚好在某个时间点过期恰有大量请求同时访问这个 key全部打到数据库。解决方法加互斥锁setnx key查完数据库后再释放锁其他线程等待重试设置逻辑过期在缓存 value 中写入过期时间后台异步刷新热点 key 不过期由服务端主动更新。雪崩大量 key 在同一时间段集中过期导致大量请求贯穿缓存打到数据库。解决方法给过期时间增加随机数如 0~300 秒让过期时间分散对缓存做永不过期但数据更新时主动刷新使用多级缓存Redis 之上再加一层本地缓存。6. Redis 分布式锁标准命令SET key value NX EX seconds原子性key 通常用业务资源名value 使用唯一随机 ID如 UUID避免释放别人的锁。释放锁时要先比 value 再用 Lua 脚本删除保证原子性防止误删过期后又被其他线程抢到的锁。如果要更可靠可以用 Redisson 的RLock它会自动续期看门狗机制。原理解释Redisson 内部使用 Lua 脚本加锁默认 30 秒锁存活如果业务没执行完会在 30 秒的三分之一处自动续期。RedLock 是 Redis 官方提出的一种跨多实例锁算法要求多个独立的 Redis 节点中大多数都设置成功才算获取锁。但社区对其安全性有争议实际使用较少。注意Redis 主从架构中如果 master 宕机锁数据未同步到 slave锁会丢失。因此很多场景会改用 ZooKeeper / etcd 的强一致性锁。7. Kafka 消息顺序性Kafka 中一个主题Topic可以有多个分区Partition每个分区内部是有严格顺序的但分区之间不保证顺序。全局顺序只使用一个分区比如使用 Partition 1 的主题。但这样把所有消息都放在一个分区里消费并发度受限于单分区吞吐量低不适用于大多数场景。局部有序按业务 key如订单 ID、用户 ID通过 producer 的 hash 分区器把相同 key 的消息发送到同一个分区。这样同一业务实体的消息在分区内被顺序写入。消费端如果单线程消费同一个分区天然就是有序的。如果要用多线程并发处理需要自己设计内存队列将消息按 key 哈希到多个队列每个队列由一个单线程线程池消费这样既保证同一个 key 的顺序又能利用多核。需要注意重试、消费失败后的再次投递、消息积压等都可能打破顺序所以最终往往需要结合业务逻辑判定是否需要严格顺序。8. Resilience4j 熔断降级Resilience4j 是一个轻量级容错库其主要功能包括熔断器CircuitBreaker、限流RateLimiter、重试Retry、隔舱Bulkhead、时间限制TimeLimiter等。熔断器状态关闭CLOSED→ 打开OPEN→ 半开HALF_OPEN→ 关闭。失败率达到阈值如 5 次内失败 50%时熔断器打开后续调用快速失败不请求下游经过一段冷却时间进入半开状态允许少量试探请求成功后恢复关闭。使用方式在 Spring Boot 中引入spring-cloud-starter-circuitbreaker-resilience4j然后写CircuitBreaker(name videoService, fallbackMethod fallback) public Video getVideo(Long id) { ... }fallback 方法需要与原方法参数一致或加一个异常参数。可以结合RateLimiter限制 QPSRetry做临时失败重试Bulkhead限制并发调用线程数。第三轮问题详解9. 大文件分片上传与断点续传分片上传流程前端用File.slice()把文件切成固定大小的分片比如 5MB并为每个分片生成序号。调用后端接口创建上传任务uploadId返回 uploadId 和已上传的分片列表。前端逐个上传分片。每个分片请求携带 uploadId、分片序号、文件内容。后端收到分片后保存到临时目录或对象存储如 OSS 的 multipart 上传。所有分片上传完成后前端调用合并接口后端把分片按序号合并为完整文件同时更新数据库记录。断点续传因为每个分片都是独立上传的前端在异常中断后只需要查询哪些分片已经上传成功然后上传缺失的分片即可。如果使用阿里云 OSS / 腾讯云 COS 对象存储在线分片上传本身就是成熟的解决方案支持的上传任务可以管理并且分片数据存储在后端不占用应用服务器磁盘。自己实现时要注意分片文件后缀名用.part加 uploadId 和序号合并时用支持随机的文件流合并完成后必须清理临时分片上传的分片大小要一致最后一片可以不足。服务器重启恢复如果保存了 uploadId 与分片元数据存于 MySQL/Redis重启后可以根据 uploadId 找到已上传分片继续合并。如果部署在多实例上必须把分片存储到共享文件系统或对象存储不能只依赖本地磁盘。10. Elasticsearch 与倒排索引Elasticsearch 是一个分布式的基于 Lucene 的搜索引擎常用于内容搜索、日志分析、全站检索。倒排索引把每个文档的内容分词建立“词条Term”到“文档 ID 列表”的映射。比如“搞笑猫”被分词为“搞笑”“笑猫”“猫”等词搜索“搞笑”时能迅速找到包含该词的文档。中文分词使用 IK AnalyzerIK 分词器支持自定义词典比默认的 standard 或 cjk 分词更符合中文语义。同步方案常见有 Canal 监听 MySQL binlog 同步到 ES或者使用 Logstash / Flink CDC简单场景也可以定时扫表批量同步。相关度评分Elasticsearch 默认为 BM25 算法它是 TF-IDF 的改进版通过词频、逆文档频率、字段长度归一化来计算分数。可使用function_score查询让业务指标浏览量、点赞数参与加权排序。使用建议搜索接口只查 ES写入落到 MySQL 后再同步高速查询时要设置合理的索引 mapping避免 usetext做精确匹配精确匹配用keyword。11. 微服务注册发现核心组件注册中心Registry存储所有服务的实例信息IP、端口、健康检查地址。服务提供者启动时注册并按心跳/租约周期续约服务消费者从注册中心拉取服务列表并缓存到本地然后根据负载均衡策略选择一个实例发起调用。Eureka经典 Netflix OSS 组件。Client 每 30 秒发心跳若 90 秒没收到心跳则注销实例。Eureka Server 之间相互注册默认有最终一致性。Eureka 服务端本身无明显主从即使失去部分节点仍能提供服务。Consul使用 Raft 协议强一致。支持服务发现、健康检查和 KV 配置中心。Spring Cloud Consul 可以替代 Eureka Config Server。Nacos阿里开源既支持 AP服务发现又支持 CP配置管理功能强大。gRPC 和 Thrift 是 RPC 框架它们有自己的服务注册接口但通常需要结合服务发现组件如 gRPC etcd / Nacos。在 Spring Cloud 中负载均衡器从 Ribbon 演进到 Spring Cloud LoadBalancerOpenFeign 集成 LoadBalancer 后通过服务名即可调用。12. Dockerfile 与 Spring Boot 部署Spring Boot 应用生成的 jar 包是可直接运行的 fat jar。简单 Dockerfile 如下FROM openjdk:11-jre WORKDIR /app COPY target/app.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]为了充分利用 Docker 缓存通常把依赖层和业务层分离先在基础镜像中复制pom.xml执行mvn dependency:go-offline或mvn package -DskipTests用-fl方式预拉依赖。再复制源码并构建。更现代的方式是使用 Google 的 Jib 插件或 Spring Boot 的layertools。spring-boot-maven-plugin支持将 jar 分成dependencies、spring-boot-loader、snapshot-dependencies、application多个层Docker 构建时只需在依赖层未改变时直接复用缓存。注意基础镜像版本使用 JRE 而不是 JDK 可以减小体积也可以考虑eclipse-temurin、azul-zulu等精简镜像或使用 Alpine glibc 的镜像。以上就是谢飞机这次面试的全过程以及完整的知识点解析。希望各位同学能从中既能看到“水货”的欢乐也能看到真正需要掌握的硬核技术。祝大家面试顺利早日拿到大厂 offer而不是只收到一句“回家等通知”。

相关新闻