Spring Cloud 专家级面试题库

格式:知识点原理 → 面试表达模板 → 追问应对


一、注册中心原理

Q1. Eureka、Nacos、Zookeeper 的 CAP 选择及原因?

知识点讲解

CAP 定理:一致性©、可用性(A)、分区容错性§ 三者只能同时满足两个。网络分区§在分布式系统中必须容忍,所以只能选 CP 或 AP。

text 复制代码
Eureka → AP(可用性优先):
  设计理念:宁可返回旧数据,也不拒绝服务
  自我保护机制:心跳数低于85%阈值时,停止剔除任何实例
  适合场景:对注册信息短暂不一致可接受的微服务

Zookeeper → CP(一致性优先):
  原理:基于 Raft/ZAB 协议选主,写操作必须经 Leader 多数派确认
  代价:Leader 选举期间(约30秒)无法提供服务
  适合场景:需要强一致的分布式协调(分布式锁、配置)

Nacos → AP + CP 可切换:
  临时实例(默认):Distro 协议(AP),客户端心跳,适合微服务
  永久实例:Raft 协议(CP),服务端主动探测,适合基础设施

面试表达模板

三者的 CAP 选择反映了不同的设计权衡:Eureka 选 AP,认为注册中心短暂数据不一致比不可用危害更小;Zookeeper 选 CP,牺牲可用性保证一致性,但选举期间有服务中断风险;Nacos 最灵活,临时实例用 AP 的 Distro 协议,永久实例支持 CP 的 Raft 协议,这也是 Nacos 在国内成为主流的重要原因。

追问:为什么注册中心推荐 AP 不推荐 CP?

即使消费者拿到了稍旧的服务列表(某个实例已下线),最多导致一次调用失败并触发重试,代价可控。但如果注册中心不可用(CP 选举期间),消费者完全无法获取服务列表,整个调用链断开,危害远大于前者。


Q2. Nacos 服务注册后,消费者如何感知服务变化?

知识点讲解

text 复制代码
Nacos 2.x 实例变化感知流程:

提供者端:
  实例启动 → gRPC 长连接注册到 Nacos Server
  心跳:通过 gRPC 连接维持(默认5秒一次)
  宕机/断连:Server 侧自动检测,标记实例不健康

消费者端感知变化(两种机制):
  1. 主动推送(Nacos 2.x gRPC):
     Server 检测到实例变化 → 通过 gRPC 长连接主动推送给所有订阅该服务的消费者
     消费者收到推送 → 更新本地缓存 → 下次调用使用新列表

  2. 定时拉取兜底(30秒):
     防止推送丢失,消费者每30秒主动拉取一次

本地缓存(服务降级):
  最后一次成功的服务列表写入磁盘:
  ~/.nacos/naming/{namespace}/{group}@@{serviceName}
  Nacos 全部宕机时,从磁盘读取快照,保证服务仍可运行

示例:验证本地缓存文件

bash 复制代码
# 查看 Nacos 客户端本地缓存
ls ~/.nacos/naming/
# 例:public/DEFAULT_GROUP@@user-service 文件内容是最后一次拉取的实例列表 JSON

面试表达模板

Nacos 2.x 采用 gRPC 长连接,实例变化时 Server 主动推送给订阅者,毫秒级感知(1.x 是客户端轮询,延迟可达30秒)。消费者本地维护缓存,并写到磁盘快照,即使 Nacos 全部宕机,服务也能用快照继续运行。


二、OpenFeign 原理

Q3. OpenFeign 的底层调用原理?

知识点讲解

text 复制代码
Feign 代理生成过程:
  @EnableFeignClients → FeignClientsRegistrar
      → 扫描 @FeignClient → 注册 FeignClientFactoryBean
      → getObject() 时创建 JDK 动态代理

一次 Feign 调用流程:

方法调用
  ↓
InvocationHandler.invoke()
  ↓
Contract 解析注解(@GetMapping → HTTP GET)
  ↓
Encoder 序列化请求参数(Java 对象 → JSON/Query)
  ↓
RequestInterceptor 链(添加 Token、TraceId)
  ↓
LoadBalancer 选择实例(Nacos 缓存 → 轮询/随机)
  ↓
HTTP Client 发起请求(默认 HttpURLConnection)
  ↓
Decoder 反序列化响应(JSON → Java 对象)
  ↓
ErrorDecoder 处理非2xx响应(可触发 Fallback)

生产优化:替换 HTTP 客户端

xml 复制代码
<!-- 默认 HttpURLConnection 无连接池,高并发性能差 -->
<!-- 替换为 Apache HttpClient 5(有连接池)-->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hc5</artifactId>
</dependency>
yaml 复制代码
spring:
  cloud:
    openfeign:
      httpclient:
        hc5:
          enabled: true
        max-connections: 200          # 全局最大连接数
        max-connections-per-route: 50 # 每个目标服务最大连接数

面试表达模板

Feign 通过 JDK 动态代理为接口生成代理对象,调用方法时将注解信息(@GetMapping、@PathVariable 等)转换为 HTTP 请求,经过拦截器链(附加 Token/TraceId)→ LoadBalancer 选实例 → HTTP Client 发请求 → Decoder 反序列化。生产建议替换默认的 HttpURLConnection 为 Apache HC5,原因是前者无连接池,每次请求新建 TCP 连接,高并发下性能极差。

追问:Feign 调用时 Token 丢失怎么处理?

java 复制代码
// 通过 RequestInterceptor 透传当前请求的 Header
@Component
public class FeignTokenInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attrs =
            (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attrs != null) {
            String token = attrs.getRequest().getHeader("Authorization");
            if (token != null) {
                template.header("Authorization", token);
            }
        }
    }
}

原因:Feign 调用在独立线程池中执行,RequestContextHolder 用 ThreadLocal 存储请求上下文,不同线程无法共享。RequestInterceptor 在主线程发起调用前执行,此时 ThreadLocal 仍可访问,所以能拿到 Token。


三、Gateway 原理

Q4. Spring Cloud Gateway 的请求处理流程?

知识点讲解

text 复制代码
Gateway 基于 WebFlux(Reactor + Netty),完整流程:

HTTP Request(Netty 接收)
    ↓
DispatcherHandler(WebFlux 核心分发器)
    ↓
RoutePredicateHandlerMapping
    └── 遍历所有 Route,依次评估 Predicate(断言)
        Predicate 全部匹配 → 选中该 Route
    ↓
FilteringWebHandler
    └── 合并 GlobalFilter + 当前 Route 的 GatewayFilter
        按 Order 排序(Order 越小越先执行)
    ↓
Filter Chain Pre 阶段(鉴权、限流、日志记录开始时间)
    ↓
NettyRoutingFilter(发起对后端服务的 HTTP 请求)
    ↓
Filter Chain Post 阶段(修改响应、记录日志耗时)
    ↓
响应给客户端

Predicate 匹配顺序的坑

yaml 复制代码
# ❌ 错误:范围大的路由放前面,所有请求都匹配第一条,后面的路由永远不生效
routes:
  - id: catch-all
    uri: lb://default-service
    predicates:
      - Path=/**          # 匹配所有
  - id: user-route
    uri: lb://user-service
    predicates:
      - Path=/api/users/**

# ✅ 正确:精确路由放前面,范围大的路由放后面
routes:
  - id: user-route
    uri: lb://user-service
    predicates:
      - Path=/api/users/**  # 先匹配精确的
  - id: catch-all
    uri: lb://default-service
    predicates:
      - Path=/**            # 兜底,放最后

面试表达模板

Gateway 基于 WebFlux 非阻塞模型,核心流程是:Netty 接收请求 → RoutePredicateHandlerMapping 按 Predicate 找到匹配路由 → 合并 GlobalFilter 和路由 GatewayFilter(按 Order 排序)→ Pre 阶段过滤(鉴权/限流)→ 转发后端 → Post 阶段过滤(响应处理/记录耗时)。重要细节:路由匹配按定义顺序,精确路由要放在范围大的路由前面;GlobalFilter 对所有路由生效,GatewayFilter 只对配置的路由生效。


四、熔断限流

Q5. Sentinel 的熔断状态机和恢复机制?

知识点讲解

text 复制代码
三种状态:

CLOSED(关闭,正常放行)
  → 统计窗口内触发阈值(慢调用比例/异常比例/异常数)
  → 进入 OPEN

OPEN(熔断,直接返回 BlockException,不调用后端)
  → 经过休眠窗口时间(timeWindow 秒)
  → 进入 HALF-OPEN

HALF-OPEN(半开,放行一个探测请求)
  → 探测请求成功 → 回到 CLOSED
  → 探测请求失败 → 重新进入 OPEN(继续休眠)

三种熔断策略对比

策略 触发条件 适用场景
慢调用比例 响应时间 > 阈值的请求比例超标 下游接口变慢
异常比例 异常请求占比超标 下游接口不稳定
异常数 统计窗口内异常总数超标 低流量场景

Sentinel Dashboard 配置示例(熔断规则)

text 复制代码
资源名: getUserById
熔断策略: 慢调用比例
最大 RT: 1000ms         ← 超过1秒算慢调用
比例阈值: 0.5           ← 50%以上请求是慢调用才熔断
熔断时长: 10s           ← 熔断10秒后进入半开
最小请求数: 5           ← 统计窗口内至少5个请求才触发
统计时长: 10000ms       ← 10秒统计窗口

规则持久化到 Nacos(生产必备)

yaml 复制代码
# 服务重启后规则不丢失
spring:
  cloud:
    sentinel:
      datasource:
        flow-rules:
          nacos:
            server-addr: localhost:8848
            data-id: ${spring.application.name}-flow-rules
            group-id: SENTINEL_GROUP
            data-type: json
            rule-type: flow

面试表达模板

Sentinel 熔断有三种状态:CLOSED(正常)→ OPEN(熔断,快速失败)→ HALF-OPEN(半开,放一个探测请求)。触发条件支持慢调用比例、异常比例、异常数三种策略。生产中必须做规则持久化(推送到 Nacos),否则服务重启后规则丢失,控制台配置的规则全部失效。


Q6. Sentinel 和 Hystrix 的核心区别?

知识点讲解

text 复制代码
Hystrix(已停止维护):
  隔离模型:线程池隔离(每个资源一个独立线程池)
  优点:完全隔离,一个资源的阻塞不影响其他
  缺点:线程切换开销大(每次调用都需要线程切换),资源消耗高
  熔断:滑动窗口统计,粒度较粗

Sentinel(阿里,活跃维护):
  隔离模型:信号量隔离(限制并发线程数,在调用线程执行)
  优点:无线程切换开销,性能更好
  缺点:调用线程被占用时无法立即超时(需配合超时设置)
  熔断:支持慢调用比例、异常比例、异常数三种策略
  额外能力:实时 Dashboard、热点参数限流、系统自适应保护

面试表达模板

核心区别在隔离模型:Hystrix 用线程池隔离,每个资源分配独立线程池,代价是线程切换开销大;Sentinel 用信号量隔离,限制并发线程数,无切换开销,性能更好。另外 Sentinel 的熔断策略更丰富(三种),有实时 Dashboard,规则动态推送,这也是国内几乎全面切换 Sentinel 的原因。Hystrix 已停止维护,新项目不建议使用。


五、分布式事务

Q7. Seata AT 模式原理和适用场景?

知识点讲解

text 复制代码
AT 模式核心:两阶段提交的改进版

Phase 1(执行阶段):
  RM 拦截业务 SQL → 生成 before image(快照修改前数据)
  执行业务 SQL → 生成 after image(快照修改后数据)
  将 before/after image 存入 undo_log 表
  提交本地事务(含 undo_log,一起提交)
  向 TC 汇报分支事务结果

Phase 2(提交/回滚):
  全部成功 → TC 通知各 RM 删除 undo_log(异步,不影响业务)
  任一失败 → TC 通知各 RM 用 undo_log 生成反向 SQL 执行回滚

与传统 2PC 对比:
  传统 2PC:Phase 1 持有全局锁直到 Phase 2 完成(锁持有时间长)
  Seata AT:Phase 1 直接提交本地事务(锁释放),仅用 undo_log 保证可回滚
             → 全局锁持有时间极短,并发性能大幅提升

必须要有 undo_log 表

sql 复制代码
-- 每个参与事务的数据库都要建此表
CREATE TABLE undo_log (
    id           BIGINT AUTO_INCREMENT PRIMARY KEY,
    branch_id    BIGINT NOT NULL,
    xid          VARCHAR(128) NOT NULL,
    context      VARCHAR(128) NOT NULL,
    rollback_info LONGBLOB NOT NULL,
    log_status   INT NOT NULL,
    log_created  DATETIME NOT NULL,
    log_modified DATETIME NOT NULL,
    UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB;

使用示例

java 复制代码
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderRepository orderRepository;
    private final ProductFeignClient productClient;
    private final AccountFeignClient accountClient;

    // @GlobalTransactional = 开启 Seata 全局事务
    // 内部任何步骤(包括 Feign 调用的远程服务)失败 → 全部自动回滚
    @GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 30000)
    public Order createOrder(CreateOrderDTO dto) {
        Order order = orderRepository.save(buildOrder(dto));  // 步骤1:本地
        productClient.deductStock(dto.getProductId(), dto.getQty());  // 步骤2:远程
        accountClient.deductBalance(dto.getUserId(), order.getAmount()); // 步骤3:远程
        return order;
        // 任何步骤抛异常 → Seata 协调所有服务回滚
    }
}

面试表达模板

Seata AT 基于改进的两阶段提交:Phase 1 执行业务 SQL 的同时记录 undo_log(前后镜像),然后直接提交本地事务(不持锁等待);Phase 2 成功则删 undo_log,失败则用 undo_log 生成反向 SQL 回滚。相比传统 2PC,全局锁持有时间大幅缩短。适合大多数业务场景(对性能有极致要求的资金场景用 TCC)。


Q8. 什么时候用 Seata,什么时候用消息队列?

知识点讲解

text 复制代码
选型判断树:

这个操作需要"立即一致"吗?
  是 → 用 Seata(强一致/最终强一致)
    性能敏感?
      是 → TCC 模式
      否 → AT 模式(推荐,无侵入)

  否(允许短暂不一致)→ 用消息队列(最终一致)
    需要可靠投递?
      是 → 本地消息表 + MQ(保证消息不丢失)
      否 → 直接发 MQ

实际场景映射:
  Seata:下单扣库存+扣余额(必须同时成功/失败)
  MQ:下单成功后发优惠券、更新积分、发邮件(允许延迟)

面试表达模板

核心判断是"是否需要实时强一致":扣款/扣库存这类操作不允许任何不一致,用 Seata;发送通知/更新积分/刷新缓存这类操作允许短暂不一致,用消息队列(性能更好,吞吐更高)。原则是"能用最终一致就不用强一致",因为 Seata 引入了全局锁和网络开销,会降低系统吞吐量。


六、链路追踪

Q9. 分布式链路追踪的 TraceId 如何跨服务传递?

知识点讲解

text 复制代码
Micrometer Tracing(Spring Boot 3.x 推荐)工作原理:

请求进入 Gateway:
  生成 TraceId(全局唯一)、SpanId(当前节点)
  注入到 HTTP 请求头(B3 协议格式):
    X-B3-TraceId: abc123def456
    X-B3-SpanId: 111111
    X-B3-ParentSpanId: (空,因为是起点)
    X-B3-Sampled: 1

下游服务(user-service)接收请求:
  读取请求头中的 TraceId → 继承(不生成新的)
  生成新的 SpanId(自己的节点标识)
  将 TraceId/SpanId 注入 MDC:可以在日志中打印
  Feign 调用更下游时:自动将 TraceId 放入下游请求头

日志中打印 TraceId:
  %X{traceId}  ← Logback MDC 占位符

配置示例

xml 复制代码
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<dependency>
    <groupId>io.zipkin.reporter2</groupId>
    <artifactId>zipkin-reporter-brave</artifactId>
</dependency>
yaml 复制代码
management:
  tracing:
    sampling:
      probability: 0.1  # 生产环境采样10%(全采样性能影响大)
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans

logging:
  pattern:
    console: "%d{HH:mm:ss} [%thread] %-5level [TraceId:%X{traceId}] %msg%n"

面试表达模板

链路追踪通过在 HTTP 请求头中携带 TraceId(B3 协议)实现跨服务传递。每个服务接收请求时读取 TraceId 并继承,生成自己的 SpanId,然后将 TraceId 注入 MDC 供日志打印,调用下游时自动将 TraceId 放入请求头。这样所有服务的日志都有相同的 TraceId,可以在日志系统(ELK)中用 TraceId 过滤出一次请求的完整日志链。Feign 客户端会自动传播 TraceId,无需手动处理。


七、专家级追问

Q10. 微服务中如何实现灰度发布?

知识点讲解

text 复制代码
方案一:Nacos 权重(实例级别)
  新版本实例设置低权重(如5),旧版本高权重(95)
  LoadBalancer 按权重分配流量
  优点:简单;缺点:不能按用户/Header 定向灰度

方案二:Gateway + Header 路由(用户级别)
  灰度用户请求携带 Header:X-Gray: true
  Gateway 识别 Header → 路由到灰度服务集群
  适合:VIP 用户先体验新功能

方案三:Gateway Weight 断言(流量比例)
  配置两个路由,分别权重90%和10%
  适合:按比例切流,不关心具体是哪个用户

方案二完整实现

java 复制代码
// Gateway 灰度路由过滤器
@Component
public class GrayRouteFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String grayHeader = exchange.getRequest().getHeaders().getFirst("X-Gray");
        if ("true".equals(grayHeader)) {
            // 将目标服务名改为灰度版本
            URI grayUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
                .host("user-service-gray")  // 灰度服务名(Nacos 中注册)
                .build().toUri();
            ServerHttpRequest request = exchange.getRequest().mutate().uri(grayUri).build();
            return chain.filter(exchange.mutate().request(request).build());
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() { return -50; }
}

面试表达模板

灰度发布有三个维度:①实例级别(Nacos 权重,按流量比例);②用户级别(Gateway 识别 Header,指定用户走新版本);③流量比例级别(Gateway Weight 断言,按百分比切流)。生产中常组合使用:先用 Nacos 权重做5%流量灰度,观察指标无异常后,用 Weight 断言逐步扩大到100%,最终下线旧版本实例。


八、面试自测表

题目 能否讲清原理 能否结合项目 能否应对追问
Eureka/Nacos/ZK CAP 选择
Nacos 服务变化感知机制
Feign 代理生成 + 调用流程
Token 丢失问题及解决
Gateway 请求处理流程
Predicate 匹配顺序问题
Sentinel 三状态机
Seata AT 两阶段原理
Seata vs MQ 选型判断
TraceId 跨服务传递
灰度发布三种方案
相关推荐
weixin_426184971 小时前
系统设计面试009:设计 Facebook 新闻动态(News Feed)
面试
拾贰_C2 小时前
【OpenClaw | openai | QQ】 配置QQ qot机器人
运维·人工智能·ubuntu·面试·prompt
空中海2 小时前
Spring Boot 专家级面试题库
spring boot·后端·面试
直奔標竿2 小时前
SpringAI + RAG + MCP + Agent 零基础全栈实战(完结篇)| 27课完整汇总,Java开发者AI转型必看
java·开发语言·人工智能·spring boot·后端·spring
云烟成雨TD2 小时前
Spring AI 1.x 系列【31】向量数据库:进阶使用指南
java·人工智能·spring
counting money4 小时前
Spring框架基础(依赖注入-全注解形式)
java·数据库·spring
counting money4 小时前
Spring框架基础(依赖注入-半注解形式)
java·后端·spring
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题】【Java基础篇】第20题:HashMap在计算index的时候,为什么要对数组长度做减1操作
java·开发语言·数据结构·后端·面试·哈希算法·hash-index
逻辑驱动的ken5 小时前
Java高频面试考点场景题17
开发语言·jvm·面试·求职招聘·春招