本轮 SpringCloud微服务架构面试题库(带答案)
生成时间:2026-05-23 15:51
【面试题】1. Nacos作为注册中心,如何保证服务发现的实时性?心跳机制和健康检查是怎么配合的?
【标准答案】
这个问题要分三层来答。
第一层,心跳机制。 Nacos客户端默认每5秒发送一次心跳,服务端收到心跳后更新实例的最后心跳时间。如果15秒没收到心跳,标记为不健康;30秒还没收到,直接剔除。这个时间窗口是可以配置的,根据业务容忍度调整。
第二层,健康检查。 Nacos有两种模式:临时实例用客户端心跳,永久实例用服务端主动探测。临时实例适合微服务场景——服务挂了,自动剔除;永久实例适合数据库这种需要人工介入的基础设施。
第三层,实时性保障。 客户端本地有服务列表缓存,同时订阅服务变更事件。当服务端有实例上下线,会主动推送给订阅的客户端。这是推拉结合:心跳保活是拉,变更推送是推。所以正常情况下,服务发现是毫秒级的。
面试加分点:提到Nacos 2.x之后用gRPC长连接替代了HTTP短轮询,实时性更好了。
【面试题】2. Ribbon和Spring Cloud LoadBalancer有什么区别?生产环境怎么选?
【标准答案】
核心区别就一句话:Ribbon已死,LoadBalancer是官方接班人。
Ribbon是Netflix家的,2018年进入维护模式,不再新增功能。Spring Cloud 2020版本之后,官方用LoadBalancer替换了Ribbon作为默认负载均衡组件。
功能上对比:
- Ribbon内置7种策略:轮询、随机、加权、最小并发、一致性哈希等,功能完善但代码老旧
- LoadBalancer初期功能简单,现在也支持轮询、随机、加权,但一致性哈希这类高级策略需要自己扩展
生产选型建议:
- 新项目直接用LoadBalancer,别用Ribbon,跟官方走不吃亏
- 老项目如果用了Ribbon的高级策略,迁移成本要评估——可以先共存,逐步替换
- 如果需要自定义负载均衡策略,LoadBalancer的Reactive API更现代,写起来更顺手
代码迁移点: @LoadBalanced注解不用改,但Ribbon的IRule要换成LoadBalancer的ReactorServiceInstanceLoadBalancer接口实现。
【面试题】3. OpenFeign调用超时了怎么排查?超时配置的优先级是什么?
【标准答案】
排查思路三步走:
第一步,确认超时来源。 看日志是connect timeout还是read timeout。connect timeout是连不上服务端,可能是服务挂了或网络问题;read timeout是连上了但响应慢,可能是业务逻辑慢或下游依赖慢。
第二步,检查配置链。 OpenFeign的超时配置分两层:Feign层和Ribbon/LoadBalancer层。优先级是:Feign配置 > Ribbon配置 > 默认值。
优先级具体规则:
# Feign层配置(优先级最高)
feign.client.config.default.connectTimeout: 5000
feign.client.config.default.readTimeout: 10000
# Ribbon层配置(Feign没配时生效)
ribbon.ConnectTimeout: 3000
ribbon.ReadTimeout: 6000第三步,分布式场景还要看网关。 如果经过Spring Cloud Gateway,还要检查Gateway的超时配置,它也有自己的connect-timeout和response-timeout。
实际案例: 我之前遇到一个坑,Feign和Ribbon都配了超时,但有个服务老超时。排查发现是Gateway配置了30秒超时,但某个慢接口需要50秒。解决方案是针对特定接口单独配置超时,而不是全局放大。
【面试题】4. Sentinel和Hystrix的熔断机制有什么区别?生产环境怎么选?
【标准答案】
先说结论:新项目用Sentinel,老项目逐步迁移,Hystrix已停止维护。
核心区别对比:
| 维度 | Hystrix | Sentinel |
|---|---|---|
| 维护状态 | 2018年停止维护 | 阿里持续维护 |
| 熔断策略 | 错误比例、错误数量 | 错误比例、错误数量、慢调用比例 |
| 限流能力 | 仅有信号量隔离 | QPS限流、线程池隔离、热点参数限流 |
| 控制台 | Hystrix Dashboard(简陋) | Sentinel Dashboard(功能丰富) |
| 动态规则 | 需要自己实现 | 开箱即用,支持多种数据源 |
熔断状态机,两者一致: 关闭→开启→半开→关闭。半开状态放一个请求试探,成功就关闭,失败继续开启。
生产选型建议:
- 新项目无脑Sentinel,生态完善,控制台好用,规则动态下发
- 老项目如果Hystrix用得稳,迁移收益要评估——但长期看,技术债要还
- 如果只需要熔断,不需要限流,Resilience4j也是个轻量选择
Sentinel的杀手锏: 热点参数限流——比如限制"查询某个热门商品"的QPS,而不是限制整个接口。这个Hystrix做不到。
【面试题】5. Spring Cloud Gateway如何实现统一的JWT鉴权?Token校验失败怎么返回友好的错误信息?
【标准答案】
实现方案分三块:
第一块,过滤器链鉴权。 写一个AuthGlobalFilter实现GlobalFilter和Ordered接口,在filter方法里解析JWT。
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !validateToken(token)) {
return onError(exchange, "未授权访问", HttpStatus.UNAUTHORIZED);
}
// 解析成功,将用户信息放入header透传给下游
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", getUserIdFromToken(token))
.build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() { return -100; } // 优先级要高
}第二块,友好错误返回。 不要返回默认的401页面,要返回JSON格式的错误信息:
private Mono<Void> onError(ServerWebExchange exchange, String msg, HttpStatus status) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(status);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = "{\"code\":401,\"message\":\"" + msg + "\"}";
return response.writeWith(Mono.just(response.bufferFactory().wrap(body.getBytes())));
}第三块,白名单配置。 登录接口、健康检查接口不需要鉴权,在配置中心或数据库维护白名单路径,过滤器里判断跳过。
面试加分点: 提到Token刷新机制——Access Token短期(如30分钟),Refresh Token长期(如7天),过期前用Refresh Token换取新的Access Token,无感刷新。
【面试题】6. Nacos Config配置中心如何实现配置的热更新?应用收到配置变更后怎么自动刷新?
【标准答案】
热更新原理分三步:
第一步,长轮询监听。 Nacos客户端通过长轮询监听配置变更,服务端有变更立即推送。2.x版本用gRPC长连接,实时性比HTTP长轮询更好。
第二步,配置变更推送。 服务端配置变更后,推送给所有订阅的客户端。客户端收到新配置,更新本地缓存,然后触发变更事件。
第三步,自动刷新机制。 这是面试关键点,两种方式:
方式一:@RefreshScope注解
@RestController
@RefreshScope // 关键注解
public class ConfigController {
@Value("${app.config}")
private String config;
@GetMapping("/config")
public String getConfig() {
return config;
}
}配置变更后,Spring Cloud会销毁并重建这个Bean,重新注入最新配置。
方式二:监听配置变更事件
@Component
public class ConfigChangeListener {
@NacosConfigListener(dataId = "app.yml", groupId = "DEFAULT_GROUP")
public void onConfigChange(String newConfig) {
// 自定义处理逻辑
log.info("配置已更新: {}", newConfig);
}
}生产注意事项:
@RefreshScope有性能开销,Bean会重建,别滥用- 复杂配置变更可能需要重启应用才能完全生效,比如数据库连接池参数
- 配置回滚:Nacos支持配置历史版本,可以一键回滚
【面试题】7. 分布式链路追踪中,TraceId和SpanId是怎么传递的?跨线程调用时如何保证链路不断?
【标准答案】
先说概念:
- TraceId:整个请求链路的唯一标识,从入口生成,贯穿所有服务
- SpanId:单个处理单元的标识,每经过一个服务/组件就生成一个新的Span
传递机制:HTTP调用场景
当服务A调用服务B时,Spring Cloud Sleuth会自动把TraceId和SpanId放入HTTP Header:
traceparent(W3C标准):00-{trace-id}-{parent-span-id}-01- 或者老版本用:
X-B3-TraceId、X-B3-SpanId
服务B收到请求后,从Header提取TraceId继续使用,生成新的SpanId,并记录ParentSpanId为A的SpanId。
跨线程场景:链路断裂问题
问题场景:主线程收到请求后,丢到线程池异步处理,新线程里TraceId丢失了。
解决方案:
// 方案一:手动传递
@Async
public void asyncProcess(String traceId) {
MDC.put("traceId", traceId); // 新线程里手动设置
try {
// 业务逻辑
} finally {
MDC.clear();
}
}
// 方案二:Sleuth自动包装(推荐)
@Autowired
private Tracer tracer;
public void asyncProcess() {
Span span = tracer.nextSpan().name("async-process");
try (Tracer.SpanInScope ws = tracer.withSpan(span.start())) {
// Sleuth自动在新线程维护链路上下文
executorService.submit(() -> {
// 这里的TraceId自动继承
});
}
}方案三:使用Sleuth的LazyTraceExecutor包装线程池
@Bean
public Executor executor() {
return new LazyTraceExecutor(tracer, Executors.newFixedThreadPool(10));
}这样丢到线程池的任务会自动继承链路上下文。
【面试题】8. Seata的AT模式和TCC模式有什么区别?各适合什么场景?
【标准答案】
AT模式:自动化,零侵入,适合大多数场景
AT模式的核心是"自动补偿":
- 一阶段:执行业务SQL,自动记录前后镜像到undo_log表
- 二阶段提交:删除undo_log
- 二阶段回滚:根据undo_log反向生成补偿SQL
优点: 业务代码无侵入,像写本地事务一样写分布式事务 缺点: 有全局锁,并发性能有瓶颈;只支持SQL操作,不支持非DB资源
TCC模式:手动补偿,灵活但侵入大
TCC要求业务实现三个方法:
- Try:资源预留,比如冻结账户余额
- Confirm:确认提交,比如扣减冻结金额
- Cancel:回滚补偿,比如解冻余额
优点: 性能高(无全局锁),支持非DB资源(如Redis、MQ) 缺点: 业务侵入大,三个方法都要自己写,还要处理幂等、空回滚、悬挂等问题
选型建议:
| 场景 | 推荐模式 |
|---|---|
| 纯数据库操作、并发不高 | AT模式 |
| 高并发、性能敏感 | TCC模式 |
| 涉及非DB资源(Redis、MQ等) | TCC模式 |
| 快速上线、开发成本敏感 | AT模式 |
| 对一致性要求极高、有专业团队 | TCC模式 |
面试加分点: 提到AT模式的"写隔离"问题——全局锁保证隔离性,但可能导致死锁,需要合理设计超时时间。
【面试题】9. Redis分布式锁用Redisson比直接用SET NX有什么优势?看门狗机制是什么?
【标准答案】
SET NX的问题:
// 简单实现
jedis.set("lock:key", "value", "NX", "EX", 30);看似没问题,但有几个坑:
- 锁续期问题:业务执行超过30秒,锁自动过期,其他线程拿到锁,你的业务还在跑——并发安全问题
- 锁释放问题:线程A释放了线程B的锁(value没校验,或者校验了但不是原子操作)
- 不可重入:同一个线程多次获取同一把锁会死锁
- 集群问题:主从切换时锁可能丢失
Redisson的优势:
1. 看门狗机制(Watchdog) 默认锁过期时间30秒,Redisson后台有个定时任务(每10秒检查),如果线程还持有锁,自动续期。业务执行多久都不怕锁过期。
RLock lock = redisson.getLock("myLock");
lock.lock(); // 看门狗自动生效
try {
// 业务执行超过30秒也没事
Thread.sleep(60000);
} finally {
lock.unlock();
}2. 自动校验+Lua脚本原子释放
// Redisson内部用Lua保证原子性
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end3. 可重入锁支持 同一个线程可以多次获取同一把锁,内部用计数器记录。
4. 红锁(RedLock)解决集群问题 多节点同时获取锁,超过半数成功才算成功,避免单节点故障导致的锁丢失。
生产建议: 无脑用Redisson,别自己造轮子。看门狗+可重入+Lua脚本,这些坑Redisson都帮你踩过了。
【面试题】10. Spring Cloud Stream集成RocketMQ,如何保证消息不丢失?消费失败如何处理?
【标准答案】
消息不丢失要盯三个环节:
环节一:生产端
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
output:
producer:
sync: true # 同步发送,等待Broker确认
retry-times-when-send-failed: 3 # 失败重试3次关键配置:sync: true同步发送,配合重试机制。异步发送虽然快,但丢失了生产端不知道。
环节二:Broker端
- 开启同步刷盘:
flushDiskType=SYNC_FLUSH - 开启主从同步复制:
brokerRole=SYNC_MASTER - 这样即使Broker宕机,消息也不丢
环节三:消费端
spring:
cloud:
stream:
bindings:
input:
consumer:
ack-mode: manual # 手动确认@StreamListener(MySink.INPUT)
public void handleMessage(Message<String> message, Acknowledgment ack) {
try {
// 业务处理
process(message.getPayload());
ack.acknowledge(); // 处理成功才确认
} catch (Exception e) {
// 不确认,消息会重新投递
log.error("处理失败", e);
}
}消费失败处理策略:
- 重试队列:RocketMQ自动重试16次,间隔递增(10秒→1分钟→10分钟)
- 死信队列(DLQ):重试16次后进入死信队列,人工介入处理
- 业务补偿:关键业务可以记录失败消息到数据库,定时任务扫描重试
spring:
cloud:
stream:
rocketmq:
bindings:
input:
consumer:
delay-level-when-next-consume: 3 # 重试间隔级别生产最佳实践:
- 重要消息用同步发送
- 消费失败至少打日志,最好入库
- 死信队列要有监控告警,别让消息悄悄消失