消息队列之RabbitMQ

一、什么是MQ

  • 消息队列(Message Queue)是一种用于在应用程序之间传递消息的通信方式,消息队列允许应用程序异步地发送和接收消息,并且不需要 直接连接到对方。
  • 消息(Message)是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象
  • 队列(Queue)可以说是一个数据结构,可以存储数据。先进先出

二、主流产品

作用:

  1. 解耦:一个模块出现故障,不会影响其他模块
  2. 异步:当生产者消费者处理速度不匹配的时候,可以通过消息队列存储消息,提高响应速度
  3. 削峰:通过消息队列减轻消费者的压力,可以根据自己的速度进行响应

三、RabbitMQ的安装

1.环境

使用的是百度云的centos8

RabbitMQ下载地址:https://packagecloud.io/rabbitmq/erlang/packages/el/7/erlang-23.3.4.11-1.el7.x86_64.rpm/

erlang下载地址:

https://packagecloud.io/rabbitmq/erlang/packages/el/7/erlang-23.3.4.11-1.el7.x86_64.rpm/download.rpm?distro_version_id=140

要根据自己的操作系统版本去下载对应的版本,我下载的是rabbitmq-server-3.12.14-1.el8.noarch.rpm和erlang-25.3.2.13-1.el8.x86_64.rpm

2.安装

复制代码
rpm -ivh rabbitmq-server-3.12.14-1.el8.noarch.rpm
rpm -ivh erlang-25.3.2.13-1.el8.x86_64.rpm

3.启动

复制代码
 service rabbitmq-server start
//查看状态
 service rabbitmq-server status

rabbitmqctl start_app
rabbitmqctl status

//打开插件

rabbit-plugins enable rabbitmq_management
//打开防火墙
firewall-cmd --add-port=5672/tcp --permanent
firewall-cmd --add-port=15672/tcp --permanent
firewall-cmd --add-port=25672/tcp --permanent
firewall-cmd --add-port=4369/tcp --permanent

//重启防火墙
firewall-cmd --reload

4.打开rabbitmq控制台

添加用户,分配权限

复制代码
1.创建账号


rabbitmqctl add_user admin 123456


2.设置用户角色


rabbitmqctl set_user_tags admin administrator


3.设置用户权限

用户admin具有/vhost1这个virtual host中所有的配置,写、读权限


rabbitmqctl set_permissions [-p <vhostpath>]<user> <conf> <write> <read>


rabbitmqctl set_permissions -p "/" admin ".*" ".*"


4.

查询当前用户和角色

rabbitmqctl list_users

打开控制台地址:

云服务器ip:15672

用户名:admin 密码:admin

四、rabbitmq模型

在rabbitmq的服务器上有多个虚拟机,每个虚拟机 之间的资源是不互通的,虚拟机里面有exchange交换机和queue队列,queue用来存储和转发消息,exchange是用来绑定多个消息队列。queue中消息先存在内存,然后批量刷盘(比如1s刷盘一次)

生产者和消费者都需要与rabbitmq建立连接,并开启通道用来传送消息。

生产者可以通过交换机发送消息,message 到达 broker 的第一站,根据分发规则,匹配査询表中的 routing key,分发 消息到 queue 中去,也可以直接发送到队列当中。

消费者监听具体的队列,接收消息。

五、rabbitmq编程

1.依赖

java 复制代码
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
        </dependency>

2.生产者

java 复制代码
package com.example.rabbitmq;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;

public class FirstProducer {
    private static final String HOST_NAME = "云服务器ip";
    private static final int HOST_PORT = 5672;
    private static final String QUEUE_NAME = "test";
    private static final String VIRTUAL_HOST = "/mirror";
    private static final String USER_NAME = "bing";
    private static final String PASSWORD = "bing";
    private static final String EXCHANGE_NAME = "callback";
    public static void main(String[] args) throws  Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST_NAME);
        factory.setPort(HOST_PORT);
        factory.setVirtualHost(VIRTUAL_HOST);
        factory.setUsername(USER_NAME);
        factory.setPassword(PASSWORD);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
//        创建多个channel,如果number重复,返回null
//        Channel channel1 = connection.createChannel(1);
//        Channel channel2 = connection.createChannel(2);
        //生成交换机,如果服务端没有该交换机,新建一个,有的话,如果参数不一致会报错
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
        String message1 = "hello world1";
        channel.basicPublish(EXCHANGE_NAME, "key1", MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());

//没有交换机,直接发到队列.第一个参数是null
        String message2 = "hello world2";
        channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message2.getBytes());

    }

}

3.消费者

3.1push模式

channel.basicConsume(QUEUE_NAME, false, consumer),使用false,手写应答:

basicAck 什么时候用?

消息处理成功 → 告诉 MQ:可以删掉这条消息了

作用

  • MQ 收到 ack,永久删除消息

  • 不会再投递给任何消费者


basicReject 什么时候用?

消息处理失败 → 告诉 MQ:我处理不了

它有一个关键参数:requeue


① basicReject(..., requeue = true)

使用时机

  • 临时故障:数据库挂了、Redis 超时、网络波动

  • 你希望重新消费这条消息

行为

  • 消息重新回到队列头部

  • 马上会再次投递给消费者(可能造成循环)


② basicReject(..., requeue = false)

使用时机

  • 永久失败:消息格式错、参数非法、数据已过期

  • 再重试 100 次也没用

行为

  • 消息直接丢弃

  • 如果配置了死信队列(DLQ),会进入死信

basicNack 什么时候用?

channel.basicNack(deliveryTag, true, false);

第一个true代表批量处理,在此之前所有没有确认的消息,都进行nack,false代表处理单条消息

第二个true代表重新入队,false代表进入消息队列

java 复制代码
try {
    处理业务逻辑...
    channel.basicAck(..., false);   // 成功确认
} catch (可重试异常 e) {
    channel.basicReject(..., true);  // 重新入队
} catch (不可重试异常 e) {
    channel.basicReject(..., false); // 丢弃/死信
}
java 复制代码
package com.example.rabbitmq;

import com.rabbitmq.client.*;

import java.io.IOException;

public class FirstConsumerPush {
    private static final String HOST_NAME = "ip";
    private static final int HOST_PORT = 5672;
    private static final String QUEUE_NAME = "test";
    private static final String VIRTUAL_HOST = "/mirror";
    private static final String USER_NAME = "bing";
    private static final String PASSWORD = "bing";
    private static final String EXCHANGE_NAME = "callback";
    public static void main(String[] args) throws  Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST_NAME);
        factory.setPort(HOST_PORT);
        factory.setVirtualHost(VIRTUAL_HOST);
        factory.setUsername(USER_NAME);
        factory.setPassword(PASSWORD);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.basicQos(1);
        //处理接收到的消息,监听消息队列,消息传进来之后会触发这个方法的执行
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                System.out.println("receive message:" + new String(body));
                //手动编写应答代码
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        //false:不应答,需要在方法里面写应答的逻辑
        channel.basicConsume(QUEUE_NAME, false, consumer);
        //true:应答,在方法里面不需要写应答的逻辑
//        channel.basicConsume(QUEUE_NAME, true, consumer);
    }

}

3.2pull模式

java 复制代码
package com.example.rabbitmq;

import com.rabbitmq.client.*;

import java.io.IOException;

public class FirstConsumerPull {
    private static final String HOST_NAME = "ip";
    private static final int HOST_PORT = 5672;
    private static final String QUEUE_NAME = "test";
    private static final String VIRTUAL_HOST = "/mirror";
    private static final String USER_NAME = "bing";
    private static final String PASSWORD = "bing";
    private static final String EXCHANGE_NAME = "callback";
    public static void main(String[] args) throws  Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST_NAME);
        factory.setPort(HOST_PORT);
        factory.setVirtualHost(VIRTUAL_HOST);
        factory.setUsername(USER_NAME);
        factory.setPassword(PASSWORD);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
//每次拉取一条
        channel.basicQos(1);
        GetResponse response = channel.basicGet(QUEUE_NAME, false);
        while (response != null) {
            byte[] body = response.getBody();
            System.out.println("receive:" + new String(body));
            channel.basicAck(response.getEnvelope().getDeliveryTag(), false);
            response = channel.basicGet(QUEUE_NAME, false);
        }

        //使用拉模式,主动获取消息,需要进行资源的关闭
        channel.close();
        connection.close();
    }

}

4.创建监听

4.1生产者监听

监听器类型 监听事件 使用场景
确认监听器(Confirm) 消息是否成功投递到 RabbitMQ 服务器(交换机) 确保消息不丢失:若确认失败,可重试 / 记录日志 / 入库补偿
返回监听器(Return) 消息到达交换机,但路由到队列失败(无匹配队列) 处理 "路由失败" 的消息:如消息发错交换机、路由键错误,可重路由 / 告警
发送异常监听器(Exception) 消息发送时的网络异常、连接中断等 捕获发送过程中的运行时异常,避免程序崩溃,实现失败重试
java 复制代码
package com.example.rabbitmq;

import com.rabbitmq.client.*;

public class CallbackProducer {

    private static final String HOST_NAME = "";
    private static final int HOST_PORT = 5672;
    private static final String QUEUE_NAME = "test";
    private static final String VIRTUAL_HOST = "/mirror";
    private static final String USER_NAME = "bing";
    private static final String PASSWORD = "bing";
    private static final String EXCHANGE_NAME = "callback";
    public static void main(String[] args) throws  Exception{
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST_NAME);
        factory.setPort(HOST_PORT);
        factory.setVirtualHost(VIRTUAL_HOST);
        factory.setUsername(USER_NAME);
        factory.setPassword(PASSWORD);
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        //生成交换机,如果服务端没有该交换机,新建一个,有的话,如果参数不一致会报错
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
// Confirm 监听器:监听消息是否成功投递到 RabbitMQ Broker
      
        channel.addConfirmListener(new ConfirmListener() {
            // 消息成功发送到broker
            @Override
            public void handleAck(long deliveryTag, boolean multiple) {
                System.out.println(" [Confirm] 消息确认成功: deliveryTag=" + deliveryTag + ", multiple=" + multiple);
            }
            // 消息没有成功投递到broker
            @Override
            public void handleNack(long deliveryTag, boolean multiple) {
                System.out.println(" [Confirm] 消息确认失败: deliveryTag=" + deliveryTag + ", multiple=" + multiple);
            }
        });

        // Return 监听器:监听无法路由的消息
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) {
                System.out.println(" [Return] 消息返回:");
                System.out.println("   Reply Code: " + replyCode);
                System.out.println("   Reply Text: " + replyText);
                System.out.println("   Exchange: " + exchange);
                System.out.println("   Routing Key: " + routingKey);
                System.out.println("   Properties: " + properties);
                System.out.println("   Body: " + new String(body));
            }
        });
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .deliveryMode(2)
                .priority(1)
                .contentEncoding("UTF-8")
                .correlationId("123")
                .build();
        channel.basicPublish(EXCHANGE_NAME, "key1", properties, "hello world1".getBytes());
        channel.basicPublish(EXCHANGE_NAME, "key2", properties, "hello world2".getBytes());
        //确保能收到服务端回调
        Thread.sleep(1000);
        channel.close();
        connection.close();

    }
}

4.2消费者监听器

监听器类型 监听事件 使用场景
消息监听处理器(MessageListener) 收到消息并触发业务处理 核心监听器:消费消息的核心逻辑,对应你之前问的 basic.ack/reject 操作
消费异常监听器(ErrorHandler) 消息处理过程中抛出异常 统一捕获消费异常,避免单个消息失败导致消费者停止,可实现重试 / 死信转发
消费者生命周期监听器 消费者启动、停止、连接中断 / 恢复 监控消费者状态,如消费者宕机告警、重连逻辑、资源释放
java 复制代码
package com.example.rabbitmq;

import com.rabbitmq.client.*;

import java.io.IOException;

public class CallbackConsumer {

    private static final String HOST_NAME = "";
    private static final int HOST_PORT = 5672;
    private static final String QUEUE_NAME = "test";
    private static final String VIRTUAL_HOST = "/mirror";
    private static final String USER_NAME = "bing";
    private static final String PASSWORD = "bing";
    private static final String EXCHANGE_NAME = "callback";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST_NAME);
        factory.setPort(HOST_PORT);
        factory.setVirtualHost(VIRTUAL_HOST);
        factory.setUsername(USER_NAME);
        factory.setPassword(PASSWORD);

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

        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        channel.basicQos(1);

        channel.basicConsume(QUEUE_NAME, false, // autoAck 设置为 false,启用手动确认
                //成功收到消息
                new DeliverCallback() {
                    @Override
                    public void handle(String consumerTag, Delivery delivery) throws IOException {
                        String message = new String(delivery.getBody());
                        System.out.println("receive message: " + message + " correlationId: " + delivery.getProperties().getCorrelationId());
                        // 手动确认消息
                        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    }
                },
                //消息在队列中被取消
                new CancelCallback() {
                    @Override
                    public void handle(String consumerTag) throws IOException {
                        System.out.println("cancel: " + consumerTag);
                    }
                },
                //消费者断开连接
                new ConsumerShutdownSignalCallback() {
                    @Override
                    public void handleShutdownSignal(String consumerTag, ShutdownSignalException signal) {
                        System.out.println("signal: " + signal);
                    }
                });

        // 防止程序立即退出,保持消费者运行
        System.out.println("Waiting for messages. To exit press CTRL+C");
        Thread.sleep(Long.MAX_VALUE); // 或者使用其他阻塞方式

        // 资源关闭(实际应用中应在适当位置关闭)
        channel.close();
        connection.close();
    }
}

六、应用场景

1.hello world

发送,接收消息

2.work queue

工作模式,一个生产者,多个消费者

注意问题:

建议手动应答,避免自动应答之后处理信息失败。

3.发布订阅

加入交换机,进一步解耦,使用fanout类型的交换机,交换机自动往绑定的队列去发消息

复制代码
 channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//队列的名称可以为null
 channel.basicPublish(EXCHANGE_NAME, "", properties, "hello world1".getBytes());

4.基于内容的路由

在上一个交换机往所有的绑定队列上进行发送消息基础上,增加一个路由配置routing key,将消息发送到对应的队列上。

复制代码
 channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
        String message1 = "hello world1";
        channel.basicPublish(EXCHANGE_NAME, "key1", MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());

5.基于话题的路由

复制代码
//生产者 
//交换机是topic类型
channel.exchangeDeclare(EXCHANGE_NAME, "topic");

String message1 = "hello world1";
//绑定指定的topic
channel.basicPublish(EXCHANGE_NAME, "a.info", MessageProperties.PERSISTENT_TEXT_PLAIN, message1.getBytes());

//消费者,将队列绑定一个模糊匹配topic
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.info");

6.headers头部路由机制

不再根据routing key,而是根据头部信息进行匹配

java 复制代码
     
channel.exchangeDeclare(EXCHANGE_NAME, "headers");
//生产者队列一开始就绑定headers   
        Map<String, Object> headers=new HashMap<>();
        headers.put("key1", "value1");
        headers.put("key2", "value2");

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1",headers);
      //将headers加入properties
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
                .deliveryMode(2)
                .priority(1)
                .contentEncoding("UTF-8")
                .correlationId("123")
                .headers(headers)
                .build();
       //发送的消息会根据headers进行匹配
        channel.basicPublish(EXCHANGE_NAME, "key1", properties, "hello world".getBytes());



//消费者队列绑定headers
        Map<String, Object> headers=new HashMap<>();
        //x-match特定参数,any代表有一个匹配就可以,all代表所有匹配才行
        headers.put("x-match", "any");
        headers.put("key1", "value11");
        headers.put("key2", "value22");

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1",headers);

结果:因为两条头部都不匹配,所以消费者收不到生产者的消息

7.Publisher Confirms

核心:

  1. channel.confirmSelect();开启Publisher Confirms模式
  2. channel.waitForConfirms();同步确认等待,true代表消息确认成功,false代表确认失败
java 复制代码
package com.example.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.util.UUID;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.TimeUnit;

/**
 * RabbitMQ Publisher Confirm 机制:单条确认、批量确认、异步确认
 */
public class PublisherConfirmDemo {
    // MQ 连接配置
    private static final String HOST_NAME = "180.76.56.131";
    private static final int HOST_PORT = 5672;
    private static final String QUEUE_NAME = "test";
    private static final String VIRTUAL_HOST = "/mirror";
    private static final String USER_NAME = "bing";
    private static final String PASSWORD = "bing";
    private static final String EXCHANGE_NAME = "callback";
    private static final String ROUTING_KEY = "key1";
    // 测试消息数量
    private static final int MESSAGE_COUNT = 1000;

    public static void main(String[] args) throws Exception {
        // 1. 单条同步确认(性能最差,适合少量消息)
        System.out.println("===== 单条同步确认 =====");
        publishMessagesWithSingleConfirm(MESSAGE_COUNT);

        // 2. 批量同步确认(性能中等,适合中小量消息)
        System.out.println("\n===== 批量同步确认 =====");
        publishMessagesWithBatchConfirm(MESSAGE_COUNT, 100); // 每100条确认一次

        // 3. 异步确认(性能最优,适合高并发、大量消息)
        System.out.println("\n===== 异步确认 =====");
        publishMessagesWithAsyncConfirm(MESSAGE_COUNT);
    }

    /**
     * 1. 单条同步确认:发送一条,确认一条
     * 特点:简单但性能差(每发一条都阻塞等待确认),适合消息量极少的场景
     */
    public static void publishMessagesWithSingleConfirm(int messageCount) throws Exception {
        // 创建连接和通道
        Connection connection = getConnection();
        Channel channel = connection.createChannel();

        // 开启 Publisher Confirm 模式(核心)
        channel.confirmSelect();

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < 10; i++) {
            String message = "Single Confirm Message - " + i + " - " + UUID.randomUUID();
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());

            // 同步等待确认(阻塞)
            // waitForConfirms():返回boolean,true=确认成功,false=确认失败
            if (channel.waitForConfirms()) {
                System.out.println("消息 " + i + " 发送并确认成功");
            } else {
                System.err.println("消息 " + i + " 发送失败,需重试");
                // 此处可添加重试逻辑
            }
        }

        long endTime = System.currentTimeMillis();
        System.out.printf("单条确认发送 %d 条消息耗时:%d ms%n", messageCount, (endTime - startTime));

        // 关闭资源
        channel.close();
        connection.close();
    }

    /**
     * 2. 批量同步确认:发送一批,确认一批
     * 特点:性能比单条好,减少确认次数,但如果批量中一条失败,无法定位具体失败消息
     * @param messageCount 总消息数
     * @param batchSize 每批确认的数量
     */
    public static void publishMessagesWithBatchConfirm(int messageCount, int batchSize) throws Exception {
        Connection connection = getConnection();
        Channel channel = connection.createChannel();
        //开启 Publisher Confirm 模式
        channel.confirmSelect();

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < messageCount; i++) {
            String message = "Batch Confirm Message - " + i + " - " + UUID.randomUUID();
            channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());

            // 每积累 batchSize 条消息,批量确认一次
            if ((i + 1) % batchSize == 0) {
                // waitForConfirmsOrDie():确认失败直接抛异常,无需手动判断
                boolean isConfirmed = channel.waitForConfirms(5000); // 超时5秒
                if (!isConfirmed) {
                    throw new RuntimeException("批量确认超时,部分消息可能未被确认");
                }
                System.out.println("批量确认:第 " + (i + 1) + " 条消息确认成功");
            }
        }

        // 处理剩余未批量的消息
        if (messageCount % batchSize != 0) {
            boolean isConfirmed = channel.waitForConfirms(5000); // 超时5秒
            if (!isConfirmed) {
                throw new RuntimeException("批量确认超时,部分消息可能未被确认");
            }
            System.out.println("批量确认:剩余 " + (messageCount % batchSize) + " 条消息确认成功");
        }

        long endTime = System.currentTimeMillis();
        System.out.printf("批量确认发送 %d 条消息耗时:%d ms%n", messageCount, (endTime - startTime));

        channel.close();
        connection.close();
    }

    /**
     * 3. 异步确认:发送和确认异步执行,性能最优,需要开启ackCallback和nackCallback
     * 特点:非阻塞,高吞吐,可精准定位失败消息,适合高并发场景
     */
    public static void publishMessagesWithAsyncConfirm(int messageCount) throws Exception {
        Connection connection = getConnection();
        Channel channel = connection.createChannel();
        //开启 Publisher Confirm 模式
        channel.confirmSelect();

        // 存储未确认的消息(线程安全的有序Map,key=消息序号,value=消息内容)
        ConcurrentNavigableMap<Long, String> unconfirmedMessages = new ConcurrentSkipListMap<>();

        // 回调1:确认成功的回调
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
            if (multiple) {
                // multiple=true:批量处理,确认的是小于等于当前deliveryTag的所有消息
                //headMap用于返回一个视图,该视图包含所有键小于或等于 deliveryTag 的键值对
                ConcurrentNavigableMap<Long, String> confirmed = unconfirmedMessages.headMap(deliveryTag, true);
                confirmed.clear(); // 移除已确认的消息
                System.out.println("异步批量确认:deliveryTag=" + deliveryTag + " 及之前的消息确认成功");
            } else {
                // multiple=false:单条处理,仅确认当前deliveryTag的消息
                unconfirmedMessages.remove(deliveryTag);
                System.out.println("异步单条确认:deliveryTag=" + deliveryTag + " 消息确认成功");
            }
        };

        // 回调2:确认失败的回调
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            String failedMessage = unconfirmedMessages.get(deliveryTag);
            System.err.println("消息确认失败:deliveryTag=" + deliveryTag + ",消息内容=" + failedMessage);
            // 此处可添加重试/告警/入库补偿逻辑
            if (multiple) {
                ConcurrentNavigableMap<Long, String> failed = unconfirmedMessages.headMap(deliveryTag, true);
                failed.clear();
            } else {
                unconfirmedMessages.remove(deliveryTag);
            }
        };

        // 注册异步确认回调(核心)
        channel.addConfirmListener(ackCallback, nackCallback);

        long startTime = System.currentTimeMillis();

        for (int i = 0; i < messageCount; i++) {
            String message = "Async Confirm Message - " + i + " - " + UUID.randomUUID();
            // 获取当前消息的deliveryTag(从1开始递增)
            long deliveryTag = channel.getNextPublishSeqNo();
            //先存储后发送
            // 存储未确认的消息
            unconfirmedMessages.put(deliveryTag, message);
            // 发送消息(非阻塞)
            channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, null, message.getBytes());
        }

        // 等待所有消息确认完成(实际生产中可通过监控unconfirmedMessages是否为空判断)
        while (!unconfirmedMessages.isEmpty()) {
            Thread.sleep(10);
        }

        long endTime = System.currentTimeMillis();
        System.out.printf("异步确认发送 %d 条消息耗时:%d ms%n", messageCount, (endTime - startTime));

        channel.close();
        connection.close();
    }

    /**
     * 创建 RabbitMQ 连接(抽取公共方法)
     */
    private static Connection getConnection() throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(HOST_NAME);
        factory.setPort(HOST_PORT);
        factory.setVirtualHost(VIRTUAL_HOST);
        factory.setUsername(USER_NAME);
        factory.setPassword(PASSWORD);
        // 开启自动重连(生产环境建议开启)
        factory.setAutomaticRecoveryEnabled(true);
        factory.setNetworkRecoveryInterval(5000);
        return factory.newConnection();
    }
}

代码包含三种模式:

7.1单条确认

  • 发送一条,确认一条
  • 使用**channel.confirmSelect()**开启 Publisher Confirm 模式
  • 使用**channel.waitForConfirms()同步等待确认,**true代表确认成功,false代表确认失败
  • 特点:简单但性能差(每发一条都阻塞等待确认),适合消息量极少的场景

7.2批量同步确认

  • 发送一批,确认一批
  • 使用**channel.confirmSelect()**开启 Publisher Confirm 模式
  • 使用**channel.waitForConfirms()同步等待确认,发一批消息后调用一次,**true代表确认成功,false代表确认失败
  • 特点:性能比单条好,减少确认次数,但如果批量中一条失败,无法定位具体失败消息

7.3异步确认

  • 发送和确认异步执行,性能最优
  • 在发送消息之前,使用线程安全的map,ConcurrentNavigableMap存储未确认的消息
  • 需要开启ackCallback和nackCallback,清除消息
  • 当map为空之后,处理完成
  • 特点:非阻塞,高吞吐,可精准定位失败消息,适合高并发场景

七、SpringBoot集成RabbitMQ

1.依赖

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2.配置

java 复制代码
spring:
  # RabbitMQ 基础配置
  rabbitmq:
    host: 127.0.0.1       # MQ 服务器地址(替换为你的实际地址)
    port: 5672            # 端口(默认5672)
    username: guest       # 用户名(默认guest)
    password: guest       # 密码(默认guest)
    virtual-host: /       # 虚拟主机(默认/)
    # 连接池配置(生产环境建议开启)
    connection-timeout: 15000
    cache:
      channel:
        size: 50          # 通道缓存大小
      connection:
        size: 10          # 连接池大小
    # 生产者确认机制(Publisher Confirm)
    publisher-confirm-type: correlated  # 开启异步确认(NONE/ CORRELATED/ SIMPLE)
    publisher-returns: true             # 开启消息返回(路由失败时回调)
    # 消费者配置
    listener:
      simple:
        acknowledge-mode: manual        # 手动确认(NONE/ AUTO/ MANUAL)
        concurrency: 1                  # 最小消费线程数
        max-concurrency: 5              # 最大消费线程数
        prefetch: 10                    # 每次预取10条消息(限流)
        retry:
          enabled: true                 # 开启消费重试
          max-attempts: 3               # 最大重试次数
          initial-interval: 1000ms      # 首次重试间隔

3.配置类RabbitMqConfig

主要是定义队列,交换机,队列交换机的绑定关系

java 复制代码
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ 队列/交换机/绑定 配置类
 */
@Configuration
public class RabbitMqConfig {
    // 1. 定义常量(避免硬编码)
    public static final String EXCHANGE_NAME = "callback";
    public static final String QUEUE_NAME = "boot_test_queue";
    public static final String ROUTING_KEY = "boot.test.key";

    // 2. 声明直连交换机(Direct Exchange)
    @Bean
    public DirectExchange bootDirectExchange() {
        // 参数:名称、是否持久化、是否自动删除
        return new DirectExchange(EXCHANGE_NAME, true, false);
    }

    // 3. 声明队列(Queue)
    @Bean
    public Queue bootTestQueue() {
        // 参数:名称、是否持久化、是否排他、是否自动删除
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    // 4. 绑定交换机和队列(Binding)
//绑定队列到交换机,使用路由键
    @Bean
    public Binding bindQueueToExchange(Queue bootTestQueue, DirectExchange bootDirectExchange) {
        return BindingBuilder.bind(bootTestQueue).to(bootDirectExchange).with(ROUTING_KEY);
    }
}

4.生产者

使用init定义ConfirmReturn 回调,定义sendMessage方法,发送消息:交换机、路由键、消息内容、消息追踪ID

java 复制代码
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.UUID;

/**
 * RabbitMQ 生产者
 */
@Component
public class RabbitMqProducer {
    // 注入Spring封装的RabbitTemplate(核心发送工具)
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 初始化:配置生产者确认和返回回调
    @PostConstruct
    public void init() {
        // 1. 消息确认回调(是否成功投递到交换机)
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            String msgId = correlationData != null ? correlationData.getId() : "未知ID";
            if (ack) {
                System.out.println("消息[" + msgId + "]成功投递到交换机");
            } else {
                System.err.println("消息[" + msgId + "]投递失败:" + cause);
                // 失败重试/入库补偿逻辑可在此实现
            }
        });

        // 2. 消息返回回调(投递到交换机,但路由到队列失败)
        rabbitTemplate.setReturnsCallback(returnedMessage -> {
            String msg = new String(returnedMessage.getMessage().getBody());
            System.err.println("消息路由失败:" +
                    "路由键=" + returnedMessage.getRoutingKey() +
                    ",消息=" + msg +
                    ",原因=" + returnedMessage.getReplyText());
        });
    }

    /**
     * 发送消息
     * @param content 消息内容
     */
    public void sendMessage(String content) {
        // 生成唯一ID(用于追踪消息)
        CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
        // 发送消息:交换机、路由键、消息内容、消息追踪ID
        rabbitTemplate.convertAndSend(
                RabbitMqConfig.EXCHANGE_NAME,
                RabbitMqConfig.ROUTING_KEY,
                content,
                correlationData
        );
        System.out.println("消息发送成功,内容:" + content + ",ID:" + correlationData.getId());
    }
}

5.消费者

@RabbitListener(queues = RabbitMqConfig.QUEUE_NAME)监听指定队列

java 复制代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * RabbitMQ 消费者
 */
@Component
public class RabbitMqConsumer {
    /**
     * 监听指定队列(自动关联配置类声明的队列)
     * @param message 消息体(Spring封装的Message)
     * @param channel 原生Channel(用于手动确认)
     * @param deliveryTag 消息投递标签(唯一标识)
     */
    @RabbitListener(queues = RabbitMqConfig.QUEUE_NAME)
    public void consumeMessage(Message message, Channel channel,
                               @org.springframework.amqp.core.AmqpHeaders.DeliveryTag long deliveryTag) throws IOException {
        try {
            // 1. 解析消息内容
            String msgContent = new String(message.getBody());
            System.out.println("收到消息:" + msgContent + ",deliveryTag:" + deliveryTag);

            // 2. 模拟业务处理(可替换为实际业务逻辑)
            if (msgContent.contains("error")) {
                // 模拟:消息格式错误 → 永久失败,拒绝并进入死信队列(requeue=false)
                throw new IllegalArgumentException("消息格式非法,无法处理");
            }

            // 3. 处理成功 → 手动确认(basic.ack)
            // multiple=false:仅确认当前消息;true:确认所有小于等于当前deliveryTag的消息
            channel.basicAck(deliveryTag, false);
            System.out.println("消息[" + deliveryTag + "]确认成功");

        } catch (IllegalArgumentException e) {
            // 场景1:永久失败 → basic.nack(拒绝,不重新入队)
            channel.basicNack(deliveryTag, false, false);
            System.err.println("消息[" + deliveryTag + "]永久失败,已拒绝:" + e.getMessage());
        } catch (Exception e) {
            // 场景2:临时失败 → basic.nack(拒绝,重新入队)
            channel.basicNack(deliveryTag, false, true);
            System.err.println("消息[" + deliveryTag + "]临时失败,重新入队:" + e.getMessage());
        }
    }
}

6.测试类

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RabbitMqTest {
    @Autowired
    private RabbitMqProducer rabbitMqProducer;

    // 测试正常消息
    @Test
    public void testSendNormalMessage() {
        rabbitMqProducer.sendMessage("SpringBoot集成RabbitMQ - 正常消息");
        // 休眠1秒,确保消费者处理完成
        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    // 测试永久失败消息
    @Test
    public void testSendErrorMessage() {
        rabbitMqProducer.sendMessage("SpringBoot集成RabbitMQ - error - 永久失败消息");
        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
    }

    // 测试临时失败消息(可自定义业务异常模拟)
    @Test
    public void testSendTempFailMessage() {
        rabbitMqProducer.sendMessage("SpringBoot集成RabbitMQ - 临时失败消息");
        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

八、扩展功能

1.队列形式

1.1classic:经典队列(默认)

classic:经典队列,先进先出队列,如果消息失败,就需要重新入队,放在头部

  • 问题:如果消息积压,内存压力过大,性能可能下降,集群场景不适用
  • version1:消息来了,持久化到磁盘,内存加载,也是全部加载
  • version2:每次内存只加载需要的消息
  • 适用于消息量比较小的场景

1.2Quorum仲裁队列

  • 数据一定要持久化,而且要与其他节点进行消息同步,使用Raft 一致性算法进行消息的入队,可以处理毒消息。
  • 毒消息:设置发送的次数,当失败重发次数达到你设置的次数之后,不会再入队
  • 使用:在queueDeclare的时候,往params里面,设定x-queue-type为quorum
  • Quorum是集群队列,当集群扩容的时候,需要你手动调整队列的扩容
  • 适合长期存在的数据,对低延迟要求不高,要求数据安全与容错更高,比如电商订单,可以处理慢一点,但是不能丢失。
java 复制代码
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class QuorumQueueConfig {
    // Quorum 队列名称
    public static final String QUORUM_ORDER_QUEUE = "quorum_order_queue";

    /**
     * 声明 Quorum 队列(核心:x-queue-type=quorum)
     * 副本数默认 3,生产环境建议设为奇数(3/5)
     */
    @Bean
    public Queue quorumOrderQueue() {
        Map<String, Object> args = new HashMap<>();
        // 指定队列类型为 Quorum
        args.put("x-queue-type", "quorum");
        // 可选:设置初始副本数(默认 3)
        args.put("x-quorum-initial-group-size", 3);
        // 可选:消息过期时间(TTL),按需配置
        args.put("x-message-ttl", 86400000); // 24小时过期

        // 持久化 + Quorum 类型
        return QueueBuilder.durable(QUORUM_ORDER_QUEUE)
                .withArguments(args)
                .build();
    }
}

1.3流式队列Stream

适合消费者多,读消息比较频繁的场景

以append-only只添加的日志记录消息,持久化到磁盘,然后通过offset调整消费者的消费进度

java 复制代码
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class StreamQueueConfig {
    // Stream 队列名称
    public static final String STREAM_LOG_QUEUE = "stream_log_queue";

    /**
     * 声明 Stream 队列(核心:x-queue-type=stream)
     * 关键参数:x-max-length-bytes(最大存储字节)、x-stream-max-segment-size-bytes(分段大小)
     */
    @Bean
    public Queue streamLogQueue() {
        Map<String, Object> args = new HashMap<>();
        // 指定队列类型为 Stream
        args.put("x-queue-type", "stream");
        // 最大存储大小(10GB,按需配置)
        args.put("x-max-length-bytes", 10 * 1024 * 1024 * 1024L);
        // 消息保留时间(7天,过期自动删除)
        args.put("x-max-age", "7d");
        // 分段大小(默认 500MB,避免单个文件过大)
        args.put("x-stream-max-segment-size-bytes", 500 * 1024 * 1024L);

        // Stream 队列必须持久化
        return QueueBuilder.durable(STREAM_LOG_QUEUE)
                .withArguments(args)
                .build();
    }
}
java 复制代码
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.io.IOException;

@Component
public class StreamQueueConsumer {
    /**
     * 消费 Stream 队列(默认从最新偏移量开始消费)
     */
    @RabbitListener(queues = StreamQueueConfig.STREAM_LOG_QUEUE)
    public void consumeSystemLog(Message message, Channel channel) throws IOException {
        String content = new String(message.getBody());
        System.out.println("Stream 队列消费日志:" + content);

        // Stream 队列无需手动 ack!偏移量自动提交(默认)
        // 若需手动提交偏移量,需配置 x-stream-offset 并手动调用 channel.basicAck
    }

    /**
     * 重放历史日志(从队列头部开始消费)
     * 需在监听时指定偏移量参数
     */
    @RabbitListener(
            queues = StreamQueueConfig.STREAM_LOG_QUEUE,
            arguments = {
                    @org.springframework.amqp.rabbit.annotation.Argument(
                            name = "x-stream-offset",
                            value = "first" // first=从头消费,last=从尾消费,数字=指定偏移量
                    )
            }
    )
    public void replaySystemLog(Message message) {
        String content = new String(message.getBody());
        System.out.println("Stream 队列重放日志:" + content);
    }
}

2.死信队列

2.1什么是死信队列

消息转移到死信队列过程中,是没有确认机制的,所以是不安全的。

消息在转移到死信队列之后会在headers里面增加一些信息,来表明这是一条死信消息。

2.2参数

只有在classic和quorum队列才能设置

2.3示例

  • 首先在控制台设置一个队列加上上面的参数,定义一个死信交换机fanout类型,绑定一个死信队列
  • 然后定义一个普通的生产者-消费者,消费者使用reject拒绝消息。
  • 拒绝的消息会进入死信队列,定义一个消费者监听死信队列,会收到这条消息。

2.4基于死信队列实现延迟队列

一个正常队列,没有对应的消费者,设置一个消息的TTL,达到这个TTL消息转移到死信队列。死信队列再绑定消费者,进行正常的消费,实现一个延迟队列。

3.消息分片存储插件:rabbitmq_sharding

会对消息的 routing key 做哈希运算,然后用 hash % N(N 是绑定到交换机的队列数)来决定消息路由到哪个队列。

bash 复制代码
rabbitmq-plugins enable rabbitmq_sharding

在新建交换机的时候,会出现一个新的类型:x-modulus-hash

之后消息会均匀分到三个分片上,然后消费者消费就需要消费三次。

java 复制代码
//生产者
channel.exchangeDeclare(EXCHANGE_NAME, "x-modulus-hash");

channel.basicPublish(EXCHANGE_NAME, String.valueOf(i), properties, message.getBytes());


//消费者,只声明一个队列,队列名是交换机的名字

channel.queueDeclare(QUEUE_NAME, true, false, false, null);

//三个分片,消费三次
channel.basicConsume(QUEUE_NAME, false, consumer);
channel.basicConsume(QUEUE_NAME, false, consumer);
channel.basicConsume(QUEUE_NAME, false, consumer);

存在的问题:

  • 分片的过程中是不考虑顺序的
  • 消费者绑定队列的时候,哪个队列消费者少,就绑定到哪个队列。但是消费者数量不等于消息数量,可能会导致一部分闲死,一部分忙死,根本达不到 "负载均衡"。所以实际开发中,尽量别单独用这个插件,改用 Stream 队列 / Quorum 队列 + 业务层分片(比如按订单 ID 手动分队列),才能真正均衡。
相关推荐
回家路上绕了弯2 天前
深入解析Agent Subagent架构:原理、协同逻辑与实战落地指南
分布式·后端
用户8307196840822 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
用户8307196840824 天前
RabbitMQ vs RocketMQ 事务大对决:一个在“裸奔”,一个在“开挂”?
后端·rabbitmq·rocketmq
初次攀爬者5 天前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者7 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧8 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖8 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农8 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者8 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀8 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式