RabbitMQ:灵活路由和高可靠的消息队列

目录

1、介绍

2、核心概念

3、工作模式

4、核心价值

5、核心功能与特性

[5.1 灵活的消息路由](#5.1 灵活的消息路由)

[5.2 消息可靠性](#5.2 消息可靠性)

[5.3 高可用与集群](#5.3 高可用与集群)

[5.4 灵活的消息控制](#5.4 灵活的消息控制)

[5.5 丰富的插件生态](#5.5 丰富的插件生态)

6、优缺点

7、典型使用场景

8、主要工作模式代码示例

[8.1 简单模式](#8.1 简单模式)

[8.2 工作队列模式](#8.2 工作队列模式)

[8.3 发布/订阅模式](#8.3 发布/订阅模式)

[8.4 路由模式](#8.4 路由模式)


1、介绍

RabbitMQ 是一个开源的消息代理软件,通常被称为消息队列消息中间件 。它实现了 AMQP 高级消息队列协议标准,并提供了多种其他协议(如 MQTT、STOMP)的支持。

可以将它想象成一个高度可靠、专业的邮局

  • 应用程序(生产者) 就像寄信人,把写好的信(消息)交给邮局。

  • RabbitMQ(邮局) 负责接收、存储、路由和分发这些信件。

  • 应用程序(消费者) 就像收信人,从邮局取走自己的信件。

核心思想:生产者 发送消息到 交换机 ,交换机根据规则将消息路由到一个或多个 队列消费者 从队列中获取消息进行处理。生产者和消费者解耦。

RabbitMQ 本身是用 Erlang 编写的,原生实现了 AMQP 0-9-1。它的强大之处在于其插件架构。通过启用相应的插件,RabbitMQ 可以:

  • 成为一个 AMQP 经纪商(这是它的本职工作)。

  • 成为一个 MQTT 经纪商(通过 rabbitmq_mqtt 插件)。

  • 成为一个 STOMP 经纪商(通过 rabbitmq_stomp 插件)。

特性 AMQP MQTT STOMP
核心定位 企业级、可靠、功能丰富 物联网、轻量、高效 Web、简单、可互操作
协议格式 二进制 二进制 文本
消息模型 灵活的路由 (Exchange) 主题(Topic)的发布/订阅 目的地(Destination)的发送/订阅
可靠性 非常强(确认、持久化、事务) 可配置(3种 QoS) 一般(有确认机制,但不如AMQP丰富)
开销/性能 中等 极低 较高(文本协议,头信息大)
典型场景 金融、电商、企业集成 物联网、移动推送 Web应用、脚本语言、快速原型

2、核心概念

  • Producer(生产者)

    • 角色:发送消息的应用程序。

    • 工作 :创建一个消息,并将其发布(Push)到 RabbitMQ 的一个 Exchange 上。

  • Consumer(消费者)

    • 角色:接收消息的应用程序。

    • 工作:订阅一个队列,当有消息到达时,RabbitMQ 会将消息推送(Push)给它,或者它主动从队列中拉取(Pull)消息。

  • Message(消息)

    • 生产者发送的数据。它不仅仅包含数据(Payload),还包含一些附加属性(Headers/Properties),如:

      • content_type: 消息体的类型(如 JSON)。

      • delivery_mode: 是否持久化(2 表示持久化)。

      • reply_to: 用于指定回复队列。

      • correlation_id: 用于将 RPC 响应与请求关联起来。

  • Queue(队列)

    • 本质是一个存储在 RabbitMQ 内部的消息缓冲区。消息最终会被送到队列中,等待消费者取走。

    • 多个消费者可以订阅同一个队列 。默认情况下,RabbitMQ 会使用轮询的方式将队列中的消息分发给各个消费者。

    • 队列可以配置属性,如是否持久化(durable)、是否自动删除(auto_delete)等。

  • Exchange(交换机)

    • 这是 RabbitMQ 最核心、最强大的部分。生产者从不直接发送消息到队列,而是发送到交换机

    • 交换机的职责是:根据消息的路由键(Routing Key) 和预先定义的绑定(Binding) 规则,将消息路由到一个或多个队列中。

    • 如果消息没有匹配任何队列怎么办? 消息会被丢弃(或者返回给生产者,取决于配置)。

  • Binding(绑定)

    • 连接 Exchange 和 Queue 的"桥梁"。它告诉 Exchange:"请把带有某种 Routing Key 的消息发送到这个队列来。"
  • Routing Key(路由键)

    • 由生产者设置的一个消息属性,是一个字符串。Exchange 根据这个键来决定如何路由消息。
  • Virtual Host(虚拟主机)

    • 类似于 Web 服务器中的虚拟主机。它提供了一个逻辑上的隔离环境,拥有自己的队列、交换机和绑定。不同的项目或团队可以使用不同的 vhost,从而实现多租户隔离。
  • Connection(连接)Channel(信道)

    • Connection: 生产者/消费者与 Broker 之间的 TCP 连接。建立连接开销较大。

    • Channel: 在 Connection 内部建立的虚拟连接。几乎所有操作都在 Channel 中进行。使用 Channel 可以避免频繁创建和销毁 TCP 连接带来的巨大开销,并实现多路复用。

工作流程总结:
Producer -> Message -> Exchange (根据 Routing Key 和 Binding) -> Queue -> Consumer

3、工作模式

RabbitMQ 的工作模式主要围绕 生产者发送消息交换机 ,交换机根据特定规则将消息路由到 队列 ,最后由 消费者 从队列中获取并处理消息这一核心流程展开。下面这个表格汇总了其主要的几种工作模式及其核心特征:

工作模式 核心角色 交换机类型 路由/分发机制 典型应用场景
简单模式 (Simple) 生产者、队列、消费者 (默认交换机) 生产者直接发消息到指定队列,单个消费者消费。 一对一任务处理,如邮件单发、日志收集。
工作队列模式 (Work Queue) 生产者、队列、多个消费者 (默认交换机) 一个队列被多个消费者竞争监听,每条消息只被一个消费者获取。 任务分发与负载均衡,如抢红包、订单处理。
发布/订阅模式 (Publish/Subscribe) 生产者、交换机、多个队列、消费者 fanout 交换机将消息广播到所有绑定它的队列,每个队列的消费者都能收到同一消息。 广播通知,如邮件群发、广告推送、实时消息系统。
路由模式 (Routing) 生产者、交换机、队列、消费者 direct 生产者发送消息时指定路由键 ,交换机将消息精确路由到绑定相同路由键的队列。 选择性消费,如错误日志按级别路由、特定消息通知。
主题模式 (Topic) 生产者、交换机、队列、消费者 topic 使用通配符 匹配路由键,*匹配一个词,#匹配零个或多个词,实现灵活的消息路由。 灵活路由与多属性过滤,如物流分拣、日志按模块和级别分发。
RPC模式 (Remote Procedure Call) 客户端、服务器、回调队列 (通常使用默认交换机) 模拟远程过程调用,客户端发送请求消息并等待服务器返回响应消息。 需要同步获取结果的远程调用,如订单支付。

一个典型的消息从生产到消费的流程如下:

  1. 建立连接:生产者或消费者首先需要与 RabbitMQ Broker 建立一个 TCP 连接。

  2. 获取信道 :在连接之上,会创建一个信道,大部分实际操作都在信道中进行。

  3. 声明交换机与队列:生产者或消费者需要确保消息的目的地存在。这包括:

    • 声明交换机 :指定其类型(如 fanout, direct, topic)和属性。

    • 声明队列:并设置相关属性(如是否持久化)。

    • 建立绑定 :定义交换机和队列之间的路由规则,即 绑定 ,通常需要指定一个 绑定键

  4. 发布消息 :生产者将消息发送到指定的交换机 ,并提供一个路由键 。交换机根据自身的类型绑定规则,将消息路由到一个或多个队列中。

  5. 消费消息:消费者通过以下方式从队列中获取消息:

    • 推模式:RabbitMQ 主动将消息推送给消费者。

    • 拉模式:消费者主动从队列中拉取消息。

  6. 消息确认 :为确保消息被可靠处理,消费者在处理完消息后,需要向 RabbitMQ 发送一个确认,RabbitMQ 才会将消息从队列中移除。如果消费者处理失败,RabbitMQ 可以重新投递消息。

4、核心价值

  • 应用解耦

    • 场景: 系统A需要将数据发送给系统B和系统C。如果直接通过API调用,系统A需要知道B和C的地址,并且需要处理B或C宕机、超时等问题,耦合性极高。

    • RabbitMQ方案: 系统A只需将消息发送到RabbitMQ,系统B和C各自从RabbitMQ获取消息。系统A完全不关心谁消费消息,也不关心消费者是否在线。系统的独立性、可扩展性和容错能力大大增强。

  • 异步通信

    • 场景: 用户注册后,需要发送邮件和短信。如果同步执行,用户需要等待所有操作完成,响应时间很长。

    • RabbitMQ方案: 注册逻辑只需将"发送邮件"、"发送短信"的消息放入RabbitMQ,然后立即返回给用户。邮件和短信服务在后台异步处理。系统响应速度得到质的提升。

  • 流量削峰

    • 场景: 秒杀活动开始时,瞬时流量极高,远超后端数据库的处理能力,容易导致服务崩溃。

    • RabbitMQ方案: 将所有请求转换成消息,存入RabbitMQ队列。后端服务按照自己的能力,匀速地从队列中取出消息进行处理。消息队列像一座"水库",将瞬时的洪峰流量变得平滑,保护了后端系统的稳定性。

5、核心功能与特性

5.1 灵活的消息路由

通过不同类型的 Exchange(交换机),实现了极其灵活的路由策略:

  • Direct Exchange(直连) : 精确匹配。消息的 Routing Key 必须与 Binding Key 完全一致,才会路由到队列。常用于点对点通信。

  • Fanout Exchange(扇出) : 广播模式。它将消息路由到所有绑定到它的队列上,忽略 Routing Key。适用于发布/订阅场景。

  • Topic Exchange(主题) : 主题模式。通过通配符(* 匹配一个词, # 匹配零个或多个词)进行模糊匹配。非常灵活,适用于根据消息的多个属性进行路由的场景。

  • Headers Exchange(头交换机): 通过消息头(Headers)而不是 Routing Key 进行匹配,使用较少。

5.2 消息可靠性

  • 生产者确认(Publisher Confirm): 生产者可以确认消息是否成功送达 Broker。

    • 事务:性能差,不推荐。

    • Publisher Confirm :异步回调机制。生产者开启Confirm模式,消息被Broker持久化到磁盘后,会回送一个 ack 给生产者。这是生产环境保证消息可靠投递的推荐方式。

  • 消息持久化: 可以将 Exchange、Queue 和 Message 本身设置为持久化的,即使 RabbitMQ 服务重启,消息也不会丢失。

    • 队列持久化 :声明队列时设置 durable=true

    • 消息持久化 :发送消息时,将 delivery_mode 属性设置为 2

    • 注意

      • 仅仅设置队列持久化,重启后队列还在,但消息会丢失。必须两者都设置。

      • 仅设置持久化不能100%保证消息不丢失(例如RabbitMQ刚收到消息,还未写入磁盘时就挂了)。为了更强保证,需要使用 Publisher Confirm 机制。

  • 消费者确认(Consumer Acknowledgement): 消费者在成功处理消息后,向 RabbitMQ 发送一个 Ack(手动确认)。RabbitMQ 只有在收到 Ack 后,才会将消息从队列中删除。如果消费者在处理过程中失败(连接断开、未发送Ack),RabbitMQ 会将消息重新投递给另一个消费者。这保证了消息至少被消费一次。

5.3 高可用与集群

  • RabbitMQ 支持集群模式,可以将多个 Broker 组成一个逻辑 Broker,实现负载分担。

  • 镜像队列: 这是RabbitMQ实现高可用的主要方式。将队列镜像到集群中的多个节点上,主节点宕机后,镜像节点会自动提升为主节点,保证服务不中断。

  • 客户端需要连接集群中的多个节点,以便在主节点失效时能切换到其他节点。

5.4 灵活的消息控制

  • TTL(Time-To-Live): 可以为队列或单条消息设置过期时间,超时未消费,消息会自动过期并被丢弃(或成为死信)。

    • 消息TTL: 单条消息可以设置过期时间。

    • 队列TTL: 整个队列的消息统一设置过期时间。

    • 过期消息会变成死信(如果队列设置了死信交换机)。

  • 死信队列(Dead Letter Exchange, DLX): 一个消息在以下情况下会变成"死信",可以专门设置一个交换机和一个队列来接收这些"死信",用于后续分析、重试或人工干预。

    • 被消费者拒绝且不重新入队。

    • 消息在队列中存活时间超时。

    • 队列长度达到上限。

  • 消费端限流:

    • 当消费者处理能力有限时,为避免被压垮,可以设置 prefetchCount

    • channel.basicQos(1): 告诉Broker,在消费者未确认前,最多只推送1条消息给它。这是"公平调度"的基础。

5.5 丰富的插件生态

可以通过插件扩展功能,例如:

  • 管理界面: 提供 Web UI,便于监控和管理 RabbitMQ。

  • STOMP、MQTT 协议支持: 使其能接入更多类型的客户端。

  • Shovel / Federation: 用于在 Broker 之间迁移或同步数据。

6、优缺点

优点:

  • 成熟稳定: 经历多年生产环境考验,社区活跃,文档丰富。

  • 强大的路由功能: 多种 Exchange 类型提供了极高的路由灵活性。

  • 支持多种协议: 原生支持 AMQP,并通过插件支持 MQTT、STOMP 等。

  • 可靠性与可用性: 通过持久化、确认机制和集群/镜像队列,提供了良好的可靠性保证。

  • 管理界面友好: 自带的管理插件非常实用,便于监控和运维。

  • 跨平台: 基于 Erlang 开发,具有良好的跨平台性。

缺点:

  • 性能瓶颈: 相比于 Kafka、RocketMQ 等,RabbitMQ 的吞吐量相对较低,在超大规模(如日万亿级消息)场景下可能成为瓶颈。

  • Erlang 技术栈: 对于主要使用 Java/Python/Go 的团队来说,Erlang 的学习曲线可能带来二次开发和深度定制的困难。

  • 消息堆积能力有限: 虽然支持持久化,但其设计初衷是快速投递,当消息大量堆积时,性能会下降,而 Kafka 是为海量消息存储设计的。

  • 集群扩展稍复杂: 虽然支持集群,但其线性扩展能力不如 Kafka 那样简单直接。

三款消息队列对比:

  • 选择 RabbitMQ :如果项目是企业级应用、业务系统集成 ,需要高可靠性灵活的路由(如将不同类型的消息精准分发给不同服务),对吞吐量的要求不是极限高度,那么RabbitMQ是绝佳选择。它的管理界面和丰富的文档也能大大降低运维和学习成本。

  • 选择 RocketMQ :如果场景涉及金融交易、电商订单 等,需要严格的分布式事务 (如下单扣库存)和顺序消息保障,同时要求较高的吞吐量,RocketMQ是更针对性的解决方案。

  • 选择 Kafka :如果核心业务是日志收集、网站活动追踪、IoT数据采集实时流处理 ,需要处理海量数据 并追求极限吞吐量,Kafka是毋庸置疑的王者。

7、典型使用场景

  • 异步任务处理

    • 示例: 用户上传视频后,需要转码、生成缩略图、内容分析等耗时操作。前端立即返回"上传成功",后端通过 RabbitMQ 将转码任务分发给多个 worker 异步执行。
  • 应用解耦与微服务通信

    • 示例: 在电商微服务架构中,"订单服务"创建订单后,发送一条消息到 RabbitMQ。"库存服务"、"用户积分服务"、"物流服务"分别订阅该消息,并执行各自的业务逻辑。服务之间不直接调用。
  • 延迟任务/调度

    • 示例: 利用 TTL + 死信队列实现"30分钟后检查订单是否支付,未支付则自动取消"的功能。下单时发送一条 TTL 为30分钟的消息,消息过期后进入死信队列,由专门的消费者处理超时订单。
  • 数据同步/事件广播

    • 示例: 使用 Fanout Exchange。当主业务数据发生变化时,发布一条消息。搜索引擎、缓存、数据分析等系统都可以接收到该消息,并同步更新自己的数据。
  • 流量削峰

    • 示例: 电商秒杀系统。将瞬间涌入的下单请求全部放入 RabbitMQ 队列,订单处理服务根据自身最大处理能力,从队列中匀速拉取请求进行处理,避免系统被冲垮。

8、主要工作模式代码示例

首先需在 Maven 项目中引入依赖:

XML 复制代码
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.20.0</version> <!-- 请检查并使用最新版本 -->
</dependency>

8.1 简单模式

这是最基础的模式,生产者直接发送消息到队列。

  • 生产者代码
java 复制代码
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;

public class Producer {
    // 定义队列名称
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        // 1. 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost"); // RabbitMQ服务器地址
        factory.setUsername("guest"); // 用户名
        factory.setPassword("guest"); // 密码
        factory.setVirtualHost("/");  // 虚拟主机

        // 2. 创建连接和通道
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            
            // 3. 声明队列
            channel.queueDeclare(QUEUE_NAME, 
                false,   // durable: 是否持久化
                false,   // exclusive: 是否独占
                false,   // autoDelete: 是否自动删除
                null);   // arguments: 其他参数

            // 4. 要发送的消息
            String message = "Hello World!";
            
            // 5. 发布消息 (使用默认的AMQP默认交换机 "")
            channel.basicPublish("", 
                QUEUE_NAME, 
                null, 
                message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}
  • 消费者代码
java 复制代码
import com.rabbitmq.client.*;
import java.io.IOException;

public class Consumer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        // 声明队列,确保它存在
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

        // 回调函数,处理接收到的消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        
        // 开始消费消息
        channel.basicConsume(QUEUE_NAME, 
            true,       // autoAck: 是否自动确认
            deliverCallback, 
            consumerTag -> { });
    }
}

8.2 工作队列模式

用于在多个消费者之间分配耗时的任务。

  • 生产者代码:与简单模式类似,但通常发送更复杂的任务消息。

  • 消费者代码

java 复制代码
// ... (连接和通道创建与之前类似)
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); //  durable 设置为 true,使队列持久化
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

// 设置预取计数为1,实现公平调度
channel.basicQos(1);

DeliverCallback deliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    System.out.println(" [x] Received '" + message + "'");
    try {
        // 模拟耗时任务
        doWork(message);
    } finally {
        System.out.println(" [x] Done");
        // 手动发送确认消息,确保消息被正确处理
        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
    }
};
// 关闭自动确认 (autoAck false),处理完任务后手动确认
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, consumerTag -> { });

8.3 发布/订阅模式

使用 Fanout 交换机,将一条消息广播给多个消费者。

  • 生产者代码
java 复制代码
// ... (创建连接和通道)
// 1. 声明一个Fanout类型的交换机
String EXCHANGE_NAME = "logs";
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

// 2. 发送消息到交换机,而不是特定队列。routingKey 对于 fanout 交换机会被忽略。
String message = "This is a broadcast message!";
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
  • 消费者代码
java 复制代码
// ... (创建连接和通道)
// 1. 声明一个Fanout类型的交换机
String EXCHANGE_NAME = "logs";
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

// 2. 声明一个临时队列(唯一、连接关闭时自动删除),并获取其名称
String queueName = channel.queueDeclare().getQueue();

// 3. 将临时队列绑定到交换机上
channel.queueBind(queueName, EXCHANGE_NAME, ""); // routingKey 为空

System.out.println(" [*] Waiting for broadcast messages. To exit press CTRL+C");

DeliverCallback deliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });

8.4 路由模式

使用 Direct 交换机,根据路由键将消息发送到绑定键完全匹配的队列。

  • 生产者代码
java 复制代码
// ... (创建连接和通道)
// 1. 声明一个Direct类型的交换机
String EXCHANGE_NAME = "direct_logs";
channel.exchangeDeclare(EXCHANGE_NAME, "direct");

// 2. 设置路由键 (例如: error, warning, info)
String severity = "error";
String message = "An error occurred!";

// 3. 发布消息到交换机,并指定路由键
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes());
System.out.println(" [x] Sent '" + severity + "':'" + message + "'");
  • 消费者代码
java 复制代码
// ... (创建连接和通道)
String EXCHANGE_NAME = "direct_logs";
channel.exchangeDeclare(EXCHANGE_NAME, "direct");

String queueName = channel.queueDeclare().getQueue();

// 指定这个消费者关心的日志级别(绑定键)
String[] severities = {"error", "warning"}; // 可以绑定多个键
for (String severity : severities) {
    channel.queueBind(queueName, EXCHANGE_NAME, severity);
}

System.out.println(" [*] Waiting for direct messages. To exit press CTRL+C");

DeliverCallback deliverCallback = (consumerTag, delivery) -> {
    String message = new String(delivery.getBody(), "UTF-8");
    String routingKey = delivery.getEnvelope().getRoutingKey();
    System.out.println(" [x] Received '" + routingKey + "':'" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
相关推荐
论迹2 小时前
【JavaEE】-- Cookie &&Session
java·java-ee
czhc11400756633 小时前
Java114 LeeCode 翻转二叉树
java
一 乐3 小时前
个人理财系统|基于java+小程序+APP的个人理财系统设计与实现(源码+数据库+文档)
java·前端·数据库·vue.js·后端·小程序
盖世英雄酱581363 小时前
java深度调试技术【第四五章:多线程和幽灵代码】
java·后端
稚辉君.MCA_P8_Java3 小时前
深入理解 TCP;场景复现,掌握鲜为人知的细节
java·linux·网络·tcp/ip·kubernetes
熊猫比分站3 小时前
[特殊字符] Java/Vue 实现体育比分直播系统,支持多端实时更新
java·开发语言·vue.js
lang201509284 小时前
深入掌握 Maven Settings:从配置到实战
java·maven
scx_link4 小时前
修改JetBrains产品(IntelliJ IDEA 、PyCharm等软件)的默认插件和日志的存储位置
java·pycharm·intellij-idea
BUG?不,是彩蛋!4 小时前
Maven-Java 项目到底解决了什么痛点?
java·servlet·maven