SpringCloud微服务架构面试题库(带答案)
生成时间:2026-05-21 22:28 题目数量:10道
本轮 SpringCloud微服务架构面试题库(带答案)
【面试题】1. Nacos作为注册中心时,如何实现服务的优雅上下线?
【标准答案】优雅上线:服务启动后先注册到Nacos,但标记为"未就绪"状态,等应用完全初始化完成后再改为"健康"状态。可以通过配置spring.cloud.nacos.discovery.enabled=true和健康检查机制实现。
优雅下线:服务停止前,先从Nacos注销(调用注销接口或发送shutdown事件),让Nacos及时推送服务下线通知给其他消费者,消费者收到后从本地缓存移除该实例。同时服务端设置一个缓冲期(比如30秒),期间不再接收新请求,等现有请求处理完再真正停机。
实际项目中,我们通过实现ApplicationListener<ContextClosedEvent>监听容器关闭事件,在关闭前先调用Nacos API注销服务,等待请求处理完毕后再退出,避免了硬杀导致请求失败。
【面试题】2. Spring Cloud Gateway如何实现动态路由?有什么实际应用场景?
【标准答案】动态路由核心是利用Gateway的RouteDefinitionLocator接口,通过自定义实现类从数据库或配置中心加载路由规则,并支持实时刷新。
实现方式:定义一个DynamicRouteService类,注入RouteDefinitionWriter,从数据库查询路由配置(包含路径、断言、过滤器等),封装成RouteDefinition对象后调用save()方法写入。配合Nacos Config或数据库变更监听,路由规则变化时触发刷新,网关无需重启即可生效。
实际应用:我们项目用动态路由做灰度发布,根据请求头中的版本标识路由到不同版本的服务实例;还有多租户场景,根据tenantId路由到不同租户的专属服务集群。运维后台可以实时配置上线新服务,不需要改配置文件重启网关。
【面试题】3. Sentinel的熔断降级和Hystrix有什么区别?生产环境如何选择?
【标准答案】核心区别:
- 资源隔离方式:Hystrix默认线程池隔离,Sentinel支持线程数和信号量两种隔离方式,更灵活。
- 熔断策略:Hystrix主要基于异常比例和请求失败率;Sentinel支持慢调用比例、异常比例、异常数三种策略,还支持热点参数限流。
- 限流能力:Sentinel原生支持QPS限流、线程数限流、关联限流、链路限流,Hystrix本身不提供限流功能。
- 监控生态:Sentinel有Dashboard控制台,支持实时监控、规则配置、集群限流;Hystrix已停止维护,监控依赖Turbine。
- 性能开销:Sentinel基于滑动窗口统计,性能更好;Hystrix线程池隔离有额外线程开销。
生产选择:新项目首选Sentinel,特别是Spring Cloud Alibaba全家桶项目。老项目如果已经在用Hystrix且稳定运行,可以继续用,但建议制定迁移计划。我们团队2024年从Hystrix迁移到Sentinel,主要看中Dashboard和热点限流能力,迁移过程用Resilience4j做过渡方案。
【面试题】4. 分布式事务Seata的AT模式原理是什么?有什么优缺点?
【标准答案】AT模式原理:
- 一阶段:业务SQL执行时,Seata拦截SQL,解析语义,记录"前镜像"(修改前数据),执行业务SQL,再记录"后镜像"(修改后数据),生成行锁。整个过程在本地事务中完成,保证原子性。
- 二阶段提交:所有分支事务成功,TC(Transaction Coordinator)通知各RM(Resource Manager)异步删除undo_log。如果一阶段有失败,TC通知各RM根据undo_log反向生成SQL回滚。
- 全局锁:为了防止脏写,Seata在TC维护全局锁,一阶段本地事务提交前先申请全局锁,保证同一行数据同一时刻只有一个分布式事务能修改。
优点:
- 代码零侵入,只需加
@GlobalTransactional注解 - 性能较好,一阶段本地事务直接提交,不阻塞
- 适合大多数业务场景,学习成本低
缺点:
- 存在脏回滚风险,如果回滚时数据已被其他本地事务修改,可能导致数据不一致
- 全局锁竞争激烈时影响并发性能
- 不支持复杂SQL(如多表关联更新)
实际项目:我们订单服务用AT模式,核心逻辑是扣库存、创建订单、扣余额,一阶段提交本地事务,二阶段全局提交。性能要求高的场景考虑TCC模式,业务侵入性强但性能更好。
【面试题】5. 如何保证消息队列在微服务架构中的可靠性投递?有哪些关键环节?
【标准答案】可靠性投递要保证"消息不丢、不重复、不丢失顺序":
生产端可靠性:
- 发送确认机制:RocketMQ的
SendResult返回SEND_OK,Kafka的acks=all,RabbitMQ的Publisher Confirm - 消息持久化:发送前先持久化到本地消息表(定时任务扫描未确认消息重发)
- 失败重试:配置重试次数和间隔,记录失败日志告警
Broker端可靠性:
- 多副本同步:Kafka配置
min.insync.replicas≥2,RocketMQ配置同步刷盘 - 持久化存储:消息写入CommitLog后再返回确认
- 集群部署:避免单点故障
消费端可靠性:
- 手动ACK:业务处理成功再提交offset/ack
- 幂等性设计:消费逻辑支持重复消费(基于业务主键去重或Redis去重表)
- 死信队列:重试多次失败的消息转移到死信队列人工处理
实际项目:我们支付通知服务用RocketMQ,生产者发送前写入本地消息表(状态为发送中),定时任务扫描重发;消费者先查Redis去重表,处理成功写入Redis并返回CONSUME_SUCCESS,失败返回RECONSUME_LATER触发重试,重试16次进入死信队列告警处理。
【面试题】6. 微服务之间如何实现安全认证?Token如何在服务间传递?
【标准答案】微服务安全认证主流方案:
网关统一认证:
- 外部请求先到Gateway,Gateway集成OAuth2.0 Server或JWT校验
- 校验通过后,Gateway解析Token提取用户信息,放入请求头(如
X-User-Id、X-User-Roles) - 内部服务间调用通过请求头传递用户上下文,不再重复校验Token
服务间安全:
- 内部服务调用使用内部Token(如Service Token),Gateway签发短期Token给服务
- 每个服务配置白名单IP或mTLS双向认证
- 敏感操作二次校验(如支付、修改密码)
Token传递实现:
// 在OpenFeign拦截器中传递Token
public class AuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
String token = request.getHeader("Authorization");
if (StringUtils.isNotBlank(token)) {
template.header("Authorization", token);
}
}
}
}Refresh Token机制:
- Access Token有效期短(如2小时),Refresh Token有效期长(如7天)
- Token快过期时,前端用Refresh Token换取新Access Token
- Refresh Token只能使用一次,用后失效,防止重放攻击
实际项目:我们用Gateway + JWT做网关认证,内部服务通过Feign拦截器自动传递Token,敏感服务(支付服务)额外验证IP白名单和签名。
【面试题】7. Ribbon和Spring Cloud LoadBalancer有什么区别?为什么Ribbon被替换?
【标准答案】主要区别:
- 维护状态:Ribbon已进入维护模式(Maintenance Mode),Netflix不再积极开发;Spring Cloud LoadBalancer是Spring官方维护,持续更新。
- 依赖体系:Ribbon依赖Netflix OSS生态,与Spring Boot 2.4+版本兼容性变差;LoadBalancer是Spring Cloud原生组件,与Spring生态无缝集成。
- 配置方式:Ribbon使用
@LoadBalanced注解,配置项以ribbon.开头;LoadBalancer同样用@LoadBalanced,但配置项以spring.cloud.loadbalancer.开头,更统一。 - 负载均衡策略:Ribbon提供7种策略(轮询、随机、权重、响应时间权重等);LoadBalancer默认提供轮询和随机,但可通过
ServiceInstanceListSupplier自定义扩展,更灵活。 - 缓存机制:Ribbon默认缓存服务实例列表,可能导致服务下线后仍被访问;LoadBalancer支持配置缓存过期时间,更可控。
为什么被替换:
- Netflix OSS组件大部分停止维护(Eureka、Hystrix、Ribbon、Zuul)
- Spring Boot 2.4+移除了Ribbon的自动配置支持
- Spring Cloud 2020.x版本开始,LoadBalancer成为默认负载均衡器
迁移建议:
# Ribbon配置(旧)
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# LoadBalancer配置(新)
spring:
cloud:
loadbalancer:
ribbon:
enabled: false # 禁用Ribbon
configurations: random # 使用随机策略实际项目:我们2023年完成迁移,主要改动点是配置项迁移和自定义策略重写,业务代码零改动。
【面试题】8. 链路追踪中TraceId如何跨服务传递?异步调用时如何保证TraceId不丢失?
【标准答案】TraceId传递机制:
同步调用传递:
- Sleuth/Micrometer在HTTP请求头中注入
traceId和spanId(请求头:X-B3-TraceId、X-B3-SpanId) - 下游服务收到请求后,过滤器解析请求头,提取TraceId加入当前上下文(MDC)
- OpenFeign/RestTemplate拦截器自动将TraceId透传到下游请求头
- 日志框架通过MDC自动打印TraceId(
%X{traceId})
异步调用传递:
- 线程池传递:使用
TraceableExecutorService包装线程池,提交任务时自动复制父线程的TraceId到子线程 - @Async注解:配置
TaskDecorator,在任务执行前复制TraceId - 手动传递:异步任务提交时,从当前上下文获取TraceId,作为参数传递给异步方法,在异步方法内手动设置到MDC
// 线程池包装示例
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new ContextCopyingDecorator()); // 复制上下文
return executor;
}
// 手动传递示例
String traceId = MDC.get("traceId");
CompletableFuture.runAsync(() -> {
MDC.put("traceId", traceId);
try {
// 业务逻辑
} finally {
MDC.remove("traceId");
}
});消息队列传递: RocketMQ/Kafka消费者在消费消息时,从消息头提取TraceId设置到当前上下文,保证消息链路追踪连续。
实际项目:我们用Sleuth + Zipkin,所有日志自动带TraceId,异步任务用TraceableExecutorService包装,消息队列在生产者端将TraceId写入消息头,消费者端解析后设置到MDC,全链路追踪完整覆盖。
【面试题】9. Redis分布式锁如何解决锁续期和锁释放问题?Redisson是如何实现的?
【标准答案】锁续期问题:
- 场景:业务执行时间超过锁过期时间,锁自动释放,其他线程获取锁导致并发问题
- 解决:看门狗机制(Watchdog),后台线程定时(每10秒)检测锁是否还被持有,如果持有则续期(重置过期时间为30秒)
Redisson实现:
RLock lock = redisson.getLock("myLock");
lock.lock(); // 默认30秒过期,启动Watchdog自动续期
try {
// 业务逻辑
} finally {
lock.unlock(); // 释放锁
}锁释放问题:
- 误删问题:线程A的锁过期释放,线程B获取锁后,线程A执行完释放了线程B的锁
- 解决:释放锁时验证锁的标识(
value为UUID+线程ID),只释放自己持有的锁 - Lua脚本保证原子性:Redisson用Lua脚本执行
get + compare + del,保证原子操作
-- Redisson释放锁Lua脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endRedisson核心特性:
- 可重入锁:基于Redis Hash结构存储,
field为线程标识,value为重入次数 - 公平锁:按请求顺序获取锁,基于Redis List和ZSet实现
- 读写锁:支持读写分离,读锁共享,写锁排他
- 红锁(RedLock):多Redis实例加锁,解决单点故障问题
实际项目:我们支付服务用Redisson分布式锁防止重复支付,默认配置看门狗续期,业务超时自动释放,释放时Lua脚本验证锁标识,避免误删。集群环境用RedLock,5个Redis实例,至少3个加锁成功才算获取锁成功。
【面试题】10. Spring Cloud Gateway如何实现统一鉴权?鉴权失败如何返回自定义响应?
【标准答案】统一鉴权实现方式:
方式一:GatewayFilter全局过滤器
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
// 1. 验证Token有效性(JWT解析或调用认证服务)
if (StringUtils.isBlank(token) || !validateToken(token)) {
return authError(exchange, "Token无效或已过期", 401);
}
// 2. 解析用户信息,放入请求头传递给下游服务
UserInfo user = parseToken(token);
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", user.getUserId())
.header("X-User-Roles", user.getRoles())
.build();
return chain.filter(exchange.mutate().request(request).build());
}
private Mono<Void> authError(ServerWebExchange exchange, String msg, int code) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String body = JSON.toJSONString(Result.error(code, msg));
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100; // 优先级高,先执行
}
}方式二:集成OAuth2.0资源服务器
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://auth-server/.well-known/jwks.json鉴权失败自定义响应:
- 实现
ServerAuthenticationEntryPoint,返回自定义JSON - 异常处理器捕获
AuthenticationException,统一格式返回 - 白名单路径(如
/login、/public/**)通过WebSecurityConfigurer放行
实际项目:我们Gateway用全局过滤器做JWT校验,Token无效返回{"code":401,"message":"请重新登录"},同时根据路径和角色做RBAC鉴权,权限不足返回{"code":403,"message":"无权限访问"},所有下游服务从请求头获取用户信息,不再重复鉴权。
说明:本批次题目覆盖Nacos优雅上下线、动态路由、Sentinel vs Hystrix、Seata AT模式、消息可靠性投递、微服务安全认证、Ribbon vs LoadBalancer、链路追踪TraceId传递、Redisson分布式锁、Gateway统一鉴权等核心考点。