Skip to content

微服务面试题 - 2026-06-19 17:10 ​

生成时间:2026-06-19 17:10 | 共 10 题 | 难度递进(基础→进阶→高阶)


【面试题】1. 领域事件什么时候发、发在哪一层?为什么不要在聚合里直接调消息中间件? ​

【标准答案】领域事件要在聚合根方法执行完后、事务提交前"收集",在事务提交后"发布"。两个关键点:发在哪一层——聚合根只负责"记录事件",把事件挂在聚合根的 domainEvents 集合里,不负责发;发由应用层或领域服务的 UnitOfWork 在事务提交后统一取出来投递。为什么不在聚合里直接调 MQ?两个原因:一,聚合还在事务里没提交,你发了事件但事务可能回滚,消费者收到事件但数据库里没数据——这就是"幽灵事件";二,聚合是领域模型的核心,不应该耦合基础设施(MQ、RPC),直接依赖 MQ API 会让领域层不纯、测试变复杂。落地方案:聚合里 addDomainEvent(new OrderPaidEvent(...)),Repository 的 save 方法提交事务后,用 DomainEventPublisher.publish(events) 统一发到 MQ。如果项目不重怕麻烦,Spring 的 @TransactionalEventListener(phase = AFTER_COMMIT) 也是一条省心捷径——方法执行完、事务提交后才触发事件处理。


【面试题】2. Nacos 在 AP 和 CP 模式之间是怎么切换的?什么场景下需要切到 CP? ​

【标准答案】Nacos 同时内置了两套一致性协议:Distro 协议是自研的 AP 协议,专门给临时实例用,数据异步复制、强调可用性;Raft 协议是 CP 协议,给持久化实例和配置管理用,数据强一致。切换不是你"改一行配置"的事——临时实例天然走 Distro(AP),持久化实例走 Raft(CP),Nacos 本身是 AP+CP 混合架构。配置中心部分统一走 Raft,确保配置修改不丢失。需要切到 CP 的场景:当你的服务注册不允许出现"明明服务挂了但注册列表里还有"这种情况——比如支付网关的上游服务列表必须精确,宁可不调也不能调到死节点,这时服务方注册为持久化实例,Nacos 走 Raft 保证注册数据所有节点一致。但代价也很清楚:发生网络分区时,少数派分区的节点的写操作会被拒绝,可用性下降。大部分互联网业务场景用临时实例 AP 模式就够了,因为短期内的实例列表不准可以通过客户端重试和熔断来补偿,没必要牺牲可用性换完美一致。


【面试题】3. Spring Cloud Gateway 的路由定义除了写在 yml 里,还能怎么做动态路由?RouteDefinitionRepository 怎么玩? ​

【标准答案】写死在 yml 里的路由最大的问题是:加一条路由要改配置→提交代码→重新部署,这在频繁上下线服务的微服务体系里根本没法忍。动态路由的核心是替换默认的 InMemoryRouteDefinitionRepository,换成你自己的实现。思路很简单:实现 RouteDefinitionRepository 接口,重写 getRouteDefinitions() 方法,从你自己的数据源(MySQL/Redis/Nacos)里读出路由定义列表返回。比如你可以在 MySQL 里建张 gateway_route 表,字段存 routeId、uri、predicates、filters 的 JSON,应用启动时或收到通知时从表里加载。数据变更了怎么刷新?要么发一个 RefreshRoutesEvent 事件让 Gateway 重新加载(CachingRouteDefinitionLocator 监听这个事件会自动清缓存拉最新数据),要么用 Nacos 配置中心的监听机制,配置一变就推送路由更新。实际用 Nacos 最简单:把路由 JSON 存 Nacos 配置里,@NacosConfigListener 监听变更,一行代码刷新路由。面试官想听你理解:路由不应该只是静态配置,它是网关的"动态能力",你自己写个 Repository 存哪都行。


【面试题】4. 线程池隔离、信号量隔离、进程隔离分别怎么防雪崩?各自有什么坑? ​

【标准答案】三种隔离按资源粒度从细到粗:线程池隔离——给每个下游服务独立分配一个线程池,A 服务慢只耗光 A 的线程池,B 服务不受影响。Hystrix 默认就是这个,但坑在于线程数多了上下文切换开销爆炸,10 个服务各 20 线程就是 200 个线程在切。信号量隔离——用计数器限制并发调用数,在调用线程里直接执行,不额外创建线程,轻量得多。Resilience4j 的 Bulkhead 默认就用这个。但坑是:被限的线程是阻塞状态,如果上游用的 Tomcat 线程池,慢调用的线程等于把 Tomcat worker 也占了,间接也影响其他接口。进程隔离——把核心链路和非核心链路拆成不同服务,物理上不共用 JVM 进程,一个崩了另一个完全无关。最彻底但也最重。实战选型逻辑:RPC 调用用信号量隔离(几毫秒的调用不需要独立线程),第三方 HTTP 调用用线程池隔离(可能超时很久),核心和非核心链路进程隔离。Signalement 隔离虽然轻量但要注意:它不是"熔断"只是"限并发",要配合超时和熔断一起用才完整。


【面试题】5. 本地消息表方案的完整落地流程是怎样的?一条扣款+加款的操作,怎么保证消息不丢、消费幂等? ​

【标准答案】假设订单服务扣款后要通知积分服务加积分。落地四步:第一步,订单服务在同一个本地事务里做两件事——扣余额 + insert 一条消息记录到 message_log 表(状态=待发送)。如果扣余额失败,事务回滚,消息也不会有;如果扣余额成功,消息一定在表里。这叫"业务与消息同事务"。第二步,定时任务(每秒扫一次)查 message_log 表里状态为待发送的记录,投递到 MQ,投递成功后更新状态为已发送。MQ 投递失败?没事,下次定时任务继续扫到还会重试。第三步,积分服务消费消息,做三件事——查去重表(msg_id 是不是处理过)→ 处理过就跳过,没处理过就加积分 + insert 去重记录(这两步在同一事务里)。第四步,积分服务处理成功后返回 ACK,MQ 标记消费完成。关键保障:消息不丢靠第一步的"同事务";消费幂等靠第三步的"去重表+业务在同一事务";投递失败靠定时扫描+MQ重试双重兜底。用 MQ 事务消息能省掉定时扫描,但核心机制是一样的——都是"先把消息存到可靠的存储里,再异步投递"。


【面试题】6. 最小活跃数负载均衡是怎么工作的?和加权轮询比,什么场景下它优势碾压? ​

【标准答案】最小活跃数的核心逻辑:每个服务实例维护一个 activeCounter,调该实例时 +1,收到响应时 -1。负载均衡时遍历所有实例,选 activeCounter 最小的那个,如果多个实例的 activeCounter 一样小就加权随机选一个。本质是"谁最空闲就调谁",动态感知实例的实时负载。和加权轮询的核心区别:轮询是"静态分配"——按权重比例机械分发,不管 A 实例是不是正在处理 10 个慢请求还在给它塞;最小活跃数是"动态避让"——A 实例忙了就少给它请求,B 实例闲就多给。碾压场景很典型:服务实例配置不同——三个实例分别 2C4G、4C8G、8C16G,跑同一套代码但处理能力差 4 倍,轮询会把慢实例打到爆而大实例闲得慌,最小活跃数会自动把更多流量导向大实例。另一个场景是下游响应时间波动大的接口——某个实例刚好在 GC 响应变慢,activeCounter 居高不下,新请求自动路由到其他实例。Dubbo 的 LeastActiveLoadBalance 就是标准实现,而且它还在选择前加了"预热权重"防止刚启动的实例被一波流打死。


【面试题】7. Sentinel 规则怎么持久化到 Nacos?原始模式、Pull 模式、Push 模式的区别和优劣? ​

【标准答案】Sentinel 默认是原始模式:规则存在 JVM 内存里,重启就没了,等于每次上线都要重新在 Dashboard 里配一遍——生产环境完全不可用。Pull 模式:客户端定时从文件/配置中心拉取规则。优点是简单,不需要改源码;缺点是拉取有间隔——规则改了在下一次拉取前不生效,紧急限流可能延迟十几秒。Push 模式(生产推荐):控制台把规则推到 Nacos 配置中心,客户端监听 Nacos 配置变更实时感知,规则秒级生效。实现方式:客户端注册 Nacos 的 Listener,配置一变更就调用 FlowRuleManager.loadRules() 刷新;控制台(需要改造官方 Sentinel Dashboard 的源码)在用户保存规则时调用 Nacos 的 configService.publishConfig() 推送规则。三者选型一句话:Demo 用原始、小规模用 Pull、生产用 Push。Push 模式最大的坑是:Sentinel 官方 Dashboard 的"推送到 Nacos"功能需要自己改源码,不是开箱即用的。很多团队嫌麻烦直接放弃了 Dashboard,用 Nacos 配置管理规则再代码里监听,效果一样。


【面试题】8. etcd 怎么实现分布式锁?和 Redis、Zookeeper 锁的对比,各自最佳战场在哪? ​

【标准答案】etcd 做分布式锁依赖三个核心机制:lease(租约,相当于过期时间)、事务(原子 CAS 操作)、watch(监听 key 变化)。流程:创建 lease 设 TTL 比如 30s → 用事务 put 一个 key,条件是 CreateRevision == 0(不存在才创建,保证互斥)→ 创建成功拿到锁;创建失败说明别人先拿到了,watch 这个 key 的 Delete 事件阻塞等待 → 别人释放后收到通知再去抢。持有锁期间起 goroutine 自动续约 keepalive,防止业务没处理完锁过期。对比选型:Redis 锁(Redisson)——性能最高、部署最普遍,适合"锁持有时间短、对极端一致性不要求 100% 绝对安全"的场景,比如防重复点击、定时任务互斥;Zookeeper 锁——基于临时顺序节点 + Watcher,公平锁、可靠性高,适合对公平性有要求的场景,但吞吐量受 CP 协议写性能限制;etcd 锁——和 ZK 锁机制类似但更现代,原生 Go 实现、自带 HTTP/gRPC API,在 K8s 生态里是天然选择,适合 Kubernetes 环境下做 Leader 选举、分布式协调。一句话选型:短锁高并发选 Redis,公平锁和老 Java 生态选 ZK,K8s 原生选 etcd。


【面试题】9. 递增 ID 和非递增 ID 分别适合什么业务场景?为什么订单号不直接用数据库自增? ​

【标准答案】递增 ID 和非递增 ID 的核心矛盾在于:递增有利于数据库写入性能(B+Tree 顺序插入无页分裂),但会暴露业务信息;非递增(随机、雪花)对索引不友好,但安全。数据库自增 ID 做订单号有两个致命问题:第一,竞对可以根据订单号差值算出你的日单量——今天下单 ID 120000,明天 150000,30000 单/天直接暴露了商业机密;第二,分库分表后自增 ID 会冲突——订单库拆了 8 张表,每个表从 0 自增,ID 全重复。雪花算法解决了这两个问题:不连续(趋势递增但中间有洞,看不出业务量)+ 分布式唯一(机器 ID + 时间戳 + 序列号,天然不重复)。但是雪花算法是趋势递增不是严格递增,对 MySQL InnoDB 主键也不是最优——中间的空洞会导致页分裂。什么时候用严格递增:内部系统、数据量预测模型训练、需要按 ID 排序分页的业务,这些场景用号段模式(Leaf-Segment)拿一整个号段后在内存里顺序发号。订单号、用户 ID 这些对外展示的场景,雪花算法 + 前端脱敏是最佳组合。


【面试题】10. 大促前的全链路压测:怎么保证压测流量不影响真实用户?压测标怎么打、怎么透传、怎么隔离? ​

【标准答案】全链路压测的核心挑战是一个字——"脏":不能把压测数据写进真实数据库,不能让压测流量触发真实短信/推送,不能让压测流量污染业务报表。解决方案分四步:第一步,流量染色——压测平台在发起请求的 Header 里打上压测标(比如 x-pressure-test: true),这就是"压测身份牌"。第二步,全链路透传——中间件(RPC 框架、MQ、网关)把压测标从入口一路透传到末梢,Dubbo 的 RpcContext、Spring Cloud 的 RequestHeader 都能做,关键是"别在中间某一层把标丢了"。第三步,数据隔离——影子库:压测流量写到生产库的"影子表"或独立压测 Schema(比如 order → order_pressure),读也读影子表;影子 MQ Topic:压测消息投递到专用 Topic,不搅乱真实业务消息;影子日志:压测的日志打特殊 Tag,ELK/CLS 可过滤。第四步,外部服务 Mock——短信、推送、第三方支付必须 Mock 或拦截,绝不能真发。落地先从小流量开始(比如 1%),确认全链路标都透传到位再逐步加量。最关键的验证手段:随机抽查生产订单表里有没有压测数据,少一条规则防护就可能造成线上事故。