ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

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

大厂JAVA面试实录:Spring Boot、Redis缓存、Kafka异步与JVM调优(电商大促场景下的谢飞机) 大厂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如果映射不对容易有N1查询问题。呃……具体N1怎么解决用EntityGraph还是join fetch我不太确定。王工行第一轮先到这。你有一些概念但深度还不够。我们继续。第二轮缓存、消息与服务容错王工双十一首页有热点商品流量巨大你会怎么设计Redis缓存如果某个热点key突然失效刚好有十万请求打到数据库怎么办谢飞机这个我知道防止缓存穿透缓存空值防止雪崩过期时间加随机值防止击穿加锁。比如用setnx没有获取到锁的就等待。不过……如果锁的粒度是key那其他请求会阻塞吞吐量会低。也可以用Redisson的看门狗自动续期。嗯……我实习时写过但具体代码记不清了。王工那你再说说下单之后我们要发消息给库存系统减库存用的Kafka。怎么保证消息不丢消费者怎么保证幂等谢飞机生产者设置acksall这样Leader和ISR都确认消费者关掉自动提交处理成功后再commitSync。幂等的话给每条消息一个唯一ID消费前查Redis如果有就不处理。或者用数据库的唯一索引。嗯差不多是这样。王工那再问你一个常见的Redis和数据库的一致性怎么保证你之前说“先删缓存再更新数据库”那如果数据库更新失败了缓存又已经删了下次读会把旧数据写回缓存这不就不一致了吗谢飞机啊这个……一般不是先更新数据库再删缓存吗但更新数据库后还没删缓存的时候有线程读到了旧缓存会不一致。最稳妥的是用“延迟双删”先删缓存更新数据库休眠几百毫秒再删一次。但这样要维护休眠时间分布式环境下不好搞。实在不行就订阅Binlog通过Canal异步删缓存。我……我就是这么糊弄的。王工那微服务之间订单服务调用用户服务如果用户服务挂了你怎么做熔断降级OpenFeign和Resilience4j用过吗谢飞机用过OpenFeign接口上加FeignClient然后定义fallback类。熔断用Resilience4j加CircuitBreaker还有限流RateLimiter、重试Retry。配置可以设置失败率阈值、滑动窗口大小。但是……我发现配置多了容易乱有时候不知道是超时还是熔断把请求拦了。王工第二轮结束。你的回答都是“框架关键词”级别的没有真正落地过。最后一个问题别紧张。第三轮分布式事务、容器化与测试王工订单服务要扣库存如果库存服务是另一个团队负责你怎么保证分布式环境下两边数据一致你了解Seata或者消息最终一致性吗谢飞机这个我知道一点可以用TCCTry阶段预留资源Confirm阶段提交Cancel阶段回滚。但TCC要自己写三个方法很麻烦。也可以用消息事务比如RocketMQ的事务消息。如果是Seata的AT模式它是通过代理数据源自动生成undolog提交时注册分支事务全局事务提交时删undolog回滚时用undolog反向补偿。但是AT模式有脏读问题而且性能损耗大。我这边……之前只是看过文档没写过。王工那你们的服务怎么部署到Kubernetes上的K8s如何判断你的订单服务是否健康如何做到发布时请求不中断谢飞机我们写一个Deployment的yaml里面包含了一个Pod副本……health那边用livenessProbe和readinessProbe配置了HTTP探针路径是/actuator/health。滚动更新时用strategy.rollingUpdate设置maxUnavailable和maxSurge。优雅停机的话在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过滤订单列表业务场景在订单管理后台需要快速筛选出“已支付”status1且金额大于100元的订单并交给前端展示。技术点Lambda表达式本质是函数式接口的匿名实现。例如PredicateOrder predicate o - o.getAmount() 100 o.getStatus() 1;Stream API提供“数据流”式抽象。常用操作有filter过滤、map转换、collect收集、sorted排序。执行方式list.stream()创建串行流。list.parallelStream()创建并行流底层使用ForkJoinPool数据量大时能提升性能但有线程安全问题。底层原理Stream是一个管道流水线中间操作如filter、map是惰性的只有遇到终止操作如collect、forEach才会触发遍历。Spliterator负责分割数据源可以感知数据特征是否有序、是否去重等以优化遍历。注意不要过度使用并行流。在共享可变变量时并行流会带来并发问题。2. Spring Boot自动配置原理业务场景本项目中引入spring-boot-starter-web后就能直接使用RestController处理HTTP请求无需手动配置DispatcherServlet。技术点SpringBootApplication是一个组合注解包含SpringBootConfiguration、EnableAutoConfiguration、ComponentScan。EnableAutoConfiguration通过Import(AutoConfigurationImportSelector.class)读取META-INF/spring.factoriesSpring Boot 2.7前中的EnableAutoConfiguration属性把自动配置类加载到Spring容器。条件注解每个自动配置类都有ConditionalOnClass、ConditionalOnMissingBean等条件只有满足条件才生效。例如WebMvcAutoConfiguration会在classpath存在Servlet和DispatcherServlet时创建相关的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限制大小防止滥用动态代理、反射产生大量类。常用调优工具jstat、jmap、jstack、JConsole、Arthas。4. Hibernate与MyBatis的区别业务场景订单表有几十个字段且查询维度复杂订单号、用户ID、时间范围、状态需要选择合适的ORM。技术点Hibernate全自动ORM框架。通过注解或XML映射将实体类与表关联。提供HQL/Criteria查询能自动生成SQL。适合业务模型清晰、增删改查标准化的系统。但复杂SQL不透明容易产生N1查询问题默认懒加载访问子集合时发出额外SQL。MyBatis半自动ORM框架。SQL由开发者编写灵活可控支持动态SQLif、foreach、choose。适合表结构复杂、需要深度优化SQL的场景。但需要手写大量SQL维护成本高。选型建议订单表字段多、变化多优先MyBatis。若团队习惯JPA/Hibernate可使用Spring Data JPA配合EntityGraph或QueryDSL但依然要小心N1。解决N1在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生产者设置acksall或-1要求所有ISR副本确认。设置retries0和enable.idempotencetrue幂等生产者避免网络重试导致消息重复。Kafka服务端min.insync.replicas必须大于1且replication.factor为3保证分区Leader挂掉时副本可用。Kafka消费者关闭自动提交enable.auto.commitfalse使用手动提交。在业务处理完成后再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断路器。基于滑动窗口统计失败率超过阈值则打开后续请求直接走降级方法。支持failureRateThreshold、waitDurationInOpenState等配置。RateLimiter限流使用令牌桶或信号量。Retry重试可配置最大重试次数、指数退避等。Bulkhead隔离限制并发线程数或信号量。注意熔断降级与超时配合使用。OpenFeign需设置connectTimeout、readTimeout熔断打开时不应继续发起远程调用直接返回兜底数据如缓存的信息或“暂不可用”提示。9. 分布式事务Seata与最终一致性业务场景用户下单后订单服务在自己的数据库中插入订单同时要扣减库存服务的库存两边数据库不同。技术点分布式事务解决方案2PC两阶段提交准备阶段所有参与者锁定资源提交阶段统一提交。缺点是同步阻塞、协调者单点、数据不一致第二阶段网络失败。TCCTry预留资源、Confirm确认执行、Cancel回滚。需要业务层面实现三个操作性能较高适合一致性要求高的场景但开发复杂。本地消息表在本地事务中写业务数据和消息表通过异步发送MQ消费者处理并ACK。生产者通过定时任务补偿未发送的消息。事务消息如RocketMQ先发送半消息本地事务成功后再commit失败则rollback消费者只能看到已commit的消息。Sagas长事务每个本地事务都发布事件后续事务由事件触发失败时执行反向补偿步骤。Seata AT模式框架自动生成反向SQLundolog实现全局事务。步骤如下服务A执行本地业务通过代理数据源生成undolog插入数据同时注册分支事务到TC事务协调器。TC与全局事务使用XID关联。全局提交时删除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.enabledtrue。优雅停机在Spring Boot中配置server.shutdowngraceful spring.lifecycle.timeout-per-shutdown-phase30sK8s在删除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给测试起中文名。ParameterizedTestValueSource/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后端面试中的高频部分也是电商系统真实落地能力的分水岭。谢飞机虽然能说出关键词但在关键细节上含糊不清因而只能回家等通知。希望读者们能以此为戒深入源码、亲手实践这样才能在下一次面试中稳操胜券。
返回列表