RabbitMQ教程-基础篇(合集)

简单的消息交互案例

1 依赖引入

<!-- 引入RabbitMQ客户端依赖 -->
<dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
</dependency>

2 设置连接MQ的工具类

public class RabbitConnectionUtil {
    public static Connection getConnection() throws IOException, TimeoutException {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 工厂IP,连接RabbitMQ的队列
        factory.setHost("192.168.190.129");
        // 注意,连接端口是5672,Linux端的防火墙需要开放该端口
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");
        // 创建连接
        return factory.newConnection();
    }
}

3 创建消息发送者

实例代码:
public class RabbitMQProducer {
    public static final String QUEUE_NAME = "RabbitName";
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        // 获取信道
        Channel channel = connection.createChannel();

        /**
         * 生成队列
         * queue 队列名称
         * durable 是否持久化
         * exclusive  是否独占队列
         * autoDelete 是否自动删除队列(服务器不再使用时自动删除队列)
         * arguments 队列参数
         */
        HashMap<String, Object> param = new HashMap<>();
        param.put("name", "梦尘");
        param.put("action", "添加了一个消息");
        channel.queueDeclare(QUEUE_NAME, false, false, false, param);

        /**
         * exchange 交换机
         * 路由的key值
         * 其他参数信息
         * 发送消息的消息体
         */
        String message = "给您发了一条消息";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
        System.out.println("消息发送完毕!");
    }
}
运行成功:

由运行可以看出,队列中接收到一条为未被消费的消息。

4 创建消息接受者

实例代码:

public class RabbitMQConsumer {
    public static String QUEUE_NAME = "RabbitName";
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 接收消息回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            System.out.println(new String(message.getBody()));
        };
        // 取消消息回调
        CancelCallback cancelCallback = consumerTag -> {
            System.out.println("消息消费中断!");
        };
        /**
         * queue 消费队列名称
         * autoOk 消费成功后是否自动应答
         * consumer 消费者未成功消费回调
         * 消费者取消回调
         */
        channel.basicConsume(String.valueOf(QUEUE_NAME), true, deliverCallback, cancelCallback);
    }
}

运行结果:

5 常见错误解决

connection error; protocol method: #method<connection.close>(reply-code=530, reply-text=N

因为没有设置系统默认的Virtua lHost,如下图
解决:为用户设置permission

RabbitMQ工作队列

工作队列(又称任务队列)的主要思想是为了避免立即执行资源密集型任务,而不得不等待他完成。其作用就是将需要处理的任务放在队列(先进先出)中,一个工作进程可以取出任务并完成工作。如果启动了多个工作进程,那么这些工作线程会一起处理这些任务。
好比工厂的流水线,如果流水线上有300个产品,而工位上只有一位工人在给产品加工,可想而知其效率低下。如果在流水线后多安排几个工位,那么就可以提高产品加工速度(注意:一个产品只能被加工一次,不能被多次处理,要防止重复“加工”)。

轮询发消息

概念

rabbitmq队列处理机制默认是轮询的,即轮流按工作顺序将任务分配给工作线程(循环分配任务)。

实例代码

生产者首先往队列中加入15条消息,成功加入队列后等待消费者消费。生产者代码如下:

public static final String QUEUE_NAME = "rabbit_queue";
public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        for (int i = 0; i < 15; i++) {
            /**
             * 生成队列
             * queue 队列名称
             * durable 是否持久化
             * exclusive 是否独占队列
             * autoDelete 是否自动删除队列(服务器不再使用时自动删除队列)
             * arguments 队列参数
             */
            HashMap<String, Object> param = new HashMap<>();
            param.put("name", "梦尘");
            param.put("action", "添加了一个消息");
            channel.queueDeclare(QUEUE_NAME, false, false, false, param);

            /**
             * exchange 交换机
             * 路由的key值
             * 其他参数信息
             * 发送消息的消息体
             */
            String message = "给您发了一条消息";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println("消息发送完毕!");
        }
    }

创建5个消费者,监听到队列中有任务时进行工作。消费者代码如下:

public class Work01 implements Runnable {
    public static final String QUEUE_NAME = "rabbit_queue";

    public static void main(String[] args) {
        Runnable work01 = new Work01();
        new Thread(work01, "A").start();
        new Thread(work01, "B").start();
        new Thread(work01, "C").start();
        new Thread(work01, "D").start();
        new Thread(work01, "E").start();
    }

    @Override
    public void run() {
        try {
            String threadName = Thread.currentThread().getName();
            Connection connection = RabbitConnectionUtil.getConnection();
            Channel channel = connection.createChannel();
            // 消息接收
            DeliverCallback deliverCallback = (consumerTag, message) -> {
                System.out.println("Work01 => " + threadName + "接收到消息:" + new String(message.getBody()));
            };
            // 消息取消监听
            CancelCallback cancelCallback = (consumerTag) -> {
                System.out.println(consumerTag + "消息者取消消息发送!");
            };
            // 消息发送
            try {
                channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } catch (IOException | TimeoutException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:
可以看到里面成功添加了15条消息,接下来运行消费者查询观察消费过程。
消费者运行结果:

总结

工作队列是学习RabbitMQ的一个非常重要知识点,通过利用队列的先进先出的特点,将工作顺序分配给工作线程执行。

思考

如果D任务在被消费者处理过程中因某些原因出现bug导致任务中断,而这时候队列已经将任务分配出去并清除了队列里的任务D,这就意味着我们丢失了这条未处理完毕的任务,那么我们该怎么防止任务丢失的情况?

消息应答

概念

根据上一篇文章的思考中得知,如果任务在执行过程中发生故障导致任务中断,并且这条任务也被队列清除掉了,也就是意味着我们就丢失了一条未被执行完的任务。
消息应答的出现可以保证消息在发送过程中不丢失。其工作原理就是,消费者在接收到了队列分配的任务并且处理完这条任务后,告诉RabbitMQ它已经处理完这条任务,可以从队列中清除掉这条任务了。

自动应答

Rabbit MQ默认的应答机制就是自动应答。消息发送后立即被认为已经传送成功。即当消费者接收到消息后立即通知RabbitMQ,Rabbit MQ此时就可以直接从队列中移除该消息。
缺点:很可能会造成消息没有被完成处理就丢失消息,如:

  1. 接收过多,来不及被处理的消息,导致大量消息积压而耗尽内存,最终被操作系统强行终止(线程/进程终止任务)。

  2. 消息处理过程出现问题(Bug),导致程序抛出异常造成消息未处理完毕,而这条消息因为已经被自动应答默认从队列中移除了。

手动应答

手动应答的出现可以很好的处理自动应答的缺点,它可以通过手动控制应答机制,将当前工作线程处理失败的消息通知Rabbit MQ,转交给其他工作线程处理,减低消息丢失的机率。手动应答还可以进行批量应答,减少了网络拥堵的情况,但可能会造成消息不会被完全处理。
生产者:

/**
*
* @author xgs87762
*/
public class RabbitMQAskProducer {
 private static final String TAG = "RabbitMQAsk";
 public static void main(String[] args) throws IOException, TimeoutException {
     Connection connection = RabbitConnectionUtil.getConnection();
     Channel channel = connection.createChannel();
     Map<String, Object> params = new HashMap<>();
     params.put("author", "代码の诱惑");
     params.put("age", "22");

     for (int i = 1; i <= 50; i++) {
         /**
          * 生成队列
          * queue 队列名称
          * durable 是否持久化
          * exclusive 是否独占队列
          * autoDelete 是否自动删除队列(服务器不再使用时自动删除队列)
          * arguments 队列参数
          */
         channel.queueDeclare(TAG, false, false, false, params);
         String message = "发布消息";
         channel.basicPublish("", TAG, null, message.getBytes(StandardCharsets.UTF_8));
         System.out.println("发送了" + i + "条消息");
     }
 }
}

消费者

/**
* RabbitMQAskCustomer.java
* TODO
* Created on 2023/2/26 18:22
* 1、测试工作线程消费情况
* 2、测试工作线程出现异常,任务自动分配给其他正常工作线程消费
* @author xgs87762
* @version V1.0
*/
public class RabbitMQAskCustomer implements Runnable {
 // 队列名称
 private static final String TAG = "RabbitMQAsk";
 // 计算已执行成功任务数
 private static Integer counter = 0;

 public static void main(String[] args) {
     Runnable askCustomer = new RabbitMQAskCustomer();
     for (int i = 1; i <= 10; i++) {
         new Thread(askCustomer, "线程" + i).start();
     }
 }

 @Override
 public void run() {
     try {
         String threadName = Thread.currentThread().getName();
         Connection connection = RabbitConnectionUtil.getConnection();
         Channel channel = connection.createChannel();
         DeliverCallback deliverCallback = (consumerTag, message) -> {
             // 消息接收监听
             System.out.println(threadName + " ==> 准备开展工作...");
             RabbitMQAskCustomer.thread();

             // 测试:测试线程执行失败,观察工作任务情况
             // 结论:线程8出现问题后,队列自动将工作任务分配给了其他线程消费。
             if ("线程8".equals(threadName)) {
                 throw new IOException("Thread " + threadName + "异常");
             }
             System.out.println(threadName + " ==> 工作执行完毕!");

             /**
              * 消息应答
              * deliverTag 消息的标记 tag,表明消息的唯一标识
              * multiple 是否批量应答(一般不允许应答,防止消息丢失)
              */
             channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
             System.out.println(threadName + " ==> 消息应答提交完毕!");
             counter++;
             System.out.println("已执行 》" + counter + "《 条任务!");
         };
         CancelCallback cancelCallback = (consumerTag) -> {
             System.out.println(threadName + " ==> 消息取消");
         };
         channel.basicConsume(TAG, false, deliverCallback, cancelCallback);
     } catch (IOException | TimeoutException e) {
         e.printStackTrace();
     }
 }

 public static void thread() {
     try {
         Thread.sleep(2000);
     } catch (InterruptedException e) {
         throw new RuntimeException(e);
     }
 }
}

消息持久化

概念

RabbitMQ在新增队列后,MQ在正常运行状态下队列是保持运行的,但是,当RabbitMQ出现某种原因崩溃后出现重启或关闭后,队列及消息就会被清除。为防止队列和消息丢失,因而需要将队列和消息标记为持久化。

实例代码

消息提供者:

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

public class DurableProducer {
    public static final String QUEUE_NAME = "DURABLE_QUEUE";
    public static void main(String[] args) {
        try {
            Connection connection = RabbitConnectionUtil.getConnection();
            Channel channel = connection.createChannel();

            /**
             * @param queue Queue队列的名称
             * @param durable 持久化,如果我们声明的是持久队列(该队列将在服务器重启后存活)。
             * @param exclusive 声明独占队列(仅限于此连接)。
             * @param autoDelete 如果我们声明一个自动删除队列,则为True(服务器将在不再使用时将其删除)。
             * @param arguments 参数队列的其他属性(构造参数)
             */
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            // 动态发送消息
            Scanner scanner = new Scanner(System.in);
            String message = "";
            while (scanner.hasNext()) {
                message = scanner.next();
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
                System.out.println("[任务提供者]:" + message);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } catch (TimeoutException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果

重启前:

注:重启前把java程序停掉,队列显示结果更明显

重启后:

运行命令重启MQ

  1. systemctl restart rabbitmq-server

不公平分发

不公平分发可以理解为能者多劳,按照消费者的处理能力进行消息分配(处理快,消息分配多),以此提高队列消息处理速度,充分利用了消费者的性能。一般情况下会使用不公平分发,而不使用轮询。

  1. // 消费方设置prefetch = 1,即开启不公平分发

  2. channel.basicQos(1);

预取值

可以为消费者指定分配固定任务数量,假设有七个任务,可以给A分配2个任务,给B分配4个任务。

// 消费者A:设置prefetch = 2
channel.basicQos(2);
// 消费者B:设置 prefetch=4
channel.basicQos(4);

发布确认

消费者能消费消息的前提是,提供者能正确推送消息到队列中。然而,如果在提供者往队列中推送消息时,RabbitMQ出现某些突发意外,比如重启,导致消息推送时丢失,这时就需要手动处理和恢复推送失败的消息。

三种发布确认方式:

  1. 单个确认

问题排查相对简单,但效率低。

  1. 批量确认

出现问题不易定位具体问题。执行效率高。

  1. 异步确认

发布消息的同时,采用异步确认方式提高执行效率。

实例代码:

单次发布确认
public static void producerSingleton() throws Exception {
        Connection connection = RabbitConnectionUtil.getConnection();
        // 队列声明
        Channel channel = connection.createChannel();
        channel.queueDeclare(queenName, true, false, false, new HashMap<>());
        // 开启发布确认
        channel.confirmSelect();
        long startTime = System.currentTimeMillis();

        for (int i = 0; i < SEND_COUNT; i++) {
            String sendMessage = "第<" + i + ">消息  ";
            channel.basicPublish("", queenName, null, sendMessage.getBytes(StandardCharsets.UTF_8));
            // 单个消息马上进行发布确认
            boolean flag = channel.waitForConfirms();
            if (flag) {
                System.out.println("消息发送成功!");
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("单个发布确认:耗时:" + (endTime - startTime));
}
批量确认
public static void producerBatch() {
        try {
            Connection connection = RabbitConnectionUtil.getConnection();
            // 队列声明
            Channel channel = connection.createChannel();
            channel.queueDeclare(queenName, true, false, false, new HashMap<>());
            // 开启发布确认
            channel.confirmSelect();
            long startTime = System.currentTimeMillis();

            // 50条确认一次
            int confirmPoint = 50;

            for (int i = 0; i < SEND_COUNT; i++) {
                String sendMessage = "第<" + i + ">消息  ";
                channel.basicPublish("", queenName, null, sendMessage.getBytes(StandardCharsets.UTF_8));
                // 批量确认
                if (i % confirmPoint == 0) {
                    channel.waitForConfirms();
//                    System.out.println("消息发送成功!");
                }
            }
            long endTime = System.currentTimeMillis();
            System.out.println("批量发布确认:耗时:" + (endTime - startTime));
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        }
}
异步确认
public static void producerSync() {
        try {
            Connection connection = RabbitConnectionUtil.getConnection();
            // 队列声明
            Channel channel = connection.createChannel();
            channel.queueDeclare(queenName, true, false, false, new HashMap<>());
            // 开启发布确认
            channel.confirmSelect();
            long startTime = System.currentTimeMillis();

            // 消息确认成功回调 (消息标记,是否批量确认)
            ConfirmCallback ackCallback = (deliverTag, multiple) -> {
                System.out.println("消息监听成功" + deliverTag);
            };

            // 消息确认失败回调 (消息标记,是否批量确认)
            ConfirmCallback nackCallback = (deliverTag, multiple) -> {
                System.out.println("监听失败 ==> " + deliverTag);
            };

            // 准备消息监听器,监听哪些消息状态
            channel.addConfirmListener(ackCallback, nackCallback);

            for (int i = 0; i < SEND_COUNT; i++) {
                String sendMessage = "第<" + i + ">消息  ";
                channel.basicPublish("", queenName, null, sendMessage.getBytes(StandardCharsets.UTF_8));
            }
            long endTime = System.currentTimeMillis();
            System.out.println("批量发布确认:耗时:" + (endTime - startTime));
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        }
}

测试:

    public static void main(String[] args) throws Exception {
        // 1、单个确认: 可以快速定位确认失败位置,效率低
        producerSingleton();
        // 2、批量确认:不好定位确认位置,效率高
        producerBatch();
        // 3、异步批量确认: 兼容二者,效率高,可监听消息发送状态
        producerSync();

    }

交换机(Exchanges)

在RabbitMQ中,生产者发送消息是不会直接将消息直接推送到消息推送到队列中的,实际上推送的过程是交由交换机推送到队列中(如果没有定义交换机会使用MQ默认的交换机推送),生产者只能推送消息到交换机中。其中,在之前的推送中,队列中的每条消息只能被消费者消费一次,通过交换机我们就可以实现把消息推到不同的队列,进而实现多个消费者消相同的消息。
注:交换机可以在提供者/消费者任意一端声明。
以下案例建议先启动消费者开启监听,后启动消息提供者,方便测试结果。

交换机类型

直接交换机(Direct Exchange)

通过routingKey和bindingKey将交换机和路由绑定,交换机通过对比Key值将消息推送到指定的队列中。

消息提供者
public class DirectProduct {
    private static final String EXCHANGE_NAME = "DIRECT_EXCHANGE";
    private static final String routingKey = "direct_key";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String message = "测试一条消息";
        channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes(StandardCharsets.UTF_8));
        System.out.println("[提供者] ==> 推送成功!");
    }
}
消费者
public class DirectCustomer {
    private static final String EXCHANGE_NAME = "DIRECT_EXCHANGE";
    private static final String routingKey = "direct_key";
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 获取临时队列,队列消息消费完后自动删除
        String queueName = channel.queueDeclare().getQueue();
        // 绑定队列
        channel.queueBind(queueName, EXCHANGE_NAME, routingKey);
        DeliverCallback deliverCallback = (tag, message) -> {
            System.out.println("[消费者] ==> " + new String(message.getBody()));
        };
        CancelCallback cancelCallback = (tag) -> {
            System.out.println("消息接收失败");
        };
        // 绑定交换机和队列
        channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
    }
}
测试

消费者启动(先启动,方便监听)

提供者启动

主题交换机(Topic Exchange)

将路由按模式匹配,可以通过设置 # 或 ***** 对队列中定义的routingKey模糊匹配,匹配成功后将消息转入队列
**#:**匹配一个或多个词,如:ta.#.te == ta.tb.tc.td.te
**:**匹配一个词,如:td..te == td.tf.te

消息提供者
public class TopicProducer {
    private static final String EXCHANGE_NAME = "EXCHANGE_NAME_TOPIC";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

        // 输入
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("请输入发送消息:");
            String message = scanner.nextLine();
            if (Objects.equals(message, "exit")){
                break;
            }

            System.out.println("请输入发送路由:");
            String routerKey = scanner.nextLine();
            channel.basicPublish(EXCHANGE_NAME, routerKey, null, message.getBytes(StandardCharsets.UTF_8));
            System.out.println("[主题交换机<" + routerKey + ">]: " + message);

        }
    }
}
消费者A
public class TopicCustomer_A {
    private static final String EXCHANGE_NAME = "EXCHANGE_NAME_TOPIC";
    public static final String CODER_LURE = "coder.#.lure";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
        String queueName = channel.queueDeclare().getQueue();

        // 绑定交换机和队列
        channel.queueBind(queueName, EXCHANGE_NAME, CODER_LURE);

        System.out.println("[主题交换机<消费者A>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> 监听开始!");
        DeliverCallback deliverCallback = (tag, message) -> {
            String messageContent = "[主题交换机<消费者A>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> " + new String(message.getBody());
            System.out.println(messageContent);
        };
        CancelCallback cancelCallback = (tag) -> {
            System.out.println("[主题交换机<消费者A>] ==<接收取消,routingKey:[" + CODER_LURE + "]>==> 失败!");
        };
        // 接收消息
        channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
    }
}
消费者B
public class TopicCustomer_B {
    private static final String EXCHANGE_NAME = "EXCHANGE_NAME_TOPIC";
    public static final String CODER_LURE = "*.coder.lure";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
        String queueName = channel.queueDeclare().getQueue();

        // 绑定交换机和队列
        channel.queueBind(queueName, EXCHANGE_NAME, CODER_LURE);

        System.out.println("[主题交换机<消费者B>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> 监听开始!");
        DeliverCallback deliverCallback = (tag, message) -> {
            String messageContent = "[主题交换机<消费者B>] ==<接收成功,routingKey:[" + CODER_LURE + "]>==> " + new String(message.getBody());
            System.out.println(messageContent);
        };
        CancelCallback cancelCallback = (tag) -> {
            System.out.println("[主题交换机<消费者B>] ==<接收取消,routingKey:[" + CODER_LURE + "]>==> 失败!");
        };
        // 接收消息
        channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
    }
}
测试:

启动提供者方法,控制台信息如下:
消费者A接收

消费者B接收

首部交换机(Headers Exchange)

首部交换机和扇出交换机都不需要路由键(routingKey),交换机时通过headers头部来将消息映射到队列中。其中Hash结构中要求携带一个键 ‘x-match’ ,这个键值可以是any或者all。
any:只要在发布消息是携带有一对键值对headers满足队列定义的多个参数arguments中其中一个,就能匹配上。(需要key:value完全匹配才行)
all:需要所有entry和绑定在队列上的所有entry完全匹配

消息提供者
/**
 * HeadersProducer
 *
 * @author codeの诱惑
 * @version 1.0.0
 * @Descript 首部交换机:
 * 首部交换机和扇出交换机都不需要路由键(routingKey),交换机时通过headers头部来将消息映射到队列中
 * 其中Hash结构中要求携带一个键 ‘x-match’ ,这个键值可以是any或者all
 * any:只要在发布消息是携带有一对键值对headers满足队列定义的多个参数arguments中其中一个,就能匹配上。(需要key:value完全匹配才行)
 * all:需要所有entry和绑定在队列上的所有entry完全匹配
 * Created on 2023/5/2 17:42
 */
public class HeadersProducer {
    private static final String EXCHANGE_NAME = "HEADERS_EXCHANGE";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection conn = RabbitConnectionUtil.getConnection();
        Channel channel = conn.createChannel();
        // 声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.HEADERS);
        Map<String, Object> headers = new HashMap<String, Object>();
        headers.put("author", "coderの诱惑");
        headers.put("exchangeName", "Headers Exchange");
        headers.put("version", "v1.0");
        System.out.println("发送一条消息");
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().headers(headers).build();
        String message = "hello world!";
        channel.basicPublish(EXCHANGE_NAME, "", properties, message.getBytes(StandardCharsets.UTF_8));
    }
}
消费者
/**
 * HeaderExchangeCustomer
 *
 * @author codeの诱惑
 * @version 1.0.0
 * @Descript 首部交换机
 * Created on 2023/5/3 0:11
 */
public class HeaderExchangeCustomer {
    private static final String EXCHANGE_NAME = "HEADERS_EXCHANGE";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection conn = RabbitConnectionUtil.getConnection();
        Channel channel = conn.createChannel();
        Map<String, Object> arguments = new HashMap<String, Object>();
        // arguments.put("x-match", "all");
        // 测试不匹配
        arguments.put("all", "测试不同headers,不匹配");

        arguments.put("x-match", "any");
        arguments.put("author", "coderの诱惑");
        arguments.put("exchangeName", "Headers Exchange");
        arguments.put("version", "v1.0");

        String queueName = channel.queueDeclare().getQueue();
        channel.queueBind(queueName, EXCHANGE_NAME, "", arguments);
        System.out.println("[首部交换机]:消费者等待消息中...");
        DeliverCallback deliverCallback = (tag, message) -> {
            byte[] body = message.getBody();
            String string = new String(body);
            System.out.println(string);
        };
        CancelCallback cancelCallback = (tag) -> {
            System.out.println("取消");
        };
        channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
    }
}
测试

执行提供者后,消费者监听结果如下:

扇出交换机(Fanount Exchange)

以广播的形式向队列推送消息,即类比通过广播发布通知,所有人接收通知内容。
以下以日志输出为案例,通过推送日志内容,一个负责将日志打印控制台,一个通过负责将日志写入日志文件当中。

消息提供者
  1. public class ExProducer {
 private static final String EXCHANGE_NAME = "EXCHANGE_NAME_LOG";
 public static void main(String[] args) throws IOException, TimeoutException {
     Connection connection = RabbitConnectionUtil.getConnection();
     Channel channel = connection.createChannel();
     channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
     Scanner scanner = new Scanner(System.in);
     while (scanner.hasNext()) {
         String inputText = scanner.next();
         channel.basicPublish(EXCHANGE_NAME, "", null, inputText.getBytes(StandardCharsets.UTF_8));
         System.out.println("[生产者输出日志]: " + inputText);
     }
 }
}
消费者(控制台输出)
public class ExConsumerForConsoleLog {
 private static final String EXCHANGE_NAME = "EXCHANGE_NAME_LOG";
 public static void main(String[] args) throws IOException, TimeoutException {
     Connection connection = RabbitConnectionUtil.getConnection();
     Channel channel = connection.createChannel();
     /**
      * @param exchange 交换机名称
      * @param type 交换机类型
      */
     channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
     // 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
     String queueName = channel.queueDeclare().getQueue();
     // 绑定交换机和队列
     /**
       * @param queueName 队列名称
       * @param exchangeName 交换机名称
       * @param routingKey 路由key(由于扇出交换机是以广播形式推送消息到队列,routingKey可以为空
       *                   ,即可以忽略routingKey值
       *                   ,忽略后会向所有队列推送,如果指定了routingKey,那就和 ‘直接交换机’ 差不多了)
       */
     channel.queueBind(queueName, EXCHANGE_NAME, "");
     System.out.println("等待接收队列消息, 打印控制台...");

     DeliverCallback deliverCallback = (tag, message) -> {
         System.out.println("[控制台<消费者A>] ==<接收成功>==> " + new String(message.getBody()));
     };
     CancelCallback cancelCallback = (tag) -> {
         System.out.println("[控制台<消费者A>] ==<接收取消>==> 失败!");
     };
     // 接收消息
     channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
 }
}
消费者(日志文件输出)
public class ExConsumerForFileLog {
 private static final String EXCHANGE_NAME = "EXCHANGE_NAME_LOG";
 public static void main(String[] args) throws IOException, TimeoutException {
     Connection connection = RabbitConnectionUtil.getConnection();
     Channel channel = connection.createChannel();
     /**
      * @param exchange 交换机名称
      * @param type 交换机类型
      */
     channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);

     // 声明一个临时队列,队列名称随机,当消费者端口与队列的连接时,队列自动删除
     String queueName = channel.queueDeclare().getQueue();

     // 绑定交换机和队列
     /**
      * @param queueName 队列名称
      * @param exchangeName 交换机名称
      * @param routingKey 路由key(由于扇出交换机是以广播形式推送消息到队列,routingKey可以为空
      *                   ,即可以忽略routingKey值
      *                   ,忽略后会向所有队列推送,如果指定了routingKey,那就和 ‘直接交换机’ 差不多了)
      */
     channel.queueBind(queueName, EXCHANGE_NAME, "");
     System.out.println("等待接收队列消息, 写入日志文件...");

     DeliverCallback deliverCallback = (tag, message) -> {
         String messageContent = "[日志文件<消费者B>] ==<接收成功>==> " + new String(message.getBody());
         SimpleDateFormat sf = new SimpleDateFormat("yyyyMMddHHmmss");
         String date = sf.format(new Date());
         System.out.println(messageContent);
         writeFile(messageContent, "" + date + ".log");
     };
     CancelCallback cancelCallback = (tag) -> {
         System.out.println("[日志文件<消费者B>] ==<接收取消>==> 失败!");
     };
     // 接收消息
     channel.basicConsume(queueName, true, deliverCallback, cancelCallback);
 }

 public static void writeFile(String message, String path) {
     try {
         String basePath = new String(System.getProperty("user.dir").getBytes(StandardCharsets.UTF_8)) + "\\RabbitMQ-simple\\src\\main\\java\\com\\mc\\exchange\\logs\\";
         String absolutePath = basePath + path;
         System.out.println("存储路径:" + absolutePath);

         File Folder = new File(basePath);
         File file = new File(absolutePath);
         if (!Folder.exists()) {
             Folder.mkdirs();
         }
         if (!file.exists()) {
             file.createNewFile();
         }
         OutputStream out = new FileOutputStream(file);
         byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
         out.write(bytes);
         out.close();
     } catch (IOException e) {
         e.printStackTrace();
     }
 }
}
测试

消息提供者控制台案例
消费者输出控制台

消费者输出日志文件

默认交换机(Default Exchange)

默认交换机实际和直接交换机一样,只不过默认交换机并不用特意声明交换机名称和routingKey 。以下不作案例演示。

消息存活期(DDL)

在消息提供者发送消息时,给消息设置存活时间,当消息在该时间段内没有被消费,则该消息就会被自动清除。

MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("5000"); // 消息的存活时间为 5 秒
Message message = new Message("Hello World".getBytes(), messageProperties);
rabbitTemplate.convertAndSend("myQueue", message);

死信队列(DLX)

消费者在消费消息过程中出现意外导致消费失败即称为“死信”。而不希望这些执行失败的消息丢失,需要存储到另一个队列当中时,这种队列即为“死信队列”。
消息出现一下情况会导致 “死信”

  1. 消息被否定,消费者使用 channel.basicNack或channel.basicReject,并且requeue被设置为false时。

  2. 消息超过过期时间。消息在队列的存活时间超过了设置的生存时间(TTL)

  3. 消息数量超出队列长度(消息溢出)

以下将以“消息溢出”为案例

消费者

public class NormalConsumer {
    // 死信队列名
    private static final String DEAD_QUEUE_NAME = "dead-queue";
    // 死信交换机
    private static final String DEAD_EXCHANGE_NAME = "dead-exchange";
    // 普通队列名称
    private static final String NORMAL_QUEUE_NAME = "normal-queue";
    // 普通交换机名称
    private static final String NORMAL_EXCHANGE_NAME = "normal-exchange";
    public static final String NORMAL_ROUTING_KEY = "normal-demo";
    public static final String DEAD_ROUTING_KEY = "dead-demo";

    public static void main(String[] args) throws Exception, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 声明交换机
        channel.exchangeDeclare(DEAD_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(NORMAL_EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        // 声明队列
        Map<String, Object> normalArguments = new HashMap<>();
        // 死信队列绑定配置:死信交换机、死信routingKey、普通队列长度限制(超出长度变为死信)
        normalArguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        normalArguments.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);
        normalArguments.put("x-max-length", 4);
        channel.queueDeclare(NORMAL_QUEUE_NAME, false, false, false, normalArguments);

        // 声明死信队列
        Map<String, Object> deadArguments = new HashMap<>();
        channel.queueDeclare(DEAD_QUEUE_NAME, false, false, false, deadArguments);

        // 绑定普通交换机和普通队列
        channel.queueBind(NORMAL_QUEUE_NAME, NORMAL_EXCHANGE_NAME, NORMAL_ROUTING_KEY);
        // 绑定死信交换机和死信队列
        channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, DEAD_ROUTING_KEY);

        System.out.println("normal consumer:waiting message...");

        //消息消费成功之后的回调
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            String messageStr = new String(message.getBody(), StandardCharsets.UTF_8);
            if (messageStr.contains("5")) {
                System.out.println("mock ==> 请手动停止NomalConsumer程序,重启消费者,模拟普通队列的消息超出长度,导致死信");
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("normal consumer:" + messageStr);
        };
        channel.basicConsume(NORMAL_QUEUE_NAME, true, deliverCallback, consumer -> {
        });
    }
}

死信消费者

public class DeadConsumer {
    // 死信队列名
    private static final String DEAD_QUEUE_NAME = "dead-queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        System.out.println("dead consumer: waiting message...");
        DeliverCallback deadCallback = (consumerTag, message) -> {
            System.out.println("dead consumer: " + new String(message.getBody()));
        };
        channel.basicConsume(DEAD_QUEUE_NAME, true, deadCallback, consumerTag -> {
        });
    }
}

消息提供者

public class NormalAndDeadProducer {
    // 普通交换机名称
    private static final String NORMAL_EXCHANGE_NAME = "normal-exchange";
    public static final String NORMAL_ROUTING_KEY = "normal-demo";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        // 死信消息TTL时间
        AMQP.BasicProperties properties = new AMQP.BasicProperties()
                .builder()
                .expiration("10000")
                .build();

        for (int i = 0; i < 10; i++) {
            String message = "message info index is " + i;
            channel.basicPublish(NORMAL_EXCHANGE_NAME, NORMAL_ROUTING_KEY, properties, message.getBytes(StandardCharsets.UTF_8));
        }

    }
}

请按顺序执行以下步骤:

  1. 启动 消费者(NormalConsumer)
  2. 启动 死信消费者(DeadConsumer)
  3. 启动 消息提供者(NormalAndDeadProducer)
  4. 此刻可以看到消费者消费到了一半,这时立即关闭消费者程序的运行

  1. 重新启动一次 消息提供者,造成队列消息溢出
  2. 查看 死信消费者 控制台,可以看到,有一部分消息进入了死信队列并被 死信消费者 消费了
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值