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 跨服务传递
灰度发布三种方案
相关推荐
phltxy39 分钟前
Spring AI 可观测性与 Zipkin 实战
java·人工智能·spring
Asize1 小时前
JavaScript 数据类型解析:从 null 与 undefined 的迷思到栈堆内存真相
前端·javascript·面试
LDX前端校草1 小时前
position属性值及用法
前端·javascript·面试
凡人叶枫3 小时前
Effective C++ 条款35:考虑 virtual 函数以外的其他选择
java·c++·spring
hzhsec3 小时前
启明星辰(安全服务实习生)面试题
网络安全·面试
Wyc724094 小时前
Seata
spring cloud
AI人工智能+电脑小能手4 小时前
【大白话说Java面试题 第115题】【并发篇】第15题:说一下悲观锁和乐观锁的区别?
java·开发语言·面试
无聊的老谢4 小时前
基于 Spring Batch 的电信 MR 数据亿级记录清洗实战
spring·batch·mr
Full Stack Developme4 小时前
Spring Integration 教程
java·后端·spring
星辰_mya4 小时前
autowired和resource区别
java·后端·spring·架构·原理