格式:知识点原理 → 面试表达模板 → 追问应对
一、注册中心原理
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 跨服务传递 | |||
| 灰度发布三种方案 |