本轮 SpringCloud微服务架构面试题库(带答案)
【面试题】1. Sentinel的滑动窗口限流算法是怎么实现的?和令牌桶、漏桶相比有什么区别?
【标准答案】Sentinel默认用滑动窗口做统计,内部维护一个LeapArray结构,把时间窗口拆成多个样本格子(比如1秒窗口拆成2个500ms的格子)。请求进来时,当前格子计数+1,同时往前滑动窗口边界做统计。好处是比固定窗口更平滑,避免了窗口边界的突发流量问题。
和令牌桶、漏桶的对比:
- 滑动窗口:基于统计的限流,关注的是"过去一段时间过了多少请求",允许一定程度的突发;
- 令牌桶:以恒定速率生成令牌,桶里有令牌就能通过,允许短时突发(桶满时攒的令牌一次性消费),适合有突发场景的业务;
- 漏桶:请求进桶后以恒定速率流出,不管进来多猛,出去都是匀速的,适合做流量整形。
实际选型:Sentinel默认滑动窗口够用,需要匀速排队可以用它的排队等待模式(本质是漏桶思想),需要突发允许可以配合令牌桶策略。
【面试题】2. Eureka的自我保护机制原理是什么?生产环境要不要开启?
【标准答案】Eureka自我保护的核心逻辑:当节点在15分钟内心跳失败比例超过85%时,Eureka Server会认为网络出了问题而不是服务挂了,于是不再剔除过期实例,保护注册表不被错误清空。
原理细节:
- Eureka Client每30秒发一次心跳,Server超过90秒没收到心跳认为实例失效;
- 自我保护触发后,Server停止从注册表移除实例,即使这些实例已经真的挂了;
- 同时Server也不再和集群 peers 同步剔除操作,防止"雪崩式"下线。
生产环境建议:要开启。理由是分布式环境网络抖动很常见,如果不开启,一次网络故障可能导致大量服务被错误剔除,引发级联故障。但开启后要注意:真的挂掉的服务不会被自动清除,调用方可能拿到已死实例,所以消费端必须配好重试和熔断机制来兜底。
【面试题】3. Seata的AT模式两阶段提交具体是怎么执行的?undo_log表有什么用?
【标准答案】AT模式是Seata最常用的模式,对业务代码零侵入,核心靠数据源代理+undo_log实现:
一阶段(业务SQL + 回滚日志):
- 拦截业务SQL,解析得到表名、条件等;
- 查询修改前的数据(before image);
- 执行业务SQL;
- 查询修改后的数据(after image);
- 生成undo_log记录(before+after image),插入到业务库的undo_log表;
- 提交本地事务(业务数据+undo_log一起提交,保证原子性);
- 向TC(事务协调者)注册分支事务,上报分支状态。
二阶段提交:
- 全局提交:TC通知各分支提交,各分支异步清理undo_log(因为一阶段已经提交了,数据已经生效);
- 全局回滚:TC通知各分支回滚,各分支根据undo_log的before image生成反向SQL补偿回去,然后删除undo_log。
undo_log表的作用就是存"前镜像+后镜像",回滚时靠它做反向补偿。关键点:一阶段业务数据和undo_log在同一个本地事务里,保证要么一起成功要么一起失败。
【面试题】4. Spring Cloud Gateway的过滤器链执行顺序是怎样的?如何自定义全局过滤器实现鉴权?
【标准答案】Gateway过滤器分三种,执行顺序如下:
- Global Filter(全局过滤器)—— 所有路由都生效
- GatewayFilter(路由过滤器)—— 配在具体路由上
- 内置过滤器(如ForwardRoutingFilter等)
执行机制:所有过滤器组成一条链,按order值排序(数值越小越先执行pre阶段,越晚执行post阶段)。pre阶段按order升序执行,post阶段按order降序执行(先进后出的栈结构)。
自定义全局鉴权过滤器示例:
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(token) || !validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 验证通过,将用户信息放入header传递给下游服务
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Id", getUserId(token))
.build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() { return -1; } // 优先级最高
}要点:order设小值让鉴权尽早执行;token验证通过后把用户信息塞到header里透传给下游,下游服务就不用再重复鉴权了。
【面试题】5. Nacos作为配置中心,配置热更新的原理是什么?@RefreshScope和Nacos监听器方式有什么区别?
【标准答案】Nacos配置热更新原理:
- 客户端启动时用长轮询(Long Polling)监听配置变更,默认30秒超时;
- 服务端收到监听请求后,如果配置没变化就hold住连接直到超时;如果配置变了,立即返回变更的dataId;
- 客户端收到变更通知后,拉取最新配置,触发本地配置刷新。
两种热更新方式对比:
- @RefreshScope:Spring Cloud原生方式,配置变更后标注了@RefreshScope的Bean会被销毁重建,下次访问时从新配置创建实例。缺点是整个Bean重建,如果Bean初始化逻辑重会有性能损耗;而且只能刷新@Value标注的属性。
- Nacos监听器方式:自己注册Listener监听配置变更,在回调里手动更新属性。更灵活,可以做条件判断、部分更新,不依赖Bean重建。适合配置变更后需要执行额外逻辑的场景(比如改了数据库连接池参数需要重新建连接)。
生产建议:简单配置刷新用@RefreshScope,复杂逻辑用监听器。也可以用@ConfigurationProperties自动绑定,比@RefreshScope更优雅。
【面试题】6. 分布式锁用Redis还是Zookeeper?Redisson的看门狗机制是怎么回事?
【标准答案】选型对比:
- Redis分布式锁:性能高(内存操作),CP偏AP,主从切换时可能丢锁(主节点加锁后宕机,从节点还没同步就升级为主,另一个客户端也能加锁)。适合高性能场景,能容忍极小概率的锁失效。
- Zookeeper分布式锁:CP强一致,基于临时顺序节点实现,可靠性更高。但性能不如Redis(每次加锁要集群过半ACK)。适合对锁可靠性要求极高的场景(如金融扣款)。
Redisson看门狗机制: 问题:锁设了过期时间,但业务没执行完锁就过期了,其他线程拿到锁导致并发问题。 解决:Redisson加锁时如果不指定leaseTime,会启动一个看门狗(后台定时任务),默认每10秒(lockWatchdogTimeout/3)检查一次,如果锁还被持有就自动续期到30秒。业务执行完释放锁时看门狗停止续期。
关键代码逻辑:加锁成功后启动Timeout任务,每10秒执行一次renew,只有当前线程还持有锁才续期。如果客户端宕机,看门狗自然停止,锁30秒后自动过期释放。
注意:如果手动指定了leaseTime,看门狗不会启动——Redisson认为你自己控制了锁的生命周期。
【面试题】7. OpenFeign调用超时重试怎么配置?如何避免重试导致的重复提交问题?
【标准答案】超时配置:
feign:
client:
config:
default:
connectTimeout: 5000 # 连接超时5秒
readTimeout: 10000 # 读取超时10秒
# 或针对特定服务
order-service:
connectTimeout: 3000
readTimeout: 5000Ribbon级别超时(如果还用Ribbon):
ribbon:
ConnectTimeout: 3000
ReadTimeout: 10000
MaxAutoRetries: 0 # 当前实例重试次数
MaxAutoRetriesNextServer: 1 # 切换实例重试次数注意:Feign超时和Ribbon超时取较小值生效。
避免重试导致重复提交:
- 幂等性设计——最根本的解决方案。写操作用唯一请求ID(RequestId),服务端做去重;
- 只对读接口开启重试,写接口关闭重试(MaxAutoRetries=0, MaxAutoRetriesNextServer=0);
- 非读操作如果必须重试,接口设计成幂等的:PUT天然幂等,DELETE也幂等,POST需要用Token机制保证幂等(如支付接口的幂等Key);
- 业务层用分布式锁防重:请求进来先拿锁,处理完释放,重复请求拿不到锁直接返回。
【面试题】8. 微服务之间调用链路追踪的TraceId是怎么跨服务传递的?Sleuth是怎么实现的?
【标准答案】TraceId跨服务传递机制:
- 请求进入A服务,Sleuth生成TraceId和SpanId,放入请求上下文(ThreadLocal或Reactor Context);
- A服务调用B服务时,Sleuth自动拦截HTTP请求(通过RestTemplate/Feign拦截器),把TraceId、SpanId等信息塞到HTTP Header里(如X-B3-TraceId、X-B3-SpanId);
- B服务收到请求后,Sleuth的Filter从Header中提取TraceId和SpanId,创建新的子Span,继续往下传递;
- 依次类推,整条链路共享同一个TraceId,每个服务有独立的SpanId和ParentSpanId。
Sleuth实现细节:
- HTTP调用:通过Brave的CurrentTraceContext + Header传播器实现自动注入/提取;
- 消息队列:通过Message Header传递,消费者从消息头提取;
- 异步线程:通过TaskDecorator把TraceContext从父线程传到子线程,或手动用Tracer手动span;
版本注意:Spring Cloud 2020+之后Sleuth进入维护模式,官方推荐用Micrometer Tracing替代,但传递原理相同。
【面试题】9. Spring Cloud Gateway如何实现灰度发布?有哪些常见方案?
【标准答案】灰度发布核心是把部分流量路由到新版本服务,常见方案:
方案一:基于请求头的权重路由
spring:
cloud:
gateway:
routes:
- id: service-v1
uri: lb://order-service-v1
predicates:
- Path=/order/**
- Weight=group1, 90
- id: service-v2
uri: lb://order-service-v2
predicates:
- Path=/order/**
- Weight=group1, 1090%流量走v1,10%走v2。简单但粒度粗。
方案二:基于自定义Header/Cookie路由
- 前端在灰度用户请求中加Header(如X-Gray-Tag: gray);
- Gateway自定义RoutePredicateFactory,匹配Header路由到灰度服务;
- 适合按用户维度灰度(如内部员工先灰度)。
方案三:基于Nacos元数据+负载均衡
- 灰度实例在Nacos注册时打上metadata:gray=true;
- Gateway自定义GlobalFilter或自定义LoadBalancer,根据灰度标记选择实例;
- 不需要拆分路由配置,更灵活。
方案四:基于服务版本标签
- 实例注册时带version标签(v1/v2);
- Gateway路由断言中根据version匹配,或者Ribbon/Spring Cloud LoadBalancer自定义规则按版本过滤实例。
生产推荐:方案二+方案三组合——灰度标识从Header传进来,Gateway根据标识从Nacos筛选灰度实例,链路上用Header透传保证整条链路走灰度。
【面试题】10. 消息队列消费端如何保证消息不丢失?RocketMQ的消费者负载均衡机制是怎样的?
【标准答案】消息不丢失要三个环节都保证:
- 生产端不丢:RocketMQ支持同步发送+重试,发送失败有回调可以处理。也可以用事务消息保证"本地事务+消息发送"的原子性。
- Broker端不丢:同步刷盘(SYNC_FLUSH)+ 主从同步复制(SYNC_MASTER),确认落盘后才返回成功。异步刷盘有丢数据风险。
- 消费端不丢(最关键):
- 关闭自动ACK,业务处理成功后再手动确认消费;
- 消费失败走重试队列(RocketMQ默认16次重试阶梯延迟),超过最大重试次数进死信队列(DLQ);
- 死信队列人工处理,保证消息最终被消费。
RocketMQ消费者负载均衡机制:
- 默认用集群模式(CLUSTERING),同一个ConsumerGroup下多个实例共同消费Topic的消息;
- 负载均衡策略:默认AllocateMessageQueueAveragely(平均分配队列),每个队列只被一个消费者消费;
- 具体过程:客户端启动时向Broker获取该Group的所有消费者ID和Topic的所有MessageQueue,然后按策略分配Queue给每个消费者;
- 如果消费者上下线,触发Rebalance重新分配,期间会有短暂暂停消费;
- 注意:消费者数量超过Queue数量时,多出来的消费者分不到Queue会闲置,所以Queue数量要>=消费者数量。
避免Rebalance问题的建议:减少消费者频繁上下线,设置合理的sessionTimeout和heartbeatInterval。