Skip to content

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

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


【面试题】1. Spring Cloud Gateway 的三大核心组件 Route、Predicate、Filter 分别是什么?一个请求从进入网关到转发出去经历了什么? ​

【标准答案】Route(路由):一句话——哪个请求配哪个目标。一条路由定义了"请求长什么样(断言匹配)"→"转发到哪个下游(URI)"→"路上经过哪些过滤器"。Predicate(断言):Java 8 的 Predicate 函数式接口——输入 ServerWebExchange,返回 boolean。最常用 Path 断言 /order/** 匹配订单服务,还有 Header、Query、Method、权重断言做金丝雀。Filter(过滤器):请求在路由前和后插入的逻辑——AddRequestHeader 给下游带用户 ID、StripPrefix 把 /api/order 剥成 /order、RequestRateLimiter 令牌桶限流。请求全流程:客户端请求→Gateway HandlerMapping 遍历所有路由的 Predicate→匹配到第一条就停→走该路由的 Filter 链 pre 阶段→负载均衡选下游实例→发请求→收到响应后走 Filter 链 post 阶段→返回客户端。关键细节:Predicate 匹配顺序是路由定义顺序,不是随机,先定义先匹配——所以宽泛的路由(/**)要放最后,否则把后面的全吃了。


【面试题】2. 分布式 ID 雪花算法中,时钟回拨是什么场景、会引发什么灾难、怎么解决? ​

【标准答案】时钟回拨:服务器 NTP 校时导致系统时钟往回跳——比如从 17:29:05 跳到 17:29:02。雪花算法的 ID 里 41 位时间戳是"当前毫秒 - 基准时间",时间往回走了,生成的时间戳可能小于或等于之前生成的,ID 就重复了。ID 重复是分布式系统里最致命的 bug——主键冲突、订单被覆盖、数据串乱。三种解法由轻到重:第一,等待——回拨 5 秒以内,sleep 到上一毫秒+1 再继续生成,吞吐量瞬间掉但不报错不重复;第二,备用 workerId——时钟回拨超阈值时切到备用 workerId,实际上是"换个新机器"继续发号,旧 workerId 停用直到时间追上;第三,扩展位——Leaf 的做法是把时间戳从 41 位扩到 42 位并降低精度,同时把序列号位蹭给"时针回拨计数",始终往前走一个单位保证不重复。面试官最想听到的是:你知道这个问题的本质不是"算法 bug"而是"物理机器时钟不可靠",所以解法分两路——系统层面禁 NTP 大步回拨(NTP 配 slew 模式小步修正),代码层面做容忍。只用代码兜底不修系统时钟是本末倒置。


【面试题】3. 什么是消息队列的"幂等消费"?消费端怎么实现?为什么单靠消息 ID 去重不够? ​

【标准答案】幂等消费:同一条消息被消费 N 次,业务结果和消费一次完全相同——扣 100 元不会因为重试变成扣 200 元。实现六字诀:"查已处理→没处理再处理→记已处理"。第一步,消费者接到消息先查一张"消息消费记录表":SELECT status FROM msg_consumed WHERE msg_id = ?,如果状态是"已处理"直接 ACK 不执行业务。第二步,如果没找到,执行业务 + INSERT INTO msg_consumed 写消费记录,两步必须是同一个本地事务。第三步,如果业务执行成功但写消费记录失败——事务回滚,下次重试重新执行业务。单靠消息 ID 去重不够的原因:消息 ID 只能挡"同一条消息重复投递",挡不住"同一笔业务操作产生两条不同消息"——比如用户连点两下提交按钮,订单服务生成了两条 msgId 不同的"扣库存"消息。这种情况要靠业务唯一键——用 orderId 而不是 msgId 做去重键。核心原则:去重键必须来自业务维度(订单号、交易流水号),消息 ID 只做辅助——因为消息 ID 解决的是 "MQ 重投",业务唯一键解决的才是 "重复业务操作",二者维度不同缺一不可。


【面试题】4. SAGA 分布式事务的编排模式和协同模式有什么区别?各自致命缺点怎么绕过去? ​

【标准答案】SAGA 是"把一个大事务拆成一系列小事务,每个小事务有正操作 + 补偿操作,前一个成功依次执行下一个,任一个失败正序补偿回去"。编排模式(Orchestration):有一个总指挥(编排器)负责按顺序调服务 A→B→C,哪个失败就调补偿链 C 补偿→B 补偿→A 补偿。优点:流程显式集中,代码即文档,一目了然。致命缺点:编排器成了新的单点,逻辑膨胀后像"上帝对象"。解法:编排器本身无状态,所有决策依据事件,挂了换个实例重跑。协同模式(Choreography):没有总指挥,每个服务做完自己的事发事件,下游监听事件自己决定要不要做+做完也发事件。优点:松耦合,新增节点不用改上游。致命缺点:业务流程散落在各服务的监听器里,三点钟有人问"订单流程到底经过什么"——没人能一句话说清,排查靠猜。解法:把协同模式下的事件流转生成可视化流程图(Eventuate Tram 的做法),用工具弥补人脑不可读。选型建议:流程复杂且频繁调整 → 编排模式(改一个编排器 vs 改 N 个服务),系统成熟稳定、团队自治强 → 协同模式。国内实操 90% 用编排,可控性 > 理论美感。


【面试题】5. 消息队列怎么保证消息不丢?从生产者、Broker、消费者三端各自的机制讲。 ​

【标准答案】三端各防各,差一端都不成立。生产者端:用同步发送 + 确认回调,别用单向 fire-and-forget。RocketMQ 的 send(msg) 返回 SendResult 包含 sendStatus=SEND_OK 才叫成功;Kafka 设 acks=all + retries=3,等所有同步副本确认后再返回;RabbitMQ 发消息后 block 等 ConfirmCallback。生产端丢消息的核心原因是异步发送 crash 了没收到回执——100% 用同步确认。Broker 端:消息持久化到磁盘——RocketMQ 的 CommitLog 顺序写、Kafka 的 page cache 刷盘策略设为同步刷盘(flush.messages=1——性能差但最安全;对可靠性没那么变态的场景默认异步刷盘就够)。多副本备份——RocketMQ 主从同步设为 SYNC_MASTER,主节点写完后等从节点确认再返回。消费者端:手动提交 Offset。别开自动提交(Kafka enable.auto.commit=false),消费处理完+写业务库成功后再 commitSync()。如果消费到一半宕机,Offset 没提交,下次重新拉取同一条消息——这就要求消费端必须幂等。一句串起来:生产者"不收到 Broker 的 OK 不罢休"→Broker"数据落盘+副本复制再说 OK"→消费者"全程你说了算,处理完才交差"——三段串联缺一段,消息迟早丢。


【面试题】6. Redis 和 Redisson 的分布式锁是怎么实现可重入的?看门狗机制 Watch Dog 具体做了什么? ​

【标准答案】可重入锁的核心:同一个线程可以多次获取同一把锁而不死锁,Lock 计数加减。Redisson 实现:用 Redis 的 Hash 结构——HSET lockKey threadId 1 表示 threadId 这个线程拿到了一次锁。同一个线程再来 lock(),执行 HINCRBY lockKey threadId 1,计数变 2。解锁时 HINCRBY ... -1,计数大于 0 说明还有一层锁没释放,不真正删除 key;计数等于 0 才 DEL lockKey。整个"判断是否我的锁 + 重入计数 + 解锁条件"三步用 Lua 脚本原子执行,避免判定和操作的竞态。Watch Dog 是解决"业务没执行完锁到期了"的问题:lock() 默认锁 30 秒——但业务万一跑了 35 秒呢?Watch Dog 是一个后台定时任务,每 10 秒(lockTimeout/3)检查——如果锁还被当前客户端持有,就 PEXPIRE 续期 30 秒。续期的 Lua 脚本也会先验证锁是不是自己的——只能续自己持有的锁。如果客户端宕机了,Watch Dog 停了,30 秒后锁自动释放——这是 Watch Dog 最大的价值:"活着的持有者锁永不过期,死了的持有者锁自动释放"。面试关键一句:可重入靠 Hash + Lua 原子操作,防锁超时靠 Watch Dog 自动续期,两条腿走路缺一条就不是"企业级分布式锁"。


【面试题】7. 线上 Java 服务 OOM 了,堆 dump 文件 8 个 G,你怎么在不重启服务的前提下快速定位内存泄漏点? ​

【标准答案】五步法,不用重启。第一步:确认是不是内存泄漏。jstat -gcutil <PID> 1000 看老年代使用率——如果每次 Full GC 后老年代的使用率持续上升不回落,那就是泄漏;如果回落后又涨但涨得不快,可能是堆配太小。第二步:不 dump 全量,先抓热点。jmap -histo:live <PID> | head -30 输出存活对象排行——如果 byte[] 或某个业务对象实例数几十万且排前几,基本定位到泄漏类型。第三步:确认引用链。把前面 hot object 的 class 名扔进 Arthas 的 vmtool --action getInstances --className com.xxx.OrderDTO --limit 100,拿到 100 个实例,再用 Arthas 的 heapdump 功能只 dump 这个 class 的实例(heapdump --live /tmp/order.hprof),文件只有几百 KB 不是 8G。第四步:用 MAT 或 JProfiler 打开这个小 dump,看 GC Roots 路径——如果千个实例都被一个静态 Map 引用且 Map 只增不减,那 Map 就是泄漏根。第五步如果没法用 Arthas:jcmd <PID> GC.heap_dump /tmp/heap.hprof 全量 dump,但线上别这么搞——Full GC 且 STW 可能十几秒。高级操作不如提前埋好 HeapDumpOnOutOfMemoryError:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump/,OOM 那一刻自动 dump 然后重启。核心心法:线上别导全量 dump——Arthas 靶向 dump 特定 class 是真正的工程智慧。


【面试题】8. 微服务网关层怎么设计统一的认证鉴权方案?JWT + OAuth2 在网关层的最佳实践是什么? ​

【标准答案】设计目标:用户身份在网关验证一次,下游服务不用每个再验。四层架构。第一层:认证——网关的全局过滤器拦截所有请求(白名单除外),提取 Header 里的 Authorization: Bearer xxx,调用认证中心或自己解析 JWT。JWT 用非对称加密(RS256),网关持有公钥即可验证,不用每次问认证中心——这就是 JWT 的精髓:去中心化验证。解析成功拿出 userId、roles、tenantId 写入请求 Header 传给下游(X-User-Id: 123),下游直接取不用再解。第二层:鉴权——网关层做粗粒度权限(URL 级别),比如 /admin/** 要求角色=ADMIN。细粒度(数据权限/按钮权限)留给微服务自己的 AOP 切面做——网关不该知道"订单详情只有创建人和上级领导能看"这种业务规则。第三层:Token 续期——JWT 设短过期时间(30min),同时发一个 refresh_token(存 Redis),网关发现 access_token 过期自动拿 refresh_token 换新 access_token,对客户端透明——这叫"静默续期"。第四层:Token 吊销——JWT 无状态的最大痛点,有人离职了 Token 还在有效期内还能用。解法:网关在每次验证 JWT 后把 jti + userId 去 Redis 查一下黑名单,O(n) 但简单;或者维护一个"用户密码最后修改时间"存在 JWT 里,密码改了老 JWT 里的时间就对不上了。面试核心:网关做认证+粗粒度鉴权,微服务做细粒度鉴权,JWT 的"不外调"优势一定要讲清。


【面试题】9. 死信队列、消息重试、幂等消费三个机制怎么拼成一个"消息必达闭环"?重试次数和间隔怎么设计才不会雪上加霜? ​

【标准答案】三机制拼成闭环:正常消费→消费失败→投递到本地重试队列→重试 N 次→还失败→投递到死信队列(DLQ)→人工或定时任务处理死信→修复后重新投递回原队列。重试策略不是"立即重试",要退避。RocketMQ 默认 16 次重试,间隔 10s/30s/1m/2m/3m/4m/5m/6m/7m/8m/9m/10m/20m/30m/1h/2h,这是指数退避的变体但最后几级留了长间隔——设计精妙:消费者挂了一般是下游暂时不可用,重试前几级间隔短快速恢复,后面越来越长避免持续冲击。注意:重试不是无限的好——如果消费失败是因为消息本身有问题(格式错误、业务数据非法),重试一万次也白搭,浪费资源。解法:在已处理次数达到阈值后不再重试直接丢死信,比如 3 次就进 DLQ。另一个常踩的坑:重试消息和正常消息共用一个 Consumer 线程池——重试消息积压会把正常消费线程全占满,形成"死信级联"。解法:重试用独立线程池或者用延时消息(非实时重试),不和正常消费抢资源。闭环最后一环是死信处理:死信→告警通知开发→开发从 DLQ 控制台看消息体定位原因→手动修数据后重新投递原 Topic→消费成功。一句串起来:正常消费→退避重试→超限死信→人工兜底→回归正常——这才叫"消息必达闭环"。


【面试题】10. 多级缓存架构在微服务体系里怎么设计?浏览器缓存→CDN→Nginx 本地缓存→Redis→本地 Caffeine,每一层的职责、命中场景和失效策略各是什么? ​

【标准答案】五级缓存从近到远,每层解决上一层的延迟和容量瓶颈。第一层:浏览器缓存——Cache-Control 响应头设 max-age=3600,对图片、CSS、JS 等静态资源设 30 天+hash 文件名强缓存。职责是"用户在页面里点来点去不再请求服务器"。失效靠文件名 hash 变更自动刷新。第二层:CDN——就近边缘节点缓存,用户在北京请求命中华北 CDN 节点,10ms 返回。适合静态资源+准静态接口(商品详情里不变的部分)。CDN 面对的问题是"源站数据更新了但 CDN 缓存还没过期"——失效靠 CDN 厂商的 purge API,数据变更后通知 CDN 刷缓存。第三层:Nginx 本地缓存——proxy_cache 模块把热点接口响应缓存在 Nginx 本地磁盘或内存,适合商品详情、首页推荐等"1 分钟内不变也能接受"的数据。失效靠短过期时间(1~5min)+ 主动 purge。第四层:Redis——分布式集中缓存,全量热点数据放这,TTL 5~30 分钟。关键设计:缓存数据用多级 TTL——逻辑过期时间比物理过期时间短,物理过期前异步更新缓存(叫"逻辑过期+异步加载"),防止缓存击穿。第五层:本地 Caffeine——服务 JVM 内缓存,存最极端的超级热点数据(比如秒杀商品库存),微秒级延迟。Caffeine 比 Guava Cache 强在 W-TinyLFU 淘汰算法,命中率更接近 LRU 且内存占用更小。但多级缓存最大的坑是"更新怎么传播":五级缓存数据一致性怎么保证?答案是用"缓存最终一致"——Redis 更新后发 MQ 通知各服务清本地 Caffeine,CDN+Nginx 靠短 TTL 自愈。别追求五级强一致,每一级定义自己的"最大不一致窗口",逐级做兜底——这才是工程智慧,不是课本答辩。