Spring Cloud & 微服务面试八股文 30 道|深度详解版(傻子都能看懂)

微服务面试是 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
# Nacos 心跳相关配置
spring:
cloud:
nacos:
discovery:
heart-beat-interval: 5000 # 心跳间隔 5s
heart-beat-timeout: 15000 # 15s 没心跳标记不健康
ip-delete-timeout: 30000 # 30s 彻底从注册表删除

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
# Nacos 切换为 CP 模式(生产慎用,需要 Leader 选举)
spring:
cloud:
nacos:
discovery:
cluster-name: DEFAULT
# 通过 Nacos API 切换,不是配置文件
# curl -X PUT "http://nacos:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP"

Q6. Nacos 服务注册原理(心跳机制、临时实例 vs 持久实例)

注册流程(源码级理解)

服务启动时,NacosAutoServiceRegistration 触发注册流程:

  1. 构造 Instance 对象,包含 IP、Port、服务名、元数据等
  2. 通过 NamingService.registerInstance() 调用 Nacos Server 的 REST API
  3. Nacos Server 收到注册请求,写入内存注册表(ServiceManager)
  4. 如果是临时实例,Server 端启动一个定时任务检查心跳
1
2
3
4
5
6
7
8
9
10
// 服务注册的核心 API
@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
# bootstrap.yml(必须用 bootstrap,因为要在 ApplicationContext 之前加载)
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
// 使用 @RefreshScope 让 Bean 感知配置变更
@RestController
@RefreshScope // 配置变更后,这个 Bean 会被重新创建
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
// 手写 HTTP 请求,痛苦不堪
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 动态代理

  1. 扫描 @FeignClient 注解:Spring 启动时,FeignClientsRegistrar 扫描所有 @FeignClient 接口,注册为 FeignClientFactoryBean

  2. 生成动态代理:调用接口方法时,实际走的是 FeignInvocationHandler.invoke(),它把方法调用翻译成 HTTP 请求

  3. 集成负载均衡:代理中集成了 Spring Cloud LoadBalancer,user-service 这个服务名会被解析成实际 IP:Port

  4. 发送 HTTP 请求:底层默认用 HttpURLConnection,生产环境推荐换成 Apache HttpClient 或 OkHttp

1
方法调用 → FeignInvocationHandler → MethodHandler → LoadBalancer(服务名→IP) → HttpClient → HTTP 请求
1
2
3
4
5
6
# 切换底层 HTTP 客户端为 Apache HttpClient
feign:
httpclient:
enabled: true
max-connections: 200
max-connections-per-route: 50
1
2
3
4
5
<!-- pom.xml 引入依赖 -->
<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 # 连接超时 2s
read-timeout: 10000 # 读取超时 10s
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() {
// 参数:初始间隔100ms,最大间隔1s,最大重试次数3次
return new Retryer.Default(100, 1000, 3);
}
}

重试的坑:

  1. POST 接口开了重试 = 重复下单,生产事故
  2. 重试 + 超时叠加:3次重试 × 10s = 最长等 30s,调用方早超时了
  3. 建议:GET 幂等接口才开重试,POST/PUT/DELETE 一律不重试
1
2
3
4
5
6
7
8
9
// 好的实践:用 @FeignClient 的 configuration 参数隔离配置
@FeignClient(
name = "inventory-service",
configuration = InventoryFeignConfig.class // 仅对这个服务生效
)
public interface InventoryClient {
@GetMapping("/api/inventory/{skuId}") // GET 幂等,可重试
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
// 1. 轮询策略(Round Robin)—— 默认
// 实例列表:[A, B, C]
// 请求1→A, 请求2→B, 请求3→C, 请求4→A, ...

// 2. 随机策略(Random)
@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
// 实现权重路由:metadata 里配置 weight,权重高的多分流量
public class WeightedLoadBalancer implements ReactorServiceInstanceLoadBalancer {

@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
return serviceInstanceListSupplierProvider.getIfAvailable()
.get()
.next()
.map(instances -> {
// 根据实例 metadata 中的 weight 字段加权随机选择
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 个。

解决:超出阈值的请求直接拒绝或排队等待,保护系统不被压垮。

1
限流 = 保护自己,主动拒绝超额请求

服务熔断(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);
}

// blockHandler:BlockException 触发(限流、熔断)
public Order blockHandler(Long id, BlockException e) {
return Order.empty(); // 返回空对象
}

// fallback:业务异常触发(NPE、自定义异常等)
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); // 按 QPS 限流
rule.setCount(100); // 每秒最多 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) // 错误率超过 50%
.setStatIntervalMs(10000) // 统计时间窗口 10s
.setMinRequestAmount(10) // 最少 10 个请求才触发熔断
.setTimeWindow(5); // 熔断持续 5s

热点规则(ParamFlowRule)

针对特定参数值的限流,比如某个商品 ID 被刷爆了。

1
2
3
4
5
6
7
8
// 对 getOrder 接口的第 0 个参数(orderId)限流
ParamFlowRule rule = new ParamFlowRule("getOrder")
.setParamIdx(0) // 第 0 个参数
.setCount(10) // 每秒最多 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
// Sentinel 核心数据结构
public abstract class LeapArray<T> {
protected int windowLengthInMs; // 每个 Bucket 的时长
protected int sampleCount; // Bucket 数量
protected int intervalInMs; // 总窗口时长

// 根据当前时间获取对应的 Bucket
public WindowWrap<T> currentWindow(long timeMillis) {
// 计算当前时间对应的 Bucket 索引
int idx = calculateTimeIdx(timeMillis);
// 检查 Bucket 是否需要重置(时间已经过了一个轮回)
// ...
}
}

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
# Sentinel 接入配置
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080 # Sentinel 控制台地址
port: 8719 # 本地 Sentinel 通信端口
datasource:
nacos:
nacos:
server-addr: localhost:8848
data-id: sentinel-flow-rules
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow # 流控规则从 Nacos 动态加载

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 # lb:// 表示使用负载均衡
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 # 100% 采样(生产环境建议 0.1)
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
1
2
3
// 日志自动打印 TraceId
// 配置 logback 格式包含 %X{traceId} %X{spanId}
// 2026-09-05 10:00:00 [abc123,001] INFO OrderService - 创建订单

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;

-- 定时任务:扫描 PENDING 消息,发送到 MQ,成功后更新为 SENT
-- 库存服务:消费 MQ,处理库存扣减(幂等处理,消费过的消息不重复处理)

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); // 积分服务
// 任何一个抛异常,Seata 自动回滚所有分支事务
}

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 {
// 等待 3s,持有锁最多 10s(防死锁,会自动续期)
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
// 第一步:获取 Token
@GetMapping("/token")
public String getToken() {
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("idempotent:" + token, "1", 5, TimeUnit.MINUTES);
return token;
}

// 第二步:提交时验证并删除 Token(Lua 脚本保证原子性)
@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);

-- 插入时 trade_no 重复会抛 DuplicateKeyException,捕获即可
INSERT INTO orders (trade_no, ...) VALUES (?, ...)
ON DUPLICATE KEY UPDATE updated_at = NOW(); -- 或直接 ignore

方案三:Redis SETNX(消息消费幂等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// MQ 消息消费幂等处理
@RabbitListener(queues = "order.created")
public void onMessage(OrderMessage message) {
String key = "consumed:" + message.getMsgId();

// SETNX:只有 key 不存在时才设置成功
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
// 第一步:Gateway 解析 Token,把用户信息写入请求头
// (见 Q15 Gateway 自定义过滤器的例子)
.header("X-User-Id", userId)
.header("X-User-Roles", roles)

// 第二步:各微服务提取请求头,存入 ThreadLocal
@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(); // 必须清理,防止内存泄漏!
}
}

// ThreadLocal 工具类
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
// 第三步:Feign 调用其他服务时,把用户信息传递下去
@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 形式)。问题:

  1. 业务代码与基础设施耦合
  2. 多语言服务(Go、Python)要各自实现一套
  3. 升级 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
// 方式一:API 聚合(最常用)
// 订单详情页需要订单信息 + 用户名 + 商品名
public OrderDetailVO getOrderDetail(Long orderId) {
Order order = orderRepository.findById(orderId);
User user = userClient.getUserById(order.getUserId()); // Feign 调用
Product product = productClient.getProduct(order.getProductId());

return OrderDetailVO.of(order, user, product);
}

// 方式二:数据冗余(异步同步)
// 订单表冗余用户名(避免每次查询)
// 用户改名时,通过 MQ 异步更新订单表的 user_name 字段

// 方式三:CQRS(查询专用视图)
// 用 Elasticsearch 或 MongoDB 维护一个聚合视图,专门用于复杂查询
// 写:各服务各自写自己的 DB,通过 MQ/CDC 同步到聚合视图
// 读:直接查聚合视图

分库后的主键问题

不能再用数据库自增 ID,否则不同库的 ID 会重复。需要分布式 ID 方案:

1
2
3
// 推荐:雪花算法(Snowflake)
// 64 bit = 1bit符号位 + 41bit时间戳 + 10bit机器ID + 12bit序列号
// 每毫秒每台机器可生成 4096 个唯一 ID,时间有序,不依赖网络

Q25. API 版本控制的几种方案(URL路径、请求头、参数)

接口版本控制是微服务演进的必备能力,新旧客户端需要同时支持。

方案一:URL 路径版本(最直观,推荐)

1
2
3
4
5
6
7
@RestController
@RequestMapping("/api/v1/orders") // v1
public class OrderControllerV1 { ... }

@RestController
@RequestMapping("/api/v2/orders") // v2(新增字段,改变响应结构)
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://api.example.com/orders

方案三: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://api.example.com/orders

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
# 新版本服务注册时,在 metadata 里打标签
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
// Gateway 自定义负载均衡策略
@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
// Gateway 过滤器:按用户 ID 尾号决定是否进入灰度
@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; // 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 # Redis 健康检查
nacos:
enabled: true # Nacos 健康检查
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
# K8s deployment.yaml
livenessProbe:
httpGet:
path: /actuator/health/liveness # Spring Boot 3.x 内置
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
# 密码用 ENC() 包裹,明文只有加密方知道
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
// 自定义 Nacos 配置解密处理器
@Component
public class NacosKmsDecryptProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment env,
SpringApplication app) {
// 从环境变量获取 KMS 密钥 ID
String kmsKeyId = env.getProperty("kms.key-id");
// 调用 KMS 接口解密配置中的密文
// ...
}
}

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
<!-- logback-spring.xml:把 TraceId 写入日志 -->
<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 # 最多等 3 秒,不无限等待

第二层:限流(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)

1
2
# Nacos 中动态配置熔断规则
# 错误率超过 50% → 熔断 10s → 半开探测 → 自动恢复

第四层:服务降级(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; // 返回 -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 倍。祝面试顺利!


Spring Cloud & 微服务面试八股文 30 道|深度详解版(傻子都能看懂)
https://whyalwaysme.lol/2026/09/05/2026-09-05-spring-cloud-interview-deep/
作者
Cassiur
发布于
2026年9月5日
许可协议