消息队列之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 手动分队列),才能真正均衡。
相关推荐
你这个代码我看不懂10 小时前
@RefreshScope刷新Kafka实例
分布式·kafka·linq
麟听科技16 小时前
HarmonyOS 6.0+ APP智能种植监测系统开发实战:农业传感器联动与AI种植指导落地
人工智能·分布式·学习·华为·harmonyos
zlpzpl19 小时前
Linux安装RabbitMQ
linux·运维·rabbitmq
Wzx19801219 小时前
高并发秒杀下,如何避免 Redis 分布式锁的坑?
数据库·redis·分布式
Francek Chen20 小时前
【大数据存储与管理】分布式文件系统HDFS:01 分布式文件系统
大数据·hadoop·分布式·hdfs·架构
石去皿21 小时前
分布式原生:鸿蒙架构哲学与操作系统演进的范式转移
分布式·架构·harmonyos
AC赳赳老秦21 小时前
DeepSeek 规模化部署实战:混合云与私有云环境下的 2026 云3.0 趋势探索
数据库·人工智能·科技·rabbitmq·数据库开发·sequoiadb·deepseek
KANGBboy1 天前
spark参数优化
大数据·分布式·spark
我就是全世界1 天前
RabbitMQ架构核心拆解:从消息代理到四大组件,一文看懂异步通信基石
分布式·架构·rabbitmq