Skip to content

微服务面试题 - 2026-06-19 16:59 ​

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


【面试题】1. 共享数据库为什么是微服务最大的反模式?去中心化数据管理的三个核心原则怎么落地? ​

【标准答案】共享数据库让微服务变"分布式单体"——服务拆了但数据库没拆,A 服务直接读写 B 的表。后果:B 改表结构,A 也跟着炸,部署顺序强耦合;A 一条慢 SQL 拖慢整个 DB,B、C、D 服务全受影响;没法按服务维度独立扩缩容。去中心化数据管理三个落地原则:① 每服务独享数据库——不是物理上一定要多台 MySQL,可以是同一个实例的不同 Schema,核心是"逻辑隔离,没有跨服务的外键和 join";② 数据归属单一——哪张表归哪个服务写在文档里,其他服务需要数据必须走 API 而不是直连;③ 跨服务数据同步走异步——订单服务需要用户昵称?别 join user 表,订单表冗余存一份昵称,用户改名时通过 MQ 异步同步。落地的第一刀最痛但最关键:把原本一个库里的 join 全拆成 API 调用 + 冗余 + 最终一致,忍不了这个就别拆微服务。


【面试题】2. Eureka 的自我保护模式是什么?什么条件下触发?为什么说它是 AP 系统的经典设计? ​

【标准答案】Eureka 自我保护模式一句话:当服务端发现大量客户端心跳超时,它会怀疑"是不是我自己的网络出了问题"而不是"这些服务全挂了",于是停止剔除过期实例,保留现有注册表不变。触发条件:15 分钟内心跳失败比例超过 85%(默认值)。触发的信号是服务端页面上出现红色大字"EMERGENCY! RENEWALS ARE LESSER THAN THRESHOLD"。为什么是 AP 经典设计:CAP 视角下,发生疑似网络分区时 Eureka 选择了"可用性优先"——宁可保留可能已经挂了的实例(牺牲一致性),也不因为误判而大批量剔除健康实例导致大面积不可用。和 ZK 的对比:ZK 发生网络分区时会干掉没有心跳的临时节点(选 CP),导致大量健康服务被误杀,而 Eureka 的选择是"网络恢复后心跳自然恢复,短期内可能会调到死节点但不会全部不可用"。自我保护不是 bug 是 feature,面试官就想确认你真懂了这个设计的取舍逻辑。


【面试题】3. Zuul 1.x 的阻塞模型和 Spring Cloud Gateway 的响应式模型有什么区别?为什么 Gateway 性能碾压 Zuul 1.x? ​

【标准答案】本质区别三条:线程模型、IO 模型、内存模型。Zuul 1.x 基于 Servlet 2.5,一个请求一个线程,线程发 HTTP 到下游后阻塞等待响应,期间什么事都干不了。并发 1000 个慢请求 = 1000 个线程全 hang 住,线程上下文切换把 CPU 吃满,吞吐量塌方。Gateway 基于 WebFlux + Netty,用事件循环 + 少量工作线程,请求发到下游后线程立刻去处理下一个请求,下游回包后通过回调/事件通知回来处理。IO 模型上:Zuul 是 BIO 或伪 NIO(Servlet 3.1 的 async 只是把线程还给容器,底层还是阻塞模式),Gateway 是真 NIO 零拷贝。内存模型上:Gateway 基于 Netty 的 ByteBuf 做零拷贝直接转发,Zuul 要走 InputStream → byte[] → OutputStream 多次内存复制。一句话:Gateway 靠的是"不阻塞 = 不需要那么多线程 = 不搞上下文切换 = CPU 全干正事"。


【面试题】4. 线上 Java 微服务突然 OOM,重启恢复正常但 2 小时后再次 OOM,你怎么排查根因? ​

【标准答案】重启恢复说明不是代码 bug 导致的一次性大对象,而是慢性内存泄漏——某个地方持续往内存里塞东西不释放。排查五步:第一步,启动时加 JVM 参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,让 OOM 那刻自动 dump。第二步,用 MAT/Eclipse Memory Analyzer 打开 dump,看 Dominator Tree(支配树)——谁占了最多内存,通常前十名里就能找到罪魁祸首。第三步,用"Leak Suspects Report"自动分析,定位到具体类。第四步,常见凶手:ThreadLocal 没 remove(线程池复用导致上次请求的大对象一直被引用)、静态 Map 无界 put(比如把 TraceId 当 key 往 ConcurrentHashMap 里塞但从不清除)、长生命周期的缓存没设 TTL 和最大容量。第五步,看 GC 日志——-Xlog:gc*,如果频繁 Full GC 但回收不了多少,确认是泄漏。Arthas 的 dashboard 和 heapdump 命令可以随时在线抓 dump 不下线——生产排查的神器。最关键的教训:别光重启就完了,dump 文件是破案唯一证据,没抓到就等下一次 OOM。


【面试题】5. BASE 理论的三要素怎么理解?柔性事务和刚性事务的边界怎么画? ​

【标准答案】BASE = Basically Available(基本可用)+ Soft State(软状态)+ Eventually Consistent(最终一致)。Basically Available 不是说服务部分可用,而是说"发生故障时允许降级,保证核心功能可用"——双十一秒杀,下单按钮可以等 5 秒但"查看历史订单"直接熔断降级。Soft State 是说系统存在一个"数据正在路上"的中间状态——你付款成功了但订单状态还显示"待支付",不是 bug,是数据还在异步同步。Eventually Consistent 是说只要你停止写操作,过一段时间所有副本数据终将一致——强调"最终"不是"可能",是一定。刚性事务和柔性事务的分界线:刚性事务(ACID)用在一笔交易"错了比慢了更严重"的场景——余额不能多扣也不能少扣;柔性事务(BASE)用在"数据有个短暂差异,用户根本察觉不到"的场景——商品详情页里显示"库存 95 件",实际仓库可能已经卖了 98,下一秒就同步过来了,用户无感知。面试核心一句话:BASE 不是 ACID 的对立面,是它的妥协——为了解决分布式性能的妥协。


【面试题】6. Seata AT 模式和 TCC 模式怎么选?从一致性保证、性能、代码侵入三个维度对比? ​

【标准答案】三个维度拆开看。一致性:AT 依赖数据库本地事务 + undo_log 做自动回滚,一个业务链全部走完要么全部提交要么全部回滚,数据层面一致性很高但依赖 DB 的 ACID;TCC 的 Confirm/Cancel 是业务自己实现的,数据的"正确"靠业务逻辑保证不是靠数据库——你 Cancel 里少解冻一笔钱就是 bug,没人帮你兜底。性能:AT 需要全局锁(Seata Server 维护)保证写隔离,第二步提交前其他事务不能改同一行,并发有锁冲突;TCC 用"冻结/预留"状态做业务层的资源隔离,数据库层面没有额外的锁,性能更好。代码侵入:AT 对业务代码几乎零侵入——加个 @GlobalTransactional 注解就行,undo_log 自动生成;TCC 要你为每个业务接口写三套逻辑(Try/Confirm/Cancel),开发量大但灵活性极高。选型结论:金融类强一致 + 并发不高 → AT 省心;高并发电商秒杀库存扣减 → TCC(配合 Redis 做资源冻结);内部管理系统 CRUD → 别上分布式事务,上个本地消息表 + MQ 就够。


【面试题】7. Spring Cloud 配置热更新 @RefreshScope 的原理是什么?为什么 @Value 注解有时候刷新不生效? ​

【标准答案】@RefreshScope 本质是"懒加载代理 + 缓存失效"。标注了 @RefreshScope 的 Bean 不会被直接注入到调用方,而是注入一个 CGLIB 代理对象。每次调用代理对象的方法时,先从 RefreshScope 缓存里取真实 Bean;如果缓存里有就返回,没有就重新创建。当配置刷新事件触发(ContextRefresher.refresh()),RefreshScope 的缓存被清空,下次调用代理对象的方法时发现缓存没了,就重新创建一个 Bean——新 Bean 里 @Value 的值自然是新的配置。@Value 不生效的场景:① 没加 @RefreshScope 或 @ConfigurationProperties——Spring 初始化 Bean 时就把配置值写死了,刷新事件只改了 Environment 里的值但 Bean 的字段没变;② 配置类里用 @Value 注入后通过构造器赋值给 final 字段——final 字段构造后就改不了了,代理也救不了;③ 单例 Bean 初始化时把配置值缓存到了成员变量,刷新后没重新初始化。最佳实践是用 @ConfigurationProperties + @RefreshScope 绑定配置类,比直接用 @Value 可控得多。


【面试题】8. 漏桶算法、令牌桶算法、固定窗口和滑动窗口的核心区别?Sentinel 为什么默认用滑动窗口? ​

【标准答案】漏桶:想象一个底部有个小洞的水桶,请求以任意速率入桶,桶底以固定速率漏水(处理请求)。桶满了就溢出(拒绝)。核心是"强制恒定输出速率",不管你流量怎么突增,出去的永远是均匀的——适合需要平滑输出的场景但并不适合突发流量削峰。令牌桶:以固定速率往桶里放令牌,请求来先拿令牌,拿到就放行,拿不到就拒绝或排队。关键区别是桶可以攒令牌——空闲时令牌积攒,突发流量来了可以一次性消耗所有积攒令牌,允许一定程度的突发。固定窗口:统计每个固定时间段内的请求数(比如每秒一个窗口),问题是窗口切换瞬间——0.9s-1.0s 和 1.0s-1.1s 两个窗口各放了一半请求,但它们实际只隔了 0.2 秒,限流被绕过。滑动窗口:把时间分成多个小格子,统计最近一段时间窗口内的请求数,窗口随时间的推进而滑动,没有"切换瞬间"的漏洞。Sentinel 默认用滑动窗口因为它精准、无间隙、统计粒度可控——统计 QPS 用 1s 窗口分两个 500ms 格子,足够精准又不过度消耗内存。对比选择:漏桶做流量整形(保护下游不被冲垮),令牌桶做突发限流(允许短时峰值),滑动窗口做精确统计限流,各有各的适用面。


【面试题】9. Redisson 的看门狗机制是怎么实现锁自动续期的?源码层面 Renewal 和可重入是怎么实现的? ​

【标准答案】看门狗解决的核心问题:你设了锁 30s 过期,但业务逻辑跑了 35s,锁提前释放了,第二个线程进来加锁成功——数据乱套。Redisson 的策略:加锁时不显式设置过期时间,Redisson 默认给 30s,然后启动一个定时任务(看门狗),每 10s(internalLockLeaseTime / 3)检查锁是否还被当前线程持有,如果是就把过期时间重新设为 30s。直到业务执行完主动 unlock,看门狗才会停。源码层面:加锁用的是 Lua 脚本原子操作——exists key == 0 时 hincrby key threadId 1 + pexpire key 30000,返回 nil 表示加锁成功;可重入的判断就是在 Lua 里 hexists key threadId,如果存在就 hincrby 计数 +1 再续期。解锁也是 Lua:hincrby 计数 -1,如果计数 > 0 说明还有重入没释放完只续期不删 key,计数 = 0 才真正 del key。看门狗存在一个隐患:如果应用突然 Full GC 停顿了 40s,看门狗线程也被冻住了,锁还是可能过期。所以关键业务还得配上 fencing token 做存储层的安全保障。面试结论:Watchdog 解决的是"正常业务执行时间不可预测"的问题,不是"极端 GC 停顿"的问题。


【面试题】10. Prometheus + Grafana + AlertManager 告警体系怎么从零搭起来?告警规则设计有哪些黄金法则? ​

【标准答案】从零搭建四步:第一步埋点——应用引入 Micrometer,暴露 /actuator/prometheus 端点,Prometheus 定期拉取指标。关键指标黄金三板斧:QPS(请求量)、P99 延迟(响应速度)、Error Rate(错误率)。第二步 Prometheus 配置——scrape_interval 设 15-30s,用 relabel_config 过滤无关指标,加 service discovery(K8s Pod annotation 或 Consul/Nacos 自动发现)。第三步 Grafana 看板——至少做三块:JVM 看板(堆内存、GC 频率、线程数)、业务看板(各接口 QPS/P99/Error)、中间件看板(Redis/MySQL 连接池、慢查询)。第四步 AlertManager 告警——Prometheus 定义告警规则,AlertManager 收敛去重后推送到企业微信/钉钉/飞书。告警规则黄金法则:① 告警要有"症状"不告"原因"——告"API 错误率 > 5%"不要告"CPU 高了"(CPU 高不一定是问题);② P99 > 阈值持续 5 分钟再告,别一超过就告,防止毛刺风暴;③ 同一服务的多条告警收敛成一条,避免告警轰炸;④ 必须配告警分级——P0(立即处理)、P1(30 分钟)、P2(工作时间),别全部都是紧急;⑤ 每个告警规则都要有 Runbook 链接,接手的人知道怎么处理。再好的监控体系也比不上一条"告了之后有人知道怎么办"的黄金法则。