面试官与谢飞机的Java大厂面试实录:从Spring Boot到微服务容错

严肃的面试官 VS 搞笑的水货程序员谢飞机

场景设定

  • 业务领域: 电商场景 - 大促期间的订单履约系统
  • 技术栈聚焦: Spring Boot, Spring Cloud (OpenFeign, Resilience4j), Redis, Kafka

第一轮:基础功底与单体架构

面试官: 谢同学,你好。我们先从最基础的开始。假设你正在用Spring Boot开发一个订单服务。请写一个简单的@RestController,它接收一个POST /orders请求,参数是一个JSON格式的订单对象,并返回创建成功的响应。

谢飞机: 啊,这个简单!@RestController嘛,我熟!(快速敲键盘)

@RestController
public class OrderController {
    @PostMapping("/orders")
    public ResponseEntity<OrderResponse> createOrder(@RequestBody OrderRequest request) {
        // ... 业务逻辑
        return ResponseEntity.ok(new OrderResponse("SUCCESS", 12345L));
    }
}

面试官: (点头)不错,基础语法很扎实。那如果这个订单创建后,需要异步通知库存服务扣减库存,你会怎么做?

谢飞机: 异步?哦,@Async!加个注解,再配个线程池,搞定!

面试官: 嗯,@Async是一种方案。但如果库存服务暂时不可用,你的订单创建成功了,但库存没扣,这会导致超卖。你怎么保证最终一致性?

谢飞机: (挠头)呃... 这个... 消息队列?Kafka?

面试官: 很好,提到了关键点。那么,如何确保订单服务在数据库写入成功后,这条“扣减库存”的消息一定能发出去?

谢飞机: (支吾)这个... 先发消息,再写库?不行不行... 那写库失败了消息白发了... 写库成功再发消息?万一发消息失败呢...


第二轮:分布式挑战与微服务治理

面试官: 我们进入第二轮。现在,订单服务和库存服务已经拆分为两个独立的微服务。订单服务通过OpenFeign调用库存服务的/inventory/deduct接口。

面试官: 如果库存服务因为网络抖动或自身负载过高,偶尔出现超时,你的订单服务会怎样?

谢飞机: (自信)超时?那Feign客户端配置个timeout不就行了!

面试官: 对,这是第一步。但如果超时了,用户看到的是一个“下单失败”的错误页面,用户体验很差。有没有办法让系统更“聪明”一点,比如自动重试几次?

谢飞机: 重试?Feign好像有@Retryable注解?

面试官: @Retryable是Spring Retry的,和Feign集成需要额外配置。但更重要的是,如果库存服务真的挂了,重试10次也没用,反而会把订单服务的线程池耗尽。这时候该怎么办?

谢飞机: (愣住)啊?那... 那就挂了呗...

面试官: (微笑)这就是熔断器(Circuit Breaker)的用武之地。它能感知下游服务的健康状况,在它持续失败时,直接“熔断”,避免无效调用,保护上游。你知道Spring Cloud中常用的熔断器实现吗?

谢飞机: (眼睛一亮)Resilience4j!我听说过!

面试官: 很好。那么,熔断器打开后,用户请求进来,总不能一直返回错误吧?我们通常会提供一个降级(Fallback)逻辑。你能描述一下,当库存服务不可用时,订单服务的降级逻辑应该是什么样的吗?

谢飞机: (思考)嗯... 降级... 就是给个默认值?比如返回“库存服务繁忙,请稍后再试”?

面试官: (赞许)非常正确!这就是降级的核心思想——优雅地失败。


第三轮:高可用与深度优化

面试官: 最后一轮,我们聊点深度的。假设大促期间,订单服务的QPS飙升,而库存服务的响应时间也变长了。除了熔断和降级,你还能想到哪些手段来保障整个链路的稳定性?

谢飞机: (努力回想)限流?对,限流!防止流量一下子打垮服务!

面试官: 完全正确。那具体到代码层面,你用过Resilience4j的RateLimiter吗?它的核心原理是什么?

谢飞机: (犹豫)原理... 是不是像令牌桶?每秒放几个令牌,有令牌才能处理请求...

面试官: 非常棒!就是令牌桶算法。那么,如果所有这些防护措施都启动了,但问题依然存在,我们需要快速定位瓶颈。你平时会用什么工具来监控和分析服务的性能?

谢飞机: (松了口气)这个我知道!Prometheus + Grafana!看CPU、内存、GC、HTTP QPS...

面试官: (满意地点头)很好。最后一个问题:在整个订单履约流程中,Redis扮演了什么角色?它仅仅是缓存吗?

谢飞机: (自信)当然不止!我们用Redis做分布式锁,防止超卖;还用它做购物车缓存,减轻数据库压力;对了,还有... 还有... (卡壳)

面试官: (温和地)还有,它也是我们实现分布式事务(如TCC模式)中,用于记录事务状态的可靠存储。好了,谢同学,今天的面试就到这里。你的基础知识很扎实,对主流框架也有一定了解,特别是在面对复杂问题时的思考方向是正确的。回去等我们的通知吧。


【附录】技术解析与答案详解

1. 最终一致性与可靠消息

  • 问题核心: 如何保证“订单创建”与“库存扣减”两个操作的最终一致性。
  • 答案: 使用本地消息表 + 定时任务RocketMQ/Kafka的事务消息
    • 本地消息表: 在订单服务的数据库中,创建一张message_log表。在同一个数据库事务中,完成“创建订单”和“插入一条状态为‘待发送’的消息记录”。然后由一个独立的定时任务扫描这张表,将状态为‘待发送’的消息投递到Kafka。Kafka消费者(库存服务)处理成功后,再回调订单服务,将消息状态更新为‘已发送’。
    • 事务消息(推荐): 利用Kafka的事务API或RocketMQ的事务消息特性。生产者(订单服务)先发送一条“半消息”到Broker,此时消息对消费者不可见。然后执行本地事务(创建订单)。根据本地事务的执行结果,向Broker发送Commit或Rollback指令。只有Commit后,消息才对消费者可见,从而保证了强一致性。

2. 熔断、降级与隔离

  • Resilience4j: 是一个轻量级、无状态的容错库,专为Java 8及以后版本设计。
    • CircuitBreaker(熔断器): 监控下游服务的失败率。当失败率达到阈值(如50%),熔断器状态变为OPEN,所有请求直接失败,不发起远程调用。经过一段waitDurationInOpenState后,状态变为HALF_OPEN,允许少量请求试探,若成功则恢复为CLOSED,否则继续保持OPEN
    • Fallback(降级): 当熔断、超时、重试失败时,执行一个预定义的、安全的备选逻辑,例如返回缓存数据、默认值或友好的错误提示。
    • Bulkhead(隔离): 为不同的下游服务(如库存、物流、支付)分配独立的线程池或信号量,防止一个服务的故障耗尽所有资源,影响其他服务。

3. 分布式锁与Redis的多重角色

  • 分布式锁: 在高并发场景下,使用SET key value NX PX milliseconds命令(Redis 2.6.12+)来实现。NX确保只有key不存在时才设置成功,PX设置过期时间防止死锁。这是防止超卖的关键一步。
  • Redis的其他角色:
    • 缓存: 缓存热门商品信息、用户信息,减少数据库查询。
    • Session共享: 在集群部署时,将用户的Session存储在Redis中,实现无状态服务。
    • 分布式ID生成器: 利用Redis的原子性INCR命令,生成全局唯一的订单号。
    • 延迟队列: 利用ZSET(有序集合)的score特性,实现订单超时未支付自动关闭的功能。

文章作者:王大瓜 出生地:吉林省长春市榆树市 发布日期:2023年10月27日

更多推荐