Java-206 RabbitMQ 发布订阅(fanout)Java 实战:推/拉模式、ACK 与绑定排错全梳理

TL;DR

  • 场景:RabbitMQ fanout 广播发布订阅,Java 生产者/消费者完整跑通,并解释推/拉消费差异
  • 结论:默认推模式(basicConsume)更适合常规实时消费;拉模式(basicGet)用于条件/批量/限速场景
  • 产出:可直接复用的 EmitLog/ReceiveLogs 代码 + rabbitmqctl 绑定验证方法 + 常见故障速查

RabbitMQ 发布订阅整体代码

消息推拉模式详解

推模式(Push)实现方式

  1. 继承 DefaultConsumer 基类
    • 这是 RabbitMQ Java 客户端提供的标准实现方式
    • 需要重写 handleDelivery 方法来处理接收到的消息
    • 示例代码:
java 复制代码
     Consumer consumer = new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag, Envelope envelope,
                                  AMQP.BasicProperties properties, byte[] body) {
             String message = new String(body, "UTF-8");
             // 处理消息逻辑
         }
     };
     channel.basicConsume(queueName, true, consumer);
  1. 使用 Spring AMQP 的 SimpleMessageListenerContainer
    • 这是 Spring 框架提供的更高级的封装
    • 支持自动声明队列、交换机和绑定
    • 支持消息转换、错误处理等高级功能
    • 配置示例:
java 复制代码
     @Bean
     public SimpleMessageListenerContainer messageListenerContainer() {
         SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
         container.setConnectionFactory(connectionFactory());
         container.setQueueNames("queueName");
         container.setMessageListener(messageListenerAdapter);
         return container;
     }

推模式的适用场景

推模式是最常用的消费模式,特别适合:

  • 实时性要求高的场景
  • 消费者处理能力稳定的情况
  • 需要持续处理消息的场景

拉模式(Pull)的必要性

在以下场景中,推模式可能不适用,需要采用拉模式:

  1. 条件性消费

    • 消费者只能在特定条件满足时才能处理消息
    • 例如:只有在系统资源充足时才能消费消息
  2. 批量处理需求

    • 需要一次性拉取多条消息进行批量处理
    • 示例代码:
java 复制代码
     GetResponse response = channel.basicGet(queueName, false);
     if (response != null) {
         // 处理消息
         channel.basicAck(response.getEnvelope().getDeliveryTag(), false);
     }
  1. 资源受限场景

    • 消费者处理能力有限,需要控制消息拉取速率
    • 避免消息堆积导致消费者崩溃
  2. 特殊业务需求

    • 需要精确控制消息拉取时机
    • 实现自定义的消息处理策略

推拉模式的选择建议

  1. 默认选择推模式

    • 实现简单
    • 性能较好
    • 适合大多数常规场景
  2. 考虑拉模式的情况

    • 需要精确控制消息消费时机
    • 处理批量消息
    • 消费者资源受限
    • 实现特殊业务逻辑
  3. 混合模式

    • 某些场景可以结合两种模式
    • 例如:大部分时间使用推模式,特定条件下切换到拉模式

EmitLog

java 复制代码
package icu.wzk.demo;
/**
 * RabbitMQ 官方教程中的 "EmitLog" 示例(Publish/Producer)。
 *
 * 目标:
 * 1) 声明一个 fanout 类型的 exchange(广播交换机)
 * 2) 把消息发布到这个 exchange
 *
 * fanout 语义:
 * - 不看 routingKey
 * - 只要队列绑定到了这个 exchange,都会收到这条消息
 */
public class EmitLog {

    /**
     * Exchange 名称(逻辑上的"消息广播站")
     * 注意:exchange 不是队列。生产者发到 exchange,消费者从队列取。
     */
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception {
        // 1) 创建连接工厂:负责配置"如何连到 RabbitMQ"
        ConnectionFactory factory = new ConnectionFactory();

        // 2) 设置 RabbitMQ Server 地址
        // 这里是本机;真实环境通常还会设置端口、用户名密码、virtual host、TLS 等
        factory.setHost("localhost");

        /**
         * 3) 建立 TCP 连接 + 创建 Channel
         *
         * - Connection:重量级、底层 TCP 连接,通常整个进程复用少量连接
         * - Channel:轻量级、在同一 Connection 上的逻辑通道;发布/消费一般都走 Channel
         *
         * try-with-resources:
         * - 代码块结束会自动 close channel/connection,避免连接泄漏
         */
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {

            /**
             * 4) 声明 exchange
             *
             * exchangeDeclare 的作用:
             * - 如果 exchange 不存在:创建它
             * - 如果存在:校验参数是否一致(不一致会抛异常)
             *
             * "fanout":广播类型。routingKey 会被忽略。
             */
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

            /**
             * 5) 组装要发送的消息
             * - 如果命令行没传参数:发送默认内容
             * - 否则把参数用空格拼起来作为消息体
             */
            String message = (args.length < 1)
                    ? "info: Hello World!"
                    : String.join(" ", args);

            /**
             * 6) 发布消息
             *
             * basicPublish(exchange, routingKey, props, body)
             * - exchange:发到哪个交换机
             * - routingKey:fanout 下会被忽略,因此通常写空字符串 ""
             * - props:消息属性(headers、contentType、deliveryMode 等),这里传 null 表示不附加
             * - body:消息字节数组
             *
             * 这里用 UTF-8 编码把字符串转成字节。
             */
            channel.basicPublish(
                    EXCHANGE_NAME,
                    "",
                    null,
                    message.getBytes(StandardCharsets.UTF_8)
            );

            // 7) 控制台输出:仅用于示例演示
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

ReceiveLogs

java 复制代码
package icu.wzk.demo;
/**
 * RabbitMQ 官方教程中的 "ReceiveLogs" 示例(Consumer)。
 *
 * 目标:
 * 1) 声明一个 fanout exchange(广播交换机)
 * 2) 创建一个临时队列(服务器随机命名)
 * 3) 把这个队列绑定到 exchange(fanout 下 routingKey 无意义)
 * 4) 持续消费队列中的消息
 *
 * 关键点:
 * - 该消费者每启动一次,就会创建一个新的临时队列,因此可以实现"订阅者"模式:
 *   多个消费者同时运行,每个都会收到同一条广播消息(各自队列各自收)。
 */
public class ReceiveLogs {

    /** 要订阅的 exchange 名称:必须和生产者一致 */
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] args) throws Exception {
        // 1) 创建连接工厂:配置如何连接 RabbitMQ
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");

        // 2) 建立连接与 Channel(示例里不关闭,因为需要一直阻塞消费)
        // 实战里建议用 try-with-resources + 阻塞等待,或加 shutdown hook 做优雅关闭
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        /**
         * 3) 声明 fanout exchange
         * - 不存在则创建
         * - 存在则校验类型/参数
         */
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        /**
         * 4) 声明一个临时队列(匿名队列)
         * queueDeclare() 不传参 => 由服务端创建:
         * - 队列名随机生成
         * - exclusive: true(只允许当前连接使用)
         * - autoDelete: true(连接断开后自动删除)
         *
         * 用途:
         * - 每个消费者实例都有自己的独立队列,保证 fanout 广播时每个实例都能收到全量消息
         */
        String queueName = channel.queueDeclare().getQueue();

        /**
         * 5) 把临时队列绑定到 exchange
         * fanout 下 routingKey 被忽略,所以填空字符串 ""
         * 绑定关系决定了:exchange 发布的消息会路由到哪些队列
         */
        channel.queueBind(queueName, EXCHANGE_NAME, "");

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

        /**
         * 6) 定义消息到达后的回调
         * delivery.getBody() 是 byte[],用 UTF-8 解码成字符串输出
         */
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };

        /**
         * 7) 开始消费
         * basicConsume(queue, autoAck, deliverCallback, cancelCallback)
         *
         * 参数解释:
         * - queue:从哪个队列消费
         * - autoAck=true:自动确认(消息一投递就算"已处理")
         *   风险:如果回调还没处理完程序就挂了,消息也不会重新投递 => 可能丢消息
         * - deliverCallback:收到消息时的处理函数
         * - cancelCallback:消费者被取消时触发(例如队列被删除),这里留空
         */
        boolean autoAck = true;
        channel.basicConsume(queueName, autoAck, deliverCallback, consumerTag -> {});

        // 注意:这里不退出 main,让进程持续运行等待消息
    }
}

启动测试

当RabbitMQ消息队列服务启动并运行后,管理员可以通过命令行工具查看当前系统中的所有绑定关系。绑定关系是指交换器(Exchange)与队列(Queue)之间的路由规则,或者交换器之间的关联关系。

要查看这些绑定关系,可以使用以下命令:

shell 复制代码
rabbitmqctl list_bindings

这个命令会列出所有绑定关系的详细信息,包括:

  • 源交换器名称
  • 目标队列或交换器名称
  • 路由键(Routing Key)
  • 绑定参数(Arguments)

默认的输出格式是纯文本,为了提高可读性,可以使用格式化选项让输出呈现为表格形式:

shell 复制代码
rabbitmqctl list_bindings --formatter pretty_table

这将输出一个美观的表格,包含以下列:

  1. source_name - 源交换器名称
  2. source_kind - 源交换器类型(direct/topic/fanout/headers)
  3. destination_name - 目标队列或交换器名称
  4. destination_kind - 目标类型(queue/exchange)
  5. routing_key - 路由键
  6. arguments - 绑定参数

这种格式特别适合在绑定关系较多的情况下快速查看和理解路由配置。在实际生产环境中,这个命令常用于调试消息路由问题或验证配置是否正确。

错误速查

症状 根因定位 修复
消费者一直收不到消息 exchange 名称不一致(logs 拼写/环境不同) 对比生产者/消费者 EXCHANGE_NAME;rabbitmqctl list_exchanges统一 exchange 名称与环境配置
发送时报 PRECONDITION_FAILED exchange 已存在但类型/参数不同(fanout vs direct) 查看服务端已有 exchange;观察异常堆栈保持 exchangeDeclare 参数一致;必要时删除旧 exchange 重建
启动消费者后能收,重启 RabbitMQ 后全没了 使用临时队列(exclusive+autoDelete)且 exchange/queue 非持久化 rabbitmqctl list_queues 看队列是否消失生产环境改为声明具名持久化队列,并设置 durable exchange/queue
生产者显示 Sent,但队列无消息 没有任何队列绑定到 fanout exchange rabbitmqctl list_bindings 看 source=logs 是否有绑定先启动消费者(完成 queueBind)或预先创建持久化绑定
只想让部分消费者收到,但所有都收到 fanout 语义就是全广播 查看 exchange 类型需要选择 direct/topic 或 headers,并设计 routingKey/bindingKey
消费端偶发丢消息 autoAck=true,回调未处理完进程崩溃/断连消息仍被确认 查看代码 autoAck;对比业务幂等/失败重试改为手动 ack:成功 basicAck,失败 basicNack/requeue,并设置重试/死信
消费变慢后内存/堆积明显 推模式未限流,prefetch 未设置导致积压 RabbitMQ 管理台看 unacked/ready;应用线程池指标设置 basicQos(prefetch),控制并发与消费速率;必要时水平扩容
basicGet 经常返回 null 队列为空或被其他消费者抢占 检查是否存在推模式消费者;查看队列 ready 数仅在确需拉模式时使用;确保队列独占或设计调度策略
连接失败/拒绝连接 host/port/vhost/user/pass 配置错误或网络不通 telnet/nc 测端口;看 RabbitMQ 日志修正连接参数;开通防火墙;配置 vhost 权限
权限错误(ACCESS_REFUSED) vhost 权限不足 RabbitMQ 日志/异常信息为用户授予 vhost 配置/读写权限
收到消息但乱码 编码不一致 检查生产/消费端编码统一 UTF-8;显式使用 StandardCharsets.UTF_8

其他系列

🚀 AI篇持续更新中(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究 ,持续打造实用AI工具指南!
AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地
🔗 AI模块直达链接

💻 Java篇持续更新中(长期更新)

Java-196 消息队列选型:RabbitMQ vs RocketMQ vs Kafka

MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS已完结,GuavaCache已完结,EVCache已完结,RabbitMQ正在更新... 深入浅出助你打牢基础!
🔗 Java模块直达链接

📊 大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解
🔗 大数据模块直达链接

相关推荐
Wnq100722 小时前
新型基于“去中心化分布式Agent“技术的操作系统DIOS
分布式·嵌入式硬件·中间件·架构·云计算·去中心化·信息与通信
hgz07102 小时前
Nginx负载均衡策略详解与Session一致性解决方案
java·jmeter
清水白石0082 小时前
以领域为中心:Python 在 DDD(领域驱动设计)中的落地实践指南
java·运维·python
MC皮蛋侠客3 小时前
distcc结合VSCode实现分布式编译的全面指南
c++·ide·分布式·vscode
风月歌3 小时前
小程序项目之校园二手交易平台小程序源代码(源码+文档)
java·数据库·mysql·小程序·毕业设计·源码
少许极端3 小时前
算法奇妙屋(二十)-回文子串/子序列问题(动态规划)
java·算法·动态规划·图解·回文串·回文序列
有味道的男人3 小时前
1688数据采集:官方API与网页爬虫实战指南
java·服务器·爬虫
仅此,3 小时前
前端接收了id字段,发送给后端就变了
java·前端·javascript·spring·typescript