大厂JAVA面试实录:Spring Boot、Redis缓存、Kafka异步与JVM调优(电商大促场景下的谢飞机)
2026/8/9 16:09:43 网站建设 项目流程

大厂JAVA面试实录:Spring Boot、Redis缓存、Kafka异步与JVM调优(电商大促场景下的谢飞机)

早晨十点,某互联网大厂会议室。面试官王工西装革履,眉头紧锁。对面坐着一位头顶呆毛、背着双肩包的年轻人,简历上写着“谢飞机,三年Java开发经验”。王工扫了一眼简历,又看了一眼谢飞机,开口了。

“谢飞机是吧?今天咱们聊点实际的。假如我们电商平台正在备战双十一,你负责订单服务,请开始吧。”

第一轮:基础与框架

王工:先来个简单的。你平时写Java 8的Stream和Lambda多吗?如果要过滤出订单列表中金额大于100元的有效订单,代码怎么写?底层原理知道多少?

谢飞机:这个简单!用filter加Lambda,比如list.stream().filter(o -> o.getAmount() > 100 && o.getStatus() == 1).collect(Collectors.toList())。Stream底层就是管道,中间操作是惰性的,遇到终止操作才一起执行。呃……底层Spliterator划分数据,可能有并行流,但我没用过并行。

王工:不错,基本功还算扎实。那我再问问,Spring Boot为什么我们引入一个spring-boot-starter-web,就能直接写Controller?它那个“自动配置”到底是什么?

谢飞机:哦!是@EnableAutoConfiguration,里面有个@Import,会加载META-INF/spring.factories里的配置类。比如WebMvcAutoConfiguration……然后条件注解@ConditionalOnClass判断类路径有没有对应class,有就装配……具体我也忘了一些。

王工:答案方向对了。那换个偏底层的:JVM内存区域有哪些?大促时高并发短请求,你会关注哪块内存?StackOverflowError什么时候发生?

谢飞机:内存区域……堆、栈、方法区……Java 8里是元空间。大促应该关注堆吧?还有栈。StackOverflowError是递归太深,把栈撑爆了。方法区存类信息,也可能OOM。反正……调优就是加堆内存?通常-Xms-Xmx设置一样,避免动态扩缩。其他我都是百度。

王工:好,再谈谈ORM。你项目里用过Hibernate和MyBatis吗?它们到底有啥区别?如果订单表有几十个字段,你会怎么选?

谢飞机:都用过。Hibernate是全自动的,MyBatis半自动。Hibernate可以自动建表、自动映射,但复杂SQL不好控制,MyBatis要自己写SQL,灵活。订单表字段多的话我选MyBatis,因为可以写动态SQL,避免查出所有字段。Hibernate如果映射不对,容易有N+1查询问题。呃……具体N+1怎么解决?用@EntityGraph?还是join fetch?我不太确定。

王工:行,第一轮先到这。你有一些概念,但深度还不够。我们继续。

第二轮:缓存、消息与服务容错

王工:双十一首页有热点商品,流量巨大,你会怎么设计Redis缓存?如果某个热点key突然失效,刚好有十万请求打到数据库,怎么办?

谢飞机:这个我知道!防止缓存穿透:缓存空值;防止雪崩:过期时间加随机值;防止击穿:加锁。比如用setnx,没有获取到锁的就等待。不过……如果锁的粒度是key,那其他请求会阻塞,吞吐量会低。也可以用Redisson的看门狗自动续期。嗯……我实习时写过,但具体代码记不清了。

王工:那你再说说,下单之后,我们要发消息给库存系统减库存,用的Kafka。怎么保证消息不丢?消费者怎么保证幂等?

谢飞机:生产者设置acks=all,这样Leader和ISR都确认;消费者关掉自动提交,处理成功后再commitSync。幂等的话,给每条消息一个唯一ID,消费前查Redis,如果有就不处理。或者用数据库的唯一索引。嗯,差不多是这样。

王工:那再问你一个常见的:Redis和数据库的一致性怎么保证?你之前说“先删缓存再更新数据库”,那如果数据库更新失败了,缓存又已经删了,下次读会把旧数据写回缓存,这不就不一致了吗?

谢飞机:啊?这个……一般不是先更新数据库再删缓存吗?但更新数据库后还没删缓存的时候,有线程读到了旧缓存,会不一致。最稳妥的是用“延迟双删”:先删缓存,更新数据库,休眠几百毫秒再删一次。但这样要维护休眠时间,分布式环境下不好搞。实在不行就订阅Binlog,通过Canal异步删缓存。我……我就是这么糊弄的。

王工:那微服务之间,订单服务调用用户服务,如果用户服务挂了,你怎么做熔断降级?OpenFeign和Resilience4j用过吗?

谢飞机:用过OpenFeign,接口上加@FeignClient,然后定义fallback类。熔断用Resilience4j,加@CircuitBreaker,还有限流@RateLimiter、重试@Retry。配置可以设置失败率阈值、滑动窗口大小。但是……我发现配置多了容易乱,有时候不知道是超时还是熔断把请求拦了。

王工:第二轮结束。你的回答都是“框架关键词”级别的,没有真正落地过。最后一个问题,别紧张。

第三轮:分布式事务、容器化与测试

王工:订单服务要扣库存,如果库存服务是另一个团队负责,你怎么保证分布式环境下两边数据一致?你了解Seata或者消息最终一致性吗?

谢飞机:这个我知道一点:可以用TCC,Try阶段预留资源,Confirm阶段提交,Cancel阶段回滚。但TCC要自己写三个方法,很麻烦。也可以用消息事务,比如RocketMQ的事务消息。如果是Seata的AT模式,它是通过代理数据源,自动生成undolog,提交时注册分支事务,全局事务提交时删undolog,回滚时用undolog反向补偿。但是!AT模式有脏读问题,而且性能损耗大。我这边……之前只是看过文档,没写过。

王工:那你们的服务怎么部署到Kubernetes上的?K8s如何判断你的订单服务是否健康?如何做到发布时请求不中断?

谢飞机:我们写一个Deployment的yaml,里面包含了一个Pod副本……health那边用livenessProbe和readinessProbe,配置了HTTP探针,路径是/actuator/health。滚动更新时,用strategy.rollingUpdate,设置maxUnavailablemaxSurge。优雅停机的话,在Spring Boot里要配置优雅停机,K8s的terminationGracePeriodSeconds要大于服务处理完旧请求的时间。嗯……我大概知道概念,配置是运维写的。

王工:最后一个,基础中的基础。写单元测试怎么用Mockito模拟外部依赖?JUnit 5和Mockito的常用注解有哪些?

谢飞机:这个我会!@ExtendWith(MockitoExtension.class),然后用@Mock模拟外部接口,@InjectMocks注入到被测类。比如模拟OrderMapper返回一个对象,用when(mapper.findById(1L)).thenReturn(order),再用verify验证有没有调用。JUnit 5还有@BeforeEach@AfterEach,参数化测试@ParameterizedTest。这个我还是有自信的。

王工:好的,我大概了解了。你先回去等通知吧。

谢飞机:啊?那我……我想知道哪里还要再补补?

王工:基础概念都沾边,但每个都差点火候。回去把Spring Boot自动配置源码、Redis分布式锁的细节、Kafka消息可靠性方案、Seata的事务流程好好看看,最好手写一遍。有消息会通知你的。

谢飞机:好的,谢谢王工,我会好好学的!


面试问题详解:从入门到进阶的业务场景与技术点

为了让刚入门的朋友也能看懂,下面把这场面试涉及的每一个问题,结合电商业务场景,给出完整的答案。

1. Java 8 Stream与Lambda:过滤订单列表

业务场景:在订单管理后台,需要快速筛选出“已支付”(status=1)且金额大于100元的订单,并交给前端展示。

技术点

  • Lambda表达式:本质是函数式接口的匿名实现。例如Predicate<Order> predicate = o -> o.getAmount() > 100 && o.getStatus() == 1;
  • Stream API:提供“数据流”式抽象。常用操作有filter(过滤)、map(转换)、collect(收集)、sorted(排序)。
  • 执行方式:
    • list.stream()创建串行流。
    • list.parallelStream()创建并行流,底层使用ForkJoinPool,数据量大时能提升性能,但有线程安全问题。
  • 底层原理:Stream是一个管道流水线,中间操作(如filtermap)是惰性的,只有遇到终止操作(如collectforEach)才会触发遍历。Spliterator负责分割数据源,可以感知数据特征(是否有序、是否去重等)以优化遍历。
  • 注意:不要过度使用并行流。在共享可变变量时,并行流会带来并发问题。

2. Spring Boot自动配置原理

业务场景:本项目中引入spring-boot-starter-web后,就能直接使用@RestController处理HTTP请求,无需手动配置DispatcherServlet。

技术点

  • @SpringBootApplication是一个组合注解,包含@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan
  • @EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class),读取META-INF/spring.factories(Spring Boot 2.7前)中的EnableAutoConfiguration属性,把自动配置类加载到Spring容器。
  • 条件注解:每个自动配置类都有@ConditionalOnClass@ConditionalOnMissingBean等条件,只有满足条件才生效。例如WebMvcAutoConfiguration会在classpath存在ServletDispatcherServlet时创建相关的Bean。
  • 用户自定义Bean优先级更高,因为@ConditionalOnMissingBean保证了当用户已定义同类型Bean时,自动配置不再重复装配。

3. JVM内存区域与调优

业务场景:双十一期间,订单服务每秒产生大量短生命周期对象(如订单DTO、HTTP请求响应对象),需要关注JVM的内存分配和回收。

技术点

  • JVM内存区域:线程私有的有虚拟机栈、本地方法栈、程序计数器;线程共享的有堆、方法区(Java 8中是元空间Metaspace)。
    • 堆:存放对象实例,是GC主要区域。新生代(Eden、Survivor0、Survivor1)和老年代。
    • 虚拟机栈:存放栈帧,栈帧里有局部变量表、操作数栈、动态链接、方法返回地址。栈深不够时抛StackOverflowError
    • 元空间:存储类元数据、方法信息、常量池等。默认使用本地内存,可能导致物理内存耗尽。
  • 大促调优关注点:
    • 堆大小:-Xms-Xmx设置为相同值,避免运行时动态扩容导致性能抖动。
    • 新生代大小:-Xmn-XX:NewRatio。如果短生命周期对象多,可适当增大新生代,减少老年代垃圾回收压力。
    • 元空间:-XX:MaxMetaspaceSize限制大小,防止滥用动态代理、反射产生大量类。
  • 常用调优工具:jstatjmapjstackJConsoleArthas

4. Hibernate与MyBatis的区别

业务场景:订单表有几十个字段,且查询维度复杂(订单号、用户ID、时间范围、状态),需要选择合适的ORM。

技术点

  • Hibernate:全自动ORM框架。通过注解或XML映射,将实体类与表关联。提供HQL/Criteria查询,能自动生成SQL。适合业务模型清晰、增删改查标准化的系统。但复杂SQL不透明,容易产生N+1查询问题(默认懒加载,访问子集合时发出额外SQL)。
  • MyBatis:半自动ORM框架。SQL由开发者编写,灵活可控,支持动态SQL(<if><foreach><choose>)。适合表结构复杂、需要深度优化SQL的场景。但需要手写大量SQL,维护成本高。
  • 选型建议:订单表字段多、变化多,优先MyBatis。若团队习惯JPA/Hibernate,可使用Spring Data JPA配合@EntityGraph或QueryDSL,但依然要小心N+1。
  • 解决N+1:在Hibernate中使用join fetch@EntityGraph,在MyBatis中使用collection的嵌套结果映射到关联查询。

5. Redis缓存击穿、穿透、雪崩

业务场景:首页热点商品详情,突然被大量请求访问。

技术点

  • 缓存穿透:查询的数据在数据库和缓存中都不存在,导致每次请求都打到数据库。
    • 解决:缓存空对象(设置较短的过期时间),或使用布隆过滤器(Bloom Filter)在缓存前快速判断key是否存在。
  • 缓存击穿:热点key在过期的一瞬间,大量请求发现缓存失效,全部去访问数据库。
    • 解决:互斥锁(分布式锁)。用SET NX EX或Redisson加锁,拿到锁的线程查询数据库并回写缓存,其他线程等待后重试。热点key可设置逻辑过期时间,后台异步刷新。
  • 缓存雪崩:大量key同时失效,或Redis宕机,导致请求打到数据库。
    • 解决:过期时间加随机值(如300秒+随机0~60秒),避免同时失效;做主从高可用与哨兵/集群;本地缓存(Caffeine)做二级缓存,减少对Redis的压力。
  • 注意:分布式锁实现要关注锁的粒度、持有时间、续期、释放安全性。建议使用Redisson的RLock,它自带看门狗自动续期。

6. Kafka消息不丢失与消费幂等

业务场景:用户下单后,订单服务发送“订单已创建”消息给库存服务,库存服务扣减库存。要求消息不能丢,且重复消息不能导致重复扣减。

技术点

  • Kafka生产者:
    • 设置acks=all(或-1),要求所有ISR副本确认。
    • 设置retries>0enable.idempotence=true(幂等生产者),避免网络重试导致消息重复。
  • Kafka服务端:min.insync.replicas必须大于1,且replication.factor为3,保证分区Leader挂掉时副本可用。
  • Kafka消费者:
    • 关闭自动提交:enable.auto.commit=false,使用手动提交。
    • 在业务处理完成后再commitSync(同步提交)或commitAsync配合回调,防止“已提交但未处理”导致消息丢失。
    • 消费端处理逻辑要幂等。常用方法:
      • 用全局唯一业务ID(如订单号+商品ID)作为Redis的key,消费时使用SETNX,只有首次插入成功才执行扣库存。
      • 数据库表加唯一约束,重复插入触发异常后不处理。
      • 利用状态机,只有“待扣减”状态才能执行扣减。

7. Redis缓存与数据库一致性

业务场景:商品库存信息需要同步到Redis,但数据库更新时缓存如何联动?

技术点

  • 常用策略:先更新数据库,再删除缓存。这是最推荐的方式,因为“数据库更新成功”是事实,直接删掉缓存,下次读取时重新加载新值即可。
  • 为什么不用“先删缓存再更新数据库”?因为删缓存与更新数据库是两个操作,可能中间失败,不是原子操作。而且数据库更新前,某个线程把旧值读回缓存,导致新值被覆盖。
  • 延迟双删:先删缓存,再更新数据库,隔几百毫秒再删一次缓存。可以缓解“读旧值回写”问题,但仍依赖时间差,且会影响性能。
  • 最终一致性的可靠方案:
    • 监听数据库Binlog,使用Canal解析出变更事件,异步删除对应的缓存。这样即使代码里删除失败,也能通过MQ补偿重试。
    • 使用本地事务+消息表:在同一个事务里更新数据库并写“删除缓存”事件,通过可靠消息(如RocketMQ事务消息)通知消费者删缓存。
  • 注意:严格强一致性很难做到,一般业务只要求最终一致性。热点数据可使用Caffeine本地缓存进一步降低穿透。

8. OpenFeign与Resilience4j实现服务容错

业务场景:订单服务通过OpenFeign调用用户服务获取地址信息,如果用户服务响应慢或宕机,订单服务不能被拖垮。

技术点

  • OpenFeign:声明式HTTP客户端,只需定义接口和@FeignClient(name = "user-service", fallback = UserClientFallback.class),Spring Cloud会自动创建动态代理,调用时相当于发送HTTP请求。
  • Resilience4j:轻量级容错库,替代Hystrix。常用组件:
    • @CircuitBreaker:断路器。基于滑动窗口统计失败率,超过阈值则打开,后续请求直接走降级方法。支持failureRateThresholdwaitDurationInOpenState等配置。
    • @RateLimiter:限流,使用令牌桶或信号量。
    • @Retry:重试,可配置最大重试次数、指数退避等。
    • @Bulkhead:隔离,限制并发线程数或信号量。
  • 注意:熔断降级与超时配合使用。OpenFeign需设置connectTimeoutreadTimeout;熔断打开时不应继续发起远程调用,直接返回兜底数据(如缓存的信息或“暂不可用”提示)。

9. 分布式事务:Seata与最终一致性

业务场景:用户下单后,订单服务在自己的数据库中插入订单,同时要扣减库存服务的库存,两边数据库不同。

技术点

  • 分布式事务解决方案:
    • 2PC(两阶段提交):准备阶段所有参与者锁定资源,提交阶段统一提交。缺点是同步阻塞、协调者单点、数据不一致(第二阶段网络失败)。
    • TCC:Try预留资源、Confirm确认执行、Cancel回滚。需要业务层面实现三个操作,性能较高,适合一致性要求高的场景,但开发复杂。
    • 本地消息表:在本地事务中写业务数据和消息表,通过异步发送MQ,消费者处理并ACK。生产者通过定时任务补偿未发送的消息。
    • 事务消息(如RocketMQ):先发送半消息,本地事务成功后再commit,失败则rollback,消费者只能看到已commit的消息。
    • Sagas(长事务):每个本地事务都发布事件,后续事务由事件触发,失败时执行反向补偿步骤。
    • Seata AT模式:框架自动生成反向SQL(undolog),实现全局事务。步骤如下:
      1. 服务A执行本地业务,通过代理数据源生成undolog,插入数据,同时注册分支事务到TC(事务协调器)。
      2. TC与全局事务使用XID关联。
      3. 全局提交时删除undolog;全局回滚时根据undolog执行反向更新。
    • AT模式的限制:不能隔离全局事务,会造成脏读,需要select for update + lock table来保证隔离,但会牺牲并发。
  • 电商扣库存场景,更推荐消息最终一致性或Saga,避免强一致带来的锁冲突和复杂回滚。

10. Kubernetes部署与优雅停机

业务场景:订单服务上线到K8s集群,希望在发布滚动更新时,旧Pod能处理完已有请求再被销毁,新Pod就绪后再接入流量。

技术点

  • Deployment:声明期望状态(副本数、镜像、标签),通过ReplicaSet管理Pod。滚动更新策略:
    strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1

    先创建新Pod,等它就绪后再删除旧Pod,确保服务不中断。

  • 健康检查:
    • livenessProbe:探测应用是否存活,失败则K8s重启容器。
    • readinessProbe:探测应用是否就绪,失败则从Service Endpoints中摘除流量。
    • 使用Spring Boot Actuator的/actuator/health作为HTTP探针路径,需要引入spring-boot-starter-actuator,且开启management.endpoint.health.probes.enabled=true
  • 优雅停机:
    • 在Spring Boot中配置:
      server.shutdown=graceful spring.lifecycle.timeout-per-shutdown-phase=30s
    • K8s在删除Pod时会先发送TERM信号,等待terminationGracePeriodSeconds(默认30秒)后强制KILL。Spring Boot收到TERM后停止接收新请求,等待在飞请求完成。
    • 要确保探针配置合理:滚动更新时,新Pod的readiness探针通过后才切流量;旧Pod被删除时,需要从Service中摘除,再接收TERM信号。
  • 注意:不能只依赖Spring优雅停机,还需要设置preStophook通知注册中心/负载均衡下线,或使用readinessProbe让流量不再到达旧Pod。

11. JUnit 5与Mockito单元测试

业务场景:为订单服务的OrderService写单元测试,但OrderMapper依赖数据库,无法在本地执行,需Mock掉。

技术点

  • JUnit 5常用注解:
    • @Test:标记测试方法。
    • @BeforeEach/@AfterEach:每个测试前后执行。
    • @BeforeAll/@AfterAll:所有测试前后执行一次,需要静态方法。
    • @DisplayName:给测试起中文名。
    • @ParameterizedTest+@ValueSource/@CsvSource:参数化测试。
    • @Nested:嵌套测试,组织复杂测试类。
  • Mockito常用注解:
    • @Mock:创建Mock对象,不会执行真实方法。
    • @InjectMocks:将被Mock对象注入到被测类中(按构造函数优先,然后setter/字段)。
    • @Spy:创建部分Mock,真实方法会执行,可以手动stub指定方法。
  • 使用步骤:
    @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock OrderMapper orderMapper; @Mock RedisTemplate redisTemplate; @InjectMocks OrderService orderService; @Test void testFindOrderById() { Order order = new Order(); order.setId(1L); when(orderMapper.selectById(1L)).thenReturn(order); Order result = orderService.findOrderById(1L); assertEquals(1L, result.getId()); verify(orderMapper).selectById(1L); } }
  • 核心方法:
    • when(...).thenReturn(...):用于打桩。
    • doThrow(...).when(...).method():用于模拟异常。
    • verify(...):验证方法是否被调用,及调用次数(times(1)never())。
    • ArgumentCaptor:捕获方法参数,对方法参数进行断言。
  • 注意:不要过度Mock,避免测试变得没有意义。对于外部I/O(数据库、Redis、MQS)应该Mock,但核心业务逻辑应尽量真实执行。

结语:以上技术点是Java后端面试中的高频部分,也是电商系统真实落地能力的分水岭。谢飞机虽然能说出关键词,但在关键细节上含糊不清,因而只能回家等通知。希望读者们能以此为戒,深入源码、亲手实践,这样才能在下一次面试中稳操胜券。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询