SpringCloud微服务架构面试题库
生成时间:2026-05-21 21:29 | 题目数:10
【面试题】1. Nacos同时支持AP和CP模式,底层是怎么切换的?什么场景该用哪种模式?
【标准答案】Nacos区分临时实例和持久实例两种注册方式,临时实例走AP模式(Distro协议),持久实例走CP模式(Raft协议)。默认注册的都是临时实例,用的就是AP模式。
切换逻辑很简单——服务实例注册时通过ephemeral字段控制:ephemeral=true(默认)走AP,ephemeral=false走CP。代码层面就是注册请求带了不同标记,Nacos Server根据标记路由到不同的一致性协议处理器。
选型原则:
- AP模式(临时实例):适用于大多数业务服务,服务实例是临时的、随时可能上下线,追求高可用和低延迟。哪怕短暂不一致,多返回几个已下线实例,客户端重试一次就行,影响可控。
- CP模式(持久实例):适用于数据库、缓存等基础服务,实例相对固定,对数据一致性要求高,不能容忍读到旧数据。比如MySQL主从切换的场景,必须确保同一时刻只有一个Master被注册。
面试加分点:可以补充说Nacos 2.x版本引入了gRPC长连接替代HTTP短连接做心跳,服务上下线感知从原来15-30秒缩短到3秒以内,AP模式下的不一致窗口大幅缩小,这是Nacos相比Eureka的核心优势。
【面试题】2. Ribbon默认的轮询策略在生产环境够用吗?如何自定义一个基于权重的负载均衡策略?
【标准答案】默认轮询策略在生产环境大概率不够用。轮询假设所有服务实例性能一致,但实际场景中不同实例的CPU、内存、网络带宽差异很大,弱实例和强实例平均分配流量会导致弱实例过载、强实例闲置。
自定义权重策略的实现思路:
方式一:继承AbstractLoadBalancerRule
public class WeightedRule extends AbstractLoadBalancerRule {
@Override
public Server choose(Object key) {
List<Server> servers = getLoadBalancer().getAllServers();
// 根据Nacos实例的weight元数据计算权重
// 加权随机算法:构建[0, 总权重)区间,随机落点
int totalWeight = servers.stream()
.mapToInt(s -> {
NacosServer ns = (NacosServer) s;
return (int) ns.getInstance().getWeight();
}).sum();
int random = ThreadLocalRandom.current().nextInt(totalWeight);
int current = 0;
for (Server s : servers) {
NacosServer ns = (NacosServer) s;
current += (int) ns.getInstance().getWeight();
if (random < current) return s;
}
return servers.get(0);
}
}方式二:配置文件指定
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.xxx.WeightedRule实际项目更推荐直接用Nacos自带的权重配置,在Nacos控制台给实例设置权重值(0.1-10),结合NacosRule就能实现加权轮询,不需要自己写。
面试加分:如果用的是Spring Cloud LoadBalancer(Ribbon已停更),可以通过实现ReactorServiceInstanceLoadBalancer接口自定义策略,思路一样,只是API不同。
【面试题】3. OpenFeign调用超时了怎么办?超时重试配置有哪些坑?
【标准答案】超时处理分三步走:确认超时层级→调优超时配置→合理设置重试。
超时层级确认:OpenFeign超时本质是底层HTTP客户端的超时。Feign默认用URLConnection,没有连接池,建议换成Apache HttpClient或OkHttp。超时有两个维度:connectTimeout(建立连接超时)和readTimeout(读取响应超时)。
配置方式:
feign:
client:
config:
default: # 全局
connectTimeout: 3000
readTimeout: 5000
order-service: # 针对特定服务
connectTimeout: 2000
readTimeout: 10000
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50重试的坑:
- 重复提交:重试默认只对GET请求安全,非幂等接口(POST/PUT)重试可能导致重复操作。必须配合
Retryer自定义重试策略,只对GET或明确幂等的接口重试。 - 重试风暴:Ribbon也有重试机制(
MaxAutoRetries),Feign重试+Ribbon重试会叠加。比如Ribbon重试1次+Feign重试1次,实际最多调用4次!建议关闭Feign的重试(Retryer.NEVER_RETRY),只保留Ribbon重试,或者反过来。 - 超时+重试叠加:每次重试都消耗完整的readTimeout时间,3次重试可能等15秒。高并发场景下线程池会被打满,触发雪崩。建议重试次数不超过2次,且总超时时间要小于上游熔断器的超时阈值。
最佳实践:配置超时→开Sentinel熔断兜底→写降级fallback→重试只对幂等接口开启且次数严格控制。
【面试题】4. Sentinel的热点参数限流是什么?能解决什么实际业务问题?
【标准答案】热点参数限流是Sentinel的特色功能,可以根据请求中的参数值做精细化限流,不同的参数值有不同的限流阈值。
核心区别:普通限流是对整个接口限流(比如/order/query每秒最多100次),热点参数限流是按参数值分别限流(比如/order/query?goodsId=100每秒最多50次,/order/query?goodsId=200每秒最多200次)。
典型业务场景:
- 秒杀商品防刷:热门商品ID的查询QPS暴涨,冷门商品流量正常。按goodsId限流,热门商品单独限制,不影响其他商品。
- 大V用户防过载:社交场景下大V发动态瞬间涌入大量点赞和评论,按userId限流,大V单独设高阈值,普通用户正常限制。
- 防止缓存穿透:某些热点Key被打爆,按参数限流保护下游DB。
配置示例:
@SentinelResource(value = "queryOrder", blockHandler = "queryBlock")
public Order queryOrder(Long orderId, String userId) { ... }
// 热点参数规则
ParamFlowRule rule = new ParamFlowRule("queryOrder")
.setParamIdx(0) // 对第1个参数orderId限流
.setCount(100) // 默认每秒100
.setGrade(RuleConstant.FLOW_GRADE_QPS);
// 对特定orderId单独配置
rule.setParamFlowItemList(List.of(
new ParamFlowItem().setObject("10086")
.setClassType(long.class.getName())
.setCount(500) // orderId=10086 每秒500
));面试加分:热点参数限流底层用LRU Cache统计每个参数值的QPS,默认容量2000,参数值太多会淘汰冷数据。如果业务参数值域特别大(比如上百万SKU),要调大PARAM_INTERVAL_BASE或考虑用普通限流替代。
【面试题】5. Spring Cloud Gateway如何实现统一鉴权?全局过滤器与局部过滤器怎么选?
【标准答案】统一鉴权是网关的核心职责,Spring Cloud Gateway通过过滤器链实现,鉴权通常用全局过滤器(GlobalFilter)。
全局过滤器实现鉴权:
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
// 1. 白名单路径放行(登录、注册等)
String path = exchange.getRequest().getPath().value();
if (isWhitePath(path)) {
return chain.filter(exchange);
}
// 2. Token校验
if (StringUtils.isBlank(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 3. 解析JWT,提取用户信息放入请求头传递给下游
Claims claims = JwtUtil.parseToken(token);
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", claims.getSubject())
.header("X-User-Role", claims.get("role", String.class))
.build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() { return -1; } // 优先级最高
}全局过滤器 vs 局部过滤器选择:
- 全局过滤器(GlobalFilter):鉴权、日志、跨域、限流——所有请求都要走的逻辑,用全局的。不需要配置,注册即生效。
- 局部过滤器(GatewayFilter):只对特定路由生效,比如某个服务的请求需要加特殊Header、做参数转换。需要在route配置中声明。
鉴权架构关键点:
- Token只在网关验证一次,解析后把用户信息通过Header传递给下游服务,避免下游重复验签。
- 下游服务信任网关传入的Header,但要注意防伪造——内网部署时网关是唯一入口,Header不会从外部绕过;如果担心,可以加一个网关签名Header做二次校验。
- Refresh Token的处理:网关拦截过期Token返回特定状态码,前端拿到后走无感刷新流程,新Token重新请求。
【面试题】6. Nacos Config配置热更新的原理是什么?多环境隔离怎么实现?
【标准答案】配置热更新的核心原理是长轮询监听机制。
热更新原理:
- 客户端启动时拉取配置,同时用长轮询(Long Polling)向Nacos Server注册监听。Nacos 2.x升级为gRPC长连接,实时性更强。
- 当配置变更时,Nacos Server通过gRPC连接主动推送变更通知给客户端。
- 客户端收到通知后重新拉取最新配置,触发
RefreshEvent,Spring Cloud通过@RefreshScope或@ConfigurationProperties刷新Bean中的属性值。
两种热更新方式:
@RefreshScope:作用在Bean上,配置变更时销毁旧Bean、创建新Bean,适合需要重新初始化的Bean。有性能开销,不建议大量使用。@ConfigurationProperties:属性自动绑定,配置变更时只更新属性值,不重建Bean,性能更好,推荐优先使用。
多环境隔离实现:Nacos用三层隔离模型——Namespace → Group → Data ID。
| 层级 | 用途 | 示例 |
|---|---|---|
| Namespace | 环境隔离 | dev / test / prod |
| Group | 项目/业务隔离 | DEFAULT_GROUP / TRADE_GROUP |
| Data ID | 配置文件 | order-service.yaml |
spring:
cloud:
nacos:
config:
namespace: ${NACOS_NAMESPACE:dev} # 环境隔离
group: ${NACOS_GROUP:DEFAULT_GROUP} # 项目隔离
file-extension: yaml
shared-configs: # 共享配置
- data-id: common-db.yaml
refresh: true
- data-id: common-redis.yaml
refresh: true面试加分:生产环境建议配置加密(Nacos支持内置加密,配置jasypt或Nacos 2.x的配置加密插件),数据库密码等敏感配置不能明文存储。同时配合Git版本管理,Nacos Config支持配置历史回滚,最多保留30天,误改配置可以一键回退。
【面试题】7. SkyWalking和Zipkin在链路追踪上有什么区别?链路追踪对性能的影响怎么控制?
【标准答案】两者核心区别在架构和功能定位:
| 对比维度 | Zipkin | SkyWalking |
|---|---|---|
| 数据采集 | SDK侵入(Brave/Sleuth) | Java Agent无侵入 |
| 存储后端 | 内存/MySQL/Elasticsearch | Elasticsearch/H2/MySQL |
| 告警能力 | 无 | 内置告警规则 |
| 服务拓扑 | 简单依赖图 | 实时拓扑+指标聚合 |
| 性能损耗 | 中等(SDK埋点) | 低(Agent字节码增强) |
| 语言支持 | 多语言SDK | Java为主,其他语言用SDK |
| 协议 | HTTP/Kafka | gRPC/HTTP |
选型建议:简单微服务用Zipkin够用,轻量上手快;大规模生产环境(几百个服务以上)选SkyWalking,无侵入+告警+拓扑更实用。
性能影响控制:
- 采样率控制:不是每个请求都追踪。Zipkin通过
spring.sleuth.sampler.probability控制,SkyWalking通过agent.sample_n_per_3_secs控制每3秒采样N条。生产环境建议采样率5%-10%,异常时临时调高。 - 异步上报:追踪数据不能阻塞业务线程。Zipkin用Kafka异步上报,SkyWalking用gRPC批量上报+本地缓冲区,网络抖动时先缓存本地。
- 控制Span数量:一个请求不是所有调用都要打Span。Redis缓存、本地方法调用等低价值Span可以忽略,SkyWalking支持
trace.ignore_path过滤。 - 存储优化:Elasticsearch设置合理的index lifecycle policy,链路数据保留7-15天即可,超期自动删除。
面试关键点:链路追踪的性能损耗通常在3%-5%以内,但采样率调到100%时可能达到10%以上。生产环境核心链路采样率100%+非核心链路低采样的混合策略最实用。
【面试题】8. Seata AT模式的全局锁机制是怎么工作的?和XA模式比有什么优劣?
【标准答案】AT模式是Seata最常用的模式,核心是两阶段提交+全局锁+undo_log。
AT模式执行流程:
- 一阶段:业务SQL执行前,拦截SQL解析语义,查询修改前的数据保存到
undo_log表,然后执行业务SQL并提交本地事务。此时本地事务已提交,其他非分布式事务可以读到未确认的数据(读未隔离)。 - 全局锁:一阶段本地事务提交后,向TC(事务协调器)注册全局锁,锁定该行数据的全局修改权。如果另一个分布式事务也要改同一行,会等全局锁释放,超时后回滚。
- 二阶段提交:TC通知各分支提交,异步清理undo_log。
- 二阶段回滚:TC通知各分支回滚,根据undo_log反向补偿,恢复数据到修改前。回滚前还要校验当前数据是否被脏写(和undo_log中的after镜像对比),如果被改了就要人工介入。
AT vs XA对比:
| 维度 | AT模式 | XA模式 |
|---|---|---|
| 本步事务提交 | 一阶段就提交 | 二阶段才提交 |
| 锁持有时间 | 全局锁(短) | 数据库锁(长) |
| 性能 | 高,本地事务快速释放 | 低,长事务持锁 |
| 侵入性 | 需要undo_log表 | 无侵入,数据库原生支持 |
| 隔离性 | 写隔离(全局锁),读默认未隔离 | 完全隔离 |
| 数据库要求 | 无特殊要求 | 支持XA协议 |
AT模式的优势:性能好,一阶段本地事务就提交了,不长时间持有数据库锁,并发度高。 AT模式的劣势:一阶段提交后数据对其他事务可见(脏读),虽然全局锁保护了写隔离,但读隔离需要加@GlobalLock+SELECT FOR UPDATE才能保证。
面试加分:AT模式最大的风险是脏写——一阶段已提交,回滚时发现数据被改了。生产环境建议核心业务表加@GlobalLock读隔离,或者改用TCC模式手动控制资源预留。
【面试题】9. Redisson的看门狗机制是什么?解决了什么问题?
【标准答案】看门狗(Watchdog)解决的是分布式锁过期但业务未执行完的问题。
问题场景:设置锁的过期时间30秒,但业务执行了40秒。第30秒锁自动释放,其他线程拿到锁,此时两个线程同时持有锁——分布式锁失效!
看门狗原理:
- Redisson加锁时不指定过期时间(
leaseTime=-1),默认锁30秒。 - 加锁成功后,启动一个后台定时任务(Watchdog),每隔
lockWatchdogTimeout/3(默认10秒)检查:如果锁还被当前线程持有,就自动续期到30秒。 - 业务执行完毕正常释放锁时,取消看门狗任务。
- 如果持有锁的客户端宕机,看门狗自然停止,锁30秒后自动过期释放。
源码关键逻辑:
// 加锁成功后
long ttl = lockWatchdogTimeout; // 默认30秒
// 定时续期
Timeout task = commandExecutor.getServiceManager().newTimeout(() -> {
if (lockHolder.isHeldByCurrentThread()) {
// 续期到30秒
expiration.renewExpire();
// 重新调度自己
reschedule(task);
}
}, ttl / 3, TimeUnit.MILLISECONDS); // 每10秒检查一次使用注意事项:
- 必须不指定leaseTime:如果
lock(30, TimeUnit.SECONDS)指定了过期时间,看门狗不会启动!这是最常见的坑。 - 看门狗超时时间可调:
lockWatchdogTimeout默认30秒,可以配置更短或更长,但要大于业务最大执行时间。 - 单节点问题:看门狗只在单节点有效。Redis主从切换时,锁可能丢失。对安全性要求高的场景用RedLock(多节点加锁,半数以上成功才算获取锁),但RedLock有争议——Martin Kleppmann认为它不安全,Antirez反驳。实际项目大多数用单节点+看门狗就够用了。
【面试题】10. Spring Cloud Stream消费失败怎么处理?消息积压了怎么救?
【标准答案】消费失败和消息积压是两个相关但不同的问题,分开处理。
消费失败处理:
- 重试:Spring Cloud Stream内置重试,配置
spring.cloud.stream.bindings.input.consumer.max-attempts=3,默认重试3次。重试有退避策略,避免立即重试打爆下游。 - 死信队列(DLQ):重试全部失败后,消息进入死信队列,不丢弃。RabbitMQ配置:
spring:
cloud:
stream:
bindings:
orderInput:
consumer:
max-attempts: 3
rabbit:
bindings:
orderInput:
consumer:
auto-bind-dlq: true # 自动创建死信队列
republish-to-dlq: true # 失败消息转发到DLQ
dlq-ttl: 86400000 # 死信保留24小时- 手动ACK+补偿:对于RocketMQ/Kafka,用手动确认模式,处理成功才commit,失败走重试或补偿逻辑。可以写
@ServiceActivator监听死信队列,做人工干预或自动修复后重新投递。
消息积压救援:
- 紧急扩容消费者:增加消费者实例数,但要注意分区数(Kafka的partition数)是消费者并行度上限,partition不够要同步扩partition。
- 临时转发+并行消费:把积压的消息快速转发到一个新Topic,这个新Topic配更多partition,用临时消费者集群并行消费。
- 跳过非核心消息:如果是日志类消息,可以临时丢弃或只消费最新N条,先恢复实时业务。
- 根因排查:积压通常是消费端瓶颈。看消费者耗时(是DB慢查询还是第三方接口超时?),针对性优化。RocketMQ可以看消费TPS和积压量的监控曲线。
预防措施:
- 生产端限流(Sentinel),不让流量暴涨直接打穿MQ
- 消费端做好幂等,积压恢复时重复消费不出错
- 设置合理的队列容量告警,积压量超过阈值立即通知
- 核心业务和非核心业务分Topic,避免非核心消息挤占核心消息的消费资源