微服务面试是 Java 后端绕不开的坎。本文 30 道题全部来自一线大厂真实面试,每道题都给你讲清楚”是什么 → 为什么 → 怎么用 → 踩过什么坑”,保证傻子都能看懂,老手也能查漏补缺。
Q1. 为什么要微服务?单体架构 vs 微服务架构的优劣对比
单体架构(Monolith)
所有功能打成一个 JAR/WAR 包部署。小团队、早期项目用起来很爽:开发简单、调试方便、部署一条命令搞定。
但是,项目大了之后就是噩梦:
- 代码耦合严重:改一行代码,不知道会不会把隔壁模块搞崩。
- 部署风险高:一个小功能上线,整个系统都要重新打包部署、重启。
- 扩展性差:用户模块压力大,但你没办法只扩容用户模块,只能整个应用扩。
- 技术栈锁定:全部 Java,想用 Python 做推荐算法?没门。
微服务架构
把大单体按业务领域拆成独立的小服务,每个服务:
- 独立部署、独立扩容
- 可以用不同技术栈
- 故障隔离,一个服务挂了不影响全局
代价也是真实的:
| 维度 |
单体 |
微服务 |
| 开发复杂度 |
低 |
高(需要解决分布式问题) |
| 部署复杂度 |
低 |
高(N 个服务需要 K8s/Docker) |
| 运维成本 |
低 |
高(日志聚合、链路追踪、服务治理) |
| 扩展性 |
差 |
好(按需扩容) |
| 故障隔离 |
差 |
好(熔断降级) |
| 团队协作 |
差(大团队) |
好(康威定律,服务=团队边界) |
结论:团队小、业务简单 → 单体。团队 20+ 人、业务复杂 → 微服务。别为了微服务而微服务,过度设计是毒药。
Q2. CAP 定理详解:C(一致性)、A(可用性)、P(分区容错)三选二的权衡
CAP 定理是分布式系统的”不可能三角”,由 Eric Brewer 在 2000 年提出。
三个字母分别是什么
C(Consistency,一致性):所有节点在同一时刻看到的数据是完全一样的。你写入节点 A,立刻去节点 B 读,读到的是最新值。
A(Availability,可用性):系统一直可以正常响应请求,不会返回错误(但数据可能不是最新的)。
P(Partition Tolerance,分区容错性):网络分区(节点间通信中断)发生时,系统仍然能正常运作。
为什么只能三选二
在分布式系统中,网络故障是必然会发生的,所以 P(分区容错)是必须保证的,不然你的”分布式系统”其实是个摆设。
所以现实中只有两种选择:
- CP(选 C 放弃 A):网络分区时,拒绝服务或等待数据同步完成,保证数据一致。代表:ZooKeeper、HBase、Etcd
- AP(选 A 放弃 C):网络分区时,继续提供服务,但数据可能是旧的。代表:Cassandra、CouchDB、Eureka
举个真实场景
银行转账:你从 A 账户转 1000 元到 B 账户。此时必须选 CP,宁可系统报错,也不能出现钱扣了但对方没收到。
电商商品库存:选 AP,超卖了可以后续处理,但不能因为节点故障就让整个购物车无法访问。
1 2 3 4
| 面试速记: - Zookeeper = CP(选主期间不可用) - Eureka = AP(每个节点独立运行,数据可能不一致) - Nacos = 可配置,默认 AP,可切换 CP
|
Q3. BASE 理论:Basically Available、Soft State、Eventually Consistent
BASE 是 CAP 的妥协产物,是对 AP 系统的进一步描述,由 Ebay 的架构师 Dan Pritchett 提出。
三个词的含义
BA(Basically Available,基本可用):系统在出现故障时,允许损失部分可用性,但核心功能仍然可用。
例子:双 11 期间,淘宝搜索变慢了(响应时间变长),或者某些非核心功能(评价、收藏)暂时不可用,但支付、下单核心链路照样跑。
S(Soft State,软状态):允许系统中的数据存在中间状态,即数据同步有一个过程,不是瞬间完成的。
例子:你在北京修改了头像,上海的服务器过几秒才同步到最新头像,这个”过渡期”就是软状态。
E(Eventually Consistent,最终一致性):系统保证在没有新的更新操作的情况下,经过一段时间后,数据最终会达到一致状态。
例子:分布式消息队列,消息最终一定会被消费,但不保证立即消费。
BASE vs ACID
| 维度 |
ACID(关系型数据库) |
BASE(分布式系统) |
| 一致性 |
强一致 |
最终一致 |
| 可用性 |
事务期间可能锁表 |
始终可用 |
| 适用场景 |
金融、银行 |
电商、社交、推荐 |
面试金句:BASE 不是放弃一致性,而是用最终一致性换取更高的可用性和性能,适合对实时一致性要求不高的业务场景。
Q4. 服务注册与发现的原理(服务端注册 vs 客户端注册,健康检查机制)
为什么需要服务注册与发现
微服务环境下,服务实例的 IP 和端口是动态变化的(K8s 扩缩容、故障重启)。不能再把地址硬编码在配置里,需要一个”电话本”——注册中心。
两种注册模式
1. 客户端注册(Client-side Registration)
服务自己启动时,调用注册中心 API 把自己的地址注册进去,关闭时注销。代表:Eureka、Nacos
1
| 服务启动 → 发送 POST /registry 给注册中心 → 心跳维持 → 服务关闭发送注销
|
优点:简单,服务自己控制注册/注销时机。
缺点:每个服务都要集成注册客户端 SDK,与注册中心耦合。
2. 服务端注册(Server-side Registration)
由第三方(如 K8s、负载均衡器)负责注册,服务本身无感知。代表:Kubernetes + CoreDNS
1
| K8s 创建 Pod → 自动注册到 CoreDNS → Pod 销毁自动注销
|
优点:服务无需感知注册逻辑,语言无关。
缺点:依赖基础设施,复杂度转移到运维层。
健康检查机制
| 方式 |
说明 |
代表 |
| 心跳检测 |
客户端定时发心跳,超时没有则摘除 |
Eureka、Nacos 临时实例 |
| 主动探测 |
注册中心定时 HTTP/TCP 探测服务端点 |
Nacos 持久实例 |
| TTL 过期 |
心跳维持 TTL,TTL 到期自动摘除 |
Consul |
1 2 3 4 5 6 7 8
| spring: cloud: nacos: discovery: heart-beat-interval: 5000 heart-beat-timeout: 15000 ip-delete-timeout: 30000
|
Q5. Nacos vs Eureka 的核心区别(CP vs AP、配置中心、健康检查机制差异)
这道题是面试必考,记住下面这张表,背下来:
| 维度 |
Nacos |
Eureka |
| CAP 模型 |
AP(默认)/ CP(可切换) |
AP |
| 配置中心 |
内置,支持动态配置推送 |
不支持,需搭配 Spring Cloud Config |
| 健康检查 |
支持 HTTP、TCP、心跳、MySQL 等多种探测 |
仅客户端心跳 |
| 实例类型 |
临时实例(心跳)+ 持久实例(主动探测) |
仅临时实例 |
| 社区活跃度 |
阿里维护,活跃 |
Netflix 官方停止维护 |
| 一致性协议 |
Raft(CP 模式)/ Distro(AP 模式) |
纯 AP,无一致性协议 |
| 雪崩保护 |
没有(Nacos 靠 Sentinel) |
有自我保护机制 |
| 管理界面 |
功能丰富的控制台 |
简单的监控页面 |
Eureka 的自我保护机制
当 Eureka Server 在短时间内收到的心跳数低于阈值(默认 85%),它会进入自我保护模式,不再删除任何服务实例,认为这是网络波动而非服务真的挂了。
这样做是 AP 的体现:宁可保留过期注册信息,也不误删健康实例。
Nacos 为什么更流行:一个组件搞定注册中心 + 配置中心,减少基础设施复杂度。Eureka 2.x 停止维护后,Nacos 成为国内最主流的选择。
1 2 3 4 5 6 7 8
| spring: cloud: nacos: discovery: cluster-name: DEFAULT
|
Q6. Nacos 服务注册原理(心跳机制、临时实例 vs 持久实例)
注册流程(源码级理解)
服务启动时,NacosAutoServiceRegistration 触发注册流程:
- 构造
Instance 对象,包含 IP、Port、服务名、元数据等
- 通过
NamingService.registerInstance() 调用 Nacos Server 的 REST API
- Nacos Server 收到注册请求,写入内存注册表(
ServiceManager)
- 如果是临时实例,Server 端启动一个定时任务检查心跳
1 2 3 4 5 6 7 8 9 10
| @Autowired private NamingService namingService;
Instance instance = new Instance(); instance.setIp("192.168.1.100"); instance.setPort(8080); instance.setEphemeral(true); namingService.registerInstance("order-service", instance);
|
临时实例 vs 持久实例
| 维度 |
临时实例(ephemeral=true) |
持久实例(ephemeral=false) |
| 心跳方式 |
客户端主动发心跳 |
Server 主动探测 |
| 挂掉后行为 |
自动从注册表删除 |
保留,标记为不健康 |
| 适用场景 |
普通微服务(弹性扩缩容) |
固定物理机、第三方服务 |
| 一致性协议 |
Distro(AP) |
Raft(CP) |
心跳机制细节
1 2 3 4
| 客户端每 5s 发送一次心跳(PUT /nacos/v1/ns/instance/beat) Server 检查: - 15s 内没收到心跳 → 标记为不健康(unhealthy) - 30s 内没收到心跳 → 从注册表删除
|
1 2 3 4 5 6 7 8 9 10
| spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 service: order-service ephemeral: true metadata: version: v1.0 env: prod
|
Q7. Nacos 配置中心:长轮询机制(客户端如何感知配置变更)
为什么不用普通轮询
普通轮询:客户端每隔 N 秒问服务器”配置改了吗?”。如果 N=1s,10000 个客户端就是 10000 QPS,浪费。如果 N=60s,配置改了要等最多 60s 才感知,太慢。
Nacos 的长轮询(Long Polling)
核心思路:客户端发起 HTTP 请求,如果配置没有变化,服务端不立即返回,而是挂起请求,等到配置发生变化或者超时(默认 30s)才返回。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 客户端 Nacos Server | | |--- HTTP GET /configs (timeout=30s) -->| | (服务端挂起请求) | | (配置没变,等待中...) | | | | (配置被修改了!) | |<-------- 返回变更的 dataId ---------| | | |--- 拉取最新配置 ------------------>| |<-------- 返回新配置值 -------------| | | |--- 立即再次发起长轮询 ------------>|
|
好处:
- 配置变更后毫秒级感知(不用等轮询间隔)
- 没有变更时,只有一个挂起的请求,对服务端压力极小
源码层面的关键类
ClientLongPolling:客户端长轮询任务
LongPollingService:服务端管理挂起的请求
LocalDataChangeEvent:配置变更事件,触发通知
1 2 3 4 5 6 7 8 9 10
| spring: application: name: order-service cloud: nacos: config: server-addr: 127.0.0.1:8848 file-extension: yaml refresh-enabled: true
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @RestController @RefreshScope public class OrderController {
@Value("${order.timeout:30}") private int timeout;
@GetMapping("/timeout") public int getTimeout() { return timeout; } }
|
Q8. OpenFeign 的原理(动态代理 + HttpClient,Ribbon/LoadBalancer 集成)
没有 OpenFeign 之前
1 2 3 4
| RestTemplate restTemplate = new RestTemplate(); String url = "http://user-service/api/users/" + userId; User user = restTemplate.getForObject(url, User.class);
|
OpenFeign 让你这样写
1 2 3 4 5 6 7 8 9 10 11 12
| @FeignClient(name = "user-service") public interface UserClient {
@GetMapping("/api/users/{id}") User getUserById(@PathVariable("id") Long id); }
@Autowired private UserClient userClient;
User user = userClient.getUserById(1L);
|
原理:JDK 动态代理
扫描 @FeignClient 注解:Spring 启动时,FeignClientsRegistrar 扫描所有 @FeignClient 接口,注册为 FeignClientFactoryBean
生成动态代理:调用接口方法时,实际走的是 FeignInvocationHandler.invoke(),它把方法调用翻译成 HTTP 请求
集成负载均衡:代理中集成了 Spring Cloud LoadBalancer,user-service 这个服务名会被解析成实际 IP:Port
发送 HTTP 请求:底层默认用 HttpURLConnection,生产环境推荐换成 Apache HttpClient 或 OkHttp
1
| 方法调用 → FeignInvocationHandler → MethodHandler → LoadBalancer(服务名→IP) → HttpClient → HTTP 请求
|
1 2 3 4 5 6
| feign: httpclient: enabled: true max-connections: 200 max-connections-per-route: 50
|
1 2 3 4 5
| <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
|
Q9. OpenFeign 超时配置(connectTimeout / readTimeout)和重试机制的坑
超时配置(Spring Cloud 2021+ 新写法)
1 2 3 4 5 6 7 8 9 10 11
| spring: cloud: openfeign: client: config: default: connect-timeout: 2000 read-timeout: 10000 order-service: connect-timeout: 1000 read-timeout: 5000
|
重试机制的大坑
OpenFeign 默认不重试(Retryer.NEVER_RETRY),这是故意的。
为什么默认不重试? 因为重试有副作用!对于非幂等接口(比如下单、扣款),重试会导致重复操作。
1 2 3 4 5 6 7 8 9 10
| @Configuration public class FeignConfig {
@Bean public Retryer feignRetryer() { return new Retryer.Default(100, 1000, 3); } }
|
重试的坑:
- POST 接口开了重试 = 重复下单,生产事故
- 重试 + 超时叠加:3次重试 × 10s = 最长等 30s,调用方早超时了
- 建议:GET 幂等接口才开重试,POST/PUT/DELETE 一律不重试
1 2 3 4 5 6 7 8 9
| @FeignClient( name = "inventory-service", configuration = InventoryFeignConfig.class // 仅对这个服务生效 ) public interface InventoryClient { @GetMapping("/api/inventory/{skuId}") int getStock(@PathVariable String skuId); }
|
Q10. 客户端负载均衡:Spring Cloud LoadBalancer 的轮询 vs 随机策略
客户端 vs 服务端负载均衡
服务端负载均衡(Nginx):客户端请求打到 Nginx,Nginx 再转发给后端服务,客户端不知道后端有几个实例。
客户端负载均衡(Spring Cloud LoadBalancer):客户端自己从注册中心拉取服务实例列表,然后自己选一个实例发请求。没有额外的网络跳转。
两种内置策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@Configuration public class LoadBalancerConfig {
@Bean public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer( Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new RandomLoadBalancer( loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name ); } }
@LoadBalancerClient(name = "order-service", configuration = LoadBalancerConfig.class) public class MyConfig {}
|
自定义权重策略(面试加分项)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@Override public Mono<Response<ServiceInstance>> choose(Request request) { return serviceInstanceListSupplierProvider.getIfAvailable() .get() .next() .map(instances -> { int totalWeight = instances.stream() .mapToInt(i -> Integer.parseInt( i.getMetadata().getOrDefault("weight", "1"))) .sum(); return new DefaultResponse(selectedInstance); }); } }
|
Q11. 服务熔断 vs 服务降级 vs 服务限流的区别
这三个概念经常被混淆,用一个点外卖的场景来解释:
服务限流(Rate Limiting)
问题:外卖平台来了 10000 个并发请求,系统只能处理 1000 个。
解决:超出阈值的请求直接拒绝或排队等待,保护系统不被压垮。
服务熔断(Circuit Breaker)
问题:你调用餐厅查询接口,餐厅服务挂了,你的请求都超时,线程全部阻塞,最终把自己也搞崩了。
解决:熔断器监控调用成功率,失败率超过阈值时,自动断开对该服务的调用,快速返回失败,防止故障扩散。
1 2
| 熔断 = 保护上游,检测到下游故障后主动"断路" 熔断器三个状态:关闭(正常)→ 打开(快速失败)→ 半开(试探恢复)
|
服务降级(Fallback)
问题:系统高峰期资源紧张,或者依赖服务熔断了,需要给用户一个说法。
解决:提供一个备用响应,比如”推荐功能暂时不可用,显示默认商品列表”。
1
| 降级 = 用户体验兜底,把非核心功能关掉,保核心链路
|
三者关系
1 2 3 4 5
| 限流 → 请求进来之前就过滤 熔断 → 检测到下游故障,主动断路 降级 → 熔断/限流触发后,返回的兜底响应
通常:限流 + 熔断 是触发条件,降级 是执行动作
|
Q12. Sentinel 的核心概念(资源、规则、流控规则、降级规则、热点规则)
资源(Resource)
资源是 Sentinel 的核心概念,可以是一个方法、一个接口、一段代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @SentinelResource( value = "getOrder", // 资源名 blockHandler = "blockHandler", // 触发限流/熔断时的处理方法 fallback = "fallback" // 业务异常时的兜底方法 ) public Order getOrder(Long id) { return orderRepository.findById(id); }
public Order blockHandler(Long id, BlockException e) { return Order.empty(); }
public Order fallback(Long id, Throwable t) { log.error("getOrder fallback, id={}", id, t); return Order.empty(); }
|
流控规则(FlowRule)
1 2 3 4 5 6 7 8
| FlowRule rule = new FlowRule(); rule.setResource("getOrder"); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(100); rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
FlowRuleManager.loadRules(Collections.singletonList(rule));
|
降级规则(DegradeRule)
1 2 3 4 5 6
| DegradeRule rule = new DegradeRule("getOrder") .setGrade(CircuitBreakerStrategy.ERROR_RATIO.getType()) .setCount(0.5) .setStatIntervalMs(10000) .setMinRequestAmount(10) .setTimeWindow(5);
|
热点规则(ParamFlowRule)
针对特定参数值的限流,比如某个商品 ID 被刷爆了。
1 2 3 4 5 6 7 8
| ParamFlowRule rule = new ParamFlowRule("getOrder") .setParamIdx(0) .setCount(10) .setParamFlowItemList(Arrays.asList( new ParamFlowItem().setObject("VIP_ORDER").setCount(100) ));
|
Q13. Sentinel 的滑动窗口限流算法原理
为什么不用固定窗口
固定窗口(比如每秒限 100 个)有个临界问题:
1 2 3 4
| [0-1秒窗口] 在 0.9s 放了 100 个请求 [1-2秒窗口] 在 1.1s 又放了 100 个请求
实际上 0.9s 到 1.1s 这 200ms 内通过了 200 个请求!2倍于限制!
|
滑动窗口算法
Sentinel 使用滑动时间窗口,把一个大的时间窗口(比如 1s)分成多个小的时间槽(Bucket),每个槽记录该时间段内的请求数。
1 2 3 4 5 6 7
| 1s 窗口分成 10 个 100ms 的 Bucket:
时间: 0 100 200 300 400 500 600 700 800 900 1000ms 槽: [B0] [B1] [B2] [B3] [B4] [B5] [B6] [B7] [B8] [B9] 计数: 5 10 8 12 15 9 11 7 13 10
当前时刻是 950ms,滑动窗口统计的是 [50ms ~ 950ms] 这 900ms 内的总请求数
|
滑动窗口的优点:
- 时间窗口随时间流动,不存在固定窗口的临界突刺问题
- 精度更高(Bucket 越小,精度越高)
Sentinel 的实现:LeapArray
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public abstract class LeapArray<T> { protected int windowLengthInMs; protected int sampleCount; protected int intervalInMs;
public WindowWrap<T> currentWindow(long timeMillis) { int idx = calculateTimeIdx(timeMillis); } }
|
Q14. Sentinel vs Hystrix 的核心区别(线程隔离 vs 信号量,熔断恢复机制)
隔离机制
Hystrix 线程池隔离:为每个依赖服务分配一个独立的线程池。调用方的线程不会因为依赖服务卡住而阻塞。
1 2
| 优点:真正的隔离,调用方线程不会被拖死 缺点:线程上下文切换开销大,需要为每个服务配置线程池大小
|
Hystrix 信号量隔离:用计数器限制并发数,超过就拒绝。
Sentinel 的隔离:主要靠并发线程数限流(类似信号量)+ 流控,没有线程池隔离。性能开销小得多。
熔断恢复机制对比
| 维度 |
Hystrix |
Sentinel |
| 熔断触发 |
错误率超阈值 |
错误率/慢调用比例/异常数 |
| 恢复探测 |
固定时间后放一个请求 |
半开状态,按比例探测 |
| 规则配置 |
代码/注解 |
控制台/代码/Nacos(动态) |
| 维护状态 |
Netflix 停止维护 |
阿里持续维护 |
| 性能 |
较高(线程池切换) |
低(基于计数器) |
| 实时监控 |
Dashboard(需额外部署) |
内置控制台,实时数据 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| spring: cloud: sentinel: transport: dashboard: localhost:8080 port: 8719 datasource: nacos: nacos: server-addr: localhost:8848 data-id: sentinel-flow-rules group-id: DEFAULT_GROUP data-type: json rule-type: flow
|
Q15. Spring Cloud Gateway 的工作原理(Filter Chain、Predicate、Route)
三个核心概念
Route(路由):路由是网关的基本单元,由 ID、目标 URI、Predicate 和 Filter 组成。
Predicate(断言):判断请求是否匹配路由规则,比如请求路径是不是 /api/order/**,请求头是不是包含某个值。
Filter(过滤器):对请求/响应进行处理,比如鉴权、限流、添加请求头、日志记录。
请求处理流程
1 2 3 4 5 6 7 8 9 10 11 12 13
| 客户端请求 ↓ Gateway Handler Mapping(根据 Predicate 匹配路由) ↓ Gateway Web Handler ↓ Pre Filter Chain(前置过滤器:鉴权、限流、日志) ↓ Proxied Service(实际的微服务) ↓ Post Filter Chain(后置过滤器:修改响应、跨域处理) ↓ 客户端响应
|
配置示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path=/api/order/** - Header=X-Request-Id, \d+ - Method=GET,POST filters: - StripPrefix=1 - AddRequestHeader=X-Gateway-Version, 2.0 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20
|
自定义全局过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Component @Order(-1) public class AuthFilter implements GlobalFilter {
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !validateToken(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); }
ServerHttpRequest request = exchange.getRequest().mutate() .header("X-User-Id", extractUserId(token)) .build();
return chain.filter(exchange.mutate().request(request).build()); } }
|
Q16. Gateway 与 Nginx 的职责划分(网关 vs 反向代理)
不是竞争关系,是分层协作
很多人以为用了 Gateway 就不需要 Nginx 了,其实不对。
Nginx 负责(基础设施层):
- SSL 证书终止(HTTPS → HTTP)
- 静态资源服务
- 基础的负载均衡(多个 Gateway 实例间)
- DDoS 防护(连接数限制)
Spring Cloud Gateway 负责(业务层):
- 服务路由(按路径/请求头路由到不同微服务)
- 统一鉴权(JWT 验证)
- 限流(基于 Redis 的精细化限流)
- 协议转换(HTTP → gRPC)
- 灰度路由(按用户 ID/版本分流)
1 2 3 4 5 6 7
| [客户端] ↓ HTTPS [Nginx] ← SSL 终止、静态资源、基础限流 ↓ HTTP [Gateway 集群] ← 动态路由、鉴权、业务限流 ↓ [微服务 A / B / C]
|
一个常见误区
有人把所有逻辑都写在 Gateway 里(包括业务逻辑)。这是错的。Gateway 应该是轻量的,复杂业务逻辑放进来会让 Gateway 成为新的瓶颈。原则:Gateway 只做横切关注点(鉴权、限流、路由),不做业务逻辑。
Q17. 链路追踪:Sleuth/Micrometer Tracing + Zipkin 的 TraceId/SpanId 传播原理
为什么需要链路追踪
微服务调用链:用户请求 → Gateway → 订单服务 → 库存服务 → 支付服务。一个请求出问题,你怎么知道是哪个环节慢了?链路追踪把整个调用链串起来。
核心概念
- TraceId:一次完整请求的唯一 ID,贯穿整个调用链
- SpanId:一个服务调用的 ID,一个 Trace 包含多个 Span
- ParentSpanId:调用方的 SpanId,形成树形结构
传播原理
1 2 3 4 5
| Gateway(TraceId=abc, SpanId=001) ↓ HTTP Header: X-B3-TraceId=abc, X-B3-SpanId=002, X-B3-ParentSpanId=001 订单服务(TraceId=abc, SpanId=002) ↓ HTTP Header: X-B3-TraceId=abc, X-B3-SpanId=003, X-B3-ParentSpanId=002 库存服务(TraceId=abc, SpanId=003)
|
每个服务接收到请求时,从 Header 里取出 TraceId 和 ParentSpanId,生成自己的 SpanId,然后上报给 Zipkin。
Spring Boot 3.x 配置(Micrometer Tracing)
1 2 3 4 5 6 7 8
| <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-tracing-bridge-brave</artifactId> </dependency> <dependency> <groupId>io.zipkin.reporter2</groupId> <artifactId>zipkin-reporter-brave</artifactId> </dependency>
|
1 2 3 4 5 6 7
| management: tracing: sampling: probability: 1.0 zipkin: tracing: endpoint: http://zipkin:9411/api/v2/spans
|
Q18. 分布式事务的4种方案:2PC、TCC、Saga、本地消息表
为什么分布式事务这么难
单机事务:BEGIN → 操作A → 操作B → COMMIT,数据库保证原子性。
微服务:操作A在服务1,操作B在服务2,两个数据库,网络随时可能断。
方案对比
| 方案 |
原理 |
优点 |
缺点 |
适用场景 |
| 2PC |
协调者两阶段提交 |
强一致 |
同步阻塞,单点故障 |
数据库层面(XA) |
| TCC |
Try-Confirm-Cancel |
无锁,性能好 |
开发量大,需实现三个方法 |
资金转账 |
| Saga |
长事务拆成子事务+补偿 |
无全局锁 |
只有最终一致,补偿逻辑复杂 |
订单流程 |
| 本地消息表 |
事务 + MQ 最终一致 |
简单可靠 |
对 MQ 强依赖,有延迟 |
跨服务通知 |
TCC 举例:转账 100 元
1 2 3 4 5 6 7 8 9 10 11
| Try 阶段(预留资源): - 账户A:冻结 100 元(余额 - 100,冻结金额 + 100) - 账户B:预增 100 元(预增金额 + 100)
Confirm 阶段(提交): - 账户A:冻结金额清零,正式扣减 - 账户B:预增金额转为实际余额
Cancel 阶段(回滚,Try 成功但 Confirm 失败时): - 账户A:解冻 100 元 - 账户B:预增金额清零
|
本地消息表举例
1 2 3 4 5 6 7 8 9
| BEGIN; INSERT INTO orders (id, ...) VALUES (...); INSERT INTO local_messages (id, topic, payload, status) VALUES (uuid(), 'order.created', '{"orderId":123}', 'PENDING'); COMMIT;
|
Q19. Seata AT 模式的原理(undo_log、全局事务、分支事务)
Seata 是什么
Seata 是阿里开源的分布式事务框架,支持 AT、TCC、Saga、XA 四种模式。AT 模式是最常用的,对业务代码无侵入,只需要一个注解。
AT 模式原理
Seata AT 模式基于两阶段提交,但做了优化:第一阶段立即提交(释放锁),第二阶段再决定是否回滚。
关键组件:
- TC(Transaction Coordinator):Seata Server,协调全局事务
- TM(Transaction Manager):事务发起方,标注
@GlobalTransactional
- RM(Resource Manager):各个参与事务的微服务
undo_log 表(核心)
1 2 3 4 5 6 7 8 9 10 11
| CREATE TABLE undo_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, branch_id BIGINT NOT NULL, xid VARCHAR(100) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT NOT NULL, log_created DATETIME NOT NULL, log_modified DATETIME NOT NULL );
|
执行流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 1. TM 开启全局事务 → TC 生成全局事务 XID
2. RM(订单服务)执行 SQL: - Seata 代理数据源,拦截 SQL - 执行前:查询并保存"前镜像"(原始数据) - 执行 SQL - 执行后:查询并保存"后镜像"(修改后数据) - 把前后镜像写入 undo_log - 本地事务提交(释放本地锁!) - 上报分支事务状态给 TC
3. TC 收集所有 RM 的状态: - 全部成功 → 通知 RM 删除 undo_log(异步) - 有失败 → 通知 RM 根据 undo_log 回滚(用前镜像覆盖数据)
|
1 2 3 4 5 6 7 8
| @GlobalTransactional(timeoutMills = 30000, name = "create-order") public void createOrder(OrderDTO dto) { orderService.create(dto); inventoryService.deduct(dto); pointService.addPoint(dto); }
|
Q20. 分布式锁的3种实现:Redis(Redisson)、Zookeeper、数据库,各自优劣
为什么需要分布式锁
多个服务实例同时操作一个共享资源(比如库存),synchronized 只在单 JVM 内有效,分布式环境需要跨进程的锁。
三种实现对比
| 实现 |
原理 |
优点 |
缺点 |
| Redis(Redisson) |
SETNX + 过期时间 + Lua 脚本 |
性能最高,毫秒级 |
Redis 主从切换时可能丢锁(RedLock 解决) |
| ZooKeeper |
临时顺序节点 + Watch |
强一致,锁不会丢 |
性能差,ZK 是 CP 系统 |
| 数据库 |
唯一索引 / SELECT FOR UPDATE |
实现简单 |
性能最差,有数据库单点风险 |
Redis 分布式锁(Redisson 推荐)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Autowired private RedissonClient redissonClient;
public void deductInventory(Long skuId, int count) { RLock lock = redissonClient.getLock("lock:inventory:" + skuId); try { boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS); if (!acquired) { throw new BusinessException("系统繁忙,请重试"); } doDeduct(skuId, count); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }
|
Redisson 的看门狗机制:锁快过期时(默认 30s 的 2/3),自动续期,防止业务没执行完锁就过期了。
常见坑
1 2 3 4
| 坑1:加了锁但没设置过期时间,服务崩了 → 死锁 坑2:用 Redis 主从,master 写锁成功后宕机,slave 没同步 → 锁丢失(用 RedLock 解决,但有争议) 坑3:锁过期时间设太短,业务没跑完锁就过期了(Redisson 看门狗解决) 坑4:unlock 时不判断是否是自己持有的锁(高并发下可能解了别人的锁)
|
Q21. 幂等性设计:Token 机制 vs 数据库唯一键 vs Redis SETNX 防重复提交
为什么要幂等
网络超时导致重试、用户双击按钮、MQ 消息重复投递——这些都可能导致同一操作执行多次。幂等性保证”多次调用和一次调用效果相同”。
方案一:Token 机制(防表单重复提交)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @GetMapping("/token") public String getToken() { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set("idempotent:" + token, "1", 5, TimeUnit.MINUTES); return token; }
@PostMapping("/order") public Response createOrder(@RequestHeader("Idempotent-Token") String token, @RequestBody OrderDTO dto) { String luaScript = "if redis.call('get', KEYS[1]) == '1' then " + " return redis.call('del', KEYS[1]) " + "else return 0 end";
Long result = redisTemplate.execute( new DefaultRedisScript<>(luaScript, Long.class), Collections.singletonList("idempotent:" + token) );
if (result == null || result == 0) { throw new BusinessException("请勿重复提交"); }
return orderService.create(dto); }
|
方案二:数据库唯一键
1 2 3 4 5 6
| ALTER TABLE orders ADD UNIQUE KEY uk_trade_no (trade_no);
INSERT INTO orders (trade_no, ...) VALUES (?, ...) ON DUPLICATE KEY UPDATE updated_at = NOW();
|
方案三:Redis SETNX(消息消费幂等)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @RabbitListener(queues = "order.created") public void onMessage(OrderMessage message) { String key = "consumed:" + message.getMsgId();
Boolean isFirst = redisTemplate.opsForValue() .setIfAbsent(key, "1", 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(isFirst)) { log.info("消息已消费,跳过:{}", message.getMsgId()); return; }
processOrder(message); }
|
Q22. 微服务间调用如何传递用户信息(ThreadLocal + RequestContextHolder + Feign Interceptor)
问题场景
用户登录后,JWT Token 里有 userId、roles 等信息。Gateway 验证 Token 后,后续微服务怎么知道当前用户是谁?
解决方案:Feign 拦截器链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
|
.header("X-User-Id", userId) .header("X-User-Roles", roles)
@Component public class UserContextInterceptor implements HandlerInterceptor {
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userId = request.getHeader("X-User-Id"); String roles = request.getHeader("X-User-Roles");
UserContext.set(new UserInfo(userId, roles)); return true; }
@Override public void afterCompletion(...) { UserContext.clear(); } }
public class UserContext { private static final ThreadLocal<UserInfo> HOLDER = new ThreadLocal<>();
public static void set(UserInfo user) { HOLDER.set(user); } public static UserInfo get() { return HOLDER.get(); } public static void clear() { HOLDER.remove(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Component public class UserFeignInterceptor implements RequestInterceptor {
@Override public void apply(RequestTemplate template) { UserInfo user = UserContext.get(); if (user != null) { template.header("X-User-Id", user.getUserId()); template.header("X-User-Roles", user.getRoles()); } } }
|
注意:线程池和异步场景下,ThreadLocal 会丢失!需要用 TransmittableThreadLocal(阿里 TTL 库)来解决跨线程传递的问题。
Q23. 服务网格(Service Mesh)和 Istio 是什么,与 Spring Cloud 的关系
Service Mesh 解决的问题
Spring Cloud 把服务治理逻辑(负载均衡、熔断、链路追踪)都放在业务代码里(以 SDK 形式)。问题:
- 业务代码与基础设施耦合
- 多语言服务(Go、Python)要各自实现一套
- 升级 Spring Cloud 版本需要重新部署所有服务
Service Mesh 把这些逻辑下沉到基础设施层,以 Sidecar 代理的形式注入到每个服务旁边,业务代码完全不感知。
Istio 架构
1 2 3 4 5 6 7 8
| [服务 A] ↔ [Envoy Sidecar A] ↕ mTLS [服务 B] ↔ [Envoy Sidecar B] ↕ [Istiod(控制平面)] ├── Pilot(流量管理) ├── Citadel(证书管理) └── Galley(配置验证)
|
所有服务间通信都经过 Envoy Sidecar,Istiod 统一管理流量规则,不需要改一行业务代码。
与 Spring Cloud 的关系
| 维度 |
Spring Cloud |
Service Mesh(Istio) |
| 实现层次 |
应用层(SDK) |
基础设施层(Sidecar) |
| 语言要求 |
Java |
语言无关 |
| 运行依赖 |
JVM |
K8s |
| 学习曲线 |
中 |
高 |
| 适用阶段 |
早中期 |
成熟期,大规模 K8s |
结论:中小团队用 Spring Cloud 足够;超大规模、多语言技术栈才值得引入 Istio。
Q24. 微服务的数据库拆分策略(每服务独立 DB,跨服务查询怎么处理)
核心原则:每个服务有且只有自己的数据库
为什么:数据库共享 = 数据耦合,一个服务的慢查询会拖垮所有服务。
拆分策略
1. 按业务领域拆分(DDD 限界上下文)
1 2 3 4
| 用户服务 → user_db(users, user_profiles) 订单服务 → order_db(orders, order_items) 商品服务 → product_db(products, categories, inventory) 支付服务 → payment_db(payments, refunds)
|
2. 跨服务查询的几种处理方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public OrderDetailVO getOrderDetail(Long orderId) { Order order = orderRepository.findById(orderId); User user = userClient.getUserById(order.getUserId()); Product product = productClient.getProduct(order.getProductId());
return OrderDetailVO.of(order, user, product); }
|
分库后的主键问题
不能再用数据库自增 ID,否则不同库的 ID 会重复。需要分布式 ID 方案:
Q25. API 版本控制的几种方案(URL路径、请求头、参数)
接口版本控制是微服务演进的必备能力,新旧客户端需要同时支持。
方案一:URL 路径版本(最直观,推荐)
1 2 3 4 5 6 7
| @RestController @RequestMapping("/api/v1/orders") public class OrderControllerV1 { ... }
@RestController @RequestMapping("/api/v2/orders") public class OrderControllerV2 { ... }
|
优点:直观,易缓存,URL 即版本。
缺点:URL 语义上版本不应是资源的一部分。
方案二:请求头版本
1 2 3 4 5 6 7 8
| @GetMapping(value = "/orders", headers = "API-Version=1") public ResponseEntity<OrderV1DTO> getOrderV1() { ... }
@GetMapping(value = "/orders", headers = "API-Version=2") public ResponseEntity<OrderV2DTO> getOrderV2() { ... }
curl -H "API-Version: 2" http:
|
方案三:Accept 请求头(RESTful 最正统)
1 2 3 4 5 6
| @GetMapping(value = "/orders", produces = "application/vnd.company.v2+json") public OrderV2DTO getOrderV2() { ... }
curl -H "Accept: application/vnd.company.v2+json" http:
|
Gateway 层统一版本路由
1 2 3 4 5 6 7 8 9 10 11 12
| spring: cloud: gateway: routes: - id: order-v1 uri: lb://order-service-v1 predicates: - Path=/api/v1/orders/** - id: order-v2 uri: lb://order-service-v2 predicates: - Path=/api/v2/orders/**
|
Q26. 灰度发布的实现方案(Nacos 元数据 + Gateway 路由规则)
什么是灰度发布
新版本先发布给小部分用户(比如 5% 的流量、内部测试用户),验证没问题后再全量发布。也叫金丝雀发布(Canary Release)。
实现方案:Nacos 元数据 + Gateway
1 2 3 4 5 6 7 8
| spring: cloud: nacos: discovery: metadata: version: v2 env: canary
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @Component public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
@Override public Mono<Response<ServiceInstance>> choose(Request request) { HttpHeaders headers = ((RequestDataContext) request.getContext()) .getClientRequest().getHeaders(); boolean isCanary = "true".equals(headers.getFirst("X-Canary"));
return serviceInstanceListSupplierProvider.getIfAvailable() .get().next().map(instances -> { List<ServiceInstance> targets = isCanary ? instances.stream() .filter(i -> "canary".equals(i.getMetadata().get("env"))) .collect(toList()) : instances.stream() .filter(i -> !"canary".equals(i.getMetadata().get("env"))) .collect(toList());
if (targets.isEmpty()) targets = instances;
int idx = ThreadLocalRandom.current().nextInt(targets.size()); return new DefaultResponse(targets.get(idx)); }); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Component public class CanaryFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id"); boolean isCanary = userId != null && Long.parseLong(userId) % 100 < 5;
ServerHttpRequest request = exchange.getRequest().mutate() .header("X-Canary", String.valueOf(isCanary)) .build(); return chain.filter(exchange.mutate().request(request).build()); } }
|
Q27. 微服务的健康检查:Spring Boot Actuator /actuator/health 的集成
Actuator 健康检查配置
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| management: endpoints: web: exposure: include: health, info, metrics, prometheus endpoint: health: show-details: when-authorized show-components: always health: db: enabled: true redis: enabled: true nacos: enabled: true
|
1 2 3 4 5 6 7 8 9 10
| curl http://localhost:8080/actuator/health { "status": "UP", "components": { "db": { "status": "UP", "details": { "database": "MySQL" } }, "redis": { "status": "UP" }, "diskSpace": { "status": "UP", "details": { "free": 10737418240 } } } }
|
自定义健康检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Component public class ThirdPartyApiHealthIndicator implements HealthIndicator {
@Autowired private ThirdPartyClient client;
@Override public Health health() { try { boolean ok = client.ping(); return ok ? Health.up().withDetail("api", "reachable").build() : Health.down().withDetail("api", "unreachable").build(); } catch (Exception e) { return Health.down(e).build(); } } }
|
K8s 探针集成
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 30 periodSeconds: 10
readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 10 periodSeconds: 5
|
Q28. 配置加密:Jasypt 或 Nacos + KMS 对敏感配置加密
为什么要加密配置
数据库密码、API Key、私钥不能明文存在配置文件或代码仓库里,一旦仓库泄露或内部人员查看,后果不堪设想。
方案一:Jasypt 加密(简单场景)
1 2 3 4 5
| <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>
|
1 2 3 4 5 6 7 8 9
| spring: datasource: password: ENC(Bn9oL3xkP8VQn9hGbVdMRzk=)
jasypt: encryptor: password: ${JASYPT_PASSWORD} algorithm: PBEWITHHMACSHA512ANDAES_256
|
1 2 3
| java -cp jasypt-3.0.5.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI \ input="my_db_password" password="my_secret_key" algorithm="PBEWITHHMACSHA512ANDAES_256"
|
方案二:Nacos + KMS(生产推荐)
阿里云 KMS(密钥管理服务)存储真正的密钥,Nacos 配置里存加密后的密文。服务启动时,从 KMS 获取密钥解密。
1 2 3 4 5 6 7 8 9 10 11 12
| @Component public class NacosKmsDecryptProcessor implements EnvironmentPostProcessor { @Override public void postProcessEnvironment(ConfigurableEnvironment env, SpringApplication app) { String kmsKeyId = env.getProperty("kms.key-id"); } }
|
Q29. 微服务的日志聚合:ELK(Elasticsearch + Logstash + Kibana)架构
为什么要日志聚合
50 个微服务,每个 3 个实例 = 150 个日志文件。出问题时一台一台服务器 grep 日志?效率极低。日志聚合把所有日志集中到一个地方,统一搜索。
ELK 架构
1 2 3
| 微服务 A → [Logstash/Filebeat] → Elasticsearch → Kibana(可视化) 微服务 B → [Logstash/Filebeat] ↗ 微服务 C → [Logstash/Filebeat] ↗
|
- Filebeat:轻量级日志采集器,安装在每台服务器上,读取日志文件发送给 Logstash
- Logstash:日志处理管道,解析、过滤、格式化日志,输出到 ES
- Elasticsearch:存储日志,支持全文搜索
- Kibana:日志可视化,搜索、图表、告警
与链路追踪结合(关键配置)
1 2 3 4 5 6 7
| <appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender"> <destination>logstash:5000</destination> <encoder class="net.logstash.logback.encoder.LogstashEncoder"> <customFields>{"app":"order-service","env":"prod"}</customFields> </encoder> </appender>
|
1
| <pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%X{traceId},%X{spanId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
|
日志里有 TraceId 之后,可以在 Kibana 里直接搜 TraceId,把一次请求的所有服务日志串联起来。
成本更低的替代方案
- Loki + Grafana(Grafana 全家桶,占用资源少,适合中小团队)
- 阿里云 SLS / 腾讯云 CLS(托管服务,省运维)
Q30. 面试高频:你的项目中如何解决微服务的雪崩问题?(完整答题模板)
什么是雪崩
服务 A 调用 B,B 调用 C,C 出问题了响应慢。大量请求卡在 B 等待 C,B 的线程池耗尽,B 也挂了。A 等 B,A 也挂了。一个服务的故障像雪崩一样把整个系统搞垮。
完整答题模板(背这个去面试)
“我们项目中通过以下几层防护来解决雪崩问题:”
第一层:超时控制
1 2 3 4 5 6 7
| spring: cloud: openfeign: client: config: default: read-timeout: 3000
|
第二层:限流(Sentinel)
1 2 3 4 5 6 7
| @SentinelResource(value = "createOrder", blockHandler = "createOrderBlock") public Order createOrder(OrderDTO dto) { ... }
public Order createOrderBlock(OrderDTO dto, BlockException e) { throw new BusinessException("系统繁忙,请稍后再试"); }
|
第三层:熔断(Sentinel)
第四层:服务降级(Feign Fallback)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @FeignClient(name = "inventory-service", fallbackFactory = InventoryFallbackFactory.class) public interface InventoryClient { @GetMapping("/stock/{skuId}") int getStock(@PathVariable String skuId); }
@Component public class InventoryFallbackFactory implements FallbackFactory<InventoryClient> { @Override public InventoryClient create(Throwable cause) { return skuId -> { log.error("库存服务降级,skuId={}", skuId, cause); return -1; }; } }
|
第五层:线程池隔离(可选)
不同重要性的接口用不同线程池,一个慢接口不影响核心接口。
第六层:快速失败 + 友好提示
降级后不是直接报错,而是给用户友好提示,记录错误日志,发告警通知。
加分句(体现深度思考)
“我们还接入了链路追踪(Micrometer Tracing + Zipkin),雪崩发生时能快速定位是哪个服务开始出问题的,缩短了故障处理时间。同时在压测阶段,我们用 Sentinel 的热点规则对高频访问的商品 ID 做了额外限流保护,避免局部热点导致雪崩。”
总结
| 知识点 |
核心结论 |
| CAP |
P 必选,AP vs CP 按业务需求 |
| Nacos |
默认 AP,注册 + 配置二合一 |
| OpenFeign |
动态代理 + LoadBalancer,POST 别开重试 |
| Sentinel |
滑动窗口,比 Hystrix 性能好 |
| Gateway |
Filter Chain,只做横切,不做业务 |
| Seata AT |
undo_log 实现无侵入分布式事务 |
| 分布式锁 |
Redisson + 看门狗,注意解锁判断 |
| 雪崩防护 |
超时 + 限流 + 熔断 + 降级,多层叠加 |
这 30 道题覆盖了微服务面试 80% 的考点。建议每道题自己能用大白话讲出来,比死记硬背效果好 10 倍。祝面试顺利!