Skip to content

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

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


【面试题】1. DDD 的聚合怎么设计才不算"过度"?聚合根怎么选、大小怎么控制、常见的反模式有哪些? ​

【标准答案】聚合设计的核心一句话:事务一致性边界。聚合内的实体必须在同一个事务里修改——这就是你画聚合边界的锚点。聚合根怎么选:挑那个"如果它不存在,其他实体没意义"的那个实体——订单聚合的根是订单头,没有订单哪有订单行?订单行不可能独立存在。聚合大小的控制:一个小聚合最好只包含一个根实体 + 1~3 个值对象或子实体,别搞出一个"订单聚合包含用户、商品、库存"的怪物——那不是聚合那是上帝对象。三个常见反模式:第一,大聚合——把整个业务域塞进一个聚合,一次修改锁住半张表;第二,裸聚合——聚合根的所有属性都 public set,外部代码直接 order.status = xxx 绕过领域逻辑,等于没做聚合;第三,跨聚合事务——订单聚合的方法里直接改库存聚合的数据,正确做法是改完发领域事件,库存自己处理。面试官想听你区分"数据关联"和"事务一致性边界"——数据在查询层面可以 JOIN,但聚合边界只看"哪些东西改了必须一起成功一起失败"。


【面试题】2. 配置中心怎么做到"配置灰度发布"?一个配置项从修改到全网生效,安全发布流程是怎样的? ​

【标准答案】配置灰度的核心是:一部分实例先用新配置跑,跑稳了再全量。Apollo 的做法最直观:每个配置项可以建多个"灰度发布规则",规则决定了哪些实例看到新值、哪些继续看旧值。比如按客户端 IP 前 10% 的实例走新配置,跑 10 分钟没告警就全量放开。Nacos 的做法是通过"配置版本 + 标签路由"——发布新配置时打上 beta 标签,只有带了 beta 标签的客户端拉取这个版本,其他客户端拉正式版。安全发布流程四步:第一步,配置变更发到灰度集,观察灰度机器的业务指标(错误率、延迟)5~10 分钟;第二步,灰度无异常则全量发布,但保留旧版本快照;第三步,全量后继续监控 30 分钟——有些问题不是马上暴露的(比如内存泄漏要跑一阵子);第四步,一旦出问题一键回滚——Apollo 的"回滚到上一个发布版本"一个按钮就回去了。最大的坑不是技术是流程——很多人直接把配置一发就走,根本不做灰度。面试关键:配置变更的风险不亚于代码变更,灰度、监控、回滚三步缺一不可。


【面试题】3. 网关限流的四种算法——固定窗口、滑动窗口、漏桶、令牌桶,各自原理和最致命的缺陷是什么? ​

【标准答案】固定窗口:把时间切成固定窗口(1 秒一个),窗口内计数超过阈值就拒绝。致命缺陷在边界——窗口最后 100ms 来了 100 个请求、下个窗口前 100ms 又来了 100 个,200ms 内过了 200 个请求但限流设的 100 QPS,限流被绕过。这叫"临界突刺"。滑动窗口:把窗口再切成 N 个小格子(如 10 个 100ms 格子),随时间滑动,统计最近窗口内的请求总数。解决了固定窗口的临界突刺问题,但格子越细内存越大——精度和内存的 trade-off。漏桶:固定速率流出请求,桶满了就拒绝。优点是流量曲线被削平、输出绝对平滑;致命缺陷是——突发流量被一刀切死,秒杀场景前 10ms 的流量刚冒头就被拒了,后端明明能扛却被限了。令牌桶:以恒定速率往桶里放令牌,请求来了拿令牌,拿不到就限。核心优势就是能处理突发——桶里可以囤令牌,瞬间大流量来了先消耗囤积的令牌,桶空了才开始限流。选型:需要平滑输出 → 漏桶;需要容忍突发 → 令牌桶;内存敏感且精度不要求极致 → 固定窗口;生产级推荐 → 滑动窗口,被 Sentinel 和 Resilience4j 大量使用。


【面试题】4. RocketMQ 的事务消息是怎么做到"本地事务和消息发送要么都成功要么都失败"的?半消息和回查机制具体怎么协作? ​

【标准答案】事务消息解决的经典问题:下单 + 发消息通知扣库存,如果发消息成功了但本地事务回滚了,库存已经扣了——这就是分布式事务的元凶。RocketMQ 的做法分两步——半消息(Half Message)和回查。流程:第一步,生产者发一条"半消息"给 Broker,Broker 收到后不投递给消费者,先持久化并标记为 PREPARED 状态,然后返回确认;第二步,生产者收到确认后执行本地事务(比如下单),执行成功后向 Broker 发 COMMIT,Broker 把消息标为可消费状态投递给消费者;执行失败发 ROLLBACK,Broker 删除消息。问题来了——如果生产者执行完本地事务后还没来得及发 COMMIT/ROLLBACK 就宕机了?回查机制上场:Broker 定时扫描长时间处于 PREPARED 状态的消息,反向调生产者的回查接口(checkLocalTransaction),问"这笔事务到底成功没",生产者根据本地数据库里的订单状态返回 COMMIT 或 ROLLBACK。回查接口必须幂等可重入——Broker 可能重试多次。面试核心:半消息是"先占坑",回查是"帮你擦屁股",两者配对才能保证本地事务和消息的原子性。注意约束:事务消息不支持延迟发送,不支持批量,一条消息一个事务。


【面试题】5. Prometheus 的 Counter、Gauge、Histogram、Summary 四种指标类型各适合监控什么?接口 QPS 和 P99 延迟分别用什么? ​

【标准答案】Counter:只增不减的计数器——请求总数、错误次数、累计消费消息数。特点是单调递增,重启后归零重新算。接口 QPS 不是直接拿 Counter 看绝对值,而是 rate(http_requests_total[1m]) 算每秒增长率。Gauge:可增可减的瞬时值——JVM 堆内存使用量、活跃连接数、CPU 温度。适合看"现在是什么状态"。Histogram:把数据分桶统计——每个请求延迟丢进对应桶(<10ms 的桶、<50ms 的桶、<100ms 的桶...),可以算出 P50/P90/P99。用法就是 histogram_quantile(0.99, rate(http_duration_seconds_bucket[5m])) 直接出 P99。Summary:在客户端侧直接算出分位值,不靠服务端分桶。Histogram 和 Summary 的核心区别:Histogram 可以事后聚合不同实例的数据算整体 P99(服务端分桶灵活),Summary 的分位值在客户端算好了无法跨实例聚合,但精度更高。选型建议:接口 QPS → rate(counter[1m]);P99 延迟 → Histogram(因为能聚合多实例,Grafana 画大盘必备);单个实例的精确延迟分布 → Summary。绝大多数场景用 Histogram 就够了,只有需要极其精确的客户端侧分位值时才用 Summary。


【面试题】6. 一致性哈希在 RPC 负载均衡和分布式缓存中的用法有什么不同?各自最常踩的坑是什么? ​

【标准答案】同一个算法,两个场景的用法有本质区别。RPC 负载均衡场景:目的是"同一个请求尽量路由到同一台机器"。比如 Dubbo 的一致性哈希负载均衡取 hash(请求参数) mod 2^32 映射到环上,找最近的虚拟节点→物理节点。好处是请求亲和性——同一个 userId 的请求总打到同一台机器,本地缓存命中率高。但坑也在这:如果这台机器挂了,这个 userId 的所有请求都会路由到环上顺时针的下一个节点,可能导致那个节点流量瞬间暴增→级联崩溃。解法是给每台物理节点配 160 个虚拟节点,均匀分散在环上,一台挂了它的 160 个点均匀分摊到其他所有机器,而不是全怼到一个邻居上。分布式缓存场景(Memcached/Redis Cluster 客户端路由):目的是"节点增减时最少量的缓存数据需要迁移"。坑完全不同——缓存场景用的是"实例一致性哈希",虚拟节点数量的选择极关键:太少(10 个)→ 数据倾斜严重,某台机器存了 50% 数据;太多(1000 个)→ 节点变更时虚拟节点重新分配的判断太重。150~200 个是业界经验值。还有一个 RPC 场景不需要但缓存场景要命的坑——"缓存雪崩的放大":一台缓存挂了,它的数据分散到其他节点,其他节点内存本来 80% 了突然多塞数据,可能触发 LRU 淘汰大量热数据 → 连锁雪崩。解法是一致性哈希 + 每个节点加一个"权重缓冲",扩容时逐步加权重而不是一次性全量。


【面试题】7. Sentinel 的滑动窗口统计是怎么在 O(1) 时间复杂度下做到精确计数的?LeapArray 滑动窗口算法的核心设计是什么? ​

【标准答案】Sentinel 的 LeapArray 核心是"时间窗口数组 + 无锁 CAS 写入"的双保险。数据结构:一个固定长度的数组(默认 60 个格子),每个格子代表一个时间窗口(默认 1000ms/2 = 500ms)。为什么 60×500ms = 30s?因为统计窗口是 1 分钟。LeapArray 的精妙设计在"当前时间落在哪个格子上"——当前时间 / 窗口长度 = 数组下标,直接 O(1) 定位到格子。如果当前时间落在格子 B 内,直接 CAS 把格子 B 的计数器+1,不需要加锁。核心来了:如果当前时间超过格子 B 的时间范围了怎么办?说明这个格子是旧窗口的数据——先用 CAS 把格子重置(计数器归零、时间戳更新为当前窗口),再计数。为什么是 O(1)?获取总计数时不是遍历所有格子加和——维护一个 total 变量,写入时 CAS(当前格子 +1, total +1),读取直接读 total。但 total 和格子可能有一点点"最终一致"——格子更新了但 total 还没同步,最多差几百毫秒的计数,对于限流场景完全可接受。和传统滑动窗口的最关键区别:传统滑动窗口要维护时间戳列表,每次来请求要扫列表删过期数据(O(n)),Sentinel 用固定数组 + 当前时间直接定位格子(O(1)),精度通过格子数量调(60 个格子精度 ≈ 30s/60 = 500ms)。面试关键:数组下标 O(1) 定位 + CAS 无锁写入 + total 近似实时 = 高并发限流的工程智慧。


【面试题】8. 缓存更新策略——Cache Aside、Read/Write Through、Write Behind 的区别?数据库和缓存双写时,到底先删缓存还是先写数据库? ​

【标准答案】四种模式,核心区别在"谁负责同步缓存"。Cache Aside:应用自己管——查数据先查缓存,miss 了查 DB 并回填缓存;写数据直接写 DB,然后删缓存(让下次读重新加载)。最常用也最灵活,但问题是代码散在各处,容易漏删。Read Through:缓存挡在 DB 前面——读请求经过缓存层,miss 了缓存层自己去查 DB 回填,应用只和缓存打交道。Write Through:写请求也经过缓存层——写缓存的同时写 DB,缓存和 DB 同步更新。Write Behind(Write Back):写请求只写缓存就返回,缓存层异步批量刷到 DB。吞吐量最高但丢数据风险最大——缓存挂了还没刷到 DB 的数据永久丢失。双写顺序是最经典的坑:先删缓存再写 DB → 删完缓存后还没写完 DB,另一个线程读进来 miss 缓存读到旧 DB 再把旧数据回填缓存,缓存里永远是旧数据。先写 DB 再删缓存 → 写 DB 成功后删缓存失败(网络超时),缓存里还是旧数据。正确答案:先写 DB 再删缓存——因为写 DB 成功 + 删缓存失败的概率远小于"删缓存后短暂窗口内并发读"的概率。但重点不是排序,是兜底——删缓存失败要重试(放到 MQ 里异步重试),或者设短 TTL 让旧缓存自然过期。面试金句:Cache Aside + 先写 DB 再删缓存 + 删除失败异步重试 + 短 TTL = 99% 场景的最优解。别用复杂的分布式一致性协议,划不来。


【面试题】9. 线上 Tomcat 线程池打满了,大量请求排队超时返回 503,你怎么不停服快速止血?事后根因排查从哪几个维度入手? ​

【标准答案】止血三步不到一分钟。第一步:限流削峰——在网关层立刻给该服务的接口配 QPS 限流规则,把超过处理能力的请求直接挡在门外返回降级响应,保住已经排队的请求。如果用了 Sentinel/Nacos 配限流秒级生效最好,没有就紧急上 Nginx limit_req。第二步:快速扩容——K8s 就 kubectl scale 翻倍 Pod,传统部署就立刻加机器拉新实例。如果是因为下游依赖慢导致的线程堆积,第三步:熔断下游——把那个慢调用的下游直接熔断,让堆积的线程快速失败释放,别让下游拖死上游。根因排查五维度:第一,线程栈定位——jstack <PID> > thread.log,看线程都在干什么。如果 200 个线程全 BLOCKED 在同一个数据库连接 getConnection(),那就是连接池满了;全 WAITING 在 httpclient.execute(),那就是下游超时。第二,数据库维度——SHOW PROCESSLIST 看有没有大量长事务或慢查询把连接占着,连接池再大也没用,连接都卡在慢 SQL 上。第三,下游调用维度——SkyWalking 里看 P99 延迟突增的下游接口,可能是网络抖动或下游 GC。第四,线程池参数维度——核心线程数配了多少?最大线程数配了多少?队列长度多少?队列用 LinkedBlockingQueue 配了 10000 但最大线程数只有 50,等于设了一个超大蓄水池——慢请求把队列灌满后快速失败配置失效。第五,代码维度——有没有在 Tomcat 线程里做 CPU 密集计算或调第三方接口没设超时。根治公式:合理的线程池参数(核心=最大,队列用小容量有界队列)+ 所有外部调用设超时 + 熔断 + 降级。


【面试题】10. 事件溯源架构中,事件 schema 从 v1 升级到 v2 了,历史事件怎么兼容?三种升级策略各适合什么场景? ​

【标准答案】事件溯源的核心约束是"事件不可变"——发出去就不能改。但业务一定会变,v1 的 OrderPlaced 只有 userId 字段,v2 要加 orderSource(渠道来源),历史事件没有这个字段怎么办?三种升级策略从简单到复杂。策略一:多版本处理器——每种版本有独立的处理逻辑。v1 事件进 v1 处理函数,v2 事件进 v2 函数。在读模型投射层做兼容——投影到读模型时,v1 事件给 orderSource 填默认值"UNKNOWN"。优点是零迁移成本、不影响历史数据,缺点是新版本越来越多后代码会变成"if v1 else if v2 else..."的屎山。策略二:Upcaster 转换——在事件被消费前,通过中间层把旧版本事件转换成新版本。"OrderPlaced v1" 流转到处理器前,Upcaster 补充默认字段转成 v2 格式。下游永远只看到最新版本。优点是消费者代码干净,缺点是历史事件量大了 Upcaster 扫描会拖慢启动。策略三:快照重建——停服或夜间窗口,重放全部历史事件 + 应用 Upcaster 生成全新的 v2 事件流,老事件归档。好处是一劳永逸,缺点是重放海量事件可能好几个小时,且重建过程中要同时维护两套事件流。实战选型:事件量小且版本升级频繁 → 多版本处理器(最简单);事件量大但下游消费者要统一 → Upcaster(最主流);业务有停服窗口且事件量可接受 → 快照重建(最干净)。面试官要听的是:你知道"事件不可变"和你用哪一个升级策略是两回事——事件内容不变,变的是消费事件的代码怎么理解它。