Skip to content

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

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


【面试题】1. 微服务拆多细才合适?"两个披萨团队"和康威定律怎么指导服务粒度划分? ​

【标准答案】微服务粒度没有绝对值,但有三条黄金线。最小粒度:一个服务不要小于一个2~3人团队能维护的范围——如果你一个人同时维护5个微服务,每次改需求要改3个服务的代码、联调4个服务、部署5个,这不是微服务,是分布式地狱。康威定律说"系统设计会复制组织的沟通结构"——如果你的团队是"订单组+用户组+商品组"三拨人,系统就自然会拆成订单服务、用户服务、商品服务。反康威定律也成立:如果你硬拆出15个微服务但只有5个后端,最终这些服务会因为一个人改了A服务的Model然后B服务的调用崩了——因为人去跨越服务边界的沟通成本比代码还高。"两个披萨团队"(Amazon的Jeff Bezos提出)——一个团队的规模应该控制在两个披萨能喂饱的人数(6~8人),一个团队负责一个或少数几个微服务。总结一句:微服务的粒度不是由"功能边界"决定,而是由"团队边界和沟通成本"决定。拆得动的才叫微服务,拆不动的叫分布式单体。


【面试题】2. CAP理论中"分区容错性"(P)为什么是分布式系统的必选项?有人说可以选CA放弃P,这可能吗? ​

【标准答案】分区容错性就是系统在任意网络分区(节点间消息丢包、延迟、断连)的情况下还能继续工作。为什么P是必选项——因为分布式系统里网络分区不是if而是when:交换机故障、网线被挖断、机房停电、云服务商抖动,迟早会来。当你"放弃P"时,你的假设是"网络永远可靠",但这在物理世界不存在——一旦网络真的分区了,你所谓的CA系统就会彻底不可用或数据不一致。所以更准确的问法不是"CAP三选二",而是"网络分区发生时,你选C还是A"——选C(一致性)就拒绝写入直到分区恢复,系统暂时不可用;选A(可用性)就继续接受写入但不同分区的数据可能冲突。面试官最想听到的:你说的不是放弃P,而是在P发生时的取舍策略——关键业务(支付)选CP宁可短时间不可用也不能丢数据,非关键业务(浏览)选AP宁可短暂不一致也要让用户能用。这才是工程思维。


【面试题】3. 一致性哈希的原理是什么?为什么加"虚拟节点"能解决数据倾斜问题? ​

【标准答案】一致性哈希把整个哈希空间映射成一个0到2^32-1的圆环,节点(服务器)和数据(请求)都hash到这个环上。数据顺时针找到第一个节点就是它要落的目标。这样加节点或者删节点时,只有紧挨着这个节点的数据会重新迁移——不像取模hash(key % N)那样节点数一变所有数据全得搬家。但普通一致性哈希有个大坑:如果节点太少,它们在环上分布不均匀——三个节点可能挤在一个90°的弧上,剩下270°全靠一个节点扛,这就是数据倾斜。虚拟节点就是给每个物理节点创建多个虚拟副本——比如物理节点A创建A#1、A#2、A#3…共100个虚拟节点,均匀散布在整个环上。数据顺时针找虚拟节点,虚拟节点再映射回物理节点。这样一来物理节点A负责的数据就是它100个虚拟节点各自"管辖弧段"的总和,因为虚拟节点均匀分布,每个物理节点的负载就趋向均匀。面试一句:虚拟节点的本质是"把一个物理节点的权重打散到环上多个位置",虚拟节点数量越多,数据分布越均匀——代价是内存里维护的映射表变大。Dubbo一致性哈希负载均衡默认用160个虚拟节点。


【面试题】4. Gateway的过滤器链有几种类型?自定义GlobalFilter做统一鉴权怎么实现? ​

【标准答案】Gateway的过滤器分两类。RouteFilter(路由过滤器):作用在单个路由上,在application.yml里配置filters下,比如AddRequestHeader、StripPrefix、RequestRateLimiter,只拦截匹配当前Route的请求。GlobalFilter(全局过滤器):作用于所有路由,不需要在配置文件里挨个配,通过实现GlobalFilter接口自动生效——适合做跨切面的公共逻辑(鉴权、日志、链路追踪)。Gateway内置了多个GlobalFilter,比如NettyRoutingFilter(实际转发请求)、LoadBalancerClientFilter(从注册中心选实例)。自定义统一鉴权的GlobalFilter三步走:第一步实现GlobalFilter, Ordered两个接口,filter()方法里拿到ServerWebExchange → 从Header取出Token → 调鉴权服务或自己解析JWT → 验证不通过直接exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED)并return exchange.getResponse().setComplete()短路返回。第二步getOrder()返回一个较小的值(比如-100)让鉴权过滤器排在前面——因为Gateway的Filter Chain是按Order从小到大执行的。第三步用@Component让Spring自动扫描注册即可。实际项目中通过set()方法排除特定路径(登录接口)不走鉴权。面试关键:你要说清楚GlobalFilter和RouteFilter的分工——GlobalFilter搞横切(每个请求都穿的公用逻辑),RouteFilter搞路由专属(某个特定接口才需要的定制逻辑)。


【面试题】5. Seata AT模式的"一阶段"和"二阶段"分别做了什么?undo_log表的作用是什么?AT和TCC有什么本质不同? ​

【标准答案】Seata AT模式建立在"每个微服务都有自己的数据库"之上。一阶段:业务SQL照常执行(INSERT/UPDATE/DELETE)——但Seata的代理数据源同时在undo_log表里记录修改前的数据快照。比如UPDATE扣库存100件,undo_log会记下这行数据修改前的值(库存=500),以及它属于哪个全局事务的哪个分支。一阶段提交后,数据已经真实落库(不锁资源),但全局事务还没完。二阶段-提交:全局事务成功,TC(事务协调器)通知各分支删除undo_log记录,异步清理即可。二阶段-回滚:全局事务失败,各分支根据undo_log里的前镜像做逆向SQL恢复数据(库存改回500),再删undo_log。undo_log表就是"后悔药"——记下你改了什么、改之前长啥样,错了能倒回去。AT和TCC根本区别:AT是"事后补偿"——先提交真实的业务SQL,错了用undo_log回滚,业务代码不用改,只需加一个@GlobalTransactional注解。TCC是"事前预留"——你得自己实现Try/Confirm/Cancel三个方法,每个都写一遍业务逻辑,侵入性大但能精确控制资源。AT适合90%的场景(效率高、改造小),TCC适合需要精确资源控制的(资金冻结、库存预占)。选AT还是TCC看一条:你的业务能不能用undo_log自动回滚——能就别写那么多TCC代码。


【面试题】6. 熔断器有哪三种状态?为什么"半开"状态是整个熔断机制的关键设计? ​

【标准答案】熔断器三种状态是一个闭环。关闭状态(Closed):正常工作,所有请求正常通过,同时统计错误率。当错误率达到阈值(比如5秒内错误率>50%),熔断器打开。打开状态(Open):熔断器拒绝所有请求,直接走降级逻辑返回兜底数据,不调用真实的下游。打开状态持续一段时间后(比如30秒),熔断器进入半开状态。半开状态(Half-Open):允许少量请求(探测请求)去真实调用下游——如果这些请求成功了,说明下游恢复了,熔断器切换回关闭状态,恢复正常;如果还是失败,熔断器重新回到打开状态,继续拒绝。为什么半开是关键?如果没有半开,熔断器只有"关"和"开"两态——打开后你怎么知道下游恢复了?是定时到了直接全放?那万一还没好全量请求又打过去,把刚恢复的服务再次打挂。半开状态实现了"灰度恢复"——用小流量探路,确认路是通的再全放。Sentinel和Resilience4j都支持半开并允许配置探测请求数量和超时时间。面试一句:半开状态是熔断器的"自愈机制"——不是一刀切的开关,而是像断路器合闸前先测一下线路还短路不。


【面试题】7. 用ZooKeeper的临时顺序节点实现分布式锁,和Redis实现有什么本质区别?分别适合什么场景? ​

【标准答案】本质区别三个维度。第一,锁的可靠性:ZK基于Zab(CP协议)保证强一致性——Leader写入成功才返回,不会出现"我以为拿到锁但实际没拿到"。Redis主从架构是AP——主节点写入可能还没同步到从节点就挂了,从升主后锁信息丢失,另一个客户端又拿到同一把锁。第二,释放机制:ZK靠临时节点——客户端会话断开(进程挂了、网络断了)临时节点自动删除,锁自然释放,不需要"看门狗"续期。Redis靠过期时间——必须设TTL,业务跑太久得靠看门狗续期。第三,性能:Redis是单线程内存操作,加锁解锁都是O(1)级;ZK是写Leader、过半同步,一次加锁有fsync开销,QPS不在一个量级。选型结论:对一致性要求极高的场景(扣库存、资金操作)→ ZK,挂了自动释放无泄漏风险;高并发、锁持有时间短(毫秒级)、能容忍极小概率的重复获取→ Redis,性能好得多。进阶方案:两全其美就用Redisson的RedLock——同时在多个独立Redis实例上加锁,超过半数成功才算获取,兼顾性能和安全性。面试一句话:Redis分布式锁是"赌Redis不出事",ZK分布式锁是"出事了也能兜得住"——就看你的业务赌不赌得起。


【面试题】8. OpenTelemetry统一了可观测性标准,它的架构是怎样的?直接装SkyWalking+Prometheus+ELK和用OpenTelemetry有什么区别? ​

【标准答案】OpenTelemetry(OTel)不是一个产品,是一个标准+工具集,定义了数据采集的统一规范。架构三层:最下面是Instrumentation层——提供各语言的SDK和自动探针(Java Agent、Python、Go等),在应用进程内采集Traces、Metrics、Logs。中间是OTel Collector——独立的Agent进程,负责接收、处理、导出数据。最上面是Backend——把数据发给后端存储分析系统(Jaeger、Prometheus、Elasticsearch、Datadog等)。核心价值是"厂商中立"——你的代码只需要用OTel API埋一次点,后端想从Jaeger换成SkyWalking,改个Collector的Exporter配置就行,零代码改动。直接用SkyWalking+Prometheus+ELK的问题是"三国分治"——SkyWalking管Tracing但用自己私有协议埋点,Prometheus管Metrics但要用自家的Client库暴露接口,ELK管Logging要配Filebeat。三种数据三种采集方式、三种数据格式、三个面板,当你排查一个慢请求时,Trace在SkyWalking看、日志在Kibana查、指标在Grafana看——三个窗口来回切,而且TraceId和日志里的TraceId格式都不一样。OTel的价值就是统一这一步——一次埋点三种数据都采集,一条TraceId把Trace、Metric、Log全串起来。但OTel是"标准",不提供存储和UI,还得对接Jaeger/Prometheus做后端——所以不是"OTel替代SkyWalking",而是"OTel采集 + SkyWalking/Jaeger做存储展示"。


【面试题】9. 全链路灰度(流量染色)是怎么实现的?从网关到RPC到MQ全链路怎么传递灰度标签? ​

【标准答案】全链路灰度的核心是"给流量打标签"——一个请求从进网关到最终返回,带着一个"我是灰度"的标记在所有服务间传递,每个服务根据这个标记决定走新逻辑还是旧逻辑。实现分三层。第一层网关染色:网关根据规则(比如Header里有x-gray: true、或者userId取模100=0~4就是5%灰度用户)判断出该请求是灰度流量,在请求的透传Header里打上x-gray-tag: gray。第二层RPC透传:微服务间RPC调用(Dubbo/Feign/gRPC)需要把灰度标记一路带下去——Dubbo用RpcContext里的Attachment、Feign用RequestInterceptor追加Header、gRPC用Metadata,总之确保调下游时灰度标记不丢。业务代码里通过读取上下文判断当前请求是灰度还是正常,决定走if(gray)新逻辑 else 老逻辑。第三层MQ染色:异步消息也要灰度隔离——灰度的订单创建事件只能被灰度的消费者消费,否则灰度订单被正常消费者处理就走了老逻辑。RocketMQ靠给灰度流量发消息到独立的灰度Topic(order-topic-gray),灰度消费者只订阅灰度Topic。Kafka靠Header携带灰度标记,消费者先过滤Header再决定是否处理。核心难点不是技术实现而是标记不丢失——网关→服务A→RPC→服务B→MQ→服务C,这中间任何一个链路丢了灰度标记,灰度就断了。面试一句:全链路灰度的本质是"流量身份"——给每个请求发一张身份证,一路上靠身份证决定走哪条路。


【面试题】10. 缓存和数据库的双写一致性有哪些方案?Cache-Aside模式的"先删缓存再写DB"和"先写DB再删缓存"一直有争议,到底怎么选? ​

【标准答案】四种主流方案各有场景。Cache-Aside(旁路缓存):应用层自己管理缓存——读的时候先查缓存,miss了查DB并回写缓存;写的时候更新DB然后删缓存。这是最常用最朴素的方案。Read/Write Through:缓存层代理DB读写——应用只和缓存交互,缓存负责同步到DB。适合对应用屏蔽DB细节的场景。Write Behind(异步写回):应用只写缓存,缓存异步批量刷到DB。性能最高但丢数据风险大——Redis挂了还没刷到DB的数据就没了。双写方案:同时更新DB和缓存。一致性最好但耦合紧、代码重。Cache-Aside的"删缓存时机"争议:先删缓存再写DB——问题在于A删了缓存→B来读miss查DB拿到旧数据→B回写旧数据到缓存→A更新DB。最终DB是新数据、缓存是旧数据,不一致且永不过期。先写DB再删缓存——A更新DB→A删除缓存→B来读miss去DB拿新数据→B回写新数据到缓存。一致了。但极端场景:A更新DB还没删缓存→B来读拿到旧缓存数据→A删缓存→到此为止B已经返回了旧数据给用户。B拿到的不是最新的,但下一次读就是最新的了。结论:先写DB再删缓存是工程上更好的选择,因为不一致窗口更小且只会发生一次。两个兜底手段:删除失败重试——用Canal监听binlog,删除缓存失败就MQ异步重试;设置合理的过期时间——哪怕各种极端情况出了不一致,TTL一到缓存过期自然修复。面试一句话:缓存一致性没有银弹,Cache-Aside+先写DB再删缓存+过期时间兜底是生产最稳妥的组合。


本次10题覆盖考点:微服务架构设计原则(康威定律+粒度)→ 分布式核心理论(CAP的P必选)→ 负载均衡策略(一致性哈希+虚拟节点)→ 服务网关(Gateway过滤器链)→ 分布式事务(Seata AT模式)→ 服务熔断降级限流(熔断器三态半开)→ 分布式锁(ZK vs Redis)→ 可观测性三支柱(OpenTelemetry)→ 灰度发布与金丝雀部署(全链路灰度)→ 分布式缓存设计(双写一致性)

题型分布:概念题(1,2) | 原理题(3,6,7) | 方案对比题(5,7,10) | 架构设计题(4,9) | 故障排查题(8)

难度递进:基础(1-3) → 进阶(4-7) → 高阶架构(8-10)