跳转至

Spring Cloud 微服务生态

🚪 1. Spring Cloud Gateway 的工作原理是什么?

1.1 从请求到转发的四个核心组件

Spring Cloud Gateway 是基于 WebFlux + Netty 的反应式网关,它的整个处理过程可以抽象成一个管道,有三个关键角色:

角色 作用
Route(路由) 定义请求与目标服务之间的映射关系,包含 id、uri、predicates 和 filters。
Predicate(断言) 用 Java 8 的函数式接口来匹配请求,比如路径匹配、Header 匹配、请求参数匹配等。
Filter(过滤器) 对请求或响应做修改,分为两种: GatewayFilter(作用于单个路由)和 GlobalFilter(作用于所有路由)。

一个请求经过网关时,会先遍历所有 Route,找到第一个匹配的 Predicate 集合,然后执行该路由上配置的 Filter 链,最终通过 Netty 客户端转发到下游服务。

1.2 底层执行流程(WebFlux 非阻塞模型)

Client Request → HttpWebHandlerAdapter
→ RoutePredicateHandlerMapping(匹配路由)
→ FilteringWebHandler(组装过滤器链)
→ NettyRoutingFilter(发起代理请求)
→ 响应反向经过 Filter 链 → 返回客户端

整个过程中 没有阻塞线程,完全基于 Reactor 的事件循环。每个请求在 Netty 连接池中获取一个客户端连接,通过 WebClient 发出异步 HTTP 调用。

1.3 扩展点和常见用法

  • 自定义全局过滤器:实现 GlobalFilter 接口,可以全局控制鉴权、日志、流量染色。

  • 路由动态刷新:通过 GatewayControllerEndpoint 或集成配置中心(Nacos/Apollo)动态修改路由,不用重启。

它和 Zuul 1.x 最大的区别是异步非阻塞和函数式编程,从而在面对高并发时消耗更少的线程资源。


🧬 2. Spring Cloud Feign 的底层原理是什么?如何实现负载均衡和重试?

2.1 Feign 的本质:动态代理 + 契约解析

Feign 本身是一个 声明式 HTTP 客户端,它通过 JDK 动态代理,把接口方法调用转换成 HTTP 请求。

@FeignClient(name = "order-service", configuration = FeignConfig.class)
public interface OrderClient {
    @GetMapping("/orders/{id}")
    Order getOrder(@PathVariable Long id);
}

当调用 getOrder(123) 时,背后发生了什么?

  • Feign 在运行时为 OrderClient 生成代理对象。

  • 通过 Contract(默认是 Spring MVC 的 SpringMvcContract)解析方法上的注解,生成 RequestTemplate

  • 代理方法拦截后,将参数值填充到 RequestTemplate,交给 Client 组件执行 HTTP 请求。

  • Spring Cloud Feign 使用 FeignBlockingLoadBalancerClient 或早期的 LoadBalancerFeignClient 替换默认的 HTTP 客户端,集成了负载均衡。

2.2 负载均衡集成

Spring Cloud Feign 依赖 Spring Cloud LoadBalancer(或 Ribbon)实现客户端负载均衡。过程:

  1. 解析 @FeignClient(name = "order-service") 中的服务名。

  2. 通过服务发现组件(Nacos/Consul)获取该服务名对应的所有实例。

  3. 内置的 ReactorServiceInstanceLoadBalancer 根据策略(轮询/随机/权重)选出一个实例。

  4. RequestTemplate 中的目标 URL 替换为选中的真实地址,发起请求。

如果你想替换负载均衡算法,只需要自定义一个 ServiceInstanceListSupplierLoadBalancer Bean。

2.3 重试机制如何实现

Feign 自带了重试功能,但需要显式开启:

  • Feign 内置重试器:在 Feign.Builder 中配置 Retryer.Default,可以设置最大重试次数、初始间隔等。不过这只适用于幂等方法,GET 请求默认是安全的。

  • 配合 Spring Retry:在配置中增加 spring.cloud.loadbalancer.retry.enabled=true,并提供一个 RetryTemplate Bean。这样底层 LoadBalancer 在发现连接失败时会自动重试下一个实例,起到“故障转移”的效果。

spring:
  cloud:
    loadbalancer:
      retry:
        enabled: true
feign:
  client:
    config:
      default:
        retryer: feign.Retryer.Default

注意:只应该重试那些幂等的请求,非幂等请求重试可能导致数据重复或状态异常,需要配合接口幂等设计一起使用。


🛡️ 3. Agent 调用多个微服务,如何用 Gateway 统一做鉴权和限流?

这个问题很贴合业务实际。Agent 服务背后通常会调用模型推理、知识库、工具链等多个微服务,如果每个服务都单独鉴权限流,不仅重复开发,还会导致安全管控零散。Gateway 作为唯一入口,正是统一横切关注点的最佳位置。

3.1 统一鉴权:GlobalFilter 实现 JWT 校验

我们可以写一个全局过滤器,对所有 /api/** 的请求进行 JWT 校验:

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 从请求头中提取 Authorization
        String token = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        // 2. 校验 token,并解析出用户信息
        if (token == null || !TokenUtils.verify(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        // 3. 将用户信息写入请求头,传递给下游服务
        ServerHttpRequest newRequest = exchange.getRequest().mutate()
                .header("X-User-Id", TokenUtils.getUserId(token))
                .build();
        return chain.filter(exchange.mutate().request(newRequest).build());
    }
}
  • 这样下游微服务直接从 X-User-Id 头中获取身份,无需重复解析 JWT。

  • 也可以在这里集成 OAuth2、API Key 等其他认证方式。

3.2 统一限流:Redis + Lua 实现分布式限流

Gateway 内置了 RequestRateLimiter GatewayFilter,一般我们选用 Redis 实现生产级的分布式限流:

spring:
  cloud:
    gateway:
      routes:
        - id: agent-route
          uri: lb://agent-core-service
          predicates:
            - Path=/api/v1/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10   # 每秒允许的请求数
                redis-rate-limiter.burstCapacity: 20   # 突发最大容量
                key-resolver: "#{@userKeyResolver}"     # 基于用户维度的限流

需要注册一个 KeyResolver Bean,例如基于用户 ID 或 IP:

@Bean
public KeyResolver userKeyResolver() {
    return exchange -> Mono.justOrEmpty(
        exchange.getRequest().getHeaders().getFirst("X-User-Id")
    );
}

这样每个用户每秒最多 10 个请求,超过时返回 429 Too Many Requests

3.3 与 Agent 场景深度结合

  • API Key 鉴权:每个 Agent 客户端分配一个 API Key,网关过滤器验证 Key 的有效性和权限范围,无需在每个微服务重复实现。

  • 动态路由:根据请求路径 /agent/tool-* 动态路由到不同的工具微服务,由 Gateway 统一控制。

  • 限流维度:除了用户维度,还可以针对 Agent 调用的 LLM 推理接口设置更严苛的限流(比如每分钟只允许 100 次),防止成本失控。

  • 监控和日志:在过滤器中记录每次请求的 userIdserviceNamecost 到 Micrometer,方便在大盘看到哪个 Agent 在狂刷接口。

把鉴权限流集中到网关,就像给整个 Agent 系统装上了智能门禁和流量阀门——既保证了安全性,又避免了下游服务的防护短板,是所有高流量 AI 应用必须要走的一步。

🔍 4. Nacos 作为注册中心和配置中心的核心原理?与 Eureka 的区别?

Nacos 是阿里巴巴开源的服务发现和配置管理平台,它把 注册中心 和 配置中心 合二为一,核心优势在于对 CAP 理论的灵活支持,以及比 Eureka 更丰富的数据模型。

4.1 注册中心核心原理

  • 数据模型:Nacos 将服务信息抽象为 NamespaceGroupServiceClusterInstance 五层结构,天然支持多环境隔离、同城多活。

  • 注册与发现机制:

  • 服务提供者启动时,通过 OpenAPI 向 Nacos Server 发送 POST /nacos/v1/ns/instance 注册自己,包含 IP、端口、元数据等信息。
  • 服务消费者订阅服务名,Nacos Server 将当前健康的实例列表推送给消费者。后续若实例变化,使用 UDP 推送 + 定时轮询 混合机制保证最终一致。

  • 健康检查:

  • 临时实例 采用客户端主动上报心跳(5s 间隔),Nacos 15s 内未收到心跳则标记不健康,30s 剔除。这符合 AP 模型,适合弹性扩缩的微服务。
  • 持久实例 由 Nacos Server 主动探测(TCP/HTTP),即使客户端断连也不会立即删除,适合数据库等有状态服务,体现 CP 模型。

  • 集群模式与 CAP 切换:

  • Nacos Server 集群使用简化的 Raft 协议(CP 模式)或 Distro 协议(AP 模式)进行数据同步。默认 AP,但可以在注册持久实例或进行配置管理时切换到 CP,这是它最大的灵活性所在。

4.2 配置中心核心原理

  • 配置存储:配置数据保存在内置 Derby(单机)或 MySQL(集群)中,Nacos Server 启动时加载到内存。

  • 配置推送:客户端通过长轮询(Long Polling)监听配置变更。客户端发起监听请求,服务端阻塞 30s 等待配置变化,如果有变化则立即返回新配置;若超时则返回空,客户端马上发起下一次长轮询。这种方式既保证了实时性,又避免了轮询风暴。

  • 动态刷新:配合 Spring Cloud Alibaba Nacos Config,使用 @RefreshScope@Value + RefreshAutoConfiguration,可在运行时刷新 Bean。

4.3 与 Eureka 的核心区别

维度 Nacos Eureka 1.x
功能范围 注册中心 + 配置中心 纯注册中心,配置需配合 Spring Cloud Config
CAP 模型 可切换 AP/CP 严格 AP(优先可用性,牺牲一致性)
健康检查 支持临时实例心跳(AP)和持久实例主动探测(CP) 仅客户端心跳 + 自我保护模式(即便所有实例都不健康也不剔除)
服务变更通知 UDP 推送 + 定时拉取,秒级 客户端增量拉取(30s),变更传播较慢
多数据中心 原生支持命名空间、集群级别就近路由 需要手动配置 Region/Zone
控制台 功能丰富的 Web 管理界面,可实时上下线实例、灰度发布 界面相对简单,操作能力有限
生态 完美融入 Spring Cloud Alibaba,支持阿里系中间件 Netflix 全系,但已进入维护模式

Eureka 的自我保护模式虽然避免了因网络分区导致的大量实例误剔,但在实际生产中也容易掩盖真正下线的服务,导致调用失败。而 Nacos 通过 AP/CP 模式切换 解决了这个难题。


🧪 5. Agent 平台有多个版本的模型服务(GPT-4、Claude、本地模型),如何用微服务架构实现灰度路由和流量切换?

这是一个典型的“多版本服务治理”场景。灰度路由的终极目标是:在不中断线上服务的前提下,将特定流量导向新版本模型,实现可控的体验验证和迁移。

我习惯把整个灰度拆成三个层级:入口标识 → 标签传递 → 路由选择。

5.1 架构设计:基于元数据的服务分组

假设我们有三个模型服务:

  • model-service-gpt4 (v1 稳定版)

  • model-service-gpt4 (v2 新版本,需要灰度)

  • model-service-claude

  • model-service-local-llama

在 Nacos 注册中心,我们可以给每个实例打上丰富的 元数据标签:

spring:
  cloud:
    nacos:
      discovery:
        metadata:
          model-type: gpt4
          version: v1.0
          ability: text,code
          # 灰度标签
          gray-release: stable

灰度节点的标签可以设为 gray-release: canary

5.2 入口标识:如何区分灰度流量

在 API Gateway 层,我们可以根据请求头(如 X-Gray-Tag: trueX-User-ID 白名单)或请求参数来判断该请求是否应该走灰度。

比如,内部测试人员带特殊 Header 的请求被识别为灰度流量:

// Gateway 全局过滤器
if (exchange.getRequest().getHeaders().containsKey("X-Gray-Enable")) {
    // 设置灰度标记,向下游传递
    exchange.getRequest().mutate().header("X-Gray-Release", "canary");
}

5.3 标签传递:保证整条链路灰度一致

如果 Agent 调用链路是 Gateway → 编排服务 → 模型服务,必须确保灰度标记沿着调用链传递。我们可以用 Spring Cloud Sleuth 或者自定义 RequestInterceptor(Feign) / ClientHttpRequestInterceptor(RestTemplate)来透传灰度头。

@Bean
public FeignInterceptor feignInterceptor() {
    return (requestTemplate) -> {
        String grayTag = MDC.get("gray-release"); // 从上下文获取
        if (grayTag != null) {
            requestTemplate.header("X-Gray-Release", grayTag);
        }
    };
}

5.4 路由选择:在负载均衡中实现按标签匹配

最关键的一步在于,服务消费者从 Nacos 拿到所有实例后,需要根据灰度标记选择对应的节点。我们可以自定义 ServiceInstanceListSupplier 来过滤实例。

Spring Cloud LoadBalancer 默认会返回所有健康实例,我们扩展它:

public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        // 从上下文中获取灰度标记
        String grayTag = GrayContextHolder.get();
        List<ServiceInstance> instances = discoveryClient.getInstances("model-service");
        // 如果灰度流量,优先筛选 canary 节点,若无则回退 stable
        if ("canary".equals(grayTag)) {
            List<ServiceInstance> canaryList = instances.stream()
                .filter(i -> "canary".equals(i.getMetadata().get("gray-release")))
                .collect(Collectors.toList());
            if (!canaryList.isEmpty()) {
                return Mono.just(new DefaultResponse(canaryList.get(random.nextInt(canaryList.size()))));
            }
        }
        // 普通流量仅选择 stable 节点
        List<ServiceInstance> stableList = instances.stream()
            .filter(i -> "stable".equals(i.getMetadata().get("gray-release")))
            .collect(Collectors.toList());
        return Mono.just(new DefaultResponse(stableList.get(random.nextInt(stableList.size()))));
    }
}

然后将这个 GrayLoadBalancer 注册为默认的负载均衡器。

5.5 更灵活的手段:利用 Nacos 配置中心动态控制灰度规则

把灰度的 白名单用户、流量比例 等规则放在 Nacos 配置中心,运行时动态刷新,无需重启。例如:

# 配置 dataId: model-gray-rule
gray:
  percentage: 10
  white-list: user123,user456
  model-mapping:
    canary: gpt4-v2

在 Gateway 或负载均衡器中实时读取这些配置,决定是否将请求标记为灰度。甚至可以通过修改 Nacos 配置,在线将灰度比例从 10% 调到 50%,瞬间完成流量切换。

5.6 多模型流量切换的高级玩法

如果需要根据请求内容(比如用户问的是代码问题,还是翻译问题)路由到不同的模型(GPT-4 负责代码,Claude 负责翻译),可以利用 Gateway 的 Path 或 Header 断言 + Nacos 元数据过滤 来做 内容路由。服务名统一叫 model-service,不同模型用不同的集群或元数据区分,实现按能力分发。

整个方案最核心的设计理念是:用元数据给实例打标,用染色标记透传流量,用负载均衡策略实现路由隔离。这套思路不仅适用于模型服务,也同样适用于多版本 API、多地域部署等灰度场景。当你的 Agent 平台需要从单一模型平滑过渡到多模型混合推理时,这种架构能让你在不修改任何业务代码的前提下,动态调整流量,真正做到了“发布即上线,上线可验证,验证再全量”。


2.6 Spring Bean 作用域与依赖注入

1、基础题:Spring Bean 的作用域有哪些?默认是什么?

难度级别:⭐(singleton、prototype、request、session、application)

Answer

  • singleton(默认):整个容器一个实例

  • prototype:每次请求新实例

  • request:每个 HTTP 请求一个实例(Web 环境)

  • session:每个 Session 一个实例(Web 环境)

  • application:每个 ServletContext 一个实例


[补充下]

2、进阶题:Spring 中@Lazy 注解的作用是什么?如何解决循环依赖?

难度级别:⭐⭐⭐⭐(三级缓存、提前暴露、构造器注入失效、@Lazy 原理)

1️⃣ Common Answer

@Lazy 是懒加载,用的时候才创建 Bean。循环依赖的话,Spring 会自动解决,加@Lazy 也可以。但是构造器注入的循环依赖解决不了。

2️⃣ Impressive Answer

  1. @Lazy 的本质:注入一个代理对象,真实 Bean 第一次使用时才创建。可打破循环依赖,也可优化启动速度(大 Bean 懒加载)。

  2. Spring 循环依赖解决方案:setter/字段注入的循环依赖靠三级缓存解决:

  3. 一级缓存:成品 Bean
  4. 二级缓存:早期暴露的 Bean(未填充属性)
  5. 三级缓存:ObjectFactory,用于生成 AOP 代理的早期引用

  6. 构造器注入失效原因:构造器执行时 Bean 还没暴露,无法注入循环依赖。解决:字段/ setter 注入,或其中一个加 @Lazy

  7. 最佳实践:推荐构造器注入(不可变、易测试),循环依赖时局部用@Lazy 打破环,而不是全局改用字段注入。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
结构性 说了@Lazy 和三级缓存但较浅 @Lazy 原理→三级缓存→构造器失效→最佳实践
技术深度 不知道三级缓存的具体作用 清楚三级缓存各自存放什么
实践经验 没说如何优雅解决 推荐构造器注入 + 局部@Lazy 的组合
面试官印象 知道现象 理解 Spring 设计哲学,有架构权衡能力

3、进阶题:@Autowired、@Resource、@Inject 三种注入方式的区别?Spring 推荐哪种?

难度级别:⭐⭐⭐(byType vs byName、JSR-250 vs JSR-330 vs Spring 原生、构造器注入推荐理由)

1️⃣ Common Answer

@Autowired 是 Spring 的注解,按类型注入。@Resource 是 Java 标准注解,按名称注入。@Inject 也是 Java 标准的,和 @Autowired 差不多。Spring 推荐用构造器注入。

2️⃣ Impressive Answer

  1. 三者对比
维度 @Autowired @Resource @Inject
来源 Spring 原生 JSR-250(Java 标准) JSR-330(Java 标准)
匹配策略 先 byType,再 byName 先 byName,再 byType 先 byType,再 byName
required 属性 支持(默认 true) 不支持 不支持
配合限定符 @Qualifier name 属性 @Named
  1. Spring 官方推荐构造器注入,理由:
  2. 不可变性:字段可以声明为 final,保证依赖不会被意外修改
  3. 完整性:对象创建时就注入所有依赖,不会出现"半初始化"状态
  4. 可测试性:单元测试时直接 new 传参,不依赖 Spring 容器
  5. 循环依赖暴露:构造器注入会在启动时暴露循环依赖,而不是运行时才发现

  6. 实践建议:必选依赖用构造器注入,可选依赖用 setter 注入 + @Autowired(required=false);避免字段注入(虽然代码最少,但可测试性最差)。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
结构性 只说了基本区别 对比表格→推荐理由→实践建议
技术深度 不知道匹配策略的差异 清楚 byType/byName 的优先级区别
实践经验 只说了"推荐构造器注入" 给出 4 个具体理由和必选/可选的区分策略
面试官印象 知道有区别 理解设计哲学,有最佳实践意识

4、场景题:Agent 系统中有多个 LLM 客户端实现(OpenAI、Claude、本地模型),如何用 Spring 优雅管理多实现的动态切换?

难度级别:⭐⭐⭐(@Qualifier、策略模式 + Map 注入、@ConditionalOnProperty、SPI 机制)

1️⃣ Common Answer

可以定义一个 LlmClient 接口,然后写多个实现类。用 @Qualifier 注解指定注入哪个。或者用 @ConditionalOnProperty 根据配置决定加载哪个实现。运行时切换的话可以用策略模式。

2️⃣ Impressive Answer

  1. 策略模式 + Map 自动注入(最推荐):
public interface LlmClient {
    String getModelName();
    ChatResponse chat(ChatRequest request);
}

@Service
public class LlmRouter {
    private final Map<String, LlmClient> clientMap;

    // Spring 自动将所有 LlmClient 实现注入到 Map 中
    // key = Bean 名称,value = Bean 实例
    public LlmRouter(Map<String, LlmClient> clientMap) {
        this.clientMap = clientMap;
    }

    public ChatResponse route(String modelName, ChatRequest request) {
        LlmClient client = clientMap.get(modelName);
        if (client == null) {
            throw new UnsupportedModelException("不支持的模型: " + modelName);
        }
        return client.chat(request);
    }
}
  1. 动态切换:路由策略存 Nacos 配置中心,通过 @RefreshScope 实现运行时切换默认模型,无需重启。

  2. 扩展性设计:新增模型只需实现 LlmClient 接口并注册为 Spring Bean,路由器自动感知,零修改扩展(开闭原则)。

  3. 配合 @ConditionalOnProperty:按环境启停,如测试环境不加载 GPT-4 客户端(节省成本),生产环境全量加载。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
结构性 列举了几种方式但没有组合 Map 注入→动态切换→扩展性→环境隔离,完整方案
技术深度 不知道 Map 自动注入的特性 利用 Spring 的 Map 自动收集所有实现
实践经验 没考虑运行时切换 配合 Nacos + @RefreshScope 实现热切换
面试官印象 知道策略模式 能用 Spring 特性优雅实现,有架构设计能力

2.7 响应式编程 Spring WebFlux

1、基础题:Spring WebFlux 和 Spring MVC 有什么区别?

难度级别:⭐⭐(阻塞 vs 非阻塞、Reactor、背压、适用场景)

Answer

维度 Spring MVC Spring WebFlux
编程模型 阻塞式(Servlet) 非阻塞响应式(Reactor)
容器 Tomcat/Jetty Netty/Undertow
数据流 Request → Response Flux/Mono 响应流
适用场景 传统 CRUD、IO 密集型 高并发、流式处理、SSE

2、进阶题:WebFlux 中的 Mono 和 Flux 是什么?如何处理异常?

难度级别:⭐⭐⭐(Reactor、onErrorResume、onErrorReturn、全局异常处理)

1️⃣ Common Answer

Mono 是返回 0 或 1 个元素,Flux 是返回多个。异常处理可以用 onErrorReturn 返回默认值,或者用 onErrorResume 继续执行。也可以在全局用@ControllerAdvice 处理。

2️⃣ Impressive Answer

  1. Mono/Flux 本质:Reactor 的响应式类型,Mono 表示 0/1 个元素(如 Optional),Flux 表示 0~N 个元素(如 List)。惰性执行,订阅后才触发。

  2. 异常处理策略

  3. 局部处理onErrorReturn(默认值)onErrorResume(恢复流)onErrorMap(转换异常)
  4. 全局处理:实现 WebExceptionHandler 或用@ControllerAdvice + @ExceptionHandler(WebFlux 版本)
  5. finally 语义doFinally(SignalType→回调),无论成功/失败都执行

  6. 背压(Backpressure):下游控制上游生产速率,避免内存溢出。Reactor 默认策略是 onNext 拉动式,Flux 会自动处理。

  7. Agent 场景:流式输出(SSE)用 Flux<ServerSentEvent<String>>;多工具并行调用用 Flux.mergeSequential() 保持顺序,Flux.combineLatest() 合并结果。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
结构性 说了 Mono/Flux 和几种处理方式 类型定义→异常处理→背压→Agent 场景
技术深度 不知道背压的概念 能解释背压的作用和 Reactor 的实现
实践经验 没有场景化应用 给出 Agent 流式输出和多工具并发的方案
面试官印象 会用 WebFlux 理解响应式编程范式,能解决复杂场景


4、进阶题:WebFlux 的线程模型是什么?为什么不能在响应式链中调用阻塞 API?

难度级别:⭐⭐⭐(EventLoop 模型、Scheduler 调度、publishOn/subscribeOn、阻塞检测)

1️⃣ Common Answer

WebFlux 用的是 Netty 的 EventLoop 线程模型,线程数很少,默认是 CPU 核心数。如果在响应式链里调用阻塞 API,会把 EventLoop 线程阻塞住,导致其他请求也处理不了。所以阻塞操作要放到单独的线程池里。

2️⃣ Impressive Answer

  1. EventLoop 线程模型:Netty 默认创建 CPU 核心数 个 EventLoop 线程,每个线程负责多个 Channel 的 I/O 事件。所有请求共享这几个线程,靠非阻塞 I/O + 事件驱动实现高并发。

  2. 阻塞的致命影响:假设 4 核 CPU = 4 个 EventLoop 线程,一个阻塞调用占住 1 个线程 500ms,就意味着 25% 的处理能力被浪费。如果 4 个线程都被阻塞,整个服务完全停止响应

  3. 正确处理阻塞调用

// 错误:直接在响应式链中调用阻塞 API
Mono.just(request)
    .map(req -> jdbcTemplate.query(...));  // 阻塞 EventLoop!

// 正确:用 subscribeOn 切换到阻塞线程池
Mono.fromCallable(() -> jdbcTemplate.query(...))
    .subscribeOn(Schedulers.boundedElastic());  // 切到弹性线程池
  1. publishOn vs subscribeOnsubscribeOn 影响整个链的订阅线程(从源头切换);publishOn 影响下游操作的执行线程(从当前位置切换)。阻塞调用用 subscribeOn,后续处理切回用 publishOn

3️⃣ Key Differences

维度 Common Answer Impressive Answer
结构性 知道不能阻塞但说不清后果 线程模型→阻塞影响量化→正确写法→调度器区别
技术深度 不知道 publishOn 和 subscribeOn 的区别 清楚两者的作用范围差异
实践经验 只说了"放到线程池" 有具体的代码对比(错误 vs 正确)
面试官印象 知道原则 能量化阻塞影响,知道如何正确处理

5、场景题:Agent 需要同时调用 3 个外部 API(搜索、天气、数据库),如何用 WebFlux 实现并发调用并合并结果?

难度级别:⭐⭐⭐(Mono.zip、Flux.merge、超时控制、fallback 降级)

1️⃣ Common Answer

可以用 Mono.zip 把三个 Mono 合并,它们会并发执行。或者用 Flux.merge 合并多个流。加个 timeout 设置超时时间,超时了就返回默认值。

2️⃣ Impressive Answer

  1. Mono.zip 并发合并
public Mono<AgentContext> gatherContext(String query) {
    Mono<SearchResult> searchMono = searchClient.search(query)
        .timeout(Duration.ofSeconds(3))
        .onErrorResume(e -> Mono.just(SearchResult.empty()));

    Mono<WeatherInfo> weatherMono = weatherClient.getWeather(query)
        .timeout(Duration.ofSeconds(2))
        .onErrorResume(e -> Mono.just(WeatherInfo.unavailable()));

    Mono<DbResult> dbMono = Mono.fromCallable(() -> dbService.query(query))
        .subscribeOn(Schedulers.boundedElastic())
        .timeout(Duration.ofSeconds(5))
        .onErrorResume(e -> Mono.just(DbResult.empty()));

    return Mono.zip(searchMono, weatherMono, dbMono)
        .map(tuple -> AgentContext.builder()
            .search(tuple.getT1())
            .weather(tuple.getT2())
            .dbResult(tuple.getT3())
            .build());
}
  1. 关键设计点
  2. 独立超时:每个调用设置不同的超时时间(搜索 3s、天气 2s、DB 5s),而非统一超时
  3. 独立降级onErrorResume 返回空结果而非抛异常,一个失败不影响其他
  4. 阻塞隔离:DB 查询是阻塞的,用 subscribeOn(Schedulers.boundedElastic()) 隔离到弹性线程池

  5. 进阶优化:如果某个 API 非必需(如天气),可以用 Mono.zipDelayError() 延迟错误处理,优先返回已完成的结果;配合 Mono.firstWithSignal() 实现"谁先返回用谁"的竞速模式。

3️⃣ Key Differences

维度 Common Answer Impressive Answer
结构性 只说了 Mono.zip 完整代码→独立超时/降级→阻塞隔离→进阶优化
技术深度 不知道阻塞调用需要隔离 DB 查询用 subscribeOn 切线程池
实践经验 统一超时 每个调用独立超时和降级策略
面试官印象 知道 API 有生产级的并发编排经验