消息队列的核心概念与应用(RabbitMQ快速入门)

消息队列(RabbitMQ)

消息队列的应用场景(作用):在多个不同的系统、应用之间实现消息的传输(当然了也可以存储)。不需要考虑传输应用的编程语言、系统、框架等等(也就是应用解耦)。

消息队列主要由四部分组成:生产者(Producer)、消费者(Consumer)、消息(Message)和消息队列(queue)

为何需要使用消息队列呢?这主要归结为消息队列的几个核心特性:

首先是异步处理,所谓的异步处理,意味着生产者在发送完消息后可以立即转而进行其他任务,而无需等待消费者处理消息。这样生产者就无需等待消费者接收消息才能进行下一步操作,避免了阻塞。这与我们之前讨论的异步化处理非常类似,消息队列使我们的系统具备了这种能力。

其次,消息队列还有削峰填谷的能力。削峰填谷是指当消费者的处理能力有限时,例如,AI应用可能每隔几秒才能处理一次智能生成服务。而用户的请求量又很大,例如,新上线的系统有大量用户同时使用,我们可以先将用户的请求存储在消息队列中,然后消费者或实际执行应用可以按照自身的处理能力逐步从队列中取出请求。

比如说,同一时间有10万个请求涌入。若直接在系统内部立刻处理这 10 万个请求,系统很可能会因处理不过来而过载甚至宕机。而如果将这 10万个请求放入消息队列中,消费者或下游处理系统就可以以自己可处理的恒定速率,比如每秒处理1个请求,慢慢消费并执行。这样的话,就很好地保护了系统,将原本的流量高峰平滑地分散开,就像水管中的恒定流速一样,以稳定的方式进行处理。

这是消息队列所具有的削峰填谷功能。虽然线程池也能实现削峰填谷的效果,但它并没有消息队列这样的存储灵活性,或者说,消息队列能实现持久化存储。

Windows安装最新版RabbitMQ步骤

在安装RabbitMQ之前需要先安装Erlang,因为RabbitMQ依赖于它,Erlang 是一种小众高性能的编程语言。专为高并发和分布式系统设计的函数式编程语言,最初由瑞典电信公司爱立信开发。它以其强大的并发处理能力、容错机制和分布式特性而闻名,广泛应用于电信、即时通讯、分布式计算和 Web 应用等领域。

我们先来下载最新版的Erlang 28:https://www.erlang.org/downloads#prebuilt

然后无脑点击下一步next,无脑进行安装install,不需要勾选任何选项。

接着再去下载RabbitMQ的最新版

官网的windows安装:https://www.rabbitmq.com/docs/install-windows

点击安装包进行安装,如果没有安装弹窗是正常的,有点慢需要等待1分钟左右。

依旧无脑点击下一步next,无脑进行安装install,不需要勾选任何选项。

然后按Win+R键输入services.msc查看是否安装成功,服务是否已启动:

最后来安装一下rabbitmq的监控面板:

复制代码
rabbitmq-plugins.bat enable rabbitmq_management

进入刚才安装rabbitmq的文件里面的sbin目录输入cmd然后输入上面的命令

接着可以通过http://localhost:15672访问监控面板,用户名和密码默认都是guest。

如果访问监控面板是无法访问此页面,可以按Win+R键输入services.msc重新启动服务即可解决:

**补充:**如果你要在远程服务器访问监控面板就不能使用默认的用户名和密码,会被拦截,而是要创建一个管理员账号。

创建管理员账号方式:https://www.rabbitmq.com/docs/access-control

RabbitMQ入门

到java程序中编写示例代码演示看看,在pom.xml中引入rabbitmq依赖:

复制代码
<!--        rabbitmq客户端-->
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.28.0</version>
</dependency>

单向发送(1对1)

可参考官方文档:https://www.rabbitmq.com/tutorials/tutorial-one-java

生产者示例代码:

复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.nio.charset.StandardCharsets;

public class Send {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

消费者示例代码:

复制代码
public class Recv {

    private final static String QUEUE_NAME = "hello";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        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(), StandardCharsets.UTF_8);
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

**Channel :频道,**理解为操作消息队列的 client(比如 jdbcClient、redisClient),提供了和消息队列 server 建立通信的传输方法(为了复用连接,提高传输效率)。程序通过 channel 操作rabbitmq(收发消息)。

**DeliverCallback:**收到消息后,如何处理。

**channel.basicConsume():**用于启动和管理监听消费者的核心方法。

参数如下

queue:要消费的消息队列名称

autoAck:设置true为自动确认,消息一送达就自动确认;设置false为手动确认,需要调用channel.basic()手动确认

deliverCallback:收到消息后如何处理(回调DeliverCallback

cancelCallback:消费者被取消时如何处理的回调函数

**channel.basicPublishd():**发送消息的核心方法。

参数如下

exchange:要发送到的交换机名称,如何使用默认交换机则为""空字符串

routingKey:路由键,交换机根据类型和路由键将消息路由到队列,对于默认交换机的路由键就是队列名

props:消息属性,可以设置消息的持久化(MessageProperties.PERSISTENT_TEXT_PLAIN)、优先级、过期时间等。如果不需要特殊属性,可以传递 null

body:消息内容,消息体字节数组形式byte[]

**channel.queueDeclare()**创建消息队列的方法。

参数如下

queue:消息队列名称(注意,同名称的消息队列,只能用同样的参数创建一次)

durable:消息队列重启后,消息是否丢失(设置为true队列持久化重启服务后队列不丢失)

exclusive:是否只允许当前这个创建消息队列的连接操作消息队列

autoDelete:没有人用队列后,是否要删除队列

arguments:额外参数,用于设置其它高级特性

常用的 arguments 参数:

  • x-message-ttl: 队列中消息的存活时间(毫秒)。过期消息会被丢弃或进入死信队列。
  • x-expires: 队列在多久未被使用(毫秒)后自动删除。
  • x-max-length: 队列允许的消息最大条数。超过限制时,旧消息会被移除。
  • x-max-length-bytes: 队列允许的最大总字节数。超过限制时,旧消息会被移除。
  • x-dead-letter-exchange: 指定一个死信交换机,被拒绝、过期或超长的消息会被重新发布到此交换机。
  • x-dead-letter-routing-key: 与死信交换机搭配使用,指定死信消息的路由键。
  • x-max-priority: 支持消息优先级。队列会尝试优先处理优先级高的消息。
  • x-single-active-consumer: 设置为 true 时,队列只允许一个活跃的消费者,其他消费者作为备份。
  • x-queue-mode: 队列模式,如 "lazy"(惰性模式),将消息尽可能存储在磁盘上,减少内存使用。
  • x-queue-type: 队列类型,如 "quorum"(仲裁队列)或 "classic"(经典队列,默认)。

多消费者(1对多)

可参考官方文档:https://www.rabbitmq.com/tutorials/tutorial-two-java

生产者示例代码:

复制代码
public class MultiProducer {

    private static final String TASK_QUEUE_NAME = "multi_queue";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

            // 使用scanner快速便捷模拟生产者发送多条消息
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()) {
                String message = scanner.nextLine();
                channel.basicPublish("", TASK_QUEUE_NAME,
                                     MessageProperties.PERSISTENT_TEXT_PLAIN,
                                     message.getBytes("UTF-8"));
                System.out.println(" [x] Sent '" + message + "'");
            }
        }
    }

}

消费者示例代码:

复制代码
public class MultiConsumer {

    private static final String TASK_QUEUE_NAME = "multi_queue";

    public static void main(String[] argv) throws Exception {
        // 建立连接
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        final Connection connection = factory.newConnection();
        // 使用for循环模拟创建多个消费者
        for (int i = 0; i < 2; i++) {
            final Channel channel = connection.createChannel();

            channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

            // 每个消费者最多同时处理一个任务
            channel.basicQos(1);

            // 定义了如何处理消息
            int finalI = i;
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");

                try {
                    // 处理工作
                    System.out.println(" [x] Received '" + "编号:" + finalI + ":" + message + "'");
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                    // 停 20 秒,模拟机器处理能力有限
                    Thread.sleep(20000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                } finally {
                    System.out.println(" [x] Done");
                    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                }
            };
            // 开启消费监听
            channel.basicConsume(TASK_QUEUE_NAME, false, deliverCallback, consumerTag -> {
            });
        }
    }
}

消息队列的交换机

交换机的作用就是提供消息转发功能,类似于网络路由器要解决的问题,根据一定的规则决定怎么把消息转发(路由)到不同的队列上,好让消费者从不同的队列消费。

**绑定(Binding):**就是交换机和队列关联起来,也可以叫路由。

Fanout(广播)交换机

可参考官方文档:https://www.rabbitmq.com/tutorials/tutorial-three-java

Fanout交换机是将消息广播到所有绑定到该交换机的队列中,无论其路由键是什么。这意味着每个绑定的队列都会接收到相同的消息。

**适用场景:**适用于需要将消息广播到多个消费者的场景,比如实时更新或通知系统。

生产者示例代码:

复制代码
public class FanoutProducer {

  private static final String EXCHANGE_NAME = "fanout-exchange";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {
        // 创建交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String message = scanner.nextLine();
            channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
  }
}

消费者示例代码:

复制代码
public class FanoutConsumer {

  private static final String EXCHANGE_NAME = "fanout-exchange";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel1 = connection.createChannel();
    Channel channel2 = connection.createChannel();
    // 声明交换机
    channel1.exchangeDeclare(EXCHANGE_NAME, "fanout");
    // 创建队列,随机分配一个队列名称
    String queueName = "xiaolou_queue";
    channel1.queueDeclare(queueName, true, false, false, null);
    channel1.queueBind(queueName, EXCHANGE_NAME, "");

    String queueName2 = "xiaov_queue";
    channel2.queueDeclare(queueName2, true, false, false, null);
    channel2.queueBind(queueName2, EXCHANGE_NAME, "");
    channel2.queueBind(queueName2, EXCHANGE_NAME, "");

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

    DeliverCallback deliverCallback1 = (consumerTag, delivery) -> {
        String message = new String(delivery.getBody(), "UTF-8");
        System.out.println(" [小楼] Received '" + message + "'");
    };

    DeliverCallback deliverCallback2 = (consumerTag, delivery) -> {
      String message = new String(delivery.getBody(), "UTF-8");
      System.out.println(" [小v] Received '" + message + "'");
    };
    channel1.basicConsume(queueName, true, deliverCallback1, consumerTag -> { });
    channel2.basicConsume(queueName2, true, deliverCallback2, consumerTag -> { });
  }
}
Direct(直接)交换机

可参考官方文档:https://www.rabbitmq.com/tutorials/tutorial-four-java

Direct交换机是根据消息的路由键,将消息定向发送到绑定到该交换机的队列中。绑定的队列需要指定一个路由键。只有队列的路由键与消息的路由键完全匹配时,该消息才会被路由到这个队列。

**适用场景:**适用于只需将消息发送到特定队列的场景,比如特定用户或特定任务处理。

生产者示例代码:

复制代码
public class DirectProducer {

  private static final String EXCHANGE_NAME = "direct-exchange";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String userInput = scanner.nextLine();
            String[] strings = userInput.split(" ");
            if (strings.length < 1) {
                continue;
            }
            String message = strings[0];
            String routingKey = strings[1];

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
        }

    }
  }
  //..
}

消费者示例代码:

复制代码
public class DirectConsumer {

    private static final String EXCHANGE_NAME = "direct-exchange";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");

        // 创建队列,随机分配一个队列名称
        String queueName = "xiaolou_queue";
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, EXCHANGE_NAME, "xiaolou");

        // 创建队列,随机分配一个队列名称
        String queueName2 = "xiaov_queue";
        channel.queueDeclare(queueName2, true, false, false, null);
        channel.queueBind(queueName2, EXCHANGE_NAME, "xiaov");

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

        DeliverCallback xiaoyuDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [xiaolou] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        DeliverCallback xiaopiDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [xiaov] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        channel.basicConsume(queueName, true, xiaoyuDeliverCallback, consumerTag -> {
        });
        channel.basicConsume(queueName2, true, xiaopiDeliverCallback, consumerTag -> {
        });
    }
}
Topic(主题)交换机

可参考官方文档:https://www.rabbitmq.com/tutorials/tutorial-five-java

Topic交换机是根据消息的路由键来进行匹配,允许使用通配符(如 * 和 #)。其中,*用于匹配一个单词,#用于匹配零个或多个单词。因此,可以构建一些复杂的路由模式。

**适用场景:**适用于需要实现基于模式匹配的消息路由,比如日志系统中不同级别的日志消息的处理。

生产者示例代码:

复制代码
public class TopicProducer {

  private static final String EXCHANGE_NAME = "topic-exchange";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    try (Connection connection = factory.newConnection();
         Channel channel = connection.createChannel()) {

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            String userInput = scanner.nextLine();
            String[] strings = userInput.split(" ");
            if (strings.length < 1) {
                continue;
            }
            String message = strings[0];
            String routingKey = strings[1];

            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
        }
    }
  }
  //..
}

消费者示例代码:

复制代码
public class TopicConsumer {

  private static final String EXCHANGE_NAME = "topic-exchange";

  public static void main(String[] argv) throws Exception {
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost");
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();

    channel.exchangeDeclare(EXCHANGE_NAME, "topic");

      // 创建队列
      String queueName = "frontend_queue";
      channel.queueDeclare(queueName, true, false, false, null);
      channel.queueBind(queueName, EXCHANGE_NAME, "#.前端.#");

      // 创建队列
      String queueName2 = "backend_queue";
      channel.queueDeclare(queueName2, true, false, false, null);
      channel.queueBind(queueName2, EXCHANGE_NAME, "#.后端.#");

      // 创建队列
      String queueName3 = "test_queue";
      channel.queueDeclare(queueName3, true, false, false, null);
      channel.queueBind(queueName3, EXCHANGE_NAME, "#.测试.#");

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

      DeliverCallback xiaoaDeliverCallback = (consumerTag, delivery) -> {
          String message = new String(delivery.getBody(), "UTF-8");
          System.out.println(" [xiaoa] Received '" +
                  delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
      };

      DeliverCallback xiaobDeliverCallback = (consumerTag, delivery) -> {
          String message = new String(delivery.getBody(), "UTF-8");
          System.out.println(" [xiaob] Received '" +
                  delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
      };

      DeliverCallback xiaocDeliverCallback = (consumerTag, delivery) -> {
          String message = new String(delivery.getBody(), "UTF-8");
          System.out.println(" [xiaoc] Received '" +
                  delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
      };

      channel.basicConsume(queueName, true, xiaoaDeliverCallback, consumerTag -> {
      });
      channel.basicConsume(queueName2, true, xiaobDeliverCallback, consumerTag -> {
      });
      channel.basicConsume(queueName3, true, xiaocDeliverCallback, consumerTag -> {
      });
  }
}
Headers交换机

RabbitMQ中的headers交换机是一种特殊类型的交换机,使用消息头(headers)来路由消息。它与其他类型的交换机不同,不会根据路由键或者绑定键进行匹配,而是根据消息的headers信息进行匹配。

headers交换机的使用场景:通常是在需要根据消息头的多个属性进行匹配和路由的情况下。例如,你可能需要根据消息的多个属性(如消息的格式、类型、版本等)来选择不同的消费者进行处理。

headers交换机在RabbitMQ的默认配置下不常用,因为通常使用其他类型的交换机(如direct、topic、fanout)已经能够满足大多数的路由需求。只有在需要根据消息头进行复杂的匹配时才会使用headers交换机,性能差且复杂。因此,不推荐使用

死信队列

可参考官方文档:https://www.rabbitmq.com/docs/dlx

死信队列:专门处理死信的队列(它就是一个普通队列,只不过是专门用来处理死信的,你甚至可以理解这个队列的名称叫"死信队列")

为了保证消息的可靠性,比如每条消息都成功消费,需要提供一个容错机制,失败的消息怎么处理。

死信:过期的消息、拒收的消息、消息队列满了、处理失败的消息的统称

消息进入死信队列的情况条件:

1、消息被拒绝确认

2、消息因TTL过期

3、消息因队列长度限制被丢弃

4、消息被返回次数超过交付限制(在某些队列中,消息返回到队列的次数可能有一个默认的交付限制。这意味着如果消息被多次返回却始终无法被成功处理,它最终会被丢弃或移出队列)

死信交换机

专门给死信队列转发消息的交换机注意,它就是一个普通交换机,只不过是专门给死信队列发消息而已,理解为这个交换机的名称就叫"死信交换机")。也存在路由绑定死信可以通过死信交换机绑定到死信队列。

实现步骤:

1.创建死信交换机和死信队列,并绑定关系

2.给失败后需要容错处理的队列绑定死信交换机

3.需要容错处理的队列绑定转发规则,指定死信之后要转发到的死信队列

4.程序读取死信队列中的消息,进行处理

生产者示例代码:

复制代码
public class DlxDirectProducer {

    private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";
    private static final String WORK_EXCHANGE_NAME = "direct2-exchange";


    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
            // 声明死信交换机
            channel.exchangeDeclare(DEAD_EXCHANGE_NAME, "direct");

            // 创建队列,随机分配一个队列名称
            String queueName = "xiaolou_dlx_queue";
            channel.queueDeclare(queueName, true, false, false, null);
            channel.queueBind(queueName, DEAD_EXCHANGE_NAME, "xiaolou");

            String queueName2 = "v_dlx_queue";
            channel.queueDeclare(queueName2, true, false, false, null);
            channel.queueBind(queueName2, DEAD_EXCHANGE_NAME, "v");

            DeliverCallback xiaolouDeliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                // 拒绝消息
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                System.out.println(" [xiaolou] Received '" +
                        delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
            };

            DeliverCallback vDeliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8");
                // 拒绝消息
                channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
                System.out.println(" [v] Received '" +
                        delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
            };

            channel.basicConsume(queueName, false, xiaolouDeliverCallback, consumerTag -> {
            });
            channel.basicConsume(queueName2, false, vDeliverCallback, consumerTag -> {
            });


            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()) {
                String userInput = scanner.nextLine();
                String[] strings = userInput.split(" ");
                if (strings.length < 1) {
                    continue;
                }
                String message = strings[0];
                String routingKey = strings[1];

                channel.basicPublish(WORK_EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
                System.out.println(" [x] Sent '" + message + " with routing:" + routingKey + "'");
            }

        }
    }
    //..
}

消费者示例代码:

复制代码
public class DlxDirectConsumer {

    private static final String DEAD_EXCHANGE_NAME = "dlx-direct-exchange";

    private static final String WORK_EXCHANGE_NAME = "direct2-exchange";

    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("localhost");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(WORK_EXCHANGE_NAME, "direct");

        // 指定死信队列参数
        Map<String, Object> args = new HashMap<>();
        // 要绑定到哪个交换机
        args.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        // 指定死信要转发到哪个死信队列
        args.put("x-dead-letter-routing-key", "v");

        // 创建队列,随机分配一个队列名称
        String queueName = "xiaodog_queue";
        channel.queueDeclare(queueName, true, false, false, args);
        channel.queueBind(queueName, WORK_EXCHANGE_NAME, "xiaodog");

        Map<String, Object> args2 = new HashMap<>();
        args2.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        args2.put("x-dead-letter-routing-key", "xiaolou");

        // 创建队列,随机分配一个队列名称
        String queueName2 = "xiaocat_queue";
        channel.queueDeclare(queueName2, true, false, false, args2);
        channel.queueBind(queueName2, WORK_EXCHANGE_NAME, "xiaocat");

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

        DeliverCallback xiaolouDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            // 拒绝消息
            channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
            System.out.println(" [xiaodog] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        DeliverCallback xiaovDeliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            // 拒绝消息
            channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false);
            System.out.println(" [xiaocat] Received '" +
                    delivery.getEnvelope().getRoutingKey() + "':'" + message + "'");
        };

        channel.basicConsume(queueName, false, xiaolouDeliverCallback, consumerTag -> {
        });
        channel.basicConsume(queueName2, false, xiaovDeliverCallback, consumerTag -> {
        });
    }
}

RabbitMQ在实战中的实现步骤

上面的入门demo是使用的官方的客户端,下面我将在springboot项目中使用封装好的客户端Spring Boot RabbitMQ Starter。

首先在项目的pom.xml中引入starter-amqp依赖,要和你的springboot版本保持一致!

复制代码
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
    <version>2.7.2</version>
</dependency>

接着编写application.yml配置:

复制代码
spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

创建生产者:

复制代码
@Component
public class MessageProducer {

    @Resource
    private RabbitTemplate rabbitTemplate;

    public void sendMessage(String exchange, String routingKey, String message) {
        rabbitTemplate.convertAndSend(exchange, routingKey, message);
    }
}

创建消费者:

复制代码
@Component
@Slf4j
public class MessageConsumer {

    // 指定程序监听的队列和确认机制
    @RabbitListener(queues = {"code_queue"}, ackMode = "MANUAL")
    // @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag获取消息的唯一标识(delivery tag),
    // 用于后续的手动消息确认(ACK)或拒绝(NACK)操作,
    // 在消息处理失败时,可以使用 delivery tag 来拒绝消息,
    // 这是 RabbitMQ 消息确认机制中的重要组成部分,确保消息能够被正确处理,避免消息丢失。
    public void receiveMessage(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        log.info("Received message: {}", message);
        try {
            channel.basicAck(deliveryTag, false);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

创建交换机和队列:

复制代码
/**
 * 用于创建测试程序用到的交换机和队列(只用在程序启动前执行一次)
 */
public class MqInitMain {

    public static void main(String[] args) {
        try {
            ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            String EXCHANGE_NAME = "code_exchange";
            channel.exchangeDeclare(EXCHANGE_NAME, "direct");

            // 创建队列,随机分配一个队列名称
            String queueName = "code_queue";
            channel.queueDeclare(queueName, true, false, false, null);
            channel.queueBind(queueName, EXCHANGE_NAME, "my_routingKey");
        } catch (Exception e) {

        }

    }
}

编写单元测试来测试结果:

复制代码
@SpringBootTest
class MyMessageProducerTest {

    @Resource
    private MessageProducer messageProducer;

    @Test
    void sendMessage() {
        messageProducer.sendMessage("code_exchange", "my_routingKey", "你好");
    }
}

怎么测试是否成功呢?首先只要执行一次main然后停掉,查看控制面板如下图:

接着启动MainApplication启动类,再启动MyMessageProducerTest测试类,查看效果:

成功收到"你好"消息。

可以参考上述步骤,对项目的交换机、队列等参数进行替换,并且改造业务代码逻辑来实现RabbitMQ。

相关推荐
Remember_9932 小时前
【数据结构】深入理解Map和Set:从搜索树到哈希表的完整解析
java·开发语言·数据结构·算法·leetcode·哈希算法·散列表
小北方城市网2 小时前
接口性能优化实战:从秒级到毫秒级
java·spring boot·redis·后端·python·性能优化
小北方城市网2 小时前
Redis 缓存设计与避坑实战:解决穿透 / 击穿 / 雪崩
java·大数据·数据库·redis·python·elasticsearch·缓存
jiayong232 小时前
MINA框架面试题 - 进阶篇
java·io·mina
鸡蛋豆腐仙子2 小时前
Spring的AOP失效场景
java·后端·spring
郑州光合科技余经理2 小时前
O2O上门预约小程序:全栈解决方案
java·大数据·开发语言·人工智能·小程序·uni-app·php
roo_12 小时前
JAVA学习-MAC搭建java环境和spring boot搭建
java·学习·macos
小北方城市网2 小时前
SpringBoot 全局异常处理最佳实践:从混乱到规范
java·spring boot·后端·spring·rabbitmq·mybatis·java-rabbitmq