微服务面试题 - 2026-06-19 19:24
生成时间:2026-06-19 19:24 | 共 10 题 | 难度递进(基础→进阶→高阶)
【面试题】1. 微服务之间用 RPC 直接调用和用领域事件异步协作,分别适合什么场景?两者混用时最容易踩的坑是什么?
【标准答案】RPC 直接调用适合"请求-响应"模式——A 调 B 就是为了拿 B 的结果做下一步判断,比如下单前实时查库存是否够。优点是链路短、逻辑直白,缺点是 A 和 B 形成运行时耦合——B 挂了 A 直接被拖垮。领域事件适合"通知-反应"模式——A 发生某事后广播事件,关心这件事的服务自己处理,比如"订单创建后"→发积分、发通知、写审计日志。优点是完全解耦——订单服务不需要知道积分服务的存在,缺点是不能同步获取返回值。混用时最容易踩的坑是"半异步半同步"——订单创建后 RPC 调库存扣减同步返回结果,但发积分用事件异步处理。问题出在:如果扣库存成功但发积分异步失败了,订单已经是"已支付"状态但积分没到账。正经解法是用 Outbox 模式把"发积分事件"和"扣库存操作"放在同一个事务里保证原子投递。面试一句:RPC 是"我让你现在回答",领域事件是"我做了件事,你爱管不管"——混用时搞清楚每一次交互到底是"必须立刻知道答案"还是"告诉你一声就行"。
【面试题】2. Nacos 的 AP 模式和 CP 模式分别是怎么切换的?什么场景下需要临时切到 CP 模式?
【标准答案】Nacos 对服务注册走 AP、对配置管理走 CP——这是内置的两套协议,不需要你手动切换。服务注册底层走 Distro 协议(阿里自研的 AP 协议):节点间异步同步注册信息,网络分区时每个分区都能独立接受注册请求,分区恢复后自动合并数据。为什么是 AP?因为注册中心短暂出现不一致(一个 Pod 已经被删了但某些节点还没同步到)的代价远小于服务注册不可用——顶多多打一个不存在实例,重试一次就行。配置管理走 Raft 协议(CP):任何配置变更必须过半数节点确认才能返回成功。为什么是 CP?因为配置不一致的代价巨大——两个节点看到不同版本的配置,一个服务按旧配置限流(比如 QPS=1000)一个按新配置(QPS=100),线上行为割裂。生产上你需要关心的是:当 Nacos 集群只剩少数节点存活(比如 3 节点挂了 2 个),配置管理会拒绝写入——这是符合预期的安全行为,提示你赶紧修节点而不是关掉 CP。面试一句:Nacos 的双协议不是让你选的,是它替你选的——服务发现"宁可多发绝不漏发"走 AP,配置管理"必须一致绝不含糊"走 CP。
【面试题】3. 网关层统一鉴权和微服务内部鉴权怎么分层?什么场景下网关验过之后微服务内部还要再验一次?
【标准答案】标准做法是两道防线——网关做粗粒度鉴权,微服务做细粒度鉴权。网关层做的事情:验证 Token 有效性(JWT 验签、过期检查)、解析用户身份(userId、tenantId)、把认证后的用户上下文通过 Header 透传给下游。网关不做的事情:权限校验——"这个用户有没有删除订单 #12345 的权限"不在网关做,网关不知道订单 #12345 属于谁。微服务内部做细粒度鉴权:根据网关传过来的 userId 做数据级权限检查——"这个订单的 owner 是不是当前 userId"。网关验完微服务还必须验的场景:一、内部 RPC 调用绕过网关——A 服务调 B 服务不走网关,如果 B 不做鉴权等于大门锁了但窗户开着;二、权限与业务数据绑定——"部门经理可以查看本部门报表",只有报表服务自己知道部门关系,网关管不了;三、多租户隔离——SaaS 系统里 tenantId 正确但 userId 传错可能跨租户访问,微服务必须校验 userId 和 tenantId 的归属关系。面试一句:网关是小区门卫——看你是不是这个小区的(验证身份);微服务是你家门锁——看这间房是不是你的(验证权限)。门卫认识你不代表你可以进别人家。
【面试题】4. 配置变更推送后,应用是怎么感知并刷新 Bean 的?Spring Cloud Config + Bus 广播和 Nacos 长连接推各有什优劣?
【标准答案】两种推送路径完全不同。Spring Cloud Config + Bus 的路径:Config Server 暴露 /monitor 端点→你或用 Git Hook 或用定时任务触发→Config Server 通过 RabbitMQ/Kafka 的 Bus 通道广播 RefreshRemoteApplicationEvent→所有订阅了该通道的应用收到事件→调用 ContextRefresher.refresh()→重新绑定 @RefreshScope 的 Bean。核心卡点在于依赖 MQ——Mq 挂了配置无法刷新。Nacos 的路径:客户端和 Nacos 服务端维持 gRPC 双向流,配置变更时服务端直接 Push 一个 ConfigChangeNotify 请求给客户端→客户端收到后发 ConfigQueryRequest 拉最新配置→对比 MD5 不一致→触发 RefreshEvent。对比:Config+Bus 的优点是生态兼容——非 Java 应用也能通过 HTTP 拉配置+MQ 消息感知变更;缺点是多一个 MQ 依赖,链路长故障点也多。Nacos 的优点是无外部依赖,一条 gRPC 通道搞定注册+配置+推送,毫秒级推送;缺点是 SDK 强绑定 Java/Go 等 Nacos 客户端。面试一句:Config+Bus 是"打电话通知全公司开会"——有外线(MQ)就行,Nacos 是"对讲机内网直呼"——快但得带同一台对讲机。
【面试题】5. BASE 理论的"柔性状态"在数据库层面怎么实现?最终一致性窗口大小怎么确定?
【标准答案】柔性状态就是"允许数据短时间内处于中间态"。数据库层面通常不靠 DB 事务来实现,而是靠业务字段标记状态机。比如转账:不是用数据库事务保证"从 A 扣钱和给 B 加钱同时成功",而是:A 扣钱成功后记录状态 status=DEBITED,给 B 加钱的异步任务去轮询或消费这个状态来完成加钱。在这个窗口期,B 的余额还没增加——这就是柔性状态。最终一致性窗口的确定取决于两个因子:一是业务容忍度——用户发起提现后能等 3 秒还是 3 分钟?C 端高交互场景窗口控制在秒级(消息队列+消费者立即处理),B 端报表对账场景窗口可以到小时级(T+1 跑批)。二是系统处理能力——窗口大小 = 消息积压量 / 消费者处理速率。100 万条待处理消息、消费者每秒处理 1000 条,窗口就是 1000 秒。落地做法是在监控里盯"待处理消息量"或"补偿任务滞后时间",超过窗口上限就拉告警不要等着用户投诉。面试一句:柔性状态不是"不管了,爱一致不一致",而是"我明确告诉你你要等多久,且我盯着这个窗口别裂大"。
【面试题】6. SAGA 事务的编排模式(Orchestration)和协同模式(Choreography)各是什么工作原理?一个退货退款的长 SAGA 里,补偿链断了怎么办?
【标准答案】编排模式:有一个 SAGA 协调器(Orchestrator)作为总指挥——它按顺序告诉订单服务"创建退货单"→收到成功响应后告诉库存服务"回退库存"→再告诉支付服务"发起退款"。每个步骤成功或失败都由协调器决定下一步做什么。好处是流程集中可见、好排查,坏处是协调器变成单点和业务逻辑上帝对象。协同模式:没有总指挥,每个服务各自监听事件并做出反应——订单服务发布"退货申请已创建"事件,库存服务监听到后自动回退库存并发布"库存已回退",支付服务监听后发起退款。好处是高度解耦,坏处是流程散落在各服务代码里看不见全局——售后客服问"我的退款到哪一步了",你需要在四个服务的日志里拼凑。补偿链断了(比如退款接口调三次全失败)的处理分两层:短期——协调器/事件消费者本地持久化待补偿任务到 DB + 定时任务重试,至少保证不丢;长期——进入人工处理队列,运营后台展示"这笔退款自动处理失败,请人工在支付宝后台操作退款"。关键设计:SAGA 的补偿不是"保证一定成功"而是"保证一定能感知到失败且不丢失败信息"。面试一句:编排像导游拿旗子带着全团走,协同像每人拿张地图自己走——无论哪种,丢人了得有对讲机喊救援。
【面试题】7. 加权最小连接数负载均衡算法是怎么工作的?和加权轮询相比,在长连接和短连接场景下表现差多少?
【标准答案】加权最小连接数算法核心是"谁最闲派给谁":维护每个实例的权重 w 和当前活跃连接数 c,新请求到来时计算每个实例的负载值 c / w,选值最小的那个分配。权重高的实例可以有更多连接才算"同等忙"。加权轮询是"按权重轮着来"——权重 3:1:1 的实例顺序是 AABAC AABAC...不考虑实际负载。在短连接场景(HTTP REST 微服务):每个请求几百毫秒就结束,连接数变化极快,最小连接数的优势不明显——轮询的分配已经足够均匀,采集连接数的开销反而变成了多余。在长连接场景(WebSocket、gRPC Stream、数据库连接池):连接一挂就是几分钟甚至几小时,实例 A 可能被 500 个长连接占满而实例 B 只有 10 个——加权轮询仍然"公平"地发给 A,A 直接被打爆。最小连接数能天然避开已经满载的实例。工程实现上,最小连接数需要实时获取每个实例的活跃连接数——要么从客户端调用计数器拿,要么通过注册中心的健康数据中继。面试一句:轮询是无视现实死板按表分配,最小连接数是看一眼谁有空再派活——高并发短连接差别不大,长连接场景是生死之差。
【面试题】8. 限流算法里,滑动窗口相比固定窗口解决了什么问题?在并发量很高的情况下,滑动窗口怎么做到精确且内存可控?
【标准答案】固定窗口的致命缺陷是"临界突刺"——你在 0 秒 ~ 1 秒之间允许 100 个请求,1 秒 ~ 2 秒之间也允许 100 个,但问题是:0.5 秒 ~ 1.5 秒这个跨窗口区间里实际通过了 200 个请求,刚好是你的限流阈值两倍。攻击者只要把流量集中在每个窗口边界就能精准绕过固定窗口限流。滑动窗口把时间轴切分成更小的分片(比如 10 个小格子,每格 100ms),每次判断时取"当前时间往前推 1 秒"内的所有格子计数之和。格子越细越精确。并发高时精确+内存可控的做法是"数组 + 原子 CAS 循环复用"。Sentinel 的实现:在内存里开一个固定大小的数组(比如 60 格,格数固定不增长),用 currentTimeMillis() / 1000 % 60 算出当前时间落在哪个格子。每次请求过来:先 CAS 把当前格子的计数+1,然后取最近 N 个格子的计数总和判断是否超阈值。关键是不用锁——单个格子的更新用 AtomicLong.addAndGet(),窗口求和是遍历数组原子读,线程安全零锁开销。面试一句:固定窗口是"整点整分靠粗刻度猜时间",滑动窗口是"秒表连续计时"——细粒度决定了防突刺能力,原子 CAS 决定了高并发下不被锁拖死。
【面试题】9. Redisson 可重入锁 Redis 里存的 Hash 结构是什么样的?lua 脚本加锁的逻辑怎么保证"同一个线程多次加锁"和"不同线程互斥"同时成立?
【标准答案】Redisson 可重入锁在 Redis 里的存储结构是一个 Hash:key = 锁名称(比如 myLock),field = UUID:线程ID(标识当前持有锁的客户端+线程),value = 重入次数。加锁的 lua 脚本逻辑分三步(一次 EVAL 原子执行):第一步——exists myLock 查锁不存在→hset myLock UUID:thread1 1,pexpire myLock 30000——直接拿锁,重入计数=1;第二步——exists myLock 锁存在→hexists myLock UUID:thread1 检查是不是我自己持的→是→hincrby myLock UUID:thread1 1 重入计数+1,续期 TTL——同一个线程可重入;第三步——锁存在但 field 不是我的→返回剩余 TTL 代表锁被别人持有,上层自旋等待。解锁的 lua 脚本:hincrby myLock UUID:thread1 -1 重入计数-1,如果结果>0 表示还有外层方法在用锁→只减计数不解锁;如果结果=0→del myLock 完全释放锁,广播释放消息通知等待队列。面试一句:Hash 的 field 区分"谁拿的锁",value 记录"拿了几层"——同一线程每进一层 lock() 计数+1,每出一层 unlock() 计数-1,计数归零才真正交钥匙。
【面试题】10. CQRS 读写分离在微服务里怎么落地?什么时候应该拆成两个独立的微服务,什么时候在同一服务内做读写分离就够了?
【标准答案】CQRS 在微服务里有三个层次的落地深度。第一层——同库同服务读写分离:代码层面把 Command(写操作)和 Query(读操作)分成不同的 Service 类甚至不同的 Controller。Command 走主库、Query 走只读副本。没有分布式开销,适合查询和写入的数据模型差别不大的场景。第二层——同服务读写分库:一个服务内部,Command 写 MySQL,Query 读 Elasticsearch——MySQL 的 Binlog 通过 Canal 实时同步到 ES。查询性能大大提升(全文搜索、聚合分析),但增加了数据同步延迟和双写一致性的复杂度。第三层——拆成独立微服务:订单 Command 服务和订单 Query 服务是两个独立部署、独立扩缩容的微服务。Command 服务只管处理"下单、取消"这些写操作,Query 服务只管"我的订单列表、订单详情"这些读操作。什么时候拆独立微服务?读和写的 QPS 要求极度不对称——写 100 QPS 只需要 2 个 Pod,读 10000 QPS 需要 20 个 Pod,不拆的话写服务被读流量占了资源;读和写的 SLA 不同——读可以接受 5 秒延迟但绝不能挂,写挂 30 秒也勉强能扛;读的数据来自多个领域聚合——"订单详情"需要拼接订单、用户、商品、物流五个领域的数据,Query 服务更适合做数据聚合层。面试一句:CQRS 的核心不是"必须拆两个微服务",而是"读和写的数据模型、性能要求、扩展方向不一样时,别把它们绑在一起"——同服务内分代码层也是一种 CQRS。
📋 考点覆盖:微服务架构设计原则(领域事件 vs RPC 协作选择)→ 服务注册与发现(Nacos AP/CP 双协议原理)→ 服务网关(网关鉴权与微服务鉴权分层)→ 配置中心(配置变更推送刷新机制对比)→ 分布式核心理论(BASE 柔性状态与一致性窗口设计)→ 分布式事务方案(SAGA 编排 vs 协同模式)→ 负载均衡策略(加权最小连接数 vs 加权轮询)→ 服务熔断降级限流(滑动窗口 vs 固定窗口算法)→ 分布式锁实现(Redisson 可重入锁 Hash 存储结构)→ 消息驱动架构(CQRS 读写分离落地)
题型分布:概念题(1,2) | 原理题(7,8,9) | 方案对比题(4,6) | 架构设计题(3,5,10)
难度递进:基础(1-3) → 进阶(4-7) → 高阶架构(8-10)