RabbitMQ学习笔记


一、相关概念

1.1. 什么是MQ?

MQ本质上是个队列,队列中存放的内容是message,是一种跨进程的通信机制,用于上下游传递消息。使用了MQ之后,消息发送方只需要依赖MQ,不用依赖其它服务。

1.2. 为什么要用MQ?

流量削峰
削峰从本质上来说就是更多地延缓用户请求,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。面临大量高并发数据库写入操作,数据库可能短时间处理不了这些大量请求,会造成数据库宕机的情况。针对这一情况,可以将这些请求消息存储到消息队列中,将大量同步请求变成异步间接执行;队列一段承受瞬时的流量洪峰,另一端平滑的将消息传递出去,缓解了数据库的压力。

应用解耦
以创建订单为例,创建订单过程中,会调用很多其他系统,譬如:库存系统、物流系统、支付系统等。这些系统全部调用成功之后,订单才能被创建出来,但调用过程中,这些系统可能会发生故障,导致订单创建失败。使用MQ之后,可以将这些未确认信息(调用失败系统所需要的信息)存储到队列中,等待系统恢复后继续执行,订单主线程直接返回订单信息,用户感受不都系统故障,提高了系统额度可用性。

异步处理
有些服务调用是异步的,例如A调用B,B需要耗费很长时间,但是A不知道B什么时候结束,这时候可以每隔一段时间查询B是否执行结束;另一种是创建一个callback回调函数,B结束后执行回调函数通知A执行完成;但是使用了队列之后,B执行完成之后将通知消息放到队列中,A监听队列,消息一发送到队列中,A就会立即接收,减小了程序的复杂性

1.3 MQ的分类

  • ActiveMQ
    优点: 单击吞吐量万级,时效性ms级,基于主从架构实现高可用性,较低的概率丢失数据。
    缺点: 官方社区维护越来越少,高吞吐量场景较少使用。
  • Kafka
    优点: 大数据领域常用,吞吐量高,时效性高;分布式,一个数据多个副本,少数机器宕机时,不会丢失数据;用户采用PULL方式获取数据,消息有序,确保消息至少会被消费一次;日志领域比较成熟。
    缺点: Kafka单机超过64个队列/分区,load就会发生明显的飙高现象,队列越多,load越高,队列发送的响应时间就会变慢;采用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持试;支持消息顺序,但是一台代理宕机时,消息就会乱序;社区更新较慢。
  • RocketMQ
    优点: 单机吞吐量十万级,可用性非常高,分布式架构,消息可以做到 0 丢失,MQ 功能较为完善,还是分布式的,扩展性好,支持 10 亿级别的消息堆积,不会因为堆积导致性能下降。
    缺点: 支持的客户端语言不多,目前是 java 及 c++,其中 c++不成熟;社区活跃度一般,没有在 MQ核心中去实现 JMS 等接口,有些系统要迁移需要修改大量代码
  • RabbitMQ
    优点: 由于erlang 语言的高并发特性,性能较好;吞吐量到万级,MQ 功能比较完备,健壮、稳定、易用、跨平台、支持多种语言。
    缺点: 商业版需要收费,学习成本较高

1.4 基本概念

在这里插入图片描述

  • **Broker:**接收和分发消息,RabbitMQ Server就是Message Broker
  • Virtual Host: 基于多租户和安全因素考虑的,当多个租户共用一个Message Broker服务时,可以划分多个vhost,每个用户在自己的vhost中创建独立的交换机(exchange)和队列。
  • Connection: publisher/consumer与Broker之间的TCP连接。
  • Channel: 在大量消息建立连接时创建TCP connection会造成大量的资源浪费,channel是在connection内部的虚拟逻辑连接,每个channel带有自己独立的id,方便客户端和message broker识别,极大地减少了连接时的资源浪费。
  • Exchange: 消息到达Broker的第一站,根据分发规则,匹配表中的RoutingKey,将消息发送到相对应的队列中。
  • Queue: 消息在这等待被取走
  • Binding 交换机和队列之间的虚拟关系,binding中可以包含routingKey,通过routingkey可以识别将消息分发到哪个队列中。

二、六大模式

2.1 基本模式(Hello World)

在这里插入图片描述
单个生产者和单个消费者,使用默认交换机;"P"表示消费者,"C"表示消费者,中间是消息缓冲区(队列)。

  • 生产者代码:
public class Producer {

    private static final String QUEUE_NAME = "hello";
    private static final String message = "hello rabbitmq";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 工厂设置相关连接信息
        factory.setHost("192.168.137.***");   // host: 连接ip地址
        factory.setUsername("admin");    // 连接用户名和密码
        factory.setPassword("*******");  

        // 新建连接
        Connection connection = factory.newConnection();

        // 创建通用用户传输消息
        Channel channel = connection.createChannel();

        /**
         * 队列声明
         * String queue:队列名字
         * boolean durable:是否持久化,队列的声明默认是放在内存中
         * boolean autoDelete:队列中的数据消费完成之后是否自动删除
         * boolean exclusive:队列是否是排它的,true表示该队列声明为排它队列,仅对首次声明它的channel可见;false表示不同channel都可以使用该队列
         * Map<String, Object> args: 设置队列的一些其它参数
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        /**
         * 生产者发布消息
         * String exchange:发送到哪一个交换机,使用默认交换机,发送到队列中
         * String routingKey:路由Key,这里直接写所有发送的队列名,后续在探究这个参数到底是什么意思
         * BasicProperties props:其它参数属性
         * byte[] bpdy: 发送的消息内容(消息体)
         */
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println("消息发送完毕");

    }
}
  • 消费者代码:
public class Consumer {

    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建工厂,获取connection
        ConnectionFactory factory = new ConnectionFactory();

        // 设置工厂的参数
        factory.setHost("192.168.137.102");
        factory.setUsername("admin");
        factory.setPassword("56234948qaz");

        // 获取连接
        Connection connection = factory.newConnection();

        // 消费者感觉只要创建channel,不需要声明队列的一些属性
        Channel channel = connection.createChannel();
        System.out.println("等待接收消息");


        DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {  // String 其实是ConsumerTag, 用于客户端生成一个消费者标识: Delivery中包含的是消息内容
            System.out.println(new String(message.getBody()));
        };

        CancelCallback cancelCallback = (String consumerTag) -> {
            System.out.println("消息传递中断");
        };

        /** channel消费处理消息
         *
         * String queue: 队列名称
         * boolean autoAck: 接收到队列传递过来的消息后ack服务器(应答服务器, 让队列知道消息接收成功), false表示手动应答, 类似于redis中基于stream实现的队列策略。
         * DeliverCallback: 当一个消息发送过来并之后自动执行的回调接口,重写内容是我们处理消息的逻辑。
         * CancelCallback: 当一个消费者取消订阅时的回调接口(未成功接收消息时执行的回调接口)
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

思考:消费者是根据队列名匹配队列,进行消费;RacketMQ消费者组第根据其所订阅的Topic类型与分区queue进行匹配再消费。

2.2 工作模式(Work Queues)

在这里插入图片描述
相比于基本模式,工作模式可以有多个消费者消费同一个队列(默认采用轮询的方式将队列的消息分发给消费者,不是共同使用同一条消息),提高任务的处理速度。

  • 封装获取Connection的方法
public class RabbitMqUtil {


    public static Connection getConnection() throws IOException, TimeoutException {
        // 创建工厂
        ConnectionFactory factory = new ConnectionFactory();

        factory.setHost("192.168.137.***");    // ip地址
        factory.setUsername("admin");     // 用户名
        factory.setPassword("*********");   // 密码

        Connection connection = factory.newConnection();

        return connection;
    }
}
  • 生产者和消费者的代码与基本模式相比没有改变,只是另开一个线程创建消费者,共用同一个队列。

2.3 发布订阅模式(Publish/Subscribe)

消息应答概念
消费者在接收并处理消息之后,通知rabbitmq已经处理消息了,可以将消息删除。

自动应答
消息发送之后立即被认为发送成功,这种模式需要在高吞吐量和数据传输安全方面做权衡。因为这种模式下消息发出时,消费者端connecton/channel断开,或者消费者端出现逻辑错误报错,恢复时但消息已经被删除,导致消息丢失;另一方面,由于消息发送立即确认,消费者端积压大量的消息,此时可能会出现消费者端崩溃的情况,所以在实际开发中,尽量选择手动确认消息。

消息应答的方法

  • Channel.basicAck(long deliveryTag, boolean multiple):用于肯定确认,RabbitMQ已知道该消息被成功处理,可以将该消息丢弃。
  • Channel.basicNack(long deliveryTag, boolean multiple, boolean requeue): 用于否定确认
  • Channel.basicReject(long deliveryTag, boolean requeue):用于否定确认

Multiple批量应答
在这里插入图片描述
在实际业务中,最好不要批量应答。

消息自动重新入队
如果消费者由于某种原因(其通道已关闭或TCP连接已丢失),导致未发送ack确认消息,RabbitMQ默认会将其重新入队;如果此时其它消费者可以处理,RabbitMQ很快将其分配给另一个消费者。这样,即使消费者偶尔死亡,也会确保消息不会丢失。

消费者代码:

DeliverCallback deliverCallback = (consumerTag, delivery) -> {


    System.out.println("接收到消息:" + new String(delivery.getBody()));

    // 千万不要忘记手动应答,通知队列消息处理完毕
    // 第一个参数是所处理的消息的标识,第二个参数是是否批量确认(最好不批量确认)
    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};

boolean ack = false;    // 取消自动确认
channel.basicConsume(QUEUE_NAME, ack, deliverCallback, cancelCallback);

RabbitMQ持久化
RabbitMQ由于某种原因崩溃,如果队列和消息都没有实现持久化的话,那么之前的消息和队列都将会被删除。要实现持久化,必须实现队列和消息持久化。

  • 队列持久化
    rabbitmq 如果重启,非持久化队列就会被删除掉,如果要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化
// 队列通过durable=true,可以设置队列持久化(生产端声明时)
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
  • 消息持久化
    要想让消息实现持久化需要在消息生产者修改代码"PERSISTENT_TEXT_PLAIN" 添加这个属性,将消息保存到磁盘中。但是消息持久化并不能完全保证消息不会丢失,存储到磁盘会存在一段时间,消息还没有完全存储到磁盘,这时RabbitMQ崩溃,持久性保证并不强。但是对于简单任务而言已经绰绰有余了,如果需要更有力的持久化策略,需要之后的高级发布确认部分。
// 通过basicProperties属性MessageProperties.PERSISTENT_TEXT_PLAIN设置消息持久化, 将消息存储到磁盘中,但不能完全保证消息不会丢失
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));

不公平分发
之前我们采用的都是默认的轮询分发策略,多个消费者一轮一轮接收消息。但是,有些消费者处理很快,有些消费者处理特别慢,这就导致了很多空闲的消费者。
为了避免这种情况,我们可以设置参数channel.basicQos(1)

channel.basicQos(1);   // 设置不公平分发,默认0是公平分发,充分利用资源
chanel.basicConsume(...........)

在这里插入图片描述

意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个
任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的 worker 或者改变其他存储任务的策略。Prefetch涉及到下面所说的预期值,实际开发中并不会单纯地设置1(1虽然最保守,但是吞吐量会大大减少),可以根据实际情况设置prefetch(预取值),只要不使用默认值0就行(代表轮询分发)。

预取值
在这里插入图片描述

消费者连接节点存在未确认消息缓冲区,开发者希望限制此缓冲区的大小,以避免无限制的未确认消息堆积。这时候可通过basicQos方法设置"预取计数"来完成,该值定义通道上允许的未确认消息的最大数量, 一旦数量达到设定值,RabbitMQ将不会向此通道发送更多的消息,除非有一个未确认消息被确认。预取值为 1 是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微提高点100 到 300 范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。

发布确认策略
上面的都是关于消息应答方面的(从消费方角度)、声明队列持久化或者发布信息到磁盘这几方面避免消息丢失。发布确认策略感觉是从生产方尽量避免消息丢失问题。

原理
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息

开启发布确认策略的方法
发布确认策略默认是关闭的,想要开启得调用confirmSelect()方法,每当想要开启消息确认策略,都需要在channel上调用此方法
channel.confirmSelect(); // 开启消息确认

发布确认策略有三种实现方法:

  • 单个确认发布
    单个确认发布是一种最简单的确认方式,是一种同步确认发布方式。即没发送一个消息之后只有它被确认之后,下一个消息才能继续发送。waitForConfirm(Long) 消息被确认之后会返回true,超时未返回将会抛出异常返回false,这样会知道哪个消息发送失败,后续消息好像会继续发送(不太确定,因为是发布确认本质是异步,发送失败应该会执行发送失败的逻辑回调函数,不会影响其它的消息发送,只是在时间上影响;确认缓慢也会影响下一条消息的发送);
    缺点:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布。

实现代码:

public static void publishMessageIndividually() throws Exception {
	 try (Channel channel = RabbitMqUtils.getChannel()) 
	 {
		 String queueName = UUID.randomUUID().toString();
		 channel.queueDeclare(queueName, false, false, false, null);
		 //开启发布确认
		 channel.confirmSelect();
		 long begin = System.currentTimeMillis();
		 
		 for (int i = 0; i < MESSAGE_COUNT; i++) {
			 String message = i + "";
			 channel.basicPublish("", queueName, null, message.getBytes());
			 //服务端返回 false 或超时时间内未返回,生产者可以消息重发
			 boolean flag = channel.waitForConfirms();
			 
			 if(flag){
			 	System.out.println("消息发送成功");
	 	     }
	 	  }
	 long end = System.currentTimeMillis();
	 System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
	 
	 }
}
  • 批量发布确认
    与单个发布确认相比,批量发布确认能提高发布吞吐量,但也是一种同步发布确认方式,会有阻塞之后消息发布的问题;这种的方式缺点就是:发布的消息数量达到指定batchsize采取这些消息,但是若某一个消息发送失败,并不知道一批中的哪个消息发送失败,无法进行回调(发布还是一条一条消息的法,确认是一批一批确认)。

实现代码:

public static void publishMessageBatch() throws Exception {
	 try (Channel channel = RabbitMqUtils.getChannel()) {
		 String queueName = UUID.randomUUID().toString();
		 channel.queueDeclare(queueName, false, false, false, null);
		 //开启发布确认
		 channel.confirmSelect();
		 //批量确认消息大小
		 int batchSize = 100;
		 //未确认消息个数
		 int outstandingMessageCount = 0;
		 long begin = System.currentTimeMillis();
		 
		 for (int i = 0; i < MESSAGE_COUNT; i++) {
			 String message = i + "";
			 channel.basicPublish("", queueName, null, message.getBytes());
			 outstandingMessageCount++;
			 
			 if (outstandingMessageCount == batchSize) {
				 channel.waitForConfirms();
				 outstandingMessageCount = 0;
			 }
		 }
		 
		 //为了确保还有剩余没有确认消息 再次确认
		 if (outstandingMessageCount > 0) {
		 	channel.waitForConfirms();
		 }
		 
		 long end = System.currentTimeMillis();
		 System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms");
		 
		 }
}
  • 异步发布确认
    异步发布确认的核心要点:
  1. 异步发送消息,主线程生产者只管发送消息,另一个线程用接收Broker确认消息并调用回调函数;
  2. 由于是异步,所以得使用能够被线程发现的集合用于存储发送的消息并进行线程之间消息的传递,例如ConcurrentSkipListMap;
  3. channel通道得额外添加confirmListener监听器用于子线程执行接收到消息的回调函数(接收成功、接收失败);
  4. 使用map的原因是可以给消息体添加序号,接收成功之后可以将map中的消息移除掉,表示消息接收成功,剩下的表示接受失败的消息,使用map可以很方便的将序号和消息体联系,方便操作移除等。
    优点: 吞吐量高,即使出现消息发布失败的情况下,也能很好地处理;这也是最优的发布却确认实现方式。

实现代码:

public class MessageConfirm {

    private static final Integer MESSAGE_COUNT = 10000;

    public static void main(String[] args) throws IOException, TimeoutException {
        publishMessageAsync();
    }


    // 异步确认消息
    public static void publishMessageAsync() throws IOException, TimeoutException {
        Connection connection = RabbitMqUtil.getConnection();
        Channel channel = connection.createChannel();

        String queueName = UUID.randomUUID().toString();
        channel.confirmSelect();    // 开启消息确认
        ConcurrentSkipListMap<Long, String> map = new ConcurrentSkipListMap<>();


        /**
         * 处理异步未确认消息:通过基于内存的能够被发布线程访问的队列存储所有发布信息,成功确认的则从队列中移除
         * 使用线程安全并且有序的哈希表,有序是通过序号与消息内容联系起来
         * 支持高并发访问
         */


        /**
         * deliverTag: 是当前消息的序列号
         * multiple: true表示批量处理;false表示一个一个处理
         */
        ConfirmCallback ackCallback = (deliverTag, multiple) -> {
            if (multiple) {
                // headMap() 返回key小于(小于等于)当前消息序号的的未确认消息, 返回的是个map
                ConcurrentNavigableMap<Long, String> headMap = map.headMap(deliverTag, true);// true表示小于等于
                headMap.clear();   // 确认成功, 则从map中移除
            } else {
                map.remove(deliverTag);   // 只清除当前序列号的消息
            }
        };


        ConfirmCallback nackCallback = (deliverTag, multiple) -> {
            String message = map.get(deliverTag);
            System.out.println("未确认的消息为: " + message + "===> 消息编号为" + deliverTag);
        };


        // 异步确认得创建监听器, 生产者一直发送消息到队列, 不需要自己确认, 使用监听器返回确认消息
        channel.addConfirmListener(ackCallback, null);   // 第一参数是确认收到消息的回调;第二个参数是未确认发布消息的回调

        long start = System.currentTimeMillis();


        for (Integer i = 0; i < MESSAGE_COUNT; i++) {
            String message = i + "";
            map.put(channel.getNextPublishSeqNo(), message);  // channel.getNextPublishSeqNo() 获取下一条将要发布的消息的序列号
            channel.basicPublish("", queueName, null, message.getBytes());
        }

        long end = System.currentTimeMillis();

        System.out.println("异步确认" + MESSAGE_COUNT + "条消息耗费的时间为: " + (end - start) + "ms");
    }
}

思考:虽然知道了发布未确认的消息,但是一般是怎么结解决的?就是RabbitMQ怎么实现发布重试?我后续再看看

2.4 路由模式(Routing)

Exchanges(交换机概念)
在这里插入图片描述

RabbitMQ消息传递模型的核心思想:生产者生产的消息从不会直接发送到队列,生产者只能将消息发送到交换机(exchange)。交换机一方面接收消息,一方面将这些消息推入到特定队列或者推入到许多队列或者丢弃它们,这是由交换机的类型决定。

Exchanges的类型
直接(direct),扇处(fanout)、标题(headers)、主题(topic)

无名交换机
之前没有学习到交换机,我们发布消息时采用空字符串(“”)标识交换机的名称,采用的是默认交换。

channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

第一个参数是交换机的名称,空字符串表示默认或无名交换机;消息能路由发送到队列中其实是有routingKey(bindingKey)指定的队列。

临时队列
可以创建一个具有随机名称的队列,并且一旦断开所有消费者的连接,队列将自动被删除。从来没有被消费者连接过将不会被自动删除。
创建临时队列的方式如下:

String queueName = channel.queueDeclare().getQueue();

绑定(Binding)
在这里插入图片描述
Binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们交换机(exchange)和哪个队列进行了绑定关系。

  • Fanout类型交换机
    将接收到的所有消息广播到它知道的所有队列中,即会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,所以此时routing key是不起作用的,可以理解为多个消费者可以使用同一条消息了。
// 声明交换机类型为Fanout类型
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");  

// 会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,所以此时routing key是不起作用的。
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
  • Direct类型交换机
    在这里插入图片描述
    Direct类型的交换机是根据指定的Routing Key将消息分发到队列中,如果发送消息指定Routing Key为"black"时,消息只会发送到Q2队列中。如果想要使用Direct类型的交换机进行多队列的消息分发,可以对这几个队列使用相同的Routing Key进行绑定。

2.5 Topic通配符模式

Topic型交换机

Topic类型的交换机是对消息分发的进一步划分,"Direct"型交换机的Routing Key只能指派一个层次的消息分发,而"Topic"型交换机可以使用多层次的Routing Key对消息分发进行划分。

Topic型交换机采用固定格式的"Routing Key"方式,必须是一个单词列表,以点号分隔开

  • *(星号)可以代替一个单词
  • #(井号)可以代替多个单词

在这里插入图片描述
Routing Key为"quick.orange.rabbit"时,消息会被分发到Q1和Q2队列中。Topic类型的Routing Key有点像通配符。

2.6 RPC模式

RPC模式目前还没有接触,先空着…


三、死信队列

死信的概念: 死信消息从字面上可以理解为无法被消费的消息,consumer从queue中获取消息消费,但是由于某种原因导致queue中的消息无法被消费,并且没有后续的处理,普通消息就变成了死信消息,死信消息一般会被转入到死信队列中。

应用场景: 订单超时自动取消

死信的来源:

  • 消息TTL过期
  • 队列达到最大长度(队列满了,无法继续添加数据到mq中)
  • 消息被拒绝消费(basic.reject或basic.nack)并且requeue=false

死信队列的框架
在这里插入图片描述
一般都会存在一个普通的交换机和队列,当普通队列中的消息无法被消费时,消息会普通队列中转发到死信队列中;由于这个原因,所以得在普通队列声明时,得在普通队列中声明死信交换机名、死信交换机和队列绑定的Routing Key。这样,死信消息才能从普通队列中转发到死信队列中。

代码举例
生产发送消息通过props属性设置消息的TTL时间。

public class Producer {

    public static final String NORMAL_EXCHANGE = "normal_exchange";

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

        System.out.println("生产者准备发送消息............");
        // 设置发送消息的TTL时间, 利用basicProperties()方法
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
        

        for (int i = 1; i < 11; i++) {
            String message = "info" + i;
            // 生产者发送消息只会通过交换机和Routing Key分发到指定队列中,生产方不需要声明交换机和队列,以及之间的绑定关系。
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes());
            System.out.println("消息发送成功: " + message);
        }
    }
}

消费端比较复杂,需要声明普通交换机、死信交换机、普通队列已经死信队列等信息

public class Receiver01 {

    public static final String NORMAL_EXCHANGE = "normal_exchange";      // 普通交换机名称
    public static final String DEAD_EXCHANGE = "dead_exchange";        // 死信交换机名称
    public static final String NORMAL_QUEUE = "normal_queue";           // 普通队列名称
    public static final String DEAD_QUEUE = "dead_queue";               // 死信队列名称

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

        // 声明普通交换机和死信交换机  direct类型
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        // 声明死信队列,这里只举例,不需要持久化。
        channel.queueDeclare(DEAD_QUEUE, false, false, false, null);

        // 声明普通队列, 并通过属性参数与死信交换机建立联系
        Map<String, Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE);   // 添加死信交换机名称
        map.put("x-dead-letter-routing-key", "lisi");    // 添加死信交换机与死信队列的routingKey
        //map.put("x-max-length", 6);         // 设置普通队列的最大长度

		// 声明普通队列, 另外得添加map存储的关于一些死信的参数信息。
        channel.queueDeclare(NORMAL_QUEUE, false, false, false, map);

        // 声明死信队列
        channel.queueDeclare(DEAD_QUEUE, false, false, false, null);

        System.out.println("Receiver01准备接收消息.......");


        // 将队列和交换机进行绑定
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, "zhangsan");
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, "lisi");

        // 回调
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            if (message.equals("info5")) {
                channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);   // false表示拒绝之后不重入队列, 拒绝后必须不重新入队才能成为死信
                System.out.println("Receiver01接收的消息: " + message + "   此消息被Receiver01拒绝");
            } else {
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);   // false表示不批量确认
                System.out.println("Receiver01接收的消息: " + message);
            }
        };

        channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, consumerTag -> {});
    }
}

四、延时队列

延时队列即是用来存放需要在指定时间被处理的元素的队列,内部是有序的。

应用场景: 订单超时自动取消;预定会议提前10分钟通知参加会议…

TTL: TTL是RabbitMQ中消息或队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间。

有两种方式设置TTL:

  • 消息TTL: 是发送时针对每条消息设置的TTL时间,每条消息的TTL时间可能会不同;
rabbitTemplate.convertAndSend("X", "XC", message, correlationData -> {
	correlationData.getMessageProperties().setExpiration(ttlTime);
});
  • 队列TTL: 创建队列的时候设置队列的"x-message-ttl"属性,队列的所有消息具有相同的TTL时间。
args.put("x-message-ttl", 5000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();

区别: 如果队列设置了TTL属性,那么消息过期时,就会立马被队列丢弃(如果存在死信队列,就会被丢弃到死信队列中)。如果是消息设置TTL属性,过期时可能不会被队列立马丢弃,因为此时是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的积压情况,过期消息可能仍会存活一段时间。

4.1 代码举例

队列TTL

框架图:
在这里插入图片描述
流程:框架具有一个普通交换机、不同TTL时间属性的普通消息队列、一个死信交换机、一个死信队列。这个框架是基于死信队列实现的,当普通消息队列中的消息过期时,会将消息丢弃到死信队列,此时消费者在消费死信队列中的消息。

队列配置类:

/**
 * TTL队列的配置类
 * @author Ya-Feng Gu
 * @create 2022-12-05-10:51
 */
@Slf4j
@Configuration
public class TTLQueueConfig {

    // 声明Direct交换机, 并注入到SpringBoot中
    @Bean
    public DirectExchange xExchange() {
        return new DirectExchange(NORMAL_TTL_EXCHANGE);
    }

    // 声明死信交换机,死信交换机和普通交换机都是Direct类型的交换机
    @Bean
    public DirectExchange yExchange() {
        return new DirectExchange(DEAD_TTL_EXCHANGE);
    }

    // 声明普通队列QA及其对应TTL时间,普通队列声明时必须指定死信交换机和死信队列
    @Bean("queueA")
    public Queue normalQueueA () {
        HashMap<String, Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange", DEAD_TTL_EXCHANGE);  // 添加死信交换机
        map.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);   // 添加死信RoutingKey
        map.put("x-message-ttl", NORMAL_QUEUE_TIME_A);          // 声明普通队列的TTL时间
        return QueueBuilder.nonDurable(NORMAL_TTL_QUEUE_A).withArguments(map).build();
    }

    // 声明普通队列QB
    @Bean("queueB")
    public Queue normalQueueB () {
        HashMap<String, Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange", DEAD_TTL_EXCHANGE);  // 添加死信交换机
        map.put("x-dead-letter-routing-key", DEAD_ROUTING_KEY);   // 添加死信RoutingKey
        map.put("x-message-ttl", NORMAL_QUEUE_TIME_B);          // 声明普通队列的TTL时间
        return QueueBuilder.nonDurable(NORMAL_TTL_QUEUE_B).withArguments(map).build();
    }


    // 声明死信队列
    @Bean("deadQueue")
    public Queue deadQueue () {
        return new Queue(DEAD_TTL_QUEUE);
    }

    // 绑定普通交换机和普通队列
    @Bean
    public Binding normalBindingA(@Qualifier("queueA") Queue queue,  
    // @Qualifier注解类似有@Resource注解,不是更具bean类型而是根据执行名称从ioc容器中寻找bean
                                  @Qualifier("xExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(NORMAL_ROUTING_KEY_A);   // 绑定不要忘记RoutingKey
    }

    // 绑定普通交换机和普通队列
    @Bean
    public Binding normalBindingB(@Qualifier("queueB") Queue queue,
                                  @Qualifier("xExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(NORMAL_ROUTING_KEY_B);   // 绑定不要忘记RoutingKey
    }


    // 绑定死信交换机与死信队列
    @Bean
    public Binding deadBinding(@Qualifier("deadQueue") Queue queue,
                               @Qualifier("yExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(DEAD_ROUTING_KEY);
    }

}

生产者:

@GetMapping("/sendMessage/{message}")
    public void sendTTLMessage(@PathVariable("message") String message) {
        log.info("当前时间: {}, 发送一条消息给两条TTL队列: {}", LocalDateTime.now().toString(), message);
        rabbitTemplate.convertAndSend(NORMAL_TTL_EXCHANGE, NORMAL_ROUTING_KEY_A, "消息来自ttl队列10s的消息: " + message);
        rabbitTemplate.convertAndSend(NORMAL_TTL_EXCHANGE, NORMAL_ROUTING_KEY_B, "消息来自ttl队列40s的消息: " + message);
    }

消费者:

	// 消费者接受消息得监听队列,使用RabbitMQListener注解指定哪个队列
    @RabbitListener(queues = DEAD_TTL_QUEUE)
    public void receiveMessage(Message message, Channel channel) throws UnsupportedEncodingException {
        String msg = new String(message.getBody(), "UTF-8");
        log.info("当前时间: {}, 收到来自死信队列的消息: {}", LocalDateTime.now().toString(), msg);
    }

缺点:
是通过设置队列TTL属性实现消息过期,但是现实场景中,可能会有不同时间的延时任务,这就得增加不同时间TTL的队列,造成资源的浪费。这是可以增加一个普通队列不设置TTL属性,发送消息时使用消息TTL时间,可以灵活实现TTL延时任务。

代码优化:

在这里插入图片描述
增加一个普通队列,发消息时设置消息TTL属性。

@Component
public class MsgTtlQueueConfig {
	public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
	public static final String QUEUE_C = "QC";

	//声明队列 C 死信交换机
 	@Bean("queueC")
 	public Queue queueB(){
 		Map<String, Object> args = new HashMap<>(3);
 		//声明当前队列绑定的死信交换机
 		args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
 		//声明当前队列的死信路由 key
 		args.put("x-dead-letter-routing-key", "YD");
 		//没有声明 TTL 属性
 		return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
 	}
 
 	//声明队列 B 绑定 X 交换机
 	@Bean
 	public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
 								  @Qualifier("xExchange") DirectExchange xExchange){
	 
	 	return BindingBuilder.bind(queueC).to(xExchange).with("XC");
 	}
}

生产者代码:

@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
 	rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
 	correlationData.getMessageProperties().setExpiration(ttlTime);
 	return correlationData;
 	});
 	
 	log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}

在这里插入图片描述
在这里插入图片描述

存在巨大的问题就是如果使用消息方式设置TTL属性,消息可能不会按时"死亡",因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息延时很长很长,而第二个消息延时很短,但第二个消息并不会优先得到执行(感觉是第一个消息2000毫秒过期,丢到死信队列,此时才会处理第二条消息,第二条消息处理时,已经过期了,丢到死信队列由消费者进行处理)

4.2 RabbitMQ插实现延时队列

安装插件:
在这里插入图片描述
使用自定义交换机,采用新的交换类型(“x-delayed-message”),支持消息延时投递机制,消息传之后不会立即投递到目标队列中,而是存储在mnesia(一个分布式数据系统中)表中,到达投递时间后才会投递到目标队列中。
在这里插入图片描述
配置类: 声明延时交换机,队列

@Configuration
public class DelayedQueueConfig {

    public static final String DELAYED_EXCHANGE = "delayed.exchange";
    public static final String DELAYED_QUEUE = "delayed.queue";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";

    // 声明延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        HashMap<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");   // 延时发送消息的方式, 还是基本方式 direct
        return new CustomExchange(DELAYED_EXCHANGE, "x-delayed-message", true, false, args);
    }

    // 声明队列
    @Bean
    public Queue delayedQueue() {
        return QueueBuilder.nonDurable(DELAYED_QUEUE).build();
    }

    // 绑定交换机与队列
    @Bean
    public Binding delayedBinding(@Qualifier("delayedExchange")Exchange exchange,
                                  @Qualifier("delayedQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY).noargs();
    }


}

消息生产者: 发送消息时设置延时时间

// 通过延时插件(延时交换机)
    @GetMapping("/sendDelayedMessage/{message}")
    public void sendDelayedMessage(@PathVariable("message") String message,
                                   @RequestParam("delayedTime") int delayedTime) {
        log.info("当前时间: {}, 发送一条TTL时间为{}毫秒的消息给队列: {}", LocalDateTime.now(), delayedTime, message);
        rabbitTemplate.convertAndSend(DELAYED_EXCHANGE, DELAYED_ROUTING_KEY, message, msg -> {
            msg.getMessageProperties().setDelay(delayedTime);
            return msg;
        });
    }

消息消费者:

@RabbitListener(queues = DELAYED_QUEUE)
    public void delayedMessageConsumer(Message message, Channel channel) {
        String msg = new String(message.getBody());
        log.info("当前时间: {}, 收到来自死信队列的消息: {}", LocalDateTime.now().toString(), msg);
    }

小结:

  • 交换机:消息生产者是将消息发送到交换机,并不是直接发送到某一个队列中,而是通过RoutingKey(交换机和队列之间的绑定关系),将消息投递到队列中。交换机的类型主要有:直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)。
    扇出(Fanout)型交换机:类似广播机制。消息到达交换机时,fanout型交换机会将消息分发到它所知道的所有队列(与之绑定的所有队列),此时RoutingKey不起任何作用,多个队列(多个消费者)接收/消费消息。
    直接(direct)型交换机:根据指定的Routing Key将消息分发到指定的队列
    主题(Topic)型交换机:根据类似于通配符的"层次"的Routing Key将消息分发到与之相匹配的队列,Topic型交换机是对Routing Key的进一步划分。
    持久化: 队列持久化:通过声明队列时,将队列durable属性设置true;消息持久化:发送消息时通过设置"MessageProperties.PERSISTENT_TEXT_PLAIN"将消息设置成持久化。但是将消息持久化(单击模式下,写入到磁盘的过程中,可能出现某种原因宕机之列的情况,消息写入失败,重启丢失)。

  • 消息应答(从消费角度): 消费者消费消息成功之后,会返回ack确认信息告诉Broker成功消费消息,此时Broker会将消息从消息队列中移除 。应答分为自动应答和手动应答,自动应答即消息发出后就认为发出成功,很容易造成消息丢失。手动应答可以返回肯定确认和否定确认,否定确认表明不消费消息,如果存在死信队列的情况下,消息会转发到死信队列中,可以消费或直接丢弃。此外,还存在批量应答,但是一般情况下,不建议采用批量应答。

  • 发布确认(从生产角度): 发布确认有三种方式:单个确认发布、批量确认发布、异步确认发布。
    单个确认发布和批量确认发布都是同步的方式,吞吐量较低;一般采用的是异步确认发布,生产者只管发送消息,但channel得注册一个确认监听器(“addConfirmListener”),开启另一个线程用于调用回调函数返回确认消息。

看看SpringBoot整合RabbitMQ怎么做的,这里使用的是map将所有消息存储起来,调用回调接口时处理map中的消息。

  • 死信队列: 死信主要有三个来源:消息TTL过期、队列到达最大长度(无法添加消息到mq中)、消息被消费时被拒绝。
    要点:死信队列实现肯定得有个死信交换机和死信队列,这样普通队列中的消息才能转发到死信队列中,但是普通队列怎么知道转到哪个死信交换机呢?所以声明普通队列时,必须添加死信交换机和队列名称(RoutingKey)。

  • 延时队列: 延时队列即在指定时间时,队列中的元素被取出和处理。
    在没有使用RabbitMQ插件实现延时队列的情况下,这里采用的是基于死信队列实现;基于死信实现又分为两种情况:
    1.队列设置TTL属性,队列中的消息只要一到期,消息就会被丢弃到死信队列中被消息。但是这种情况不够灵活,因为会有许多不同时间的延时消息,这时候就得创建许多不同TTL时长的队列,造成资源的浪费。
    2.消息设置TTL属性,虽然这种情况较上面的情况比较灵活,但是存在巨大的缺陷。因为消息设置TTL与队列设置TTL不同的是,消息之后被消费之前才会对消息进行判断是否过期。
    普通队列有两个消息:一个消息是延时20s,第二个消息延时2s。没有消费者消费普通队列,按顺序只会先处理第一个消息,当第一个延时消息为20s过期时,转发到死信队列被消费。此时,第二个消息2s已经过期了,却不能先被处理。这个情况下只能依靠RabbitMQ插件实现。

突然想到了一个问题:如果消息设置了TTL属性,但是放到普通队列中,会不会立马被消费?
不会,因为一般情况下,这是TTL属性用来处理延时消息,延时消息一般是基于死信队列处理,不会有消费中监听普通队列,即普通队列中的消息不会被消费,这是TTL到期,消息自动转到死信队列中被消费者消费。这种情况下,消费者一般不会监听普通队列,而是监听死信队列。

五、高级发布确认

5.1 SpringBoo版发布确认

之前的异步发送实现方式是添加一个确认监听器,和一个发线程发现的map来实现。在SpringBoot中,需要在配置yml文件中开启发布确认模式,然后再创建一个实现了RabbitTemplate.ConfirmCallback接口的配置类并将其注入到IOC容器中的RabbitTemplate中(使用PostConstruct注解)。

@Component
@Slf4j
public class ConfirmCallbackConfig implements RabbitTemplate.ConfirmCallback{

    @Resource
    private RabbitTemplate rabbitTemplate;

    // 当前类初始化之后自动执行的方法,由于是异步调用接口的实现方法,所以这个实现方法得注入到ioc容器中的rabbitTemplate中
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);   // 这是用于交换机的确认机制

    }

    /**
     * 用于发布确认,生产者发送消息给交换机会自动调用的回调接口,类似于处理发布确认中的异步确认消息
     * @param correlationData  消息的相关数据,包含id之类的信息
     * @param ack  表示交换机是否成功接收到消息
     * @param cause  失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {   // 表示交换机成功接收到消息
            log.info("交换机成功接收到id为: {}的消息", id);
        } else {
            log.info("交换机未接收到id为:{}的消息, 原因为:{}", id, cause);
        }

    }
}

5.2 消息回退

之前的消息应答问题是消费者成功消费消息,通知mq可以将消息从队列中移除。这种情况是消息发送到交换机中,消息可路由到队列。但是会出现一种情况,交换机成功接收到消息之后,发现消息不可路由(RoutingKey匹配错误)。这时候得通知生产者发出的消息不可路由,交给消费者自己处理,否则会造成消息丢失,通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者,实现ReturnCallback接口配置类,此外还要在yml配置文件中开启消息回退。

@Component
@Slf4j
public class ConfirmCallbackConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Resource
    private RabbitTemplate rabbitTemplate;

    // 当前类初始化之后自动执行的方法,由于是异步调用接口的实现方法,所以这个实现方法得注入到ioc容器中的rabbitTemplate中
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);   // 这是用于交换机的确认机制
        rabbitTemplate.setMandatory(true);   // true表示发现消息不可路由时,将消息返回给生产者; false表示直接将消息丢弃
        rabbitTemplate.setReturnCallback(this);    // 消息回退实现接口

    }

    /**
     * 用于发布确认,生产者发送消息给交换机会自动调用的回调接口,类似于处理发布确认中的异步确认消息
     * @param correlationData  消息的相关数据,包含id之类的信息
     * @param ack  表示交换机是否成功接收到消息
     * @param cause  失败的原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {   // 表示交换机成功接收到消息
            log.info("交换机成功接收到id为: {}的消息", id);
        } else {
            log.info("交换机未接收到id为:{}的消息, 原因为:{}", id, cause);
        }

    }

    // 发现消息不可路由时,调用接口实现类将消息返回给生产者
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.info("消息发送失败: {}, 原因: {}, 交换机: {}, 路由Key: {}",
                new String(message.getBody()), replyText, exchange, routingKey);
    }


}

5.3 备份交换机

使用Mandatory参数虽然可以让生产者知道了不可路由的消息,但是无法处理这些无法路由的消息,一般只能打印日志,记录这些消息。但是使用备份交换机可以处理这些无法路由的消息,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑定的队列中并进行相应的处理。
在这里插入图片描述
需要在声明确认交换机时,添加"alternate-exchange,备份交换机名称"参数表示备份交换机的信息;另外,设置了备份交换机就不再利用"Mandatory参数"实现回退消息。

代码实现:

public class ConfirmCallbackConfig implements RabbitTemplate.ConfirmCallback {

    @Resource
    private RabbitTemplate rabbitTemplate;

    // 声明confirm交换机、队列, 备份交换机、备份队列和警告队列

    public static final String CONFIRM_EXCHANGE = "confirm_exchange";
    public static final String CONFIRM_QUEUE = "confirm_queue";
    public static final String CONFIRM_ROUTING_KEY = "confirm_key";
    public static final String BACKUP_EXCHANGE = "backup_exchange";
    public static final String BACKUP_QUEUE = "backup_queue";
    public static final String WARNING_QUEUE = "warning_queue";

    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
        // 使用备份交换机可以不使用Mandatory参数感知不可路由消息
    }


    // 声明并注入备份交换机
    @Bean
    public FanoutExchange backupExchange() {
        Exchange exchange = ExchangeBuilder.fanoutExchange(BACKUP_EXCHANGE).build();
        return (FanoutExchange) exchange;
    }


    // 声明并注入确认交换机
    @Bean
    public DirectExchange confirmExchange() {
        // 确认交换机得声明备份交换机
        Exchange exchange = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE)
                .withArgument("alternate-exchange", BACKUP_EXCHANGE).build();
        return (DirectExchange) exchange;
    }


    // 声明并注入确认队列
    @Bean
    public Queue confirmQueue() {
        return QueueBuilder.nonDurable(CONFIRM_QUEUE).build();
    }

    // 声明并注入备份队列
    @Bean
    public Queue backupQueue() {
        return QueueBuilder.nonDurable(BACKUP_QUEUE).build();
    }


    // 声明并注入警告队列
    @Bean
    public Queue warningQueue() {
        return QueueBuilder.nonDurable(WARNING_QUEUE).build();
    }

    // 队列和交换器进行绑定
    @Bean
    public Binding confirmBinding(@Qualifier("confirmExchange") Exchange exchange,
                                  @Qualifier("confirmQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange).with(CONFIRM_ROUTING_KEY).noargs();
    }

    @Bean
    public Binding backupBinding(@Qualifier("backupExchange") FanoutExchange exchange,
                                  @Qualifier("backupQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange);   // Fanout型交换机只需要绑定就行, 不需要指定RoutingKey
    }

    @Bean
    public Binding warningBinding(@Qualifier("backupExchange") FanoutExchange exchange,
                                  @Qualifier("warningQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(exchange);   // Fanout型交换机只需要绑定就行, 不需要指定RoutingKey
    }




    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机成功接收到消息, id:{}", id);
        } else{
            log.info("交换机未接收到消息, id:{}, 原因: {}", id, cause);
        }
    }
}

六、其他知识点

  1. 幂等性
    用于对于统一操作发起的一次或者多次请求的结果是一致的,不会因为多次点击而产生副作用。
    举例: 订单支付。支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了。客户再次点击支付按钮,此时会进行第二次扣款,扣款成功。多洗扣款,并产生了多条流水记录。在此前的单应用系统中,只需把数据库的操作放入到事务中即可,发生错误立即回滚,但是在响应客户端的时候也有可能出现网络异常或中断等等问题。

  2. 消息重复消费
    消费者在消费MQ中的消息时,成功执行后返回ack给mq,通知队列可以删除消息。但此时网络中断,MQ为接收到ack确认消息,mq将消息分发给其他监听此队列的消费者或等待网络恢复连接之后将消息重新投递给消费者,再次执行,此时就造成了消费重复问题。

解决思路: MQ消费者的幂等性问题的解决一般是使用全局ID(比如订单号)作为消息的id,每次消费之前使用该id判断消息是否已经消费过。主流的操作的幂等性有两种操作:a. 唯一ID +指纹码机制 b. 利用redis的原子性实现

  • 唯一ID + 指纹码机制
    指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基
    本都是由我们的业务规则拼接而来,但是一定要保证唯一性。利用查询语句通过id判断是否存在数据库中。
  • Redis原子性
    利用Redis执行setnx命令,天然具有幂等性,从而实现不重复消费。
  1. 优先级队列
    客户端通过配置队列的x-max-priority参数的方式设置一个队列支持的最大优先级(但是不能使用策略的方式配置)以此来声明一个优先级队列。优先级最大值为255、最小值为0(默认值),推荐1~10。生产者可以通过设置Basic.Properties的priority属性设置消息的优先级(值越大,优先级越高)。优先级越高,越先被消费者消费,但是带来的内存、磁盘、CPU开销越高。
  • 队列设置优先级
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);  // "x-max-priority"参数设置优先级
channel.queueDeclare("hello", true, false, false, params);
  • 消息设置优先级
// 每条消息发送时,可以设置消息在同一队列中的优先级, 但是一般在所有消息在队列中, 消息优先级才有意义
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());

如果消费者的消费速度远低于生产者生产消息的速度、Broker有消息积压的情况下,对消息设置的优先级才有意义。

  1. 惰性队列
    惰性队列尽可能地将消息存入到磁盘中,在消息者需要消费相应的消息时才会被加载到内存中。它的一个重要的设计指标是能够维护更长的队列,即支更多的消息存储。即使消费者出于某种原因下线而导致不能长时间消费消息造成消息堆积时,惰性队列就可以缓解这种问题。

默认情况下,当生产者向RabbitMQ发送消息的时候,队列中的消息会尽可能的存储在内存中,这样可以更加快速的发送给消费者。即使消息设置成持久化,在被写入磁盘的时候也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的队列消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。
在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值