电商大厂Java面试实录:Spring Boot/JVM/Redis/Kafka/微服务/安全/测试全解析

电商大厂Java面试实录:Spring Boot/JVM/Redis/Kafka/微服务/安全/测试全解析

"您好,我是面试官王工,从今天这个开场白开始,希望你接下来每一句话都经得起推敲。"面试官推了推眼镜,对面坐着一位精神抖擞、眼神中透着一丝"背了很多题"的求职者,谢飞机。

谢飞机:"面试官好,我叫谢飞机,大家都叫我飞机哥。我深耕Java八年,从Java 8到Java 17,从Spring Boot到Spring Cloud,从MySQL到Redis,从Kafka到Kubernetes,八百里外我能取一个微服务首级,三秒钟内我能定位一个JVM OOM。总之,技术这块,拿捏得死死的。"

面试官:"很好,那我们直接开始。"


第一轮:核心语言与基础框架

面试官:"先聊点基础。Java 8 和 Java 17 有什么区别?"

谢飞机:"这个简单。Java 8 是2014年出的,有 Lambda 表达式和 Stream 流,Optional 也好用。Java 17 是 LTS 版本,里面有 text block、sealed class、instanceof 模式匹配,还有 switch 表达式。反正 Java 8 能干的活,17 都能干,17 还更香。"

面试官:"那 JVM 内存结构说说看?"

谢飞机:"堆、栈、方法区嘛。堆里面放对象,栈里面放局部变量,方法区放类信息。还有程序计数器,虚拟机栈,本地方法栈。垃圾回收主要在堆里搞。"

面试官:"不错。那 Spring Boot 自动配置的原理是什么?"

谢飞机:"自动配置就是......把以前要写的配置都自动配好了。它有一个 @EnableAutoConfiguration 注解,会去读 META-INF 下面的 spring.factories 或者 AutoConfiguration.imports,然后条件注解判断,比如 @ConditionalOnClass,@ConditionalOnMissingBean。但是到底怎么加载的,嗯,我没仔细看,反正它自己就配置好了。"

面试官:"你如果遇到自动配置不生效,怎么办?"

谢飞机:"先看依赖有没有加,再看是不是有自定义 Bean 冲突了,再不行......我就重启一下项目。"

面试官:"......好吧。数据库连接池 HikariCP 为什么快?"

谢飞机:"它号称最快的连接池,因为用了字节码优化,还用了 ConcurrentBag 这种无锁集合。另外它默认最小化连接数,连接生命周期管理得好。反正我配置里直接写上 HikariCP 就行了。"

面试官:"那 MyBatis 和 JPA(Hibernate)在实际项目里怎么选?"

谢飞机:"我一般是看哪个熟用哪个。MyBatis 写 SQL 灵活,适合复杂查询;JPA 开发快,适合简单 CRUD。如果遇到复杂查询,MyBatis 直接写 SQL,JPA 可以用 @Query 写 JPQL,再复杂一点就写原生 SQL,实在不行就换 MyBatis 呗。"

面试官:"要是复杂查询还要动态排序、多表关联、分页,你怎么取舍?"

谢飞机:"呃......那就 MyBatis-Plus 吧,有 LambdaQueryWrapper,还能分页插件,挺方便的。"

面试官:"你连 MyBatis-Plus 都搬出来了。行,我们进入下一轮。"


第二轮:微服务、缓存与消息队列

面试官:"假设我们现在做一个电商系统。用户下单后,订单服务需要调用库存服务扣减库存。服务注册发现用的什么?"

谢飞机:"可以用 Eureka,也可以 Nacos,Consul 也行。我们项目用的 Spring Cloud,服务之间用 OpenFeign 调用,加一个 @FeignClient 接口,写个方法就能调了。"

面试官:"那如果库存服务响应很慢,甚至挂了,你怎么处理?"

谢飞机:"给 OpenFeign 配超时时间,服务端配 Resilience4j 做熔断降级,比如超时重试或者直接走 fallback 方法。"

面试官:"重试会不会导致重复扣库存?"

谢飞机:"那就要做幂等性了。可以用 Redis 存一个全局唯一 ID,处理过的请求直接拒绝。不过具体我没自己实现过,大概思路是这样。"

面试官:"商品详情页的 Redis 缓存,如何避免缓存穿透?"

谢飞机:"这个我会!查询一个不存在的 key,每次都会打到数据库。解决方法是把查询的空结果也缓存起来,设置一个短过期时间;或者用布隆过滤器,先判断 key 是否存在,不存在就直接返回。"

面试官:"那缓存击穿和雪崩呢?"

谢飞机:"击穿就是一个热点 key 失效,一瞬间大量请求直接打到数据库,可以用互斥锁或者逻辑过期。雪崩就是很多 key 同时过期,可以在设置过期时间时加一个随机数,避免同一时间集体失效。"

面试官:"回答得还挺顺。那下单后,你给 Kafka 发消息,怎么保证消息不丢?"

谢飞机:"生产者设置 acks=all,副本同步确认。消费者的话,关闭自动提交,手动提交 offset。这样消息就不会丢了吧?"

面试官:"如果消费成功了,但是业务处理完提交 offset 失败了,会发生什么?"

谢飞机:"那就重复消费了呗。所以消费逻辑要做幂等,比如用数据库唯一约束或者 Redis setnx 做去重。"

面试官:"那订单创建和扣库存这两个操作,怎么保证最终一致性?"

谢飞机:"这个......可以用本地消息表,或者用 Seata 做一个分布式事务。但是 Seata 那个 AT 模式我没有深入了解,反正就是写一个 @GlobalTransactional 注解就完事了。"

面试官:"@GlobalTransactional 背后的原理是什么?"

谢飞机:"它就是......全局事务协调器嘛,回滚日志什么的。但我没看过源码,担心说了不准。"

面试官:"行,那我们再聊聊安全。"


第三轮:安全、测试与部署

面试官:"电商登录,怎么用 Spring Security 和 JWT 做无状态认证?"

谢飞机:"用户登录成功后,后端生成一个 JWT 返回给前端,前端每次请求放到 Authorization 头里。然后我们在 Spring Security 里写一个过滤器,解析 JWT,验证签名,把用户信息设置到 SecurityContext 里。"

面试官:"JWT 密钥怎么管理?如果泄露了怎么办?"

谢飞机:"密钥放配置文件里。泄露了就......换一个密钥,所有用户重新登录呗。"

面试官:"那如果用户权限变了,如何让 JWT 立即失效?"

谢飞机:"啊?JWT 不是无状态的吗?要立即失效的话,可以用 Redis 存黑名单。但是我没实际做过,我一般只做登录和解析。"

面试官:"再问个场景:用户快速点击支付按钮,如何保证接口幂等性?"

谢飞机:"这个我会!前端生成一个全局唯一请求 ID,后端接收后先用 Redis 的 setnx 判断这个 ID 有没有处理过,如果处理过就返回重复请求;第一次请求就把 ID 存起来,设置过期时间。整个 setnx 和过期时间要用 Lua 脚本保证原子性。"

面试官:"你终于说对一个完整的方案了。那 Mockito 单元测试怎么用?"

谢飞机:"用 @Mock 创建一个接口的 mock 对象,然后 when(...).thenReturn(...),验证用 verify,比如 mockUserMapper.selectById(1),when,thenReturn(user),然后调用 service 后 verify 一下。"

面试官:"PowerMock 能 mock 静态方法,你了解吗?"

谢飞机:"了解一点,比如 mockStatic 的语法,但是我们项目主要用 Mockito,静态方法一般用工具类包一层,不用 PowerMock。"

面试官:"那 Docker 和 Kubernetes 部署呢?Dockerfile 怎么写?"

谢飞机:"简单:FROM openjdk:17,COPY app.jar /app.jar,ENTRYPOINT java -jar /app.jar。然后 build 之后 docker run 就行。"

面试官:"Kubernetes 里怎么做滚动更新?"

谢飞机:"用 Deployment 啊,改一下镜像版本,它会自动滚动升级。但是具体的 YAML 配置我记得不太全,什么 strategy 的 RollingUpdate 设置 maxSurge、maxUnavailable 那些,我都是复制粘贴的。"

面试官:"如果 Pod 崩溃了,怎么排查?"

谢飞机:"先 kubectl logs 看日志,然后 kubectl describe pod 看事件,如果还不行就 kubectl exec 进去看看,最后不行就重启。"

面试官:"好了,今天的面试到这里。感谢你参加我们的面试,我们会在三个工作日内通知你面试结果。你先回去等通知吧。"

谢飞机:"好的,面试官,我回去等好消息!对了,我可以发一下我的开源项目吗?虽然只有一个 star,但是是我自己写的。"

面试官:"嗯,简历上有我们就看。下一位。"


面试问题详细答案

下面把面试中涉及的关键问题逐一展开,结合电商业务场景讲清楚技术点和学习方向,方便小白们系统理解。

1. Java 8 和 Java 17 的主要区别有哪些?

核心点:Java 8 和 Java 17 都是 LTS(长期支持)版本,但中间隔了 9 个大版本。Java 8 引入了函数式编程(Lambda、Stream、Optional)、新的日期时间 API、默认方法;Java 17 则在语言和 API 上做了大量增强。

Java 8 重点

  • Lambda 表达式:简化匿名内部类,比如 list.forEach(x -> System.out.println(x))
  • Stream API:集合的链式流式操作,支持 map/filter/reduce。
  • Optional:避免空指针的容器对象。
  • 新日期时间 API:LocalDateLocalDateTimeInstant 等,线程安全。
  • 接口默认方法和静态方法。

Java 17 新增重点

  • sealed class(密封类):限制哪些类可以继承。
  • text block(文本块):用三引号写多行字符串,不再需要 \n
  • instanceof 模式匹配:if (obj instanceof String s) { ... }
  • switch 表达式:可以返回值,且支持箭头语法。
  • 新增 Record(Java 16 正式):定义不可变数据类,替代 Lombok 的部分功能。
  • 移除了一些过时的工具,比如 AOT 相关特性,也增强了 ZGCShenandoah 等垃圾回收器。

业务场景:新项目建议直接上 Java 17 或 21;老项目如果要升级,需要主要关注依赖兼容性,比如 Lombok、CGLIB 等。面试时能说清两代 LTS 的区别,已经可以超过大部分候选人。


2. JVM 内存结构是什么样的?

JVM 内存区域根据 Java 虚拟机规范分为以下几个部分:

  • 程序计数器:当前线程执行的字节码行号指示器,线程私有,无 OOM。
  • 虚拟机栈 :线程私有,每个方法调用对应一个栈帧,存储局部变量表、操作数栈、动态链接、方法出口。深度超限会抛 StackOverflowError
  • 本地方法栈:为 JVM 使用的底层本地方法服务,线程私有。
  • :线程共享,存放对象实例和数组,是 GC 回收的主要区域。通常分为新生代(Eden、Survivor)和老年代。
  • 方法区:线程共享,存储类元信息、常量、静态变量等。JDK 8 之后用元空间(Metaspace)实现,不再使用永久代,元空间使用本地内存。
  • 运行时常量池:方法区的一部分,存放编译期生成的字面量和符号引用。

学习建议:要能画图,能说明每个区域是否线程私有、会不会 OOM,以及垃圾回收发生的位置。面试官常追问"对象创建过程"和"full GC 触发条件",可以继续深入。


3. Spring Boot 自动配置的原理是什么?

Spring Boot 的自动配置核心是 @EnableAutoConfiguration。它通过 @Import 引入 AutoConfigurationImportSelector,该组件会扫描所有依赖的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(Spring Boot 2.7 之前是 spring.factories),把所有候选的自动配置类加载进来。

但加载进来不代表一定生效,自动配置类上通常有一堆条件注解:

  • @ConditionalOnClass:类路径下存在某个类才生效。
  • @ConditionalOnMissingBean:容器中没有某个 Bean 才生效,给用户覆盖的机会。
  • @ConditionalOnProperty:配置项满足条件才生效。

比如 RedisAutoConfiguration

java 复制代码
@AutoConfigureAfter(JacksonAutoConfiguration.class)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) { ... }
}

业务场景 :当你想自定义 RedisTemplate,只要在容器中声明一个叫 redisTemplate 的 Bean,自动配置就会退让。

常见追问

  • spring.factoriesAutoConfiguration.imports 区别?后者是 2.7 之后的新加载方式,支持排序。
  • 自动配置和普通 @Configuration 的区别?自动配置类也是普通配置,但优先级低,通常放在自动配置包中。

4. HikariCP 为什么快?和 C3P0 相比有什么优势?

HikariCP 是目前 Spring Boot 2.x/3.x 默认的连接池。它快在几个地方:

  • 字节码精简:HikariCP 的字节码经过优化,方法体更小,CPU 缓存命中率更高。
  • 无锁数据结构 :使用 ConcurrentBag,在获取和归还连接时采用 ThreadLocal 存储和 steal 机制,减少锁竞争。
  • 代理机制优化 :连接代理类使用 javassist 生成,避免通过 JDK 动态代理的大量反射开销。
  • 连接生命周期管理 :默认 maximumPoolSize=10minimumIdle 与连接创建策略十分高效;还支持 maxLifetime 防止服务器回收连接。

C3P0 是老牌连接池,但较重、性能一般,且配置复杂。在实际电商高并发场景,数据库连接是稀缺资源,连接池等待时间直接影响接口 RT,所以选 HikariCP。

配置示例

yaml 复制代码
spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

学习建议 :理解连接池概念(初始化、活跃、空闲、等待队列),并会分析连接池耗尽导致的 Connection is not available 异常。


5. MyBatis 和 JPA(Hibernate)如何选择?复杂查询怎么办?

这是一个经典问题。

JPA(Hibernate)

  • 优点:对象关系映射完整,CRUD 开发效率极高,缓存机制成熟,多表关联可以用实体关系自动管理。
  • 缺点:复杂查询 N+1 问题容易踩坑,SQL 优化不直观,动态 SQL 能力弱。

MyBatis

  • 优点:SQL 完全由开发者控制,方便 DBA 审查和优化,动态 SQL 非常灵活(<if><foreach> 等),适配复杂查询。
  • 缺点:需要手写大量 SQL,关联映射需要配置,维护成本高。

实际业务选型建议

  • 单表 CRUD、后台管理系统、实体关系简单:用 JPA 或 Spring Data JPA。
  • 电商交易、C 端高并发查询、复杂多表统计、需要精细控制 SQL:用 MyBatis/MyBatis-Plus。
  • 也可以混合使用:倾向 Spring Data JDBC + MyBatis。

复杂查询方案

  • MyBatis 直接写 XML 或注解 SQL,用动态 SQL 处理条件。
  • JPA 用 @Query(JPQL)或 nativeQuery = true
  • 更复杂的聚合查询,可以把数据放到 Elasticsearch 或 ClickHouse 中。
  • 不要把所有东西都塞进口 ORM,分库分表后还要考虑路由。

6. 微服务架构中服务发现和调用是怎么工作的?

电商微服务常见架构:

  • 服务提供者启动时,把自己的 IP、端口、服务名注册到注册中心(Eureka / Nacos / Consul)。
  • 服务消费者从注册中心拉取可用服务列表,并缓存到本地。
  • 服务消费者通过 OpenFeign 声明式 HTTP 客户端发起调用。OpenFeign 底层是 Ribbon 或 Spring Cloud LoadBalancer,从本地列表中负载均衡选择一个实例。

Eureka 工作过程

  • Eureka Client 注册,发送心跳,默认 30 秒一次。
  • 90 秒未收到心跳,Eureka Server 会剔除实例。
  • 消费者本地缓存服务列表,默认每 30 秒拉取一次。

Consul 不同:使用 Consul 作为注册中心,它基于共识协议(Raft),支持多数据中心,还提供键值存储、健康检查。服务注销比 Eureka 更及时。

OpenFeign 核心

java 复制代码
@FeignClient(name = "inventory-service", fallback = InventoryFallback.class)
public interface InventoryClient {
    @PostMapping("/inventory/deduct")
    Response deduct(@RequestBody DeductRequest request);
}

调用时通过动态代理生成 HTTP 请求,集成 Ribbon/LoadBalancer,还会走 Resilience4j 或 Sentinel 容错。

注意:如果注册中心挂了,本地缓存列表还能继续用一段时间,前提是消费者实例不重启。生产环境建议部署多实例注册中心。


7. 服务超时和故障如何容错?Resilience4j 怎么用?

库存服务依赖数据库、网络,可能变慢或宕机。如果订单服务对库存服务的调用一直阻塞,线程池会被耗尽,导致整个应用雪崩。

Resilience4j 是一个轻量级容错库,提供:

  • CircuitBreaker(熔断器):当错误率达到阈值,熔断打开,后续请求直接降级,不再调用下游。
  • RateLimiter(限流):限制请求速率。
  • Bulkhead(隔离):限制并发调用数,防止下游故障耗尽线程。
  • Retry(重试):对临时性失败自动重试。
  • TimeLimiter(超时):限制调用时长。

示例

yaml 复制代码
resilience4j:
  circuitbreaker:
    instances:
      inventoryService:
        failureRateThreshold: 50
        waitDurationInOpenState: 10s
        permittedNumberOfCallsInHalfOpenState: 3
        slidingWindowSize: 10
  retry:
    instances:
      inventoryService:
        maxAttempts: 3
        waitDuration: 200ms

重点提示 :重试要谨慎,要配合幂等。扣库存接口必须支持幂等,否则重试会重复扣减。可以设计一个 deduct_token 作为请求唯一键,库存表加唯一约束或 Redis 去重。


8. Redis 如何解决缓存穿透、击穿、雪崩?

电商商品详情页是最常见的缓存场景。数据流程:先查 Redis,没有则查 MySQL,把结果写回 Redis。

缓存穿透:查询一个并发很高的不存在的 key,每次都打到数据库。

  • 方案一:缓存空值,设置 3~5 分钟过期。问题:空值过多,且同一时间依然会打到数据库。
  • 方案二:布隆过滤器,把所有可能存在的 key 的哈希映射到一个位数组。不存在直接拦截。问题是布隆过滤器有误判,且删除复杂。
  • 方案三:增强参数校验,比如非法 ID 直接返回。

缓存击穿:一个热点 key 失效,比如秒杀商品,瞬间大量请求打过来,全部打到数据库。

  • 方案一:互斥锁。只让一个线程去数据库加载,其他线程等待后读取缓存。
  • 方案二:逻辑过期。缓存中不设置过期时间,而是设置一个过期时间字段,后台异步刷新,但不保证强一致。

缓存雪崩:大量 key 同时失效,或者 Redis 宕机,导致大量请求打到数据库。

  • 方案一:过期时间加随机值,比如 base + random(0, 300) 秒。
  • 方案二:多级缓存,比如本地 Caffeine + Redis,两级保护。
  • 方案三:Redis 高可用,主从+哨兵或 Redis Cluster。

兜底:数据库层面也要做限流、降级、熔断。


9. Kafka 如何保证消息不丢失和重复消费?最终一致性如何实现?

在电商下单场景,下单成功后通常会发一条"订单创建成功"的 Kafka 消息,通知库存、积分、通知等系统。

消息不丢失需要从三个端保证

  • 生产者端:acks=all,等待所有副本确认;同步发送时 Future.get() 判断是否成功;失败重试。
  • Broker 端:replication.factor >= 3,min.insync.replicas >= 2,分区 leader 切换不会丢消息。
  • 消费者端:enable.auto.commit=false,业务处理成功后再手动提交 offset。

重复消费必然存在,因为"至少一次"语义。例如消费者处理完消息,但是还没来得及提交 offset,应用崩溃,重启后从头消费。

幂等消费方案

  • 在数据库中建消息消费记录表,唯一键为消息 ID。插入前判断,如果已存在则跳过。
  • 或者使用 Redis setnx msgId value,返回 1 才处理。
  • 也可以利用业务表唯一约束,比如订单号唯一。

最终一致性:订单服务和库存服务之间,不能强一致。通常用"本地消息表 + Kafka"或基于 Kafka 事务消息。流程:

  1. 订单服务在本地数据库开启事务,插入订单,并插入一条消息记录表。
  2. 事务提交后,后台定时任务把消息发送到 Kafka。
  3. 库存服务消费消息,执行扣库存,执行成功后发送"扣减完成"消息回来。
  4. 订单服务通过定时任务扫描未完成的消息,不断重试或对账。

这种方式保证了一方成功、另一方可重试,最终达到一致。如果涉及多个系统,也可以用 Seata 的 AT/TCC 模式,但要注意性能代价。


10. 分布式事务如何处理?(Seata 与本地消息表)

电商核心链路:订单 - 库存 - 支付 - 积分。跨库、跨服务,数据库本地事务解决不了。

常见方案:

  • XA 两阶段提交:数据库原生支持,但同步阻塞、性能差。
  • TCC(Try-Confirm-Cancel):适合强一致要求高的场景,比如余额扣减。需要业务方实现 Try/Confirm/Cancel 三段逻辑。
  • AT 模式(Seata):基于全局事务协调器,通过代理数据源生成 undo log。业务代码简单,但有脏写风险。
  • 本地消息表 + MQ:最终一致性,允许短暂不一致。

Seata AT 模式核心

  • 全局事务 ID 通过 Dubbo/Feign 传播。
  • @GlobalTransactional 开启全局事务。
  • 分支事务注册到 TC,TC 协调提交或回滚。
  • 一阶段:业务数据操作 + 记录回滚日志。
  • 二阶段:全局提交,删除回滚日志;全局回滚,根据回滚日志恢复数据。

注意:AT 模式依赖数据库事务,数据源必须支持;隔离性受限于全局写锁,不适合极致并发。简单业务中,本地消息表更轻量可靠。


11. Spring Security + JWT 如何实现登录认证和授权?

JWT(JSON Web Token) 结构:Header.Payload.Signature。Payload 中常放 userIdusernameroleexp 过期时间。

Spring Security 认证流程

  • 用户 POST /login,Spring Security 的 AuthenticationManager 调用 UserDetailsService 加载用户,比对密码。
  • 密码校验通过后,生成 JWT,返回前端。
  • 前端后续请求在 Authorization: Bearer <jwt> 中携带。
  • 后端添加一个 OncePerRequestFilter,解析 JWT,验证签名,如果合法则构造 Authentication 并放入 SecurityContextHolder
  • SecurityFilterChain 中配置哪些接口需要认证、哪些是放行资源、使用无状态 SessionCreationPolicy.STATELESS

权限控制

  • 使用 @PreAuthorize("hasRole('ADMIN')") 开启方法级安全。
  • 在 JWT 的 claims 中写入权限集合,解析后封装为 GrantedAuthority

JWT 密钥管理

  • 密钥不要硬编码,使用环境变量或配置中心。
  • 使用 RS256 非对称签名,私钥放在认证服务,公钥放在资源服务。
  • 密钥泄露时,需要撤销所有令牌:可以维护 Redis 白名单/黑名单,或者保持短过期时间(如 10 分钟)。

缺点 :无法主动登出(服务端状态丢失)。解决方案是 Redis 存 token 的 jti 和用户 ID,登出时删除。


12. 如何设计接口幂等性?支付场景怎么做?

用户重复点击支付按钮,或支付回调重复通知,都必须保证只扣一次款。

幂等核心:相同请求只产生一次效果。

常见方案

  • 前端控制:按钮置灰,生成请求唯一 ID。

  • 后端使用唯一业务号:比如订单号 + 支付通道 + 支付流水号。

  • 使用 Redis SETNX

    复制代码
    SET pay_token_{orderId} 1 NX EX 300

    只有第一次设置成功才执行支付逻辑,第二次发现 key 存在则直接返回"处理中"。

  • 使用数据库唯一约束 :在支付记录表对 order_id 建唯一索引,重复插入会冲突,捕获异常后返回已处理。

  • 状态机:订单状态从 待支付 -> 支付中 -> 已支付,只有状态满足才允许变更。

Lua 原子操作:为了保证"检查 token 是否存在 + 删除 token + 执行业务"的原子性,可以使用 Lua 脚本。比如:

lua 复制代码
if redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then
  return 1
else
  return 0
end

注意:分布式锁和幂等是不同的概念。锁是保证并发互斥,幂等是保证重复请求结果一致。支付回调还需要校验签名和金额,才能防止伪造通知。


13. Mockito 和 PowerMock 怎么用?

单元测试是保障代码质量的重要手段。

Mockito 基础

  • mock(Class) 创建 mock 对象。
  • when(mock.method(args)).thenReturn(result) 设置预期行为。
  • verify(mock).method(args) 验证方法是否被调用。
  • @Mock@InjectMocks 与 JUnit 5 集成:
java 复制代码
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock
    OrderMapper orderMapper;
    @InjectMocks
    OrderService orderService;

    @Test
    void testGetOrderById() {
        Order order = new Order();
        order.setId(1L);
        when(orderMapper.selectById(1L)).thenReturn(order);
        Order result = orderService.getOrderById(1L);
        assertEquals(1L, result.getId());
        verify(orderMapper).selectById(1L);
    }
}

Mockito 限制:不能 mock 静态方法、构造方法、私有方法。此时可以用:

  • PowerMockito:PowerMockito.mockStatic(IdGenerator.class)when(IdGenerator.nextId()).thenReturn("123")
  • Mockito 3.4+ 内置 mock static 方法(需要 mockito-inline)。

业务场景:单元测试不启动 Spring 容器,用 mock 隔离外部依赖,让测试快速运行。集成测试才会用 Testcontainers 启动 Redis/MySQL。


14. 如何用 Docker 和 Kubernetes 部署 Java 服务?

Docker 部署

dockerfile 复制代码
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

构建镜像:

bash 复制代码
docker build -t my-app:1.0.0 .
docker run -d -p 8080:8080 --name my-app my-app:1.0.0

多阶段构建可以更小:

dockerfile 复制代码
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

FROM eclipse-temurin:17-jre
COPY --from=build /build/target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

Kubernetes 滚动更新 : 编写 Deployment YAML:

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: my-registry/order-service:1.0.0
          ports:
            - containerPort: 8080
          livenessProbe:
            httpGet:
              path: /actuator/health
              port: 8080
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: 8080

发布新版本:

bash 复制代码
kubectl set image deployment/order-service order-service=my-registry/order-service:1.1.0
kubectl rollout status deployment/order-service

滚动更新策略保证先启动一个新 Pod,就绪后再杀掉一个旧 Pod,逐步替换。maxUnavailable=0 表示最少有一个 Pod 可用,服务不中断。

排查问题

  • kubectl logs pod-name 看应用日志。
  • kubectl describe pod pod-name 看事件和状态。
  • kubectl exec -it pod-name -- sh 进入容器。

监控 :Java 服务通过 Micrometer 暴露 /actuator/prometheus,Prometheus 抓取指标,Grafana 展示;日志通过 Filebeat 采集到 ELK。


15. 如何做日志监控与链路追踪?

电商系统多为微服务,排查一个请求跨多个服务需要链路追踪。

  • 日志:使用 SLF4J + Logback/Log4j2,输出 JSON 格式,包含 traceId。
  • 指标:Micrometer 集成到 Spring Boot,提供有意义的监控指标:JVM、线程、HTTP 请求、数据库连接池、Redis、Kafka 消费者 lag。
  • 监控:Prometheus 定时拉取,Grafana 画面板,告警在 Alertmanager 中设置。
  • 链路追踪:使用 Jaeger 或 Zipkin。Spring Cloud Sleuth(现已变成 Micrometer Tracing)自动生成 traceId/spanId,通过 HTTP 头传递。
  • ELK:Filebeat 从容器日志文件采集,推到 Logstash 清洗,存到 Elasticsearch,Kibana 做可视化。

在电商场景中,一次下单请求会经过 gateway、order-service、inventory-service、payment-service。用 traceId 把所有日志串起来,查询一个订单问题能快速定位是哪个服务慢。


结语

谢飞机虽然很多问题答得不够深,但他能记住一些关键名词,说明有一定基础。真正的工程师需要把"背得出来"变成"写得出来",在真实故障中还要能"救得回来"。希望这篇面试实录能帮助准备 Java 面试的读者,把技术点串起来,不仅会背答案,还能在项目中思考"为什么这么设计"。最后,祝大家面试都能拿到满意的 offer,不要像谢飞机一样,回家等通知。

相关推荐
吃饱了得干活1 小时前
限界上下文之后:微服务怎么拆、上下文怎么聊?
java·后端·架构
yngsqq2 小时前
窗体快速取消
java·开发语言
岁月如歌77862 小时前
一次讲透 Redis 缓存击穿、穿透、雪崩,从原理到实战解决方案
java·后端·架构
【赫兹威客】浩哥2 小时前
基于SpringBoot+Vue3的舆情文本分析系统|Elasticsearch检索+情感分析+话题热度追踪 毕设项目
spring boot·elasticsearch·课程设计
小的~~2 小时前
面试被问懵了?为什么 Redis 单线程还能保证 Lua 脚本原子性,却偏偏选了 Lua?
redis·面试·lua
AI多Agent协作实战派2 小时前
AI多Agent协作系统实战(四十六):改了十次规则,AI员工还是老样子——会话缓存的坑
java·spring·缓存
一知半解仙3 小时前
当机器人学会泛化,而我在写Java:一名后端开发者的2026年8月21日观察手记
java·开发语言·机器人
葡萄成熟时 !11 小时前
JAVA 常用API学习笔记
java·笔记·学习
Lyyaoo.11 小时前
Spring Boot Validation 声明式参数校验
spring boot·后端·mysql