Rabbitmq超级详细的笔记,包括安装,基本命令,rabbitmq的七种消息模式,以及死信队列,延迟队列,优先级队列和惰性队列的介绍

RabbitMQ

1 RabbitMQ介绍

1.1 基本介绍

  • RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。

image-20220516163319738

  • 名词介绍

    • producer:用于发送消息
    • connect:publisher / consumer和 broker之间的TCP连接
    • channel:如果每一次访问 RabbitMQ 都建立一个Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id 帮助客户端和message broker 识别 channel,所以channel之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建立TCP connection的开销
    • broker:接收和分发消息的应用,RabbitMQ Server就是Message Broker
    • vhost:出于多用户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等。
    • Exchange:交换机是RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这些都由交换机类型决定
    • queue:消息存放的队列,生产者存放消息到队列,消费者从队列中取出消息消费
    • routing_key:生产者发送消息时,绑定在消息上的路由Key,用来指定路由规则
    • binding_key:exchange和queue之间的虚拟连接,binding中可以包含routing key,Binding信息被保存到exchange中的查询表中,用于message的分发依据,生产者发送消息携带的RoutingKey会和bindingKey对比,若一致就将消息分发至这个队列
    • consumer:消息的消费者,将消息从队列中取出
  • 应用场景

    • 异步处理
    • 应用解耦
    • 流量削峰

1.2 RabbitMQ的安装

1.2.1 ubuntu20.04 安装rabbitmq
  • 官网的安装脚本
#!/usr/bin/sh

sudo apt-get install curl gnupg apt-transport-https -y

## Team RabbitMQ's main signing key
curl -1sLf "https://keys.openpgp.org/vks/v1/by-fingerprint/0A9AF2115F4687BD29803A206B73A36E6026DFCA" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/com.rabbitmq.team.gpg > /dev/null
## Cloudsmith: modern Erlang repository
curl -1sLf https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/gpg.E495BB49CC4BBE5B.key | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg > /dev/null
## Cloudsmith: RabbitMQ repository
curl -1sLf https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/gpg.9F4587F226208342.key | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg > /dev/null

## Add apt repositories maintained by Team RabbitMQ
sudo tee /etc/apt/sources.list.d/rabbitmq.list <<EOF
## Provides modern Erlang/OTP releases
##
deb [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu focal main
deb-src [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu focal main

## Provides RabbitMQ
##
deb [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu focal main
deb-src [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu focal main
EOF

## Update package indices
sudo apt-get update -y

## Install Erlang packages
sudo apt-get install -y erlang-base \
                        erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \
                        erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \
                        erlang-runtime-tools erlang-snmp erlang-ssl \
                        erlang-syntax-tools erlang-tftp erlang-tools erlang-xmerl

## Install rabbitmq-server and its dependencies
sudo apt-get install rabbitmq-server -y --fix-missing
1.2.2 centos7 安装rabbitmq
  • centos7离线rpm安装包安装,需要有java环境
#! /root/bin
# 安装erlang
curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash
yum install -y erlang
# 安装rabbitmq之前导入相关依赖
rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
rpm --import https://packagecloud.io/gpg.key
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh | sudo bash
# 下载rabbitmq的安装包
# wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.5/rabbitmq-server-3.8.5-1.el7.noarch.rpm
# 正式安装之前导入相关依赖
rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc
yum -y install epel-release
yum -y install socat
# 安装rabbimq的rpm包
rpm -ivh /opt/packages/rabbitmq-server-3.8.5-1.el7.noarch.rpm
1.2.3 RabbitMQ的基本命令
  • 开启管理页面插件

    rabbitmq-plugins enable rabbitmq_management
    
  • 启动rabbitmq服务

    #同时开启erlang服务
    systemctl start rabbitmq-server
    #开启rabbitmq应用
    rabbitmqctl start_app
    
  • 停止rabbitmq服务

    #同时停止erlang服务
    systemctl stop rabbitmq-server
    #停止rabbitmq应用
    rabbitmqctl stop_app
    
  • 查看当前rabbitmq服务的状态

    systemctl status rabbitmq-server
    
  • 默认的guest用户无法访问管理页面,需要创建新的用户并赋予管理员权限

    # 创建新的用户并授予所有权限
    rabbitmqctl add_user admin admin
    rabbitmqctl set_user_tags admin administrator
    rabbitmqctl set_permissions -p / admin "." "." ".*"
    
  • 重启rabbitmq服务

    systemctl restart rabbitmq-server
    

2 RabbitMQ的几种消息模式

2.1 Hello,World

  • Producer:负责发送消息
  • Queue:负责存放消息
  • Consumer:负责消费队列中的消息

image-20220515085048116

  • 单生产者及单消费者简单实现

    • 生产者代码示例

      package com.lzx.hello_rabbitmq.producer;
      
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      
      import java.io.IOException;
      import java.util.concurrent.TimeoutException;
      
      public class Producer {
      	
          //声明消息队列的名称
          public static final String QUEUE_NAME="hello";
          
          public static void main(String[] args) {
              //构建连接工厂
              ConnectionFactory factory = new ConnectionFactory();
              factory.setHost("workstation");
              factory.setUsername("root");
              factory.setPassword("521520");
              try {
                  //获取连接
                  Connection connection = factory.newConnection();
                  //获取信道
                  Channel channel = connection.createChannel();
                  //声明队列
                  channel.queueDeclare(QUEUE_NAME,false,false,false,null);
                  String message = "hello, rabbitmq";
                  //发布消息
                  channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
                  System.out.println("消息发送完毕");
              } catch (IOException e) {
                  e.printStackTrace();
              } catch (TimeoutException e) {
                  e.printStackTrace();
              }
          }
      }
      
    • 消费者代码示例

      package com.lzx.hello_rabbitmq.consumer;
      
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      import com.rabbitmq.client.DeliverCallback;
      
      import java.io.IOException;
      import java.util.concurrent.TimeoutException;
      
      public class Consumer {
          //声明消息队列名称
          public static final String QUEUE_NAME="hello";
      
          public static void main(String[] args) throws IOException, TimeoutException {
              //构造连接工厂
              ConnectionFactory factory = new ConnectionFactory();
              factory.setHost("workstation");
              factory.setUsername("root");
              factory.setPassword("521520");
      		//获取连接
              Connection connection = factory.newConnection();
              //获取信道
              Channel channel = connection.createChannel();
              //声明队列
              channel.queueDeclare(QUEUE_NAME,false,false,false,null);
              //声明消息回调函数,该回调将缓冲消息直到消费者有时间消费它
              DeliverCallback deliverCallback = (consumerTag,message)->{
                  System.out.println("消费消息成功,消息体为:"+new String(message.getBody()));
              };
              //消费者消费消息
              channel.basicConsume(QUEUE_NAME,true,deliverCallback,consumerTag ->{
                  System.out.println("消费失败");
              });
          }
      }
      

2.2 Work Queues(工作队列)

2.2.1 工作队列的基本实现
  • 工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务,并不得不等待它完成。相反,我们把任务安排在以后完成。我们将任务封装为消息并将其发送到队列。在后台运行的辅助进程将从队列中取出任务并最终执行任务。当运行多个worker时,任务将以轮询的方式在它们之间共享。

image-20220515090543226

  • 单生产者,多消费者代码实例

    • 封装工具类用于获取信道

      package com.lzx.utils;
      
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      
      import java.io.IOException;
      import java.util.concurrent.TimeoutException;
      
      public class RabbitMqUtils {
      
          private static ConnectionFactory factory;
          private static Connection connection;
      
          static {
              factory = new ConnectionFactory();
              factory.setHost("workstation");
              factory.setUsername("root");
              factory.setPassword("521520");
              try {
                  connection=factory.newConnection();
              } catch (IOException e) {
                  e.printStackTrace();
              } catch (TimeoutException e) {
                  e.printStackTrace();
              }
          }
          public static Channel getChannel() throws IOException {
              if(connection!=null){
                  return connection.createChannel();
              }
              return null;
          }
      }
      
    • 生产者代码实现

      package com.lzx.hello_rabbitmq.producer;
      
      import com.lzx.utils.RabbitMqUtils;
      import com.rabbitmq.client.Channel;
      
      import java.io.IOException;
      import java.nio.charset.StandardCharsets;
      import java.util.Scanner;
      
      /**
       * 用于演示轮询消费
       */
      public class Task01 {
          private static final String QUEUE_NAME="work_queue";
      
          public static void main(String[] args) throws IOException {
              //获取连接信道
              Channel channel = RabbitMqUtils.getChannel();
              //声明消息队列
              channel.queueDeclare(QUEUE_NAME,false,false,false,null);
              //从控制台输入要发送的消息
              Scanner scanner = new Scanner(System.in);
              
              while(scanner.hasNext()){
                  String message = scanner.next();
                  //发送消息到指定工作队列
                  channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
                  System.out.println("发送消息:"+message + "成功");
              }
          }
      }
      
      
    • 消费者代码实现,通过开启允许多实例运行实现多个消费者

      image-20220515091548638

      package com.lzx.hello_rabbitmq.consumer;
      
      import com.lzx.utils.RabbitMqUtils;
      import com.rabbitmq.client.Channel;
      
      import java.io.IOException;
      
      public class Work01 {
          private static final String QUEUE_NAME = "work_queue";
      
          public static void main(String[] args) throws IOException {
              Channel channel = RabbitMqUtils.getChannel();
              channel.queueDeclare(QUEUE_NAME,false,false,false,null);
              System.out.println("工作线程work02准备接收任务");
              channel.basicConsume(QUEUE_NAME,true,((consumerTag, message) -> {
                  System.out.println("接收到消息"+new String(message.getBody()));
              }),consumerTag -> {
                  System.out.println("取消消费");
              });
          }
      }
      
2.2.2 消息确认(应答)
  • 完成一项任务可能只需要几秒钟。您可能想知道,如果一个消费者开始了一项很长的任务,而任务只完成了一部分就结束了,会发生什么情况。在我们之前的代码中,一旦RabbitMQ将一条消息发送给消费者,它就会立即标记为删除。在这种情况下,如果工作线程死亡,我们将丢失它正在处理的消息。我们还会丢失所有已发送给这个特定工作线程但尚未处理的消息。

  • 为了确保消息不会丢失,RabbitMQ支持消息确认。消费者会返回一个确认信息,告诉RabbitMQ一个特定的消息已经被接收、处理,并且RabbitMQ可以随意删除它。

  • 手动应答

    • 肯定确认
    void basicAck(long deliveryTag, boolean multiple) throws IOException;
    

    注:

    deliveryTag 表示消息标记,通过调用 delivery.getEnvelope().getDeliveryTag()得到

    boolean multiple 用于批量应答

    true表示应答该消息之前的所有消息,false表示只应答当前的消息

    • 否定确认
    void basicNack(long deliveryTag, boolean multiple, boolean requeue)
                throws IOException;
    
    • 拒绝消息
    void basicReject(long deliveryTag, boolean requeue) throws IOException;
    
  • 手动肯定确认代码实现

    DeliverCallback deliverCallback =(consumerTag, message) -> {
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("接收到消息"+new String(message.getBody()));
        		//处理完消息以后进行手动应答
                channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
            }
    
  • 消息自动重新入队:如果一个消费者在没有发送ack的情况下死亡(通道被关闭,连接被关闭,或者TCP连接丢失),RabbitMQ会认为消息没有被完全处理,并将其重新排队。如果有其他消费者同时在线,它会迅速将其重新交付给另一个消费者,这样就可以确保没有消息丢失。

2.2.3 消息持久化
  • 当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非你告诉它不要这样做。要确保消息不会丢失,需要做两件事:将队列和消息都标记为持久的。

    • 队列持久化

      boolean durable = true;
      channel.queueDeclare("durable_queue", durable, false, false, null);
      
    • 消息持久化

      import com.rabbitmq.client.MessageProperties;
      
      channel.basicPublish("", "durable_queue",
                  MessageProperties.PERSISTENT_TEXT_PLAIN,
                  message.getBytes());
      

    注:将消息标记为持久性并不完全保证消息不会丢失。虽然它告诉RabbitMQ将消息保存到磁盘,但当RabbitMQ接受了消息但还没有保存它时,仍然会有一个很短的时间窗口。此外,RabbitMQ不会对每条消息进行刷盘——它可能只是保存在缓存中,而不是真正写入磁盘。持久性保证并不强,但对于简单任务队列来说已经足够了。如果你需要更强大的保证,需要使用发布确认。

2.2.4 不公平分发
  • 注意到RabbitMQ的调度仍然不完全像我们希望的那样工作。例如,在有两个消费者的情况下,当所有奇数消息都很重,偶数消息都很轻时,一个消费者将一直很忙,而另一个几乎不做任何工作。然而RabbitMQ对此一无所知,仍然会均匀地分发消息。这是因为RabbitMQ只在消息进入队列时分派消息。它不为消费者查看未确认消息的数量。它只是盲目地将第n个消息发送给第n个消费者。

  • 为了解决上面这个问题,我们可以使用basicQos方法设置prefetchCount = 1。这告诉RabbitMQ一次不要给一个worker发送多条消息。或者,换句话说,在worker处理并确认了前一条消息之前,不要向它发送一条新消息。相反,它将把它分配给下一个不忙碌的工作者。

    int prefetchCount = 1;
    channel.basicQos(prefetchCount);
    

注1:prefetchCount 预取值,该值表示未确认的消息的最大数量,未确认的消息存放在一个未确认的消息缓冲区内,当未确认的消息达到预取值时,RabbitMQ将停止在channel上传递更多的消息。

注2:当所有的消费者线程都处于忙碌状态时,新进来的消息将会存放到消息队列中,但是消息队列的长度是有限的,当消息队列满时,你需要考虑增加消费者或者采取其他的有效措施。

2.3 Publish/Subscribe发布订阅

2.3.1 Exchanges(交换机)
  • 交换机是一个非常简单的东西,生产者只能向交换机发送消息。交换机一方面接收来自生产者的消息,另一方面将消息推送到队列中。交换器必须确切地知道如何处理接收到的消息。例如:消息应该被追加到一个特定的队列吗?消息应该被追加到多个队列吗?或者消息应该被丢弃吗?这些实现由交换机的种类进行决定。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CV91ggli-1652692910467)(https://gitee.com/lzx143421/figures/raw/master/img/image-20220515102122560.png)]

  • 交换机的种类

    • direct
    • topic
    • headers
    • fanout:将接收到的消息发送给所有与它绑定的消息队列。
    channel.exchangeDeclare("logs", "fanout");
    

    注:之前我们发布消息的时候使用的是用空字符串(“”)进行标识的默认交换机或是无名交换机,消息能路由发送到队列中其实是由routingKey(bindingkey)绑定key指定的,如果它存在的话。

2.3.2 Temporary queues 临时队列
  • 具有随机名称,并且当断开连接时能够自动删除的队列。

  • 通过如下命令可以生成一个具有随机名称的非持久化,排他的,自动删除的队列

    String queueName = channel.queueDeclare().getQueue();
    
2.3.3 Bindings 绑定关系
  • 声明交换机和队列之间的关系

    image-20220515104447176

    channel.queueBind(queueName, "logs", "");
    
2.3.4 使用fanout交换机实现简单的日志系统
  • 一个消费者将日志信息打印到控制台,另一个消费者将日志信息输出到磁盘

  • 生产者代码实现

    public class Producer {
        private static final String EXCHANGE_NAME = "logs";
        public static void main(String[] argv) throws Exception { 
            try (Channel channel = RabbitMqUtils.getChannel()) {
                //声明名为logs,类型为fanout的交换机
                channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); 
                Scanner sc = new Scanner(System.in); 
                System.out.println("请输入信息"); 
                //控制台输入模拟日志信息
                while (sc.hasNext()) { 
                    String message = sc.nextLine(); 
                    channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8")); 
                    System.out.println("生产者发出消息" + message); }
            }
        } 
    }
    
  • 消费者代码实现

    public class LogConsumer1 { 
        private static final String EXCHANGE_NAME = "logs"; 
        public static void main(String[] argv) throws Exception { 
            Channel channel = RabbitMqUtils.getChannel(); 
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout"); 
            //获取随机队列
            String queueName = channel.queueDeclare().getQueue(); 
            //将随机队列与Logs交换机绑定
            channel.queueBind(queueName,EXCHANGE_NAME,"")
            System.out.println("等待接收消息,把接收到的消息打印在屏幕....."); 
            DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                String message = new String(delivery.getBody(), "UTF-8"); 
                System.out.println("控制台打印接收到的消息"+message); 
            }; 
            channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
        } 
    }
    
    public class LogConsumer2 { 
        private static final String EXCHANGE_NAME = "logs"; 
        public static void main(String[] argv) throws Exception { 
            Channel channel = RabbitMqUtils.getChannel(); 
            channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
            //获取随机队列
            String queueName = channel.queueDeclare().getQueue(); 
            //将队列与logs交换机绑定
            channel.queueBind(queueName,EXCHANGE_NAME,"")
    		System.out.println("等待接收消息,把接收到的消息写到文件.....");
    		DeliverCallback deliverCallback = (consumerTag, delivery) -> { 
                //将日志信息写入到磁盘
                String message = new String(delivery.getBody(), "UTF-8"); 
                File file = new File("D:\\rabbitmq_info.txt"); 
                FileUtils.writeStringToFile(file,message,"UTF-8"); 
                System.out.println("数据写入文件成功");
            };
            channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
        } 
    }
    

2.4 Routing(路由)

2.4.1 binding key
  • binding 表示队列和交换机之间的关系,也就是说队列对来自这个交换机的消息感兴趣
  • binding key 表示队列对来自与其绑定的交换机的具有特定的routing_key的消息感兴趣

注1:binding key 根据交换机类型的不同有不一样的意义,对fanout类型的交换机而言,它将忽略 binding key

注2:routing key 消息发送时的路由key,binding key 队列与交换机绑定时的key,表示队列只对来自该交换机的routing key == binding key的消息感兴趣

2.4.2 Direct exchange

image-20220515112029890

  • 将消息发送到消息的routing key与其绑定的队列的 binding key一致的队列中

    channel.exchangeDeclare(EXCHANGE_NAME, "direct");
    
2.4.3 改造的日志系统

image-20220515113036434

  • 日志发送者代码实现

    public class Producer {
        private static final String EXCHANGE_NAME = "direct_logs"; 
        public static void main(String[] argv) throws Exception { 
            try (Channel channel = RabbitMqUtils.getChannel()) { 
                channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
                //创建多个routing_key 
                Map<String, String> bindingKeyMap = new HashMap<>(); 
                bindingKeyMap.put("info","普通info信息"); 
                bindingKeyMap.put("warning","警告warning信息"); 
                bindingKeyMap.put("error","错误error信息"); 
                //debug没有消费这接收这个消息 所有就丢失了 
                bindingKeyMap.put("debug","调试debug信息"); 
                for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){ 
                    String bindingKey = bindingKeyEntry.getKey(); 
                    String message = bindingKeyEntry.getValue(); 
                    channel.basicPublish(EXCHANGE_NAME,bindingKey, null, message.getBytes("UTF-8")); 
                    System.out.println("生产者发出消息:" + message); 
                } 
            }
        }
    }
    
  • 日志接收者代码实现

    public class LogConsumer1 { 
        private static final String EXCHANGE_NAME = "direct_logs"; 
        public static void main(String[] argv) throws Exception { 
            Channel channel = RabbitMqUtils.getChannel(); 
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); 
            String queueName = "disk"; 
            channel.queueDeclare(queueName, false, false, false, null); 
            //绑定error消息,只接受error日志
            channel.queueBind(queueName, EXCHANGE_NAME, "error"); 
            System.out.println("等待接收消息....."); 
            DeliverCallback deliverCallback = (consumerTag, delivery) -> { 
                String message = new String(delivery.getBody(), "UTF-8"); 
                message="接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message; 
                File file = new File("D:\\rabbitmq_error.txt"); 
                FileUtils.writeStringToFile(file,message,"UTF-8"); 
                System.out.println("错误日志已经接收"); 
            }; 
            channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
        }
    }
    
    public class LogConsumer2 { 
        private static final String EXCHANGE_NAME = "direct_logs"; 
        public static void main(String[] argv) throws Exception { 
            Channel channel = RabbitMqUtils.getChannel(); 
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); 
            String queueName = "console"; 
            channel.queueDeclare(queueName, false, false, false, null); 
            //绑定info warning消息,将info及warning消息打印到控制台
            channel.queueBind(queueName, EXCHANGE_NAME, "info"); 
            channel.queueBind(queueName, EXCHANGE_NAME, "warning"); 
            System.out.println("等待接收消息....."); 
            DeliverCallback deliverCallback = (consumerTag, delivery) -> { 
                String message = new String(delivery.getBody(), "UTF-8"); 
                System.out.println("接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message); 
            }; 
            channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
    

2.5 Topics

2.5.1 Topic exchange
  • 发送到 topic exchange 的消息不能使用任意的 routing_key ,它必须是由点分隔的单词列表。这些词可以是任何东西,但通常它们指定了与消息相关的一些特征。例如 “quick.orange.rabbit”,“stock.usd.nyse”。routing_key中可以有任意多的单词,最大限制为255字节。
  • routing_key中两个特殊的符号
    • *可以代表任意一个单词
    • #可以代表零个或多个单词

image-20220516082003446

注1:当路由键只有“#”号时,此时的topic交换机与fanout交换机一致

注2:当“*”和“#”号都没有使用时,此时的topic交换机和direct交换机一致

2.5.2 再次改进的日志系统
  • 产生日志的代码示例
public class Producer {
    private static final String EXCHANGE_NAME = "topic_logs"; 
    public static void main(String[] argv) throws Exception { 
        try (Channel channel = RabbitMqUtils.getChannel()) { 
            channel.exchangeDeclare(EXCHANGE_NAME, "topic");
            //创建多个routing_key,包含来自认证和来自内核的消息
            Map<String, String> bindingKeyMap = new HashMap<>(); 
            //来自用户认证的日志消息
            bindingKeyMap.put("auth.info","用户的普通info信息"); 
            bindingKeyMap.put("auth.warning","用户的警告warning信息"); 
            bindingKeyMap.put("auth.error","用户的错误error信息");  
            bindingKeyMap.put("auth.debug","用户的调试debug信息"); 
            //来自内核的日志消息
            bindingKeyMap.put("kern.info","内核的普通info信息"); 
            bindingKeyMap.put("kern.warning","内核的警告warning信息"); 
            bindingKeyMap.put("kern.error","内核的错误error信息");  
            bindingKeyMap.put("kern.debug","内核的调试debug信息"); 
            for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){ 
                String bindingKey = bindingKeyEntry.getKey(); 
                String message = bindingKeyEntry.getValue(); 
                channel.basicPublish(EXCHANGE_NAME,bindingKey, null, message.getBytes("UTF-8")); 
                System.out.println("生产者发出消息:" + message); 
            } 
        }
    }
}
  • 处理日志的代码示例
/**
*用于记录用户认证和内核的错误信息,将错误信息输出到磁盘
*/
public class LogConsumer1 { 
    private static final String EXCHANGE_NAME = "topic_logs"; 
    public static void main(String[] argv) throws Exception { 
        Channel channel = RabbitMqUtils.getChannel(); 
        channel.exchangeDeclare(EXCHANGE_NAME, "topic"); 
        String queueName = "disk"; 
        channel.queueDeclare(queueName, false, false, false, null); 
        //绑定error消息,接受来自不同主体的error日志
        channel.queueBind(queueName, EXCHANGE_NAME, "*.error"); 
        System.out.println("等待接收消息....."); 
        DeliverCallback deliverCallback = (consumerTag, delivery) -> { 
            String message = new String(delivery.getBody(), "UTF-8"); 
            message="接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message; 
            File file = new File("D:\\rabbitmq_error.txt"); 
            FileUtils.writeStringToFile(file,message,"UTF-8"); 
            System.out.println("错误日志已经接收"); 
        }; 
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
    }
}
/**
*用于将用户认证的所有日志信息打印到工作台
*/
public class LogConsumer2 { 
    private static final String EXCHANGE_NAME = "topic_logs"; 
    public static void main(String[] argv) throws Exception { 
        Channel channel = RabbitMqUtils.getChannel(); 
        channel.exchangeDeclare(EXCHANGE_NAME, "topic"); 
        String queueName = "console"; 
        channel.queueDeclare(queueName, false, false, false, null); 
        //绑定用户认证的所有消息
        channel.queueBind(queueName, EXCHANGE_NAME, "auth.*"); 
        System.out.println("等待接收消息....."); 
        DeliverCallback deliverCallback = (consumerTag, delivery) -> { 
            String message = new String(delivery.getBody(), "UTF-8"); 
            System.out.println("接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message); 
        }; 
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}

2.6 Remote procedure call(RPC 远程调用)

  • 远程调用:客户端发起一个耗时请求,放到请求队列中,远程服务器从请求队列中取出任务进行执行,然后将执行的结果放到请求响应队列中,客户端检查响应队列中的结果是否与自己发送的请求匹配,若匹配则取出结果,否则就忽略这个未知结果。

  • 尽管RPC在计算中是一个相当常见的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是缓慢的RPC时,问题就会出现。这样的混淆会导致不可预测的系统,并为调试增加不必要的复杂性。误用RPC不会简化软件,反而会导致不可维护的意大利面条式代码。

  • 考虑到这一点,请考虑下列建议:

    • 确保哪个函数调用是本地的,哪个调用是远程的。

    • 记录你的系统。明确组件之间的依赖关系。

    • 处理错误情况。当RPC服务器长时间关闭时,客户端应该如何反应?

  • 回调队列:为了接收响应,我们需要在请求中发送一个’callback’队列地址。

    callbackQueueName = channel.queueDeclare().getQueue();
    
    BasicProperties props = new BasicProperties
                                .Builder()
                                .replyTo(callbackQueueName)
                                .build();
    //replyTo 用于命名回调队列
    channel.basicPublish("", "rpc_queue", props, message.getBytes());
    
  • correlation Id:我们可以为每个客户端建立一个回调队列,然后利用correlation Id将请求与响应相互关联。

  • 使用远程调用实现计算Fibonacci值

    image-20220516155651806

    • RPC客户端代码

      import com.rabbitmq.client.AMQP;
      import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      
      import java.io.IOException;
      import java.util.UUID;
      import java.util.concurrent.ArrayBlockingQueue;
      import java.util.concurrent.BlockingQueue;
      import java.util.concurrent.TimeoutException;
      
      public class RPCClient implements AutoCloseable {
      
          private Connection connection;
          private Channel channel;
          private String requestQueueName = "rpc_queue";
      
          public RPCClient() throws IOException, TimeoutException {
              ConnectionFactory factory = new ConnectionFactory();
              factory.setHost("localhost");
      
              connection = factory.newConnection();
              channel = connection.createChannel();
          }
      
          public static void main(String[] argv) {
              try (RPCClient fibonacciRpc = new RPCClient()) {
                  for (int i = 0; i < 32; i++) {
                      String i_str = Integer.toString(i);
                      System.out.println(" [x] Requesting fib(" + i_str + ")");
                      String response = fibonacciRpc.call(i_str);
                      System.out.println(" [.] Got '" + response + "'");
                  }
              } catch (IOException | TimeoutException | InterruptedException e) {
                  e.printStackTrace();
              }
          }
      
          public String call(String message) throws IOException, InterruptedException {
              final String corrId = UUID.randomUUID().toString();
      
              String replyQueueName = channel.queueDeclare().getQueue();
              AMQP.BasicProperties props = new AMQP.BasicProperties
                      .Builder()
                      .correlationId(corrId)
                      .replyTo(replyQueueName)
                      .build();
      
              channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
      
              final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
      
              String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
                  if (delivery.getProperties().getCorrelationId().equals(corrId)) {
                      response.offer(new String(delivery.getBody(), "UTF-8"));
                  }
              }, consumerTag -> {
              });
      
              String result = response.take();
              channel.basicCancel(ctag);
              return result;
          }
      
          public void close() throws IOException {
              connection.close();
          }
      }
      
    • RPC服务端代码

      import com.rabbitmq.client.*;
      
      public class RPCServer {
      
          private static final String RPC_QUEUE_NAME = "rpc_queue";
      
          private static int fib(int n) {
              if (n == 0) return 0;
              if (n == 1) return 1;
              return fib(n - 1) + fib(n - 2);
          }
      
          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(RPC_QUEUE_NAME, false, false, false, null);
                  channel.queuePurge(RPC_QUEUE_NAME);
      
                  channel.basicQos(1);
      
                  System.out.println(" [x] Awaiting RPC requests");
      
                  Object monitor = new Object();
                  DeliverCallback deliverCallback = (consumerTag, delivery) -> {
                      AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                              .Builder()
                              .correlationId(delivery.getProperties().getCorrelationId())
                              .build();
      
                      String response = "";
      
                      try {
                          String message = new String(delivery.getBody(), "UTF-8");
                          int n = Integer.parseInt(message);
      
                          System.out.println(" [.] fib(" + message + ")");
                          response += fib(n);
                      } catch (RuntimeException e) {
                          System.out.println(" [.] " + e.toString());
                      } finally {
                          channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, 
                                               response.getBytes("UTF-8"));
                          channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
                          // RabbitMq consumer worker thread notifies the RPC server owner thread
                          synchronized (monitor) {
                              monitor.notify();
                          }
                      }
                  };
      
                  channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> { }));
                  // Wait and be prepared to consume the message from RPC client.
                  while (true) {
                      synchronized (monitor) {
                          try {
                              monitor.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
              }
          }
      }
      

2.7 Publisher Confirms (发布确认)

  • 生产者通过调用信道的 confirmSelect() 方法开启发布确认功能,每个需要开启发布确认的信道只能调用一次该方法,一旦信道开启发布确认,所有在该信道上发布的消息都会唯一关联一个序列号(序列号从1开始),当消息被投递到相应的队列后,broker就会给生产者发送肯定确认,生产者就直到消息已经到达了正确的队列。

    Channel channel = connection.createChannel();
    channel.confirmSelect();
    
  • 发布确认是异步的等待确认消息的,但在部分模式下需要使用基于异步通知的同步助手。

  • 发布确认有三个不同的策略

    • 单个发布确认
    • 批量发布确认
    • 异步发布确认
2.7.1 单个发布确认(Publishing Messages Individually)
  • 在信道开启发布确认之后,可以调用信道的 waitForConfirmOrDie(timeout) 方法来等待确认,当消息在timeout时间内没有确认或者得到了否定确认时,该方法将会产生异常供客户端进行处理。
  • 这种技术非常简单,但也有一个主要缺点:它显著降低了发布速度,因为消息的确认会阻塞所有后续消息的发布。这种方法交付的吞吐量不会超过每秒几百条已发布消息。然而,对于某些应用程序来说,这已经足够好了。

注:在这种模式下,客户端实际上异步接收confirm并相应地解除对waitForConfirmsOrDie的调用的阻塞。可以将waitForConfirmsOrDie看作是一个依赖于异步通知的同步助手。

  • 代码示例

    public static void publishMessagesIndividually() throws Exception {
            try (Connection connection = createConnection()) {
                Channel ch = connection.createChannel();
    			//声明随机名称的队列
                String queue = UUID.randomUUID().toString();
                ch.queueDeclare(queue, false, false, true, null);
    			//开启发布确认
                ch.confirmSelect();
                long start = System.nanoTime();
                for (int i = 0; i < MESSAGE_COUNT; i++) {
                    String body = String.valueOf(i);
                    ch.basicPublish("", queue, null, body.getBytes());
                    //等待confirm
                    ch.waitForConfirmsOrDie(5_000);
                }
                long end = System.nanoTime();
                System.out.format("Published %,d messages individually in %,d ms%n", MESSAGE_COUNT, 
                                  Duration.ofNanos(end - start).toMillis());
            }
        }
    
2.7.2 批量发布确认(Publishing Messages in Batches)
  • 与等待单个消息的确认相比,等待一批消息被确认大大提高了吞吐量。一个缺点是,在失败的情况下,我们不能确切地知道哪里出了问题,因此我们可能不得不在内存中保留整批消息来记录有意义的内容或重新发布消息。而且这个解决方案仍然是同步的,所以它阻止了消息的发布。

  • 代码示例

    /**
    *批量确认(一次100条消息)
    */
    public static void publishMessagesInBatch() throws Exception {
            try (Connection connection = createConnection()) {
                Channel ch = connection.createChannel();
    			//声明随机名称的队列
                String queue = UUID.randomUUID().toString();
                ch.queueDeclare(queue, false, false, true, null);
    			//开启发布确认
                ch.confirmSelect();
    			//声明批量数以及未确认的消息数
                int batchSize = 100;
                int outstandingMessageCount = 0;
    
                long start = System.nanoTime();
                for (int i = 0; i < MESSAGE_COUNT; i++) {
                    String body = String.valueOf(i);
                    ch.basicPublish("", queue, null, body.getBytes());
                    outstandingMessageCount++;
    				//达到规定的批量之后等待确认
                    if (outstandingMessageCount == batchSize) {
                        ch.waitForConfirmsOrDie(5_000);
                        outstandingMessageCount = 0;
                    }
                }
    			//消息发送完,但是批数不够100,但是大于0,等待确认
                if (outstandingMessageCount > 0) {
                    ch.waitForConfirmsOrDie(5_000);
                }
                long end = System.nanoTime();
                System.out.format("Published %,d messages in batch in %,d ms%n", MESSAGE_COUNT, 
                                  Duration.ofNanos(end - start).toMillis());
            }
        }
    
2.6.3 异步确认(Handling Publisher Confirms Asynchronously)
  • 调用channel的 addConfirmListener() 方法注册确认监听器,使用两个回调函数来处理肯定确认和否定确认

    Channel channel = connection.createChannel();
    channel.confirmSelect();
    channel.addConfirmListener((sequenceNumber, multiple) -> {
        // code when message is confirmed
    }, (sequenceNumber, multiple) -> {
        // code when message is nack-ed
    });
    
    • sequenceNumber:消息的序列号,可以同消息体进行关联,通过调用channel的getNextPublishSeqNo()方法可以获得将要发布的消息的序列号

      int sequenceNumber = channel.getNextPublishSeqNo());
      ch.basicPublish(exchange, queue, properties, body);
      
    • multiple:一个布尔值,false表示只处理当前sequenceNumber的消息,true表示处理小于等于sequenceNumber的消息

  • 使用ConcurrentNavigableMap来跟踪未完成的确认。这种数据结构之所以方便,有几个原因。它允许轻松地将序列号与消息(无论消息数据是什么)关联起来,并轻可以松地把给定序列Id之前的消息进行清除(以处理多个confirm /nacks)。最后,它支持并发访问,因为确认回调是在客户端库所拥有的线程中调用的,应该与发布线程保持不同。

    ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
    // ... code for confirm callbacks will come later
    String body = "...";
    outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
    channel.basicPublish(exchange, queue, properties, body.getBytes());
    
  • 异步发布确认示例

      public static void handlePublishConfirmsAsynchronously() throws Exception {
            try (Connection connection = createConnection()) {
                Channel ch = connection.createChannel();
    
                String queue = UUID.randomUUID().toString();
                ch.queueDeclare(queue, false, false, true, null);
    
                ch.confirmSelect();
    			//将消息id与消息体进行绑定
                ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
    			//肯定确认消息的处理
                ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
                    if (multiple) {
                        //批量清除sequenceNumber之前的所有消息
                        ConcurrentNavigableMap<Long, String> confirmed = 
                            outstandingConfirms.headMap(sequenceNumber, true);
                        confirmed.clear();
                    } else {
                        //清除单个消息
                        outstandingConfirms.remove(sequenceNumber);
                    }
                };
    
                ch.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
                    //以日志形式记录否定确认的消息
                    String body = outstandingConfirms.get(sequenceNumber);
                    System.err.format(
                            "Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
                            body, sequenceNumber, multiple
                    );
                    //记录之后交给肯定确认的回调函数进行处理,将否定确认的消息进行清除
                    cleanOutstandingConfirms.handle(sequenceNumber, multiple);
                });
    			
                //发布消息并将消息id与消息体进行绑定
                long start = System.nanoTime();
                for (int i = 0; i < MESSAGE_COUNT; i++) {
                    String body = String.valueOf(i);
                    outstandingConfirms.put(ch.getNextPublishSeqNo(), body);
                    ch.basicPublish("", queue, null, body.getBytes());
                }
    
                if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) {
                    throw new IllegalStateException("All messages could not be confirmed in 60 seconds");
                }
    
                long end = System.nanoTime();
                System.out.format("Published %,d messages and handled confirms asynchronously in %,d ms%n",
                                  MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
            }
        }
    
        public static boolean waitUntil(Duration timeout, BooleanSupplier condition) throws InterruptedException {
            int waited = 0;
            while (!condition.getAsBoolean() && waited < timeout.toMillis()) {
                Thread.sleep(100L);
                waited = +100;
            }
            return condition.getAsBoolean();
        }
    

注:从相应的回调中重新发布一个否定确认的消息可能很诱人,但这应该避免,因为确认回调是在I/O线程中分派的,而通道不应该做操作。更好的解决方案是将消息放入由发布线程轮询的内存队列中。像ConcurrentLinkedQueue这样的类很适合在确认回调和发布线程之间传输消息。

2.6.4 发布确认高级
  • 配置信息

    spring.rabbitmq.publisher-confirm-type=correlated
    ##NONE 禁用发布确认模式,默认值
    ##CORRELATED 发布消息成功到交换器后会触发回调方法
    ##SIMPLE 
    
  • RabbitTemplate.ConfirmCallback 接口(交换机不管是否收到消息时的回调接口)

    //交换机不管是否收到消息时的回调
    rabbitTemplate.setConfirmCallback(myCallBack)
        
    public class MyCallBack implements RabbitTemplate.ConfirmCallback{
        @Override
    	public void confirm(CorrelationData correlationData,boolean ack,String cause){
            //correlationData 表示消息相关的数据
            //ack 表示交换机是否收到消息
            //cause 交换机没有收到消息的原因
        	...
    	}
    }
    
  • RabbitTemplate.ReturnCallback(交换机收到但是不能成功路由,即交换机不能将消息转发到消息队列时的回调接口)

    //设置mandatory参数
    /**
    *true: 交换机无法路由时会将消息返回给生产者
    *false:交换机无法路由消息时,直接丢弃消息
    */
    rabbitTemplate.setMandatory(true);
    rabbitTemplate.setReturnCallback(myCallback)
        
    public class MyCallBack implements RabbitTemplate.ReturnCallback{
        @Override
    	public void returnedMessage(Message message, int replyCode,String replyText,String exchange,String routingKey){
        	//message 被退回消息
            //replyCode 退回代码
            //replyText 退回原因
            //exchange 退回交换机
            //routingKey 消息路由key
            ...
    	}
    }
    
  • 备份交换机,当消息被交换机退回时,交换机会把这条消息转发到它的备份交换机中,再由备份交换机进行转发和处理,备份交换机的类型通常是"Fanout",这样就能把所有的消息投递到与其绑定的队列中。

    //设置确认交换机的备份交换机
    ExchangeBuilder exchangeBuilder = 
        ExchangeBuilder.directExchange("confirm.exchange").durable(true)
        .withArgument("alternate-exchange","back.exchange");
    FountExchange backExchange = new FanountExcahnge("back.exchange");
    

3 几种队列

3.1 消息和队列的过期时间

  • TTL(time to live)(存活时间)

    • 设置消息过期时间的方式

      注:通过发布消息时为消息设置过期时间时,当消息过期后不一定被马上丢弃,只有当消息到队头时才会判断消息是否过期,如果队列消息积压则过期的消息也还能存活很长时间

    • 设置队列的过期时间的方式

3.2 死信队列

  • 由于某些特定的原因导致消息无法被消费,需要重新发布到其它的交换机时,消息就成为了死信。

  • 死信来源

    • 消息TTL过期

    • 队列到达最大长度,无法再添加数据到mq中

      • 通过策略声明队列的最大长度

        rabbitmqctl set_policy my-pol "^one-meg$" \
          '{"max-length-bytes":1048576}' \
          --apply-to queues
          
        rabbitmqctl set_policy my-pol "^two-messages$" \
          '{"max-length":2,"overflow":"reject-publish"}' \
          --apply-to queues  
        
      • 通过队列的可选参数声明队列的最大长度

        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-max-length", 10);
        channel.queueDeclare("myqueue", false, false, false, args);
        
    • 消息被消费者拒绝(basic.reject或者basic.nack)并且requeue=false

      注:队列的过期不会对其中的消息造成死信。

  • 成为死信的消息,需要由死信交换机(DLX)重新路由到死信队列进行消费。对于任何给定的队列,DLX可以由客户端使用队列的参数定义,也可以在服务器中使用策略定义。如果策略和参数都指定了一个DLX,则参数中指定的DLX会覆盖策略中指定的DLX。

    • 使用策略的方式指定队列的死信交换机与死信路由键

      rabbitmqctl set_policy DLX ".*" '{"dead-letter-exchange":"some.exchange.name"}' --apply-to queues
      rabbitmqctl set_policy DLX ".*" '{"dead-letter-routing-key":"some-routing-key"}' --apply-to queues
      
    • 使用队列的可选参数指定队列的死信交换机和死信路由键

      channel.exchangeDeclare("dead-exchange", "direct");
      Map<String, Object> args = new HashMap<String, Object>();
      args.put("x-dead-letter-exchange", "some.exchange.name");
      args.put("x-dead-letter-routing-key", "some-routing-key");
      channel.queueDeclare("myqueue", false, false, false, args);
      

      注:你需要指定死信消息的路由键。如果没有设置,则将使用消息本身的路由键。

3.3 延迟队列

  • 用于存放需要在指定时间被处理的元素的队列,也可以理解为在某事件发生多久之后处理消息。(例如下单30分钟后查看订单状态)

  • 实现延迟队列的方式

    • 通过设置消息的TLL,使消息过期成为死信消息,然后由死信交换机转发给死信队列,消费者消费死信队列中的消息即可

      • 声明队列时指定消息的过期时间
      • 发布消息时指定消息的过期时间

      注:当通过发布消息时为消息设置过期时间实现延迟队列时,当消息过期后不一定马上成为死信,只有当消息到队头时才会判断消息是否过期,如果队列消息积压则过期的消息也还能存活很长时间,因此若先入队的消息存活时间过长,后入队的消息存活时间短,可能实现的延迟队列不准确。

    • 使用rabbitmq插件实现延迟队列(插件列表,延迟队列插件下载地址)

      image-20220516141239505

      • 启用下载的插件

        #将下载的插件文件放到/usr/lib/rabbitmq/lib/rabbitmq_server-3.10.1/plugins/ 目录下
        mv rabbitmq_delayed_message_exchange-3.10.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.10.1/plugins/
        #启用插件
        rabbitmq-plugins enable rabbitmq_delayed_message_exchange
        
      • 代码示例

        • 延迟交换机配置
        package com.lzx.delay_queue.config;
        
        import org.springframework.amqp.core.Binding;
        import org.springframework.amqp.core.BindingBuilder;
        import org.springframework.amqp.core.CustomExchange;
        import org.springframework.amqp.core.Queue;
        import org.springframework.beans.factory.annotation.Qualifier;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        
        import java.util.HashMap;
        import java.util.Map;
        
        @Configuration
        public class DelayedQueueConfig {
            public static final String DELAYED_QUEUE_NAME = "delayed.queue";
            public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
            public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
        
            @Bean
            public Queue delayedQueue() {
                return new Queue(DELAYED_QUEUE_NAME);
            }
        
            //自定义交换机 我们在这里定义的是一个延迟交换机
            @Bean("delayedExchange")
            public CustomExchange delayedExchange() {
                Map<String, Object> args = new HashMap<>();
                //自定义延迟交换机的类型
                args.put("x-delayed-type", "direct");
                return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
            }
        
            @Bean
            public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
                                               @Qualifier("delayedExchange") CustomExchange delayedExchange) {
                return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
            }
        }
        
        
        • 生产者和消费者代码
        /**
        *生产者代码
        */
        package com.lzx.delay_queue.controller;
        
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.amqp.rabbit.core.RabbitTemplate;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.web.bind.annotation.GetMapping;
        import org.springframework.web.bind.annotation.PathVariable;
        import org.springframework.web.bind.annotation.RequestMapping;
        import org.springframework.web.bind.annotation.RestController;
        
        import java.util.Date;
        
        @Slf4j
        @RequestMapping("ttl")
        @RestController
        public class SendMsgController {
            public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
            public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
            @Autowired
            private RabbitTemplate rabbitTemplate;
            @GetMapping("sendDelayMsg/{message}/{delayTime}")
            public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
                rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, correlationData -> {
                    correlationData.getMessageProperties().setDelay(delayTime);
                    return correlationData;
                });
                log.info("当前时间:{},发送一条延迟{}毫秒的信息给队列delayed.queue:{}", new Date(),delayTime, message);
            }
        }
        /**
        *消费者代码
        */
        package com.lzx.delay_queue.consummer;
        
        import com.rabbitmq.client.Channel;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.amqp.core.Message;
        import org.springframework.amqp.rabbit.annotation.RabbitListener;
        import org.springframework.stereotype.Component;
        
        import java.util.Date;
        
        @Slf4j
        @Component
        public class DeadLetterQueueConsumer {
            public static final String DELAYED_QUEUE_NAME = "delayed.queue";
        
            @RabbitListener(queues = DELAYED_QUEUE_NAME)
            public void receiveDelayedQueue(Message message) {
                String msg = new String(message.getBody());
                log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
            }
        }
        

3.4 优先级队列

  • 任何队列都可以使用客户端提供的可选参数转换为优先级队列。目前的实现支持有限数量的优先级:255。建议取值为1 ~ 10。

  • 使用前提,只有满足以下条件才能对消息进行排序

    • 队列需要设置为优先级队列

      Map<String,Object> params = new HashMap<>();
      params.put("x-max-priority",10);
      channel.queueDeclare("priority_queue",true,false,false,params);
      
    • 消息需要设置优先级

      //
      AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
      //
      correlationData.getMessageProperties().setPriority(5);
      
    • 消息已经发送到队列中

  • 注意事项

    • 每个队列的每个优先级都有一些内存和磁盘上的成本。这还会产生额外的CPU成本,特别是在使用时,因此您可能不希望创建大量的级别。
    • 消息优先级字段被定义为无符号字节,因此在实践中优先级应该在0到255之间。
    • 没有优先级属性的消息将被视为优先级为0。优先级高于队列最大优先级的消息将被视为以最大优先级发布的消息。

3.5 惰性队列

  • 从RabbitMQ 3.6.0开始,就有了惰性队列的概念——这些队列会尽可能早地将它们的内容移动到磁盘上,并且只在用户请求时才将它们加载到RAM中,因此有惰性的说法。

  • 延迟队列的主要目标之一是能够支持非常长的队列(数百万条消息)。由于各种原因,队列可能会变得很长:

    • 消费者离线/崩溃/停机维护
    • 生产者发送的消息瞬时猛增,远多于消费者
    • 消费者处理消息太慢
  • 声明方式

    • 客户端可选参数声明
    Map<String,Object> args = new HashMap<String,Object>();
    args.put("x-queue-mode","lazy");
    channel.queueDeclare("lazy_queue",false,false,false,args);
    

    注:如果在声明时通过可选参数设置队列模式,则只能通过删除队列并使用不同的参数重新声明来更改它

    • 策略声明
    rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
    

    注:使用策略声明时可以在运行时修改队列的模式 rabbitmqctl set_policy Lazy “^lazy-queue$” ‘{“queue-mode”:“default”}’ --apply-to queues

  • 任何队列都可以使用客户端提供的可选参数转换为优先级队列。目前的实现支持有限数量的优先级:255。建议取值为1 ~ 10。

  • 使用前提,只有满足以下条件才能对消息进行排序

    • 队列需要设置为优先级队列

      Map<String,Object> params = new HashMap<>();
      params.put("x-max-priority",10);
      channel.queueDeclare("priority_queue",true,false,false,params);
      
    • 消息需要设置优先级

      //
      AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
      //
      correlationData.getMessageProperties().setPriority(5);
      
    • 消息已经发送到队列中

  • 注意事项

    • 每个队列的每个优先级都有一些内存和磁盘上的成本。这还会产生额外的CPU成本,特别是在使用时,因此您可能不希望创建大量的级别。
    • 消息优先级字段被定义为无符号字节,因此在实践中优先级应该在0到255之间。
    • 没有优先级属性的消息将被视为优先级为0。优先级高于队列最大优先级的消息将被视为以最大优先级发布的消息。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值