微服务面试题 - 2026-06-19 18:39
生成时间:2026-06-19 18:39 | 共 10 题 | 难度递进(基础→进阶→高阶)
【面试题】1. Nacos的AP模式和CP模式分别是怎么实现的?什么场景切AP、什么场景切CP?
【标准答案】Nacos同时支持AP和CP,靠底层一致性协议切换——CP模式走Raft协议,所有写操作必须Leader过半同步确认,保证强一致性;AP模式走自研的Distro协议,写操作只写负责节点,其余节点异步同步,保证高可用。两种模式对应两种服务实例类型:临时实例走AP——服务挂了心跳超时就自动踢掉,适合业务服务节点,因为节点变化频繁、允许短暂不一致(你在注册中心看到的一个已宕机的节点几秒后才消失,不致命)。持久实例走CP——注册信息需要持久化且一致性优先,适合DNS、核心路由等配置变更低频但必须准确的场景。切换方式很简单:nacos.core.auth.server.identity里配nacos,实例注册时传ephemeral=true/false决定走哪条路径。面试一句:Nacos的聪明之处在于不让你在AP和CP之间二选一——同一个集群里临时实例走AP,持久实例走CP,你得知道自己的数据是什么"命"来选模式。
【面试题】2. 配置热更新的三种主流实现方式有什么区别?@RefreshScope 的坑在哪里?
【标准答案】三种方式各有利弊。方式一:@Value + @RefreshScope——配置变更后调用/actuator/refresh或Nacos自动刷新,Spring重新创建Bean注入新值。坑是@RefreshScope本质是懒加载代理——每次访问Bean都会过一层CGLIB代理判断是否需要重建,轻微性能损耗;而且只在Bean被"使用"时才刷新,没人调它就一直不更新。方式二:@ConfigurationProperties——把配置绑定到POJO,天然支持热更新不需要加@RefreshScope,因为Spring Boot直接监听Environment变更事件更新绑定对象。推荐默认用这个。方式三:Nacos的NacosConfigService.addListener()——手动注册监听器,配置变更回调里自己处理逻辑(比如重建连接池)。适合配置变更不只是更新值、而是要触发动作的场景。@RefreshScope最大的坑:它用@Scope("refresh")重创建Bean——如果你的Bean里有状态(比如缓存、连接),重建后状态全丢了;而且如果多个Bean依赖同一个配置Key,刷新的瞬间A拿到了新值、B还是老Bean没重建,短暂不一致。面试一句:能用@ConfigurationProperties别用@RefreshScope,@RefreshScope是懒汉,@ConfigurationProperties是饿汉。
【面试题】3. Redisson的"看门狗"续期机制是怎么实现的?什么情况下看门狗会失效导致锁提前释放?
【标准答案】Redisson的看门狗(Watchdog)解决的核心问题:业务执行时间超过锁的TTL导致锁自动过期释放,另一个线程拿到锁,两个线程同时执行——这是分布式锁最经典的"锁过期释放"事故。看门狗的工作流程:加锁时如果不显式指定leaseTime(传-1),Redisson默认给锁设30秒TTL,同时启动一个后台定时任务——每30/3=10秒检查一次,如果锁还被当前线程持有(通过锁的value里的UUID+threadId判断),就把TTL重置回30秒。这样只要业务不崩,锁永远不会过期。看门狗失效三种场景:场景一——你显式指定了leaseTime(比如lock.lock(10, TimeUnit.SECONDS)),Redisson认为你自己管过期,不开看门狗,10秒到锁自动释放,业务跑15秒就出事。场景二——JVM Full GC停顿超过30秒,看门狗线程也被暂停,锁过期。场景三——Redis主从切换,主节点上的锁还没同步到从节点就挂了,锁信息丢失,看门狗在旧的Redis连接上续期到空气上,新的Redis实例上另一线程拿到锁。面试一句:Redisson的看门狗是"自动续约保险",不是"分布式一致性保险"——它能防止正常情况下的过早释放,防不了GC停顿和Redis脑裂。
【面试题】4. 令牌桶和漏桶限流算法分别是什么原理?为什么Sentinel用滑动窗口而不是漏桶?
【标准答案】漏桶算法:请求先进漏桶,漏桶以固定速率"滴水"处理请求。桶满了就拒绝新请求。特点是强行平滑——不管流量来得多猛,出去的都是匀速。但它是"暴力限流"——哪怕下游空着、当前QPS远低于处理能力,也得按固定速率排队,浪费系统能力。令牌桶:以固定速率往桶里放令牌,请求来了取令牌,取到就放行,取不到就拒绝或排队。桶有容量上限,容量就是允许的最大突发流量。特点是允许突发——平时流量低令牌堆积,突发流量来了一波拿走堆积的令牌,瞬间打出高于平均速率的QPS,只要桶没空就能扛。Sentinel用滑动窗口的决策逻辑:Sentinel不是API网关层面的匀速整形器,而是在服务入口做"统计+决策"——统计过去N秒的QPS,超过阈值就限流。滑动窗口相比令牌桶的优势是实现简单、内存占用可控、和熔断体系更好联动——限流和熔断都是基于"最近N秒的错误率/QPS"做决策,用同一套窗口计数器。面试一句:漏桶是"交管员"——一辆一辆匀速放行,令牌桶是"通行证"——平时攒证、高峰期凭证入场。Sentinel不用这两者而是用滑动窗口统计,因为它的定位是"智能决策引擎"而非"流量整形器"。
【面试题】5. TCC的"空回滚"、"悬挂"、"幂等"分别是什么?怎么用事务日志表一次性解决这三个问题?
【标准答案】三个问题是TCC落地时的实际工程坑,全是异常时序造成的。空回滚:Try阶段还没执行(网络超时TC以为Try失败了),TC直接发起Cancel。Cancel调过来发现没有Try留下的冻结记录——如果不处理,Cancel就会空转报错。解法:Cancel先查事务日志,Try没执行过就啥也不做,直接返回成功。悬挂:Try因为网络延迟卡在路上,Cancel先到了——Cancel执行完"啥也不做",然后Try才姗姗来迟执行了资源预留。这时候Try预留的资源没人会Confirm也没人会Cancel——资源"挂"在那永远释放不了。解法:Cancel执行后在日志表里标记"Cancel已执行",Try到达时先检查——如果Cancel已经执行过了,Try直接返回失败拒绝预留。幂等:Confirm或Cancel因为网络重试被执行了多次——同一笔冻结的Confirm被执行两次,余额扣两次就惨了。解法:Confirm/Cancel执行前查日志表,检查该分支事务的状态,已处理就直接返回成功。三合一方案——一张tcc_transaction_log表:三个字段tx_id + branch_id + status(TRYING/CONFIRMED/CANCELLED)。Try:插入status=TRYING(如果已存在且status≠INIT说明是悬挂,拒);Confirm:更新status=TRYING→CONFIRMED(行级锁+检查旧status防幂等);Cancel:查status,TRYING→CANCELLED回滚,不存在→空回滚什么都不做,CANCELLED→幂等返回。面试一句:TCC三大难题本质是"网络不确定性导致的乱序调用"——一张状态机表让所有操作都变成"有状态的幂等操作"。
【面试题】6. 最少活跃调用数和最短响应时间这两种负载均衡策略分别在什么场景下更优?和加权轮询比有什么本质不同?
【标准答案】最少活跃调用数(LeastActive):记录每个服务实例当前正在处理的请求数(活跃数),每次选活跃数最小的实例发请求。本质是"看谁最闲就找谁"。适合请求处理耗时差异大的场景——比如A实例正在处理大文件上传(耗时5秒),B实例刚完成上笔请求空闲了,新请求应该去B而不是轮询到A让它雪上加霜。最短响应时间(WeightResponseTime):统计每个实例最近一段时间的平均响应时间,响应时间越短的权重越高,选最快的。本质是"看谁最快就找谁"。适合实例性能不均的场景——比如因为宿主机负载不同、JVM参数差异,同一个服务不同实例天然快慢不一,让快的多扛点。和加权轮询的本质不同:加权轮询是"静态权重"——运维手工配权重,改权重靠人判断;最少活跃和最短响应是"动态权重"——根据实时运行时指标自动调整。但动态权重有滞后性——LeastActive依赖当前正在处理的请求数,瞬间并发洪峰时所有实例的活跃数都同时暴涨,选不出来。面试一句:加权轮询是"计划经济"——静态分配,最少活跃是"市场经济"——谁闲谁干,最短响应是"优胜劣汰"——谁快谁多干。
【面试题】7. 链路追踪的TraceId怎么在异步线程和MQ消息里传递?跨线程池后链路断掉的根因是什么?
【标准答案】链路断掉的根因:TraceId存储在ThreadLocal里(比如SkyWalking的ContextManager用ThreadLocal存当前Span的上下文),线程池的线程和请求线程不是同一个——ThreadLocal隔离了,子线程拿不到父线程的TraceId,生成的Span就成了孤儿Span,在SkyWalking上就断链了。解决分场景。线程池场景:包装Runnable——用TraceRunnable或SkyWalking的RunnableWrapper,在提交到线程池之前把父线程的TraceContext快照下来,子线程run时把快照还原到自己的ThreadLocal里,执行完再清理避免污染。Java Agent自动增强版:SkyWalking的Agent会自动增强JDK的ThreadPoolExecutor——在execute()方法里插入逻辑,自动传递TraceContext,你写threadPool.execute(() -> rpc.call())不需要手动包装。MQ场景:生产者发消息时手动把TraceContext.traceId()塞到MQ消息Header里,消费者收到消息先取出TraceId,ContextManager.createEntrySpan()时用ContextSnapshot恢复上下文——这样消费者生成的Span就能挂到生产者的Span下面。面试一句:链路追踪的本质是把"逻辑上连续的调用链"在"物理上隔离的线程/进程"之间串联——谁断了ThreadLocal谁就断了链路。
【面试题】8. Prometheus的四种指标类型——Counter、Gauge、Histogram、Summary——分别适合什么场景?为什么Histogram比Summary更推荐?
【标准答案】Counter:只增不减的计数器——请求总数、错误次数、订单成交量。适合算QPS(rate(http_requests_total[1m]))和错误率。Gauge:可增可减的瞬时值——CPU使用率、内存占用、连接池活跃数。Histogram:对数据做分桶统计——P99延迟、请求体大小分布。预定义好buckets(比如延迟分{1ms,5ms,10ms,50ms,100ms,500ms,1s}桶),每次记录落到对应桶里,同时自动算出_sum和_count,两个不同Histogram的数据可以聚合(A+B两个实例的P99)。Summary:和Histogram类似也统计分布,但它直接在客户端算好分位数(P50/P90/P99),不依赖服务端计算。缺点是不能聚合——A实例的P99和B实例的P99不能相加得到总P99,只能保留单个实例视角。为什么Histogram推荐:因为Prometheus是中心化存储模型,所有数据拉到Prometheus服务端再计算。Histogram允许你在PromQL里任意组合——算整个集群的P99、算某个API的P99、算某个时间窗的P99——灵活性远高于Summary。面试一句:Histogram是"我告诉你原始分布数据你自己算",Summary是"我帮你算好了但你只能看我算好的那份"——分布式系统里原始数据永远比分位值值钱。
【面试题】9. CQRS落地时,命令侧的写操作和查询侧的读模型(投影)之间怎么保持数据同步?投影延迟和一致性怎么平衡?
【标准答案】CQRS的核心是写模型和读模型的分离——命令侧(Command)用DDD聚合做业务决策写事件到Event Store,查询侧(Query)把事件投影到读优化模型(如MySQL宽表、ES索引、Redis缓存)。同步链路:命令侧写事件→事件发布→投影处理器消费事件→更新读模型。三种同步策略。同步投影:命令侧在同一个事务里写完事件+更新读模型,读模型强一致。延迟为0但耦合死了——命令侧处理变慢,扩展性差。异步投影:事件通过MQ/Kafka异步消费更新读模型,命令侧处理快但有投影延迟。用户刚下单立刻查订单列表可能看不到刚刚的订单,这就是"读自己的写"问题。解决"读自己的写":命令侧返回成功时把事件的投影结果直接塞缓存或直接返回给前端,前端先显示这份"即时结果",等读模型异步追上。对一致性要求高的查询(比如支付回调后验单状态),直接走命令侧的聚合查询而不是走读模型。工程上90%的场景都可以接受"最终一致性+读自己的写兜底",只有资金类核心查询才需要命令侧强一致查询。面试一句:CQRS的投影不是"分布式缓存",而是"广播重放历史"——写侧只管"发生什么了",读侧只管"当前看起来怎么样",两个世界通过事件建立因果关系。
【面试题】10. 微服务技术选型最容易踩的五个坑是什么?一个新人团队从零开始,中间件版本怎么选才不会被"最新版"坑死?
【标准答案】五个坑按杀伤力排序。坑一:上来就全上——注册中心Nacos+配置中心Apollo+网关Gateway+熔断Sentinel+链路SkyWalking+分布式事务Seata+消息RocketMQ+分库分表ShardingSphere,鸡血三天搭环境→一周后出问题哪个都排查不了。解法:最小可用集启动——先用Nacos+Gateway+SkyWalking三件套跑通,再加别的。坑二:追最新版——spring-cloud 2024.0.1刚出就上,结果和spring-boot 3.4.x不兼容、和Sentinel没适配、网上查问题零结果全靠翻源码。解法:永远选次新版——让先行者踩完坑修完bug你再上。坑三:不锁版本——spring-cloud-dependencies用BOM但没显式声明中间件版本,Maven自己解析出不同版本,注册中心和配置中心的客户端版本不一致导致心跳协议不兼容。解法:用dependencyManagement统一锁定所有组件版本号,写在parent pom里。坑四:技术即正义——团队没一个人用过Kubernetes就硬上K8s+Istio,结果Pod重启、网络策略不通、Sidecar注入排队——人力全花在学K8s上,业务代码一行没写。解法:技术选型第一原则不是"技术好不好"而是"团队hold不hold住"。坑五:全自研——"我们不用Nacos,自己写一个注册中心",三个月后发现要做的远不止服务注册——健康检查、元数据管理、配置推送、灰度路由……解法:开源优先,除非有明确原因(安全合规、极致性能)否则不要自研基础设施。版本选择黄金法则:选社区活跃度最高的稳定版——去GitHub看Release Note,选最新稳定版的前一个版本,确认和Spring Boot/Cloud的兼容矩阵对齐。面试一句:技术选型最高的智慧不是"选什么",而是"先不选什么"——你减掉的那些中间件才是让你活下来的原因。
📋 考点覆盖:服务注册与发现(Nacos AP/CP模式)→ 配置中心(配置热更新原理)→ 分布式锁实现(Redisson看门狗)→ 服务熔断降级限流(令牌桶/漏桶/滑动窗口)→ 分布式事务方案(TCC空回滚/悬挂/幂等)→ 负载均衡策略(最少活跃/最短响应)→ 服务链路追踪(TraceId跨线程/MQ传递)→ 可观测性三支柱(Prometheus指标类型)→ 消息驱动架构(CQRS投影一致性)→ 项目从0到1全流程实战(技术选型避坑)
题型分布:概念题(1,3) | 原理题(2,6,7) | 方案对比题(4,6,8) | 架构设计题(5,9) | 场景面试题(10)
难度递进:基础(1-3) → 进阶(4-7) → 高阶架构(8-10)