微服务面试题 - 2026-06-19 19:09
生成时间:2026-06-19 19:09 | 共 10 题 | 难度递进(基础→进阶→高阶)
【面试题】1. 单体应用拆微服务之前,必须回答的三个前置问题是什么?答不上来说明还没准备好拆分?
【标准答案】第一问:团队够不够拆?5人以下团队拆微服务是内耗——每个服务要独立CI/CD、独立监控、独立排障,运维成本吃掉开发时间。第二问:业务模型稳不稳定?两天一变的需求,今天拆的边界明天就是技术债。微服务本质是组织架构的映射——组织没定,服务边界就是空中楼阁。第三问:有没有明确的"高频变更模块"和"低频变更模块"?拆的核心动机不是"微服务很潮",而是"让高频变更的模块独立上线,不拖累稳定的模块"。整个系统变化频率都一样的话,拆不拆没区别。面试一句:不是你能拆就要拆——是你测出了"不拆更痛"才拆。找到那个痛点之前,模块化单体是最好的架构。
【面试题】2. Cache Aside、Read/Write Through、Write Behind 三种缓存更新策略有什么区别?生产上为什么绝大部分场景选 Cache Aside?
【标准答案】Cache Aside(旁路缓存):业务代码自己管缓存——读时先查缓存,没有就读DB回写缓存;写时先写DB,再删缓存。优点是逻辑简单、你完全掌控时机。缺点是两步不在同一事务,可能出现"DB更新成功但删缓存失败"的短暂脏数据。Read/Write Through:缓存层当DB代理——业务只操作缓存,缓存自己负责和DB同步。业务无感但Redis本身不支持,需要封装中间层。Write Behind:写只写缓存,后台异步批量刷DB——性能极高但缓存挂了丢数据。生产选Cache Aside的原因很朴实:实现最简单、Redis原生支持、一致性风险可控——绝大多数场景"先写DB再删缓存"已经够好了。额外两个工程细节:删缓存失败用重试或Canal监听binlog补偿;热点数据高并发下改"删缓存"为"更新缓存"减少DB穿透。面试一句:Cache Aside是手动挡——费操作但精准控制;Write Through是自动挡——省心但没法临场应变。
【面试题】3. 舱壁隔离(Bulkhead)模式有哪几种实现?各自的隔离程度和代价怎么平衡?
【标准答案】舱壁隔离的灵感来自船——隔板把船体分成独立舱室,一个进水不沉全船。微服务里三种实现,按隔离程度和代价排:线程池隔离最彻底——给每个下游依赖分配独立线程池,A调B用B池、调C用C池,B慢把B池打满但C不受影响。代价是线程数×依赖数,10个下游各10线程=100线程,内存和上下文切换开销不低。信号量隔离次之——不分配独立线程,在调用入口放计数器,超阈值拒绝。开销小但隔离不彻底——信号量满了当前线程会阻塞,如果超时设太长阻塞扩散到上游。连接池隔离最底层——不同数据库/Redis/MQ各用各的连接池,一个下游连接池打满不影响其他。生产分级策略:外部HTTP依赖(响应慢、波动大)→线程池隔离;内部RPC调用(低延迟、稳定)→信号量隔离;存储层→连接池隔离。核心思想是"分池不分海"——给核心依赖和非核心依赖至少分两个池。面试一句:舱壁隔离是花钱买平安——你没法给100个下游各建100个线程池,但至少得把命门和杂物间隔开。
【面试题】4. 消息队列怎么实现严格的消息顺序?RocketMQ 的顺序消息和 Kafka 的分区有序各是怎么做的?什么场景必须保证顺序?
【标准答案】顺序消息的关键是"同一个业务标识的消息必须发到同一个分区/队列"。RocketMQ的顺序消息:生产者发消息时指定MessageQueueSelector——按业务Key(比如订单ID)取模选队列,同一个订单的所有消息(创建→支付→发货→完成)都落到同一个MessageQueue。消费者端用MessageListenerOrderly,同一时刻一个队列只被一个线程消费,加本地锁保证顺序处理。代价是丧失了并行度——一个队列同一时刻只有一个线程在跑,吞吐量打了折扣。Kafka的分区有序:Kafka天然保证同一Partition内消息有序——生产者通过Key的哈希决定发到哪个Partition,同一个Key的消息一定在同一Partition内顺序写入。消费者端一个Partition只配一个Consumer线程来保证消费有序。什么场景必须顺序?典型如"创建订单→扣库存→扣积分"三个消息,如果扣库存先于创建订单消费到,库存校验直接报错。但大多数场景最后靠幂等+业务状态校验兜底,强顺序只在状态机严格依赖前置状态的场景才必须。面试一句:顺序的代价是并行度——你用10个线程无序处理1秒搞定,换成有序1个线程可能要10秒。大部分场景用幂等替代顺序更划算。
【面试题】5. Zookeeper 临时顺序节点锁、etcd 的 Lease 锁、RedLock 三者底层机制有什么本质区别?什么场景该选哪个?
【标准答案】ZK临时顺序节点锁:客户端在锁路径下创建带序号的临时节点,序号最小的获取锁。后面的watch前一个节点——前一个释放,下一个收到通知尝试获取。临时节点的关键:客户端和ZK的Session断开心跳超时,临时节点自动删除→锁自动释放,天然防死锁。缺点是ZK是CP系统,选主期间不可用,锁操作阻塞。etcd Lease锁:客户端先创建Lease(带TTL的租约),用该Lease创建一个Key,创建成功=拿锁。客户端需在1/3个TTL周期内续约,用CAS(Compare-And-Swap)保证同一时刻只有一个客户端能创建成功。和ZK的核心区别:etcd也是Raft CP系统,但通过Watch机制等待锁释放而非轮询,性能好于ZK;Lease过期自动释放机制比ZK的Session更灵活可控。RedLock:客户端依次向N个独立Redis实例加锁,超过半数成功且总耗时小于锁TTL就算拿锁。本质是"多数派投票",依赖概率保证互斥。选型一句话:金融/核心资源→ZK或etcd(CP保证);高并发业务互斥→Redisson(性能高,锁持有时间短时可靠性够);K8s原生项目→etcd(K8s已内置etcd集群,零额外运维)。面试一句:ZK和etcd是"法理上的强一致锁",RedLock是"工程上的概率锁"——前者靠共识协议,后者靠多数派抄底。
【面试题】6. Logging、Metrics、Tracing 三根支柱在排查一个线上问题时分别扮演什么角色?它们是怎么串联成一条证据链的?
【标准答案】三支柱不是三套独立的系统,而是一条证据链上的三个环节。Metrics是"报警器"——你收到告警"订单服务P99延迟从200ms飙升到3s",Grafana大盘告诉你什么时间、哪个服务、哪个接口出事了,但不知道原因。Tracing是"切割刀"——顺着告警时间在SkyWalking里找这个接口的慢Trace,火焰图把调用链切开看每一跳耗时:网关10ms→订单服务10ms→库存服务2800ms。定位到库存服务是凶手。Logging是"显微镜"——在ELK里搜和这个TraceId关联的库存服务日志,看到Acquiring lock for product_1234 failed, retry 3/3。立刻知道是分布式锁竞争导致线程阻塞。串联逻辑:Metrics告诉你"哪里起火了",Tracing告诉你"火在几楼哪个房间",Logging告诉你"房间里是什么材料烧起来的"。落地中两个关键:一是TraceId必须打入所有日志(MDC.put("traceId", ...)),没有TraceId的日志在分布式系统里就是孤魂野鬼;二是Metrics的告警阈值要和业务的容忍度对齐——别等技术群里客户骂街了告警才响。面试一句:三支柱分开建等于没建——Metrics无Tracing是瞎子摸象,Tracing无Logging是隔靴搔痒,三者必须联动才能从告警到代码一竿子插到底。
【面试题】7. 金丝雀发布中,数据库 schema 变更和代码部署怎么解耦?新旧两个版本的服务要同时跑在一个数据库上,你怎么保证不出事?
【标准答案】金丝雀发布期间新旧服务共存是常态——但你不可能同时维护两套数据库。核心原则是"先扩后缩,只加不删":每次变更分两个发布周期。第一个周期——扩展兼容:只加列(ALTER TABLE ADD COLUMN,且必须有DEFAULT值或允许NULL),绝不删列改列。新版本代码写新列+读新列(新列有值用新列,没有fallback到旧列),旧版本代码完全不知道新列的存在。这个版本全量发布完毕,确定没有旧版本Pod在跑了。第二个周期——清理:把旧版本代码里fallback逻辑删掉,把旧列删掉,把过渡逻辑清干净。真实案例:要把用户表的phone字段拆成phone_area和phone_number。第一步:加phone_area和phone_number两个新列(允许NULL),新版本代码读新列写新列,旧版本继续读写旧phone列。第二步:全量部署新版本后,跑批处理把phone列的数据迁移到新列。第三步:确认新版本稳定后,删掉旧phone列和fallback代码。反例警告:绝对不要在同一变更里包含"删旧列"——一旦金丝雀失败要回滚,旧版本代码发现列没了直接报错,回滚失败。面试一句:数据库变更和金丝雀发布的矛盾本质是"共享数据层和差异代码层的时间差"——你用"两次部署三步走"把这个时间差变成兼容期。
【面试题】8. Outbox Pattern + CDC(Change Data Capture)怎么绑定在一起实现跨服务的可靠消息投递?和事务消息相比有什么优劣?
【标准答案】Outbox Pattern解决的核心问题:本地事务和发消息不是原子的——订单写入DB成功但MQ挂了,消息丢了。做法是:在订单服务的同一个数据库事务里,写订单记录的同时往一张outbox表插入一条消息记录(event_type=OrderCreated,payload=json,status=PENDING)。同库同事务天然原子。然后CDC组件(Debezium/Canal)监听outbox表的binlog变更,发现INSERT就取出消息发到MQ,发成功后更新status=SENT。MQ发送失败?CDC在binlog断点续传不会丢,重试直到成功。和RocketMQ事务消息的对比:事务消息是"半消息→执行本地事务→Commit/Rollback",消息先到MQ等结果——依赖MQ的事务消息能力,强绑定RocketMQ。Outbox+CDC是"先写DB再追投"——不依赖MQ的事务特性,任何MQ都能用,甚至不用MQ、直接用HTTP回调投递。劣势是多了CDC组件运维成本,且消费端需要幂等——CDC可能重复投递。选型:团队已有RocketMQ且业务中等规模→事务消息简单;跨团队跨系统、被投递方五花八门→Outbox+CDC更解耦。面试一句:Outbox是"在自己家留字条让佣人送",事务消息是"当着快递员的面封箱",前者独立自主但多了个佣人,后者快捷但得用顺丰的快递员。
【面试题】9. Sentinel 的热点参数限流(Hot Param Flow Control)底层用的是什么数据结构?和常规的 QPS 限流相比,它解决了什么特殊场景的问题?
【标准答案】热点参数限流解决的是"整体QPS没超但某一个热点Key打爆系统"的场景。比如秒杀商品product_888888的查询QPS是20000,但全接口限流阈值是50000——整体没触发限流,但所有请求都打在同一个商品的缓存Key上,Redis被热点冲垮。Sentinel的实现用了两级缓存:第一级——滑动窗口统计全局的QPS,跑在StatisticSlot上,这是基线;第二级——通过LRU+滑动窗口的组合结构统计"具体哪个参数值的QPS超标了"。对每个资源(比如/product/detail),Sentinel维护一个固定大小的LRU Map,Key是参数值(product_888888),Value是该参数的滑动窗口计数器。参数值超过LRU容量上限时淘汰最久未访问的,Controls住了内存占用。单机每秒处理多少参数值可控,不怕参数值爆炸。和常规QPS限流的本质区别:常规限流的维度是"接口",热点限流的维度是"接口+具体某个参数值"。常规限流说"这个接口每秒最多处理1000个请求",热点限流说"但如果请求集中在某个商品上,单个商品每秒最多300"。面试一句:热点参数限流是在QPS限流的"面"上加了一层"点"的精准打击——全城不限号,但你这一条街堵死了,就只拦你这条街的车。
【面试题】10. 线上同时收到三条告警——线程池打满、数据库连接池耗尽、慢SQL数量飙升——你怎么在3分钟内判断谁是根因谁是受害者?
【标准答案】三条告警同时响,九成是慢SQL引发的连锁反应。排查逻辑是"从底层往上层推"。第一步30秒——确认慢SQL是源头:打开数据库监控(Druid/SlowLog/云厂商控制台),看告警时间段内哪条SQL执行时间突然飙高。典型特征是某条原本10ms的SQL变成了3s——可能是缺失索引、表锁等待、全表扫描。慢SQL是这起事故的第一因果链。第二步1分钟——验证连接池耗尽是被动结果:每个慢SQL持有一条数据库连接等3秒才返回,并发过来100个请求需要100个连接。正常连接池就20~50个,3秒内排队的请求把连接池瞬间吃空——Connection pool exhausted, wait timeout 3000ms。连接池本身没问题,是慢SQL把连接"人质劫持"了。第三步1分半——验证线程池打满是连锁末端:线程拿到数据库连接→发慢SQL→线程卡在JDBC socket read上等DB响应→连接不释放→线程池里的线程全卡住→新请求进来RejectedExecutionException。线程池也是受害者。处理策略:先止损再修根因——1、限流切掉新请求让系统先喘口气;2、如果SQL有快速优化方案(加索引、改hint)紧急上线;3、如果没法马上修SQL,熔断接口返回兜底数据。面试一句:三层告警的因果关系是"慢SQL拽住了连接,连接拖死了线程"——你的排查永远从底层开始,底层不动、上层再查是浪费时间。
📋 考点覆盖:微服务拆分方法论(拆分前置条件)→ 分布式缓存设计(缓存更新策略对比)→ 服务雪崩与级联故障防护(舱壁隔离模式)→ 消息驱动架构(消息顺序与幂等)→ 分布式锁实现(ZK/etcd/RedLock对比)→ 可观测性三支柱(Logging/Metrics/Tracing串联)→ 灰度发布与金丝雀部署(DB Schema变更解耦)→ 跨服务数据一致性(Outbox+CDC可靠投递)→ 服务熔断降级限流(热点参数限流原理)→ 线上故障排查与调优(慢SQL连锁故障排查)
题型分布:概念题(1,2) | 原理题(3,9) | 方案对比题(4,5,8) | 架构设计题(7) | 故障排查题(10) | 场景面试题(6)
难度递进:基础(1-3) → 进阶(4-7) → 高阶架构(8-10)