中间件-补充md

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 服务注册与发现流程

面试标准回答:

  1. 注册流程:服务启动时向 Nacos Server 发送 HTTP POST 请求,携带 IP、端口、服务名等信息。Nacos 将实例信息存储在内存中(临时实例)或内存 + MySQL(持久实例)。注册成功后,客户端每 5 秒发送一次心跳维持注册状态。
  2. 发现流程:消费者启动时从 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 上就行了。这就是灵活路由的威力。
相关推荐
程序员梅雨21 小时前
ZooKeeper面经总结
java·大数据·linux·分布式·后端·zookeeper·中间件
Wang's Blog3 天前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP 消息转换器
java·中间件·java-rabbitmq
Wang's Blog3 天前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP WorkQueue 工作队列模型
java·中间件·java-rabbitmq
snhna3 天前
Agent 上云部署实战(上):从 GitHub 到 Linux,用 Dify 跑通中间件、域名和 HTTPS
linux·中间件
小白学大数据4 天前
拼多多反爬对抗实战:Scrapy 中间件化采集架构解析
开发语言·scrapy·中间件·架构
Wang's Blog4 天前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP 的 DirectExchange 路由模式
java·中间件·java-rabbitmq
七夜zippoe4 天前
Agent 中间件架构:钩子链、插件注册与横切治理
ai·中间件·架构·agent·钩子链
谢亮_vipxieliang4 天前
Go Gin 中间件与参数验证:从入门到实战
中间件·golang·gin
code_slave(码畜)4 天前
微服务架构落地:消息队列架构设计(下篇)——消息堆积、死信治理与集群监控告警实战
spring boot·spring cloud·微服务·云原生·中间件·架构