从Java SE到微服务秒杀:大厂面试官与谢飞机的三轮回合

📅 2026/8/13 7:26:03
从Java SE到微服务秒杀:大厂面试官与谢飞机的三轮回合
从Java SE到微服务秒杀大厂面试官与谢飞机的三轮回合上午十点某互联网大厂会议室。面试官老张翻开简历对面坐着一位头发凌乱、眼神飘忽的程序员谢飞机。桌上摆着两瓶矿泉水空调温度恰到好处但谢飞机的额头还是渗出了细密的汗珠。老张推了推眼镜开口“谢飞机是吧看过你简历Java 后端方向。咱们随便聊聊你不用紧张。”谢飞机搓了搓手“张哥您随意问我要是答不上来那也算正常发挥。”老张嘴角微微上翘“那咱们先来点基础的热身。”第一轮基础热场JVM 与构建工具老张拿起笔在纸上写了个数字“你平时主要用 Java 8 还是 11或者已经用上 17 了说说这几个版本里你觉得最有用的特性。”谢飞机眼睛一亮这个他熟“我用 Java 8 最多不过最近在搞 11。8 里面 Lambda 和 Stream 太好使了写集合过滤简直爽歪歪。11 的话…有个 var 关键字哦对还有那个 HttpClient发 HTTP 请求不用再引 Apache 了。”老张点了点头“不错。那如果让你用一个项目同时给 Java 8 和 17 编译Maven 里怎么配置”谢飞机有点懵“同时那就…配两个 profile或者用 parent 的 java.version我一般都只配一个版本没玩过双的。”老张没有追问继续说“行。那 Maven 和 Gradle 你更常用哪个它们的依赖管理有什么区别”谢飞机搓了搓下巴“Maven 用的多pom.xml 写起来啰嗦但清楚。Gradle 也用build.gradle 简洁用 Groovy 写感觉执行快一点。区别…Maven 是 XMLGradle 是编程灵活还能自定义 task。”老张赞许地“嗯”了一声“Spring Boot 启动类上那些注解SpringBootApplication 到底干了什么”谢飞机松了口气“组合注解有 Configuration、EnableAutoConfiguration、ComponentScan。就是让 Spring 自动扫 bean自动配置不用自己写一堆 XML。”“很好。”老张笑着说“那 JVM 一次方法调用的内存分配过程大概是什么样的栈帧里都有什么”谢飞机眼神开始涣散“这个…就是栈啊堆啊。方法调用就压栈返回就弹栈。栈帧里有局部变量表操作数栈还有…那个…动态链接方法返回地址反正在 JVM 里转一圈就完事了。”老张听出了他的模糊但没深究低头在简历上画了个勾“基本功还行。接着来第二轮吧。”第二轮中间件与数据一致性缓存、消息和数据库老张把简历翻到项目经验“你上家公司做过电商订单系统对吧如果用户下一单扣库存和生成订单这两个操作你怎么保证一致性”谢飞机来了精神“用事务Transactional 一加数据库层面锁住天然原子性。”“如果订单服务库存服务是拆开的两个微服务呢”谢飞机挠了挠头“那…那就不能用本地事务了。得用分布式事务吧比如 Seata但 Seata 我没真正用过… 我可以搞个本地消息表或者发 MQ 消息让库存服务消费。”老张追问“那如果 Redis 里缓存了库存数量用户秒杀的时候大量请求打到 Redis缓存没命中全去数据库会发生什么”谢飞机“这题我知道缓存穿透可以用布隆过滤器或者缓存空值。”老张“那如果缓存命中了但库存只剩 1 个1000 个请求同时下单每个都读到库存是 1都能扣成功吗”谢飞机“啊这就是超卖问题。得用 Redis 的 Lua 脚本原子扣减或者加分布式锁。反正不能让并发同时扣。”老张目光如炬“如果 Redis 扣减成功了但数据库事务提交失败Redis 库存已经被扣了怎么办”谢飞机额头上又冒汗了“这个…好像要保证最终一致性。可以用 mq 发消息如果数据库失败就发消息补偿回滚 Redis。但这个过程我也说不细反正就是发消息消费者再改回来。”老张微微摇头“消息队列如果本身丢消息呢你 Kafka 怎么保证消息不丢”谢飞机“配置 acksall生产者重试消费者手动提交 offset大概这样吧。具体要不要开幂等我也记不清了反正都会配。”老张叹了口气“那你说说RabbitMQ 和 Kafka 分别适合什么场景”谢飞机“Kafka 处理高吞吐适合日志和削峰。RabbitMQ 是消息路由灵活适合业务消息。对了 Redis 的 pub/sub 也可以做消息但是不持久化挂了就没了。”老张记了几笔“行你虽然有些地方含糊但思路是有的。最后一轮聊点系统的设计题。”第三轮系统设计与云原生秒杀、JVM 调优与 CI/CD老张喝了一口水“假设我们要做一个秒杀系统从用户点击到看到结果你整体怎么设计只用一句话概括核心流程。”谢飞机抢答“先把请求挡住限流然后一步步放过去用消息队列排队Instant 秒杀。”“好。”老张点头“那用户请求先到哪一层”谢飞机“先到 Nginx然后到网关网关做限流。再进业务服务业务服务先查 RedisRedis 标记还有库存就生成一条消息丢给 Kafka然后马上返回‘排队中’。后面消费者慢慢处理订单。”老张问“如果用户要查秒杀结果呢订单状态放哪”谢飞机“订单状态可以在 Redis 存一个 key处理完给用户设成‘成功’客户端定时轮询查一下。”老张“那如果秒杀结束后要统计订单数据把数据导给大数据部门用 Flink 还是 Spark”谢飞机“呃…Flink 是流处理可以实时消费 Kafka。Spark 是批处理跑离线。一般用 Flink 做实时大屏Spark 做 T1 报表。”老张“那这个秒杀系统部署到 Kubernetes 上需要给 JVM 容器设置哪些参数特别是堆内存和 CPU 限制”谢飞机“啊”了一声“这个我熟容器里得用 -XX:MaxRAMPercentage 和 -XX:InitialRAMPercentage让 JVM 根据容器内存自动调整堆大小。不能用 -Xmx 写死因为容器限制了写死会 OOM。还要开个优雅停机配 readinessProbe 和 livenessProbe。”老张眼中闪过一丝光“不错。那如果系统线上出现了 CPU 飙升你会怎么排查从命令到分析说说思路。”谢飞机擦了擦汗“先用 top -Hp 找到线程 ID然后 jstack 把线程栈打出来找到对应 pid 的 nid 看堆栈找业务代码比如死循环或者 GC 线程。如果是 GC 问题再用 jstat 看 GC 情况最后用 MAT 分析 dump 文件。”老张笑了“虽然你有的问题答得跟浆糊一样但到了最后居然还能挤出几个词。我等会儿还有下一个面试者你先回去等通知吧。”谢飞机站起来一把握住老张的手“张哥你意思是我还有戏”老张抽回手“我是说回家等通知。”谢飞机走到门口回头“张哥我是不是没戏了”老张“你猜。”门关上老张看着简历摇了摇头又忍不住笑了笑“这孩子菜得真有意思。”附三轮面试题目的详细答案与业务场景拆解这篇文章不但给你一个乐子更重要的是让新手看完能学到东西。我们按面试官的提问逻辑把每道题背后的场景和技术点掰开揉碎讲清楚。第一轮答案Java 版本、Maven/Gradle、Spring Boot 与 JVM 栈帧1. Java 8 / 11 / 17 的特性区别Java 8引入 Lambda 表达式、Stream API、Optional、新的日期时间 APIjava.time以及接口默认方法。这是目前企业中最普及的版本很多老项目都停留在这里。Java 11在 8 基础上增加了var局部变量类型推断Java 10 就引入了标准化了 HttpClient。注意var只能用于局部变量不能用于成员变量和方法参数。另外Java 11 是 LTS长期支持版本Oracle 后来收费所以很多公司改用 OpenJDK。Java 17是一个重要的 LTS带来密封类sealed class、模式匹配 switch 预览、外键 API 等。17 之后Spring Boot 3.x 框架开始原生要求 Java 17 起步。面试时如果答不出太多先把 Lambda、Stream、var 说出来它们才是日常高频点最后提一句“每个 LTS 版本都有一些新特性我平时重点关注跟业务相关的”。千万不要不懂装懂。2. 同时为 Java 8 和 Java 17 编译的 Maven 配置真实场景有些老版本 JDK 编译出来的字节码在低版本 JDK 上运行需要指定release或source/target。Maven 中可以使用maven-compiler-plugin并通过 Maven Profile 来按环境激活profiles profile idjava8/id properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties /profile profile idjava17/id properties maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties /profile /profiles还可以用--release参数替代-source/-target因为--release不仅能控制语法版本还会限制 API 只能使用该版本的标准库。例如plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration release11/release /configuration /plugin面试时能答出“用 profile 区分环境 用 release 参数”已经加分了。3. Maven vs Gradle 依赖管理Maven基于 XML依赖坐标由 groupId、artifactId、version 组成。依赖传递采用“最近优先声明优先”有冲突时可以通过exclusions排除或使用 dependencyManagement 统一版本。Maven 生命周期固定clean, compile, test, package, install, deploy插件系统强大。Gradle基于 Groovy/Kotlin 构建脚本提供更大的灵活。增量构建快支持自定义 task。依赖配置用implementation、api、compileOnly等。Gradle 也会从 Maven Central 或 JCenter 拉包。大厂很多新项目会用 Gradle老项目用 Maven。面试时不需要说谁好谁坏重点说“Maven 重约定Gradle 重灵活”。4. SpringBootApplication 组合注解干了什么SpringBootApplication实际上等于SpringBootConfiguration // 标识这是一个 Spring Boot 配置类 EnableAutoConfiguration // 开启自动装配根据 classpath 依赖自动配置 Bean ComponentScan // 扫描当前包及其子包下的 Component、Service、Repository、Controller 等其中最关键的是EnableAutoConfiguration。它通过导入AutoConfigurationImportSelector读取META-INF/spring.factories旧版或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports新版中配置的自动配置类按条件装配ConditionalOnClass、ConditionalOnMissingBean 等决定是否需要创建对应 Bean。面试容易踩坑说成“启动流程”或“嵌套注解”。要明确它是“组合注解自动装配”。5. JVM 方法调用的内存过程 与 栈帧每个线程有自己的虚拟机栈每调用一个方法就压入一个栈帧。栈帧包含局部变量表存放方法参数和局部变量槽位slot可以复用。操作数栈存放中间计算过程和调用指令的操作数。动态链接指向运行时常量池中该方法的引用支持多态。方法返回地址方法正常退出或异常退出后回到调用位置所需的信息。方法执行完栈帧出栈局部变量引用置空对象若没有被其他地方引用之后由 GC 自动回收。高级面试还会问“方法内联”“逃逸分析”和“栈上分配”。如果你回答到“JVM 在 JIT 编译阶段会对热点代码做逃逸分析变量不逃逸可以栈上分配”面试官会眼前一亮。谢飞机只答了基本盘谈不上深入但没全错。第二轮答案分布式事务、Redis 缓存与消息可靠性1. 订单与库存的一致性为什么不能只用本地事务在单体应用里订单表和库存表在同一个数据库你可以用Transactional锁住要么全成功要么全回滚。但是在微服务架构下订单服务和库存服务往往独立部署、独立数据库本地事务管不住另一台机器的数据库。这时候需要分布式事务。常见的分布式事务方案XA/2PC两阶段提交数据库原生支持但性能差锁时间长。TCCTry-Confirm-Cancel业务层面拆分性能好但侵入性强实现复杂。本地消息表消息最终一致性在业务数据库里加一张消息表写业务和写消息在同一本地事务再通过定时任务/MQ异步通知下游。事务消息RocketMQ 支持Kafka 的 EOS 也有一定能力先发 half message执行完本地事务后 commit 或 rollback消费者端保证幂等。Seata AT 模式一种自动补偿型分布式事务框架通过全局锁和日志表实现对业务代码侵入较小。面试时谢飞机说“Transactional 加锁”只能算单体方案后来补了“MQ 本地消息表”就对了。这个问题就是希望你能说出“本地事务解决不了跨库问题必须要最终一致性”。2. 缓存穿透、击穿、雪崩缓存穿透查询一个不存在的 key缓存没有数据库没有。每次请求都打到 DB。处理办法缓存空值设置短过期时间、布隆过滤器Bloom filter拦截无效 key。缓存击穿一个热点 key 在缓存失效瞬间大量并发打到数据库。处理办法互斥锁分布式锁、逻辑过期不设置物理过期后台异步刷新。缓存雪崩大量 key 同时过期或 Redis 宕机导致请求直达 DB。处理办法过期时间加随机值Redis 高可用主从哨兵集群本地限流/熔断。谢飞机把“穿透”和“击穿”有点混。面试官故意问“缓存没命中”其实是引导但谢飞机没区分。真正的标准答案必须把这三个词说清楚。3. 秒杀超卖问题Redis Lua 原子扣减假设库存只有 11000 个并发请求。如果每个进程都是“读 Redis 库存 - 判断 0 - 减一”那么就可能出现 A 线程一读是 1B 线程一读也是 1然后都减成 0结果卖出 2 件。正确做法使用 Redis 的Lua 脚本在脚本里检查库存大于 0 再 decr这一步是原子性的。也可以使用DECR命令本身它会返回扣减后的值如果小于 0 就逆操作加回去但 Lua 更灵活。如果跨进程还要保护数据库常用分布式锁Redis setnx Redisson或乐观锁数据库 update 时where stock 0。SQL 示例update stock set num num - 1 where id ? and num 0。如果影响行数为 1 才扣成功天然避免超卖。4. 数据库事务失败但 Redis 已扣减如何补偿这属于跨缓存和数据一致性问题。推荐方案在 Redis 扣减的 Lua 脚本里同时记录一条“扣减记录”比如用 Redis List 或 Hash 存操作痕迹。下单时先发 MQ 消息或写本地消息表之后消费者执行数据库事务。如果数据库事务失败消费者发送补偿消息把 Redis 的库存加回来要使用 Lua 脚本保证原子加。最终一致性通过 MQ 事务消息或本地消息表重试来保证。更优雅的做法是“把 Redis 当作热点窗口真正的扣减还是以数据库为准”Redis 只是为了挡流量数据库才是最终状态。5. Kafka 消息不丢三大环节Kafka 生产到消费不丢消息需要从三个环节保证生产者端设置acksall即要求 ISR 中所有副本都确认收到才算成功开启重试retries0幂等enable.idempotencetrue防止重试导致重复。Broker 端设置min.insync.replicas2确保至少两个副本在线不需要关注消费者是否消费但消息默认保留 7 天。消费者端关闭自动提交enable.auto.commitfalse业务处理成功后手动提交 offset消费处理要幂等。谢飞机说“消费者手动提交 offset”这半句是对的但“生产端 acksall”和“幂等”没全说出来。面试官特别看重 Kafka 的生产者配置这是大数据量场景下的必考项。6. RabbitMQ vs KafkaRabbitMQ基于 AMQP支持复杂的路由模式direct、topic、fanout、headers提供消息确认和重试机制对低延迟和灵活路由友好。适合订单通知、任务分发等业务交互。Kafka一个分布式流平台基于分区和日志吞吐量极高支持消息回放适合日志收集、用户行为埋点、秒杀削峰、实时流处理。Kafka 不擅长复杂路由也不支持每消息 TTL 和优先级。Redis Pub/Sub轻量级广播但消息不持久化发送者不知道消费者是否收到适合集群内广播通知不适合作为可靠消息队列。秒杀场景选 Kafka因为吞吐高、削峰强订单状态通知选 RabbitMQ因为路由灵活。第三轮答案秒杀整体架构、K8s 下 JVM 调优、线上 CPU 飙升排查1. 秒杀系统整体设计思路一个典型的秒杀流程简化版前端/接入层Nginx 做静态资源分发和 IP 限流。网关层Spring Cloud Gateway 做全局限流Sentinel / Resilience4j防止突发流量打崩后端。应用层业务服务先检查 Redis 中的库存预占标记如果库存足够则用 Lua 脚本扣减 Redis 库存并生成一条唯一订单消息发送到 Kafka。异步消费层消费者从 Kafka 拉取消息串行或批量处理后生成数据库订单扣减数据库库存更新 Redis 状态为“秒杀成功”。结果查询客户端轮询查询 Redis 中的秒杀状态或查订单表。注意轮询频率要低最好让前端用 WebSocket 推送。监控层Prometheus Grafana 监控 QPS、RT、Redis 内存、Kafka 堆积等。核心要点先限流再削峰再异步化最后最终一致。2. 为什么用 Flink 又用 SparkFlink真正的流处理毫秒级延迟支持事件时间、watermark、精确一次语义。当秒杀订单产生后可以直接从 Kafka 消费实时订单流计算累计 GMV、实时销量喂给大屏。Spark主要是批处理适合 T1 报表如昨天所有订单的销售额、地域分布。Spark Streaming 虽能处理流但本质是微批次延迟不如 Flink 低。面试出现“大数据”关键词时记住一个万能句式“实时用 Flink离线用 Spark中间用 Kafka 桥接”。3. 容器化部署 JVM 参数怎么设置在 Kubernetes 里每个容器有 CPU 和内存 limits。如果 JVM 使用-Xmx固定堆大小容易造成两个问题设置太大容器内存如果小于堆 元空间 线程栈JVM 直接 OOM。设置太小浪费容器资源。正确姿势是使用adaptive heap sizing-XX:InitialRAMPercentage50 -XX:MaxRAMPercentage75 -XX:MinRAMPercentage50这样 JVM 会自动根据容器可用内存计算堆大小。注意 JDK 8u191 之后才支持对容器内存的完整感知。还有这些参数-XX:UseG1GC默认 GC适合大堆。-XX:MaxMetaspaceSize限制元空间。-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/tmp/dump.hprof崩溃时留下现场。配合 K8s 设置 readinessProbe 和 livenessProbe实现优雅上线和自动重启。谢飞机答出了 RAMPercentage说明他是真见过不只是背概念。4. 线上 CPU 飙升的排查步骤这是大厂高频实战题。完整流程如下top看系统负载记住 CPU 高进程 PID。top -Hp pid查看该进程内哪个线程占用 CPU 最高拿到线程 ID十进制。把线程 ID 转 16 进制printf %x\n tid。jstack pid stack.txt然后在文件里搜nid0x十六进制看线程栈。看是用户业务线程还是 GC 线程如果是 GC 线程使用jstat -gcutil pid 1000看 GC 频率可能是堆满或内存泄漏。如果是业务线程定位到具体代码通常是死循环、读大文件、锁竞争、正则回溯等。如果是内存问题执行jmap -dump:formatb,fileheap.hprof pid导出堆再用 Eclipse MAT 或 JProfiler 分析最重的对象。如果 CPU 过高是频繁 Full GC优先考虑调整堆大小或定位大对象持有引用。这条链路包含top / jstack / jstat / jmap / MAT是每个 Java 后端必须刻在脑子里的。谢飞机虽然很多地方“含糊其辞”但每个问题他都能沾上边尤其最后说对 RAMPercentage 和 jstack 排查说明他有实践底子只是缺乏系统性和深入性。这种候选人如果遇到一个愿意带的团队其实能很快进步。你学会了多少如果让你去面试能不能比谢飞机答得更硬气记住面试不是背八股而是把技术点串成一条解决问题的业务主链。希望这篇文章能帮你理清思路下回面试别像谢飞机一样只靠“那个…那个”混过去。要真到了大厂门口你得明白一点回家等通知不可怕可怕的是等通知的时候连 Redis 和 Kafka 都没分清楚。愿你成为下一个认真准备、系统归纳的“谢飞机”但别真的只会说“大概、应该、好像”。从今天开始把 JVM、缓存、消息、微服务和容器化全部链接起来你也能稳稳接住面试官的每一轮提问。