Spring Cloud 微服务面试笔记
一、Spring Cloud 核心组件
| 组件 | 作用 | 替代品 |
|---|---|---|
| Eureka / Nacos | 服务注册与发现 | Consul、Zookeeper |
| Ribbon / LoadBalancer | 客户端负载均衡 | --- |
| Feign / OpenFeign | 声明式 HTTP 客户端 | --- |
| Hystrix / Resilience4j | 熔断、降级、限流 | Sentinel |
| Gateway | API 网关 | Kong、APISIX |
| Config / Nacos | 分布式配置中心 | Apollo |
| Sleuth + Zipkin | 分布式链路追踪 | SkyWalking、Jaeger |
| Bus | 消息总线(配置刷新广播) | --- |
| Stream | 消息驱动微服务 | --- |
现状说明:
- 客户端负载均衡 →
LoadBalancer,已成为官方推荐的默认实现。 - 声明式调用 →
OpenFeign。 - 熔断、降级、限流 →
Sentinel。 - 分布式链路追踪 →
Zipkin/SkyWalking。
二、Eureka vs Nacos
| 对比维度 | Eureka | Nacos |
|---|---|---|
| CAP 理论 | AP(高可用,容忍数据不一致) | 同时支持 AP 和 CP(可切换) |
| 健康检查 | 客户端心跳(默认 30s) | 客户端心跳 + 服务端主动探测 |
| 配置中心 | 不支持(需配合 Spring Cloud Config) | 内置配置中心 |
| 数据一致性 | 对等复制(Peer to Peer) | 支持 Raft 协议(CP 模式) |
| 生态 | Netflix 开源,已进入维护模式 | 阿里开源,活跃迭代,支持 K8s |
| 性能 | 大规模集群下可能出现复制延迟 | 性能更好,支持百万级实例 |
Nacos 的 AP/CP 切换通过
nacos.core.protocol.raft配置实现。临时实例走 AP(Distro 协议),持久化实例走 CP(Raft 协议)。
注册中心 Nacos 宕机后会发生什么?
已有服务间调用不受影响(使用本地缓存),但新服务无法注册、服务上下线无法感知、配置无法更新。集群模式下单节点故障会自动切换到其他节点。
- 场景 1:已有服务实例之间的调用 ✅
- 客户端本地有缓存的服务列表 → 继续正常调用。
- 调用不受任何影响,直到缓存过期。
- 这是 AP 架构的核心优势。
- 场景 2:新服务实例启动 ❌
- 无法注册到 Nacos → 注册失败。
- 其他服务无法通过 Nacos 发现它。
- 但服务本身可以正常启动(取决于是否开启失败容忍)。
- 场景 3:服务实例下线/宕机 ❌
- Nacos 无法感知 → 不会从列表中剔除。
- 其他客户端缓存中仍有该实例。
- 调用方会调用到已宕机的实例 → 触发熔断降级。
- 场景 4:配置中心功能 ❌
- 无法推送新配置。
- 客户端本地缓存的配置仍然有效。
- 应用继续用旧配置运行。
Nacos Client 本地缓存机制
Nacos Client 维护了一份本地内存缓存,这是 Nacos 挂了还能继续调用的核心原因:
java
private ConcurrentHashMap<String, List<Instance>> serviceInfoMap;
缓存更新时机:
- 启动时:从 Nacos Server 全量拉取 → 写入内存 + 磁盘。
- 运行时:长轮询接收变更 → 更新内存 + 磁盘。
- Nacos 挂了:使用内存缓存 → 内存丢了读磁盘缓存。
三、Nacos 服务注册与发现流程
面试标准回答:
- 注册流程:服务启动时向 Nacos Server 发送 HTTP POST 请求,携带 IP、端口、服务名等信息。Nacos 将实例信息存储在内存中(临时实例)或内存 + MySQL(持久实例)。注册成功后,客户端每 5 秒发送一次心跳维持注册状态。
- 发现流程:消费者启动时从 Nacos 全量拉取服务列表,缓存到本地内存和磁盘。然后持续监听服务变更,有变更时 Nacos 立即推送(主动推送给所有订阅该服务的客户端,实时性可达毫秒级)。实际调用时直接从本地缓存选择实例,减少网络 IO 开销。
四、LoadBalancer 负载均衡策略
内置策略:轮询策略 、随机策略。
当内置的轮询或随机策略无法满足业务需求(如基于用户 ID 哈希、灰度发布、同机房优先、开发环境隔离等)时,可以通过实现 ReactorServiceInstanceLoadBalancer 接口来自定义算法。
五、Feign 与 OpenFeign 区别
Feign 是 Netflix 开源的原生声明式 HTTP 客户端,2019 年后已停止新功能迭代;OpenFeign 是 Spring Cloud 社区在其基础上 fork 演进的增强版本,深度适配 Spring 生态,是当前微服务项目的主流选择。
- Feign :仅支持原生自定义注解,如
@RequestLine、@Param,语法与 Spring 开发习惯完全割裂,需要重新学习 API 规范。 - OpenFeign :原生兼容 Spring MVC 全套注解,直接使用
@GetMapping、@PostMapping、@PathVariable等日常开发常用注解,无需额外记忆新语法,大幅降低学习成本。
六、Sentinel 熔断、降级、限流
使用 @SentinelResource 注解标记核心业务方法,实现限流与降级的解耦:
java
@RestController
public class OrderController {
@GetMapping("/create")
@SentinelResource(
value = "createOrder",
blockHandler = "handleBlock", // 限流/熔断兜底
fallback = "handleFallback" // 业务异常兜底
)
public String createOrder(@RequestParam String userId) {
// 模拟业务逻辑
if ("error".equals(userId)) {
throw new RuntimeException("Business Error");
}
return "Order Created";
}
// 限流/熔断后的处理
public String handleBlock(String userId, BlockException ex) {
return "System Busy, Please Try Later";
}
// 业务异常后的处理
public String handleFallback(String userId, Throwable ex) {
return "Service Unavailable: " + ex.getMessage();
}
}
Q2:Sentinel 的底层原理是什么?如何实现高性能统计?
- 滑动窗口算法:Sentinel 基于**滑动时间窗口(Sliding Window)**统计指标。将时间划分为多个小的 Bucket(默认 500ms),每个 Bucket 记录该时间段内的请求数、异常数、RT 等。
- 原子操作 :使用
LongAdder或AtomicInteger保证高并发下计数的线程安全,避免锁竞争。 - LeapArray:核心数据结构,负责窗口的复用和过期清理,确保内存占用稳定。
- 优势:相比固定窗口,滑动窗口解决了「临界点突变」问题,统计更平滑准确。
Q3:如何实现分布式限流?Sentinel 支持吗?
- 默认单机:Sentinel 默认是单机限流,每个实例独立统计。
- 集群模式 :Sentinel 支持集群限流,但需要部署独立的 Token Server(令牌服务器)。客户端向 Token Server 申请令牌,Token Server 全局控制配额。
- 替代方案 :如果不想部署 Token Server,可使用 Redis + Lua(如 Redisson RateLimiter)实现全局限流,或在网关层(Spring Cloud Gateway)做全局限流。
限流后用户体验优化
- 快速失败太生硬:不要直接返回 500 或空白页。
- 友好提示 :通过
blockHandler返回统一的 JSON 格式错误码(如code: 429, msg: "系统繁忙,请稍后重试")。 - 排队等待:对于秒杀等场景,使用「匀速排队」模式,让用户等待几秒后成功,而不是直接拒绝。
- 前端优化:前端按钮置灰、增加 loading 动画,防止用户重复点击。
七、Spring Cloud Gateway
TODO:补充 Gateway 路由、断言(Predicate)、过滤器(Filter)相关内容。
- 总体功能: 对请求提供路由及过滤功能.
八、RabbitMQ
核心概念
- Producer / Consumer:生产者与消费者模型。
- Queue:消息队列,存储消息。
- Exchange:交换机,决定消息如何路由到队列。
- Binding:队列与交换机的绑定关系。
- Routing Key / Binding Key:消息路由规则。
- 消息确认机制:自动 / 手动 ACK。
- 消息持久化:防止消息丢失。
消息的可靠传输
消息不可靠的情况可能是消息丢失、劫持等原因。丢失又分为三类:生产者丢失消息 、消息队列丢失消息 、消费者丢失消息。
1. 生产者丢失消息
RabbitMQ 提供 transaction 和 confirm 两种模式来确保生产者不丢消息:
- transaction 机制 :发送消息前开启事务(
channel.txSelect()),然后发送消息,若发送异常事务回滚(channel.txRollback()),发送成功则提交事务(channel.txCommit())。缺点是吞吐量下降。 - confirm 模式(主流):channel 进入 confirm 模式后,所有该信道上发布的消息都会被指派一个唯一 ID(从 1 开始)。消息投递到所有匹配队列后,RabbitMQ 发送 ACK(含消息唯一 ID),生产者据此知道消息已正确到达;若未能处理则发送 Nack,生产者可重试。
2. 发送方确认机制
配置文件:
yaml
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启发送方确认机制
配置属性三种取值:
none:禁用发送方确认机制。correlated:开启发送方确认机制。simple:开启发送方确认机制,并支持waitForConfirms()和waitForConfirmsOrDie()的串行调用。
一般使用 correlated 即可。simple 的 waitForConfirms() 是串行确认,性能较差,通常采用异步 confirm 模式。
通过调用 setConfirmCallback() 实现异步 confirm 模式感知消息发送结果:
java
/**
* 消息业务实现类
* @author 单程车票
*/
@Service
public class RabbitMQServiceImpl {
@Autowired
private RabbitTemplate rabbitTemplate;
@Override
public void sendMessage() {
// 发送消息
rabbitTemplate.convertAndSend(RabbitMQConfig.Direct_Exchange, routingKey, message);
// 设置消息确认回调方法
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* MQ 确认回调方法
* @param correlationData 消息的唯一标识
* @param ack 消息是否成功收到
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("ConfirmCallback...correlationData[" + correlationData + "]==>ack:[" + ack + "]==>cause:[" + cause + "]");
if (!ack) {
// 出错处理
// ...
}
}
});
}
}
3. 保证消息在 RabbitMQ Server 中的持久化
发送消息时设置消息持久化,并在创建交换机和队列时也保证持久化:
java
/**
* 消息队列
*/
@Bean
public Queue queue() {
// 四个参数:name(队列名)、durable(持久化)、exclusive(独占)、autoDelete(自动删除)
return new Queue(MESSAGE_QUEUE, true);
}
/**
* 直接交换机
*/
@Bean
public DirectExchange exchange() {
// 四个参数:name(交换机名)、durable(持久化)、autoDelete(自动删除)、arguments(额外参数)
return new DirectExchange(Direct_Exchange, true, false);
}
java
@Override
public void sendMessage() {
// 构造消息(将消息持久化)
Message message = MessageBuilder
.withBody("单程车票".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
// 向 MQ 发送消息(消息内容都为消息表记录的 id)
rabbitTemplate.convertAndSend(RabbitMQConfig.Direct_Exchange, routingKey, message);
}
生产者通过
setConfirmCallback()将信道设为 confirm 模式,所有消息被指派唯一标识。消息发送到 Server 并被确认后,生产者回调对应方法,从而感知消息是否正确投递。该模式是异步的,吞吐量大幅提升。
消息队列丢数据
开启磁盘持久化即可。持久化配置可与 confirm 机制配合:消息持久化磁盘后再给生产者发送 Ack 信号。若消息持久化前 RabbitMQ 阵亡,生产者收不到 Ack 会自动重发。这样即使 RabbitMQ 挂了,重启后也能恢复数据。
4. 保证消费者消费的消息不丢失
默认消费者应答机制为自动应答 :RabbitMQ 将消息推送给消费者后便从队列删除,若消费过程失败则消息丢失。解决方案是改为手动应答(Manual ACK),只有消费成功才删除消息。
配置文件:
yaml
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启发送方确认机制
publisher-returns: true # 开启消息返回
template:
mandatory: true # 消息投递失败返回客户端
listener:
simple:
acknowledge-mode: manual # 开启手动确认
通过 listener.simple.acknowledge-mode = manual 即可将消费者应答机制设置为手动应答。消费时通过 channel.basicAck() 与 channel.basicNack() 根据业务执行结果选择确认或丢弃。
java
/**
* 监听消费队列的消息
*/
@RabbitListener(queues = RabbitMQConfig.MESSAGE_QUEUE)
public void onMessage(Message message, Channel channel) {
// 获取消息索引
long index = message.getMessageProperties().getDeliveryTag();
// 解析消息
byte[] body = message.getBody();
//...
try {
// 业务处理
//...
// 业务执行成功则手动确认
channel.basicAck(index, false);
} catch (Exception e) {
log.info("出现异常:{}", e.getMessage());
try {
// 手动丢弃信息
channel.basicNack(index, false, false);
} catch (IOException ex) {
log.info("丢弃消息异常");
}
}
}
basicAck() 与 basicNack() 参数说明:
void basicAck(long deliveryTag, boolean multiple)(会抛异常):deliveryTag:该消息的 index。multiple:是否批量处理(true 表示一次性 ack 所有小于 deliveryTag 的消息)。
void basicNack(long deliveryTag, boolean multiple, boolean requeue)(会抛异常):deliveryTag:该消息的 index。multiple:是否批量处理(true 表示一次性 ack 所有小于 deliveryTag 的消息)。requeue:被拒绝的是否重新入队列(true 表示添加到队列末端;false 表示丢弃)。
四种核心交换机类型(直扇主头)
| 类型 | 核心特点 | 路由规则 | 典型场景 |
|---|---|---|---|
| Direct 直连 | 精准匹配,最基础最常用 | Routing Key 与 Binding Key 完全一致时投递 | 一对一通信、任务分发(如订单支付结果通知) |
| Fanout 扇出 | 广播模式,性能最高 | 忽略 Routing Key,将消息广播到所有绑定的队列 | 广播通知、日志同步(所有订阅者都收到系统通知) |
| Topic 主题 | 模式匹配,灵活性最高 | 支持通配符:* 匹配一个单词,# 匹配零个或多个单词,按 . 分词匹配 |
多维度灵活路由(按地区、业务类型分发消息) |
| Headers 头 | 属性匹配,基于消息头 | 不依赖 Routing Key,根据消息 Headers(键值对)匹配,x-match=all/any |
复杂属性筛选(极少用,通常被 Topic 替代) |
| Default 默认 | 特殊的 Direct | 无名(空字符串)的 Direct 交换机,新建队列自动以队列名作为 Binding Key | 简单场景,Routing Key 直接指定队列名即可投递 |
关键理解:交换机决定了消息的「路由逻辑」,是 RabbitMQ 灵活性的基石。选择哪种类型取决于业务需要一对一精准投递、一对多广播,还是基于模式的灵活订阅。
五种(六种)工作模式
| 模式 | 核心特点 | 典型场景 |
|---|---|---|
| 简单模式 Simple | 1:1 生产者 → 队列 → 消费者,最基础模型 | 简单任务处理、日志记录 |
| 工作队列 Work Queue | 1:N 生产者 → 队列 → 多个消费者,一条消息只能被一个消费者消费,谁先抢到消息谁负责消费.轮询分发实现负载均衡 | 任务分发、后台处理(图片处理、订单批量处理) |
| 发布/订阅 Publish/Subscribe | 使用 Fanout 交换机,1:N 生产者 → 交换机 → 多个队列 → 多个消费者,广播,每个消费者都可以收到相同的消息 | 系统通知、缓存失效、日志广播 |
| 路由 Routing | 使用 Direct 交换机,根据 Routing Key 精准路由到不同队列 | 按类别分发(日志按级别 error/info 分流) |
| 主题 Topics | 使用 Topic 交换机,在路由模式基础上支持通配符更灵活匹配 | 多维度消息订阅(如 order.*.created、*.error) |
| RPC | 客户端发请求消息,服务端处理后返回响应,通过 reply_to 和 correlation_id 关联 |
微服务间同步调用,需等待返回结果的场景 |
RabbitMQ 核心组件有哪些?
Producer / Consumer 、Connection (TCP 物理连接)、Channel (轻量级逻辑通道,一个连接多 channel,避免大量建连接)、Exchange (只路由不存储)、Queue (唯一存消息的组件)、Binding + RoutingKey 、Virtual Host(多租户权限隔离)。
它的核心架构由以下 6 个关键组件组成:
- Producer(生产者):就是发件人。负责产生消息并发送给 RabbitMQ。
- Consumer(消费者):就是收件人。从队列里取出消息并处理。
- Exchange(交换机):就是快递分拣中心。这是 RabbitMQ 最核心的设计!生产者发的包裹从来不会直接投进队列,而是先给交换机。交换机根据包裹上的「地址」(Routing Key)决定把它扔进哪个队列。
- Queue(队列):就是快递仓库。消息最终存放的地方,等待被取走。一个消息可以被拷贝到多个队列,但一个队列里的消息只能被消费一次。
- Binding(绑定):就是分拣规则表。它告诉交换机:「如果是发往北京的包裹,请扔进 A 队列」。它把 Exchange 和 Queue 连接起来。
- Virtual Host(虚拟主机) :就是独立租户。这就好比一台物理服务器上跑了多个 MySQL 数据库(db1, db2...)。VHost 之间是完全隔离的,互不影响,通常用于区分不同的业务线(比如
/order给订单组用,/user给用户组用)。
组件特性
| 组件 | 说明 |
|---|---|
| 交换机 (Exchange) | 只路由不存储 |
| 队列 (Queue) | 唯一存消息的组件 |
为什么要有 Exchange?直接发给 Queue 不行吗?
普通 MQ,如 ActiveMQ 早期,确实是点对点,直接发给队列。
而 RabbitMQ 的设计哲学就是解耦。生产者不需要知道「消息要发给谁」,它只需要知道「这消息是什么类型的」。
如果你的业务变了,比如「注册成功」的消息,原本只需要发邮件(Queue A),现在需要加一个发短信(Queue B)。
- 如果没有 Exchange,你得改代码,让生产者发两份。
- 有了 Exchange,生产者代码一行不用动,只需要在后台配置一下,把 Exchange 绑定到新的 Queue B 上就行了。这就是灵活路由的威力。