RabbitMQ教程

rabbitMq原理图

一个broker可以有多个Exchange,一个Exchange连接多个queue。 

RabbitMq 六大模式:

1、Hello world

2、Work queues

3、Publish/Subscribe

4、Routing

5、Topics

6、Publisher Confirms

高级部分

1、死信队列

2、延迟队列

3、发布确认高级

        3.1发布确认

        3.2回退消息

        3.3备份交换机

4、幂等性

5、优先级队列

6、惰性队列

安装好RabbitMQ之后:我们就可以进行代码编辑了

简单队列:

添加依赖:

P:消息的生产者
C:消息的消费者
红色:队列

生产者将消息发送到队列,消费者从队列中获取消息。

(1)添加依赖:

<dependency>
   <groupId>com.rabbitmq</groupId>
   <artifactId>amqp-client</artifactId>
   <version>3.4.1</version>
</dependency>

(2)进行RabbitMQ连接的工具类

package com.rabbitmq.util;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class ConnectionUtil {
	public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("localhost");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("root");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

对应的VirtualHost、Username、Password

注意:Virtual Hosts可以是我们自己创建的

Work模式:

一个生产者、2个消费者。
一个消息只能被 一个消费者 获取。

util的连接还是使用之前的

生产者:

package com.produceToTwoCustomer;

import com.rabbitmq.util.ConnectionUtil;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Send {
	private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        for (int i = 0; i < 100; i++) {
            // 消息内容
            String message = "" + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [0] Sent '" + message + "'");

            Thread.sleep(i * 10);
        }

        channel.close();
        connection.close();
    }
}

console:(会传输所有数字)

 [0] Sent '0'
 [0] Sent '1'
 [0] Sent '2'
 [0] Sent '3'
 [0] Sent '4'
 [0] Sent '5'
 [0] Sent '6'
 [0] Sent '7'
 ... ...

消费者1:(注意,该消费者消费的速度会快点)

package com.produceToTwoCustomer;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

/**
 * 消费者1
 */
public class Recv {
	private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, true, consumer);

        System.out.println("RabbitRev [1] start");
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [1] Received '" + message + "'");
            //休眠
            Thread.sleep(10);
        }
    }
}

cosole:(会接受所有的偶数)

RabbitRev [1] start
 [1] Received '0'
 [1] Received '2'
 [1] Received '4'
 [1] Received '6'
 [1] Received '8'
 [1] Received '10'
 [1] Received '12'
 ... ...

消费者2:(该消费者的速度会慢点)

package com.produceToTwoCustomer;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

public class Recv2 {
	private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,false表示手动返回完成状态,true表示自动
        channel.basicConsume(QUEUE_NAME, true, consumer);

        System.out.println("RabbitRev [2] start");
        
        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [2] Received '" + message + "'");
            // 休眠1秒
            Thread.sleep(1000);
        }
    }
}

console:(会接受所有的奇数)

RabbitRev [2] start
 [2] Received '1'
 [2] Received '3'
 [2] Received '5'
 [2] Received '7'
 [2] Received '9'
 ... ...

结论:因为消费者1线程停顿的时间短。应该是消费者1要比消费者2获取到的消息多才对。
RabbitMQ 默认将消息顺序发送给下一个消费者,这样,每个消费者会得到相同数量的消息。即轮询(round-robin)分发消息。

work模式的“能者多劳”

打开上述代码的注释:(在消费者代码上进行修改)

// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1);  //默认为0

//开启这行 表示使用手动确认模式
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

同时改为手动确认:
// 监听队列,false表示手动返回完成状态,true表示自动
channel.basicConsume(QUEUE_NAME, false, consumer);

我们使用basicQos( prefetchCount = 1)方法,来限制RabbitMQ只发不超过1条的消息给同一个消费者。

设置为1后,对应的ack_queue上的prefetchCount也会等于1

 不公平分发由消费者来决定

生产者(完全不变)

package com.produceToTwoCustomer;
import com.rabbitmq.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Send {
	private final static String QUEUE_NAME = "test_queue_work";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        for (int i = 0; i < 100; i++) {
            // 消息内容
            String message = "" + i;
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println(" [0] Sent '" + message + "'");

            Thread.sleep(i * 10);
        }

        channel.close();
        connection.close();
    }
}

console:(照样发送0到99)

 [0] Sent '0'
 [0] Sent '1'
 [0] Sent '2'
 [0] Sent '3'
 [0] Sent '4'
 [0] Sent '5'
 [0] Sent '6'
 ... ...
 [0] Sent '99'

消费者1(处理的快的)

package com.produceToTwoCustomer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

/**
 * 消费者1
 */
public class Recv {
	private final static String QUEUE_NAME = "test_queue_work";

	public static void main(String[] argv) throws Exception {

		// 获取到连接以及mq通道
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();

		// 声明队列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);

		// 同一时刻服务器只会发一条消息给消费者
		channel.basicQos(1);

		// 定义队列的消费者
		QueueingConsumer consumer = new QueueingConsumer(channel);
		// 监听队列,false表示手动返回完成状态,true表示自动
		channel.basicConsume(QUEUE_NAME, false, consumer);

		System.out.println("RabbitRev [1] start");
		// 获取消息
		int i =1;
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [1] Received '" + message + "'   total:"+i);
			i++;
			//休眠
			Thread.sleep(10);
			// 返回确认状态,注释掉表示使用自动确认模式
			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
		}
	}
}

console:(会收到67条信息)

RabbitRev [1] start
 [1] Received '1'   total:1
 [1] Received '2'   total:2
 [1] Received '3'   total:3
 [1] Received '4'   total:4
 ... ...
 [1] Received '99'   total:67

消费者2(处理慢)

package com.produceToTwoCustomer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

public class Recv2 {
	private final static String QUEUE_NAME = "test_queue_work";

	public static void main(String[] argv) throws Exception {

		// 获取到连接以及mq通道
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();

		// 声明队列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);

		// 同一时刻服务器只会发一条消息给消费者
		channel.basicQos(1);

		// 定义队列的消费者
		QueueingConsumer consumer = new QueueingConsumer(channel);
		// 监听队列,false表示手动返回完成状态,true表示自动
		channel.basicConsume(QUEUE_NAME, false, consumer);

		System.out.println("RabbitRev [2] start");

		// 获取消息
		int i =1;
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [2] Received '" + message + "'   total:"+i);
			i++;
			// 休眠1秒
			Thread.sleep(1000);
			//下面这行注释掉表示使用自动确认模式
			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
		}
	}
}

console(会收到33条信息)

RabbitRev [2] start
 [2] Received '0'   total:1
 [2] Received '16'   total:2
 [2] Received '23'   total:3
 ... ...
 [2] Received '98'   total:33

预取值:

 假设生产者发送 11.22.33.44.55.66.77 条消息, 我们可以指定消费者C1接受2条消息,消费者C2接受5条消息。

最后的结果是消息堆积在信道,是说堆积在信道中最多有几条

public class DirectSend {
    private static final String EXCHANGE_NAME = "test_exchange_direct";
    private static final String TASK_QUEUE_NAME = "task_queue_name";

    private static final boolean durable = false;
    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();


        channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            //设置生产者发送消息为持久化消息(要求保存到磁盘上)保存在内存中
            channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
            System.out.println("生产者发出消息");
        }
        channel.close();
        connection.close();
    }
}
public class DirectRecv {
    private final static String QUEUE_NAME = "test_queue_direct_1";

    private final static String EXCHANGE_NAME = "test_exchange_direct";
    private static final String TASK_QUEUE_NAME = "task_queue_name";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        System.out.println("c1 等待接收消息处理时间较短");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            try {
                Thread.sleep(1000L);
            }catch (Exception e){
                e.printStackTrace();
            }

            System.out.println("接收到的消息:" + new String(message.getBody(),"UTF-8"));

            //手动应答
            /**
             * 1、消息的标记 tag
             * 2、是否批量应答 false : 不批量应答信道中的小, true:批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
        };

        // 这只不公平分发
        int prefetchCount = 2;//默认是1
        channel.basicQos(prefetchCount);

        //采用手动应答
        boolean autoAct = false;
        channel.basicConsume(TASK_QUEUE_NAME, autoAct,deliverCallback,(consumerTag -> {
            System.out.println(consumerTag + "消费者消息取消接口回调逻辑");
        }));

    }

}
public class DirectRecv2 {
    private final static String QUEUE_NAME = "test_queue_direct_1";

    private final static String EXCHANGE_NAME = "test_exchange_direct";
    private static final String TASK_QUEUE_NAME = "task_queue_name";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        System.out.println("c1 等待接收消息处理时间较短");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            try {
                Thread.sleep(30000L);
            }catch (Exception e){
                e.printStackTrace();
            }

            System.out.println("接收到的消息:" + new String(message.getBody(),"UTF-8"));

            //手动应答
            /**
             * 1、消息的标记 tag
             * 2、是否批量应答 false : 不批量应答信道中的小, true:批量
             */
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
        };

        // 这只不公平分发
        int prefetchCount = 5;//默认是1
        channel.basicQos(prefetchCount);

        //采用手动应答
        boolean autoAct = false;
        channel.basicConsume(TASK_QUEUE_NAME, autoAct,deliverCallback,(consumerTag -> {
            System.out.println(consumerTag + "消费者消息取消接口回调逻辑");
        }));

    }

}

 所以执行的结果,我们需要看消费者2是否消费到了5条数据

消息应答:

          消费者完成一个任务可能需要一段时间,如果其中一个一个消费者处理一个长的任务并仅只完成了部分突然挂掉了,会发生什么情况。RabbitMq默认一旦向消费者传递一条消息,便立即将该消息标记为删除。

         为保证消息在发送过程中不丢失,rabbitMq引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitMq可以把该消息删除了。

消息应答的方法:
A.Channel.basicAck(用于肯定确认)
    RabbitMq已知道该消息并且成功的处理消息,可以将其丢弃了
B.Channel.basicNack(用于否定确认)
C.Channel.basicReject(用于否定确认)
   与channel.basicNack相比少一个参数
   不处理该消息了直接拒绝,可以将其丢弃

 

Multiple的解释
      手动应答的好处是可以批量应答并且减少网络拥堵
Multiple的true和false代表不同的意思
      true代表批量应答channel上未应答的消息
      比如说channel上有传送tag5,6,7,8 当前tag是8,那么此时5-8的这些还未应答的消息都会被确认收到消息应答
      而false只会应答8

应答消息自动重新入队

     如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,RabbitMq将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新费配给另一个消费者

c1在处理的过程,断开了。   然后重新入队,给c2消费

消息手动确认生产者

public class Send {
    private final static String QUEUE_NAME = "ack_auto_fale";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        /**
         * queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5)
         * var2(durable) 设置是否持久化, true表示队列为持久化, 持久化的队列会存盘, 在服务器重启的时候会保证不丢失相关信息,
         *                    队列是否保存消息
         * var3(exclusive:) 设置是否排他, true表示队列为排他的, 如果一个队列被设置为排他队列,
         *                    该队列仅对首次声明它的连接可见, 并在连接断开时自动删除, (这里需要注意三点:1.排他队列是基于连接
         *                    Connection可见的, 同一个连接的不同信道Channel是可以同时访问同一连接创建的排他队列;
         *                    "首次"是指如果一个连接己经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,
         *                    这个与普通队列不同;即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,
         *                    这种队列适用于一个客户端同时发送和读取消息的应用场景)
         *                    是否只供一个消费者进行消费,是否进行消息共享,true可以多个消费者消费,false只能一个消费者
         * var4(autoDelete: )设置是否自动删除。为true 则设置队列为自动删除。自动删除的前提是, 最后一个消费者端口连接以后,该队列是否自动删除
         * arguments: 设置队列的其他一些参数, 如 x-message-ttl等
         */


        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入信息:");
        while (scanner.hasNext()){
            String message = scanner.nextLine();
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
            System.out.println("生产者发出消息 : "+ message);
        }


    }
}

/**
 * 消息自动重新入队
 *       如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,
 *  RabbitMq将了解到消息未完全处理,兵将其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。
 *  这样,即使某个偶尔死亡,也可以确保不会丢失任何消息
 */

消费者

public class Recv2 {

    private final static String QUEUE_NAME = "ack_auto_fale";

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println("c1 等待接受消息");

        //声明接受消息
        DeliverCallback deliverCallback = (consumerTag, delivery)->{
            // 接受的消息 message
            String message = new String(delivery.getBody(), "UTF-8");
            try {
                Thread.sleep(10000L);
            }catch (Exception e){
                e.printStackTrace();
            }
            //我们可以在这里报错,然后看另外一个消费者是否会消费消息,如果另外一个消费者消费了,证明消息重新入队了

            System.out.println("接受到的消息 : " + message);
            //消息标记tag,表示应答的是哪个消息?
            // false代表只应答接收到的那个传递的消息, true为应答所有消息包括传递过来的消息
            //getEnvelope() 里面存放的属性
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);         // 1
            //改成false可以实现处理一个应答一个的实现
        };

        //手动应答
        boolean autoAck = false;

        /**
         * 消费者消费消息
         * 1、消费哪个队列
         * 2、消费成功之后是否要自动应答 true:自动  false:手动
         * 3、消费未成功消费的回调
         * 4、消费者取消消费的回调
         */
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, consumerTag ->{} );   // 2
        /**
         * 总体思路就是先1处理完之后, 再2进行确认消息
         */

    }
}

消息的确认模式:自动确认和手动确认

消费者从队列中获取消息,服务端如何知道消息已经被消费呢?

模式1:自动确认
只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费。
模式2:手动确认
消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。

开启手动确认:

开启自动确认模式:

结论:多劳多得原理:

怎样才能做到按照每个消费者的能力分配消息呢?联合使用 Qos 和 Acknowledge 就可以做到。
basicQos 方法设置了当前信道最大预获取(prefetch)消息数量为1。消息从队列异步推送给消费者,消费者的 ack 也是异步发送给队列,从队列的视角去看,总是会有一批消息已推送但尚未获得 ack 确认,Qos 的 prefetchCount 参数就是用来限制这批未确认消息数量的。设为1时,队列只有在收到消费者发回的上一条消息 ack 确认后,才会向该消费者发送下一条消息。prefetchCount 的默认值为0,即没有限制,队列会将所有消息尽快发给消费者。

消息和队列持久化

默认情况下RabbitMq退出或由于某种原因奔溃时,它忽视队列和消息。我们需要将队列和消息都标记为持久化

1、如何实现持久化

// 声明队列持久化
boolean durable = true;
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);


//消息持久化   MessageProperties.PERSISTENT_TEXT_PLAIN
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
// 设置生产者发送消息为持久化消息(要求保存到磁盘上),否者只在内存中

但是需要注意的是如果之前声明的队列不是持久化的,需要把原先队列删除掉,或者重新创建一个持久化的队列,不然会出现错误

将消息标记为持久化并能不完全保证不会丢失消息,尽管它告诉RabbitMq将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存储完。

消息确认保证数据持久化

 必须得消息到达队列后持久化到磁盘中告诉生产者确认了,才能保证数据(只第二步的话有可能消息到达队列,还未来得及持久化rabbitMq宕机了)

1、开启发布确认的方法

 1.1单个确认发布

         这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它 被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认 的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

          特点:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。

1.2 批量确认发布

        与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地 提高吞吐量。缺点:当发生故障导致发布出现问题时,不知道是哪个消息出现问题,我们必须将整批处理保存在内存中,

1.3异步确认发布

        异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说, 他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功, 下面就让我们来详细讲解异步确认是怎么实现的。

  问题:如何处理异步未确认消息

         最好的解决方案就是把未确认的消息放到一个基于内存的能被发布小城访问的队列,比如说ConcurrentLinkedQueue(因为多个线程执行)0这个队列在 comfirm callbacks 与发布线程之间进行消息的传递。

package rabbitmq.test.e消息确认;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;
import rabbitmq.test.coreUtil.ConnectionUtil;

import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * 发布确认模式
 *   使用的时间 比较那种确认方式是最好的
 *     1、单个确认
 *     2、批量确认
 *     3、异步批量确认
 */
public class ConfirmMessage {
    //批量发消息的个数
    public static final int MESSAGE_COUNT = 1000;

    public static void main(String[] args) throws Exception{
        // 1、单个确认
        ConfirmMessage.publishMessageIndividually(); // 耗时722ms

        // 2、批量确认
        ConfirmMessage.publishMessageBatch();// 耗时 147ms

        // 3、异步批量确认
        ConfirmMessage.publishMessageAsync();//  耗时62ms
    }

    //单个确认
    public static void publishMessageIndividually() throws Exception{
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName, true, 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());
            //单个消息就马上进行发布确认,会进行等待
            boolean flag = channel.waitForConfirms();
            if(flag){
                System.out.println(message + "  : 消息发送成功");
            }
        }

        //结束时间
        long end = System.currentTimeMillis();

        System.out.println("print cost time : " + (end-begin) +" ms ");
    }

    //批量发布确认
    public static void publishMessageBatch() throws Exception{
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName, true, false, false, null);

        //开启发布确认
        channel.confirmSelect();

        // 开始时间
        long begin = System.currentTimeMillis();


        //批量确认消息大小
        int batchSize = 100;

        //批量发送消息,批量发布确认
        for (int i=0; i<MESSAGE_COUNT; i++){
            String message = i + "";
            channel.basicPublish("", queueName, null, message.getBytes());

            //判断达到100条消息的时候,批量确认一次
            if(i % batchSize==0){
                //发布确认
                boolean flag = channel.waitForConfirms();
                if(flag){
                    System.out.println(message + "  : 消息发送成功");
                }
            }
        }

        //结束时间
        long end = System.currentTimeMillis();
        System.out.println("print cost time : " + (end-begin) +" ms ");
    }

    //异步发布确认
    public static void publishMessageAsync() throws Exception{
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 队列的声明
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName, true, false, false, null);

        //开启发布确认
        channel.confirmSelect();


        ConcurrentSkipListMap<Long, String> outstandingConfirms = new
                ConcurrentSkipListMap<>();//需要支持多线程并发安全
        //消息确认成功 回调函数
        /**
         * 1、消息标识
         * 2、是否为批量确认
         */
        ConfirmCallback ackCallback = (deliveryTag, multiple)->{//异步通知
            if(multiple){
                //批量
                //2删除到已经确认的消息,剩下就是未确认的消息
                ConcurrentNavigableMap<Long, String> confirmed
                        = outstandingConfirms.headMap(deliveryTag, true);
            }else {
                outstandingConfirms.remove(deliveryTag);
            }
            System.out.println("确认的消息 : " + deliveryTag);
        };
        //消息确认失败 回调函数
        ConfirmCallback nackCallback = (deliveryTag, multiple)->{//异步通知
            String message = outstandingConfirms.get(deliveryTag);
            //3、打印一下未确认的消息有哪些?
            System.out.println("未确认的消息 : " + message + " , 未确认消息的标记 : " +deliveryTag);
        };

        //准备消息的监听器,监听那些消息成功了,那些消息失败了  (成功, 失败)
        channel.addConfirmListener(ackCallback, nackCallback);

        long begin = System.currentTimeMillis();

        //批量发送消息
        for (int i=0; i<MESSAGE_COUNT; i++){
            String message = i + "";
            channel.basicPublish("", queueName, null, message.getBytes());
            //1、此处记录下所有要发送的消息,消息的总和
            outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
        }

        long end = System.currentTimeMillis();
        System.out.println("print cost time : " + (end-begin) +" ms ");
    }
}

订阅模式

在此之前我们可以看看其他大佬写的订阅模式:Java设计模式之观察者模式(发布/订阅模式)_程裕强的专栏-CSDN博客_java发布订阅模式

RattibMQ的局限性:

       收发模型都是发送的消息只能订阅一次,意思就是说,发布了消息之后,每一条消息只有一个订阅者能接收到该消息。发布/订阅模式是指发送消息,到某个队列,所有订阅了该队列的接受者都能接收到全部的消息。(类似观察者模式)

       

Exchange概念:交换机

交换机的图解

默认交换机

(一个队列一般绑定一个消费者,也可以一个队列绑定多个消费者,这样会导致只有一个消费者能够消费消息)

 一个队列中的一个消息只能被消费一次

RabbitMq核心思想:生产者生产的消息从不会直接发送到队列中。实际上,通常生产者都不知道这些消息传递到哪些队列中。生产者只能叫消息发动到交换机

1、Exchange的类型

直接(direct)、主题(topic)、标题(headers)、扇出(fanout)、无名(默认 “”)

2、临时队列:每当我们连接Rabbit时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称就更好了,其次一旦我们断开了消费者的连接,队列将自动删除。

 没有D的就是临时队列

创建临时队列的方式:

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

细看订阅模式和之前的模式的区别:

        这里我们使用的是Exchange而不是queue,因为我们所有生产者提交的消息都由Exchange来接受,然后Exchange按照特定的策略转发到Queue进行存储。

RabbitMQ提供了四种Exchange:fanout(广播),direct,topic,header
       本节用到的Exchange是fanout,所以针对这种进行解释fanout这种模式不需要指定队列名称,需要将Exchange和queue绑定,他们之间的关系是‘多对多’的关系(字面的意思理解就是把收到的消息推送给它所有它知道的队列中)
       任何发送到fanout Exchange的消息都会被转发到与该Exchange绑定的queue上面。
       发布/订阅模式就是是基于fanout Exchange实现的。

1、1个生产者,多个消费者
2、每一个消费者都有自己的一个队列
3、生产者没有将消息直接发送到队列,而是发送到了交换机
4、每个队列都要绑定到交换机
5、生产者发送的消息,经过交换机,到达队列,实现,一个消息被多个消费者获取的目的
注意:一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费

交换机原理图:

我们可以在后台看到多个交换机:

我们可以看到Exchanges中有 多个以 amq.*开头的交换机,(AMQP default)默认交换器。

Fanout模式

生产者:

package com.PublishOrSubscribe;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;

public class Send {
	private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        // 消息内容
        String message = "Hello World!";
        //声明一个队列,队列的名称是随机的
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

注意:消息发送到没有队列绑定的交换机时,消息将丢失,因为,交换机没有存储消息的能力,消息只能存在在队列中。(在第一次启动的时候,如果我们先启动两个消费者,再启动生产者,消费者是没办法接受到数据的,因为可能队列还没有注册进交换机中)

消费者1:

package com.PublishOrSubscribe;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

public class Recv1 {
	private final static String QUEUE_NAME = "test_queue_work1"; //消费者1队列
	private final static String EXCHANGE_NAME = "test_exchange_fanout";//交换机

	public static void main(String[] argv) throws Exception {

		// 获取到连接以及mq通道
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();

		// 声明队列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);

		// 绑定队列到交换机
		channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

		// 同一时刻服务器只会发一条消息给消费者
		channel.basicQos(1);

		// 定义队列的消费者
		QueueingConsumer consumer = new QueueingConsumer(channel);
		// 监听队列,手动返回完成
		channel.basicConsume(QUEUE_NAME, false, consumer);

		// 获取消息
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [Recv] Received '" + message + "'");
			Thread.sleep(10);

			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
		}
	}
}

消费者2:

package com.PublishOrSubscribe;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

public class Recv2 {
	private final static String QUEUE_NAME = "test_queue_work2";//消费者队列2 
    private final static String EXCHANGE_NAME = "test_exchange_fanout";//交换机

    public static void main(String[] argv) throws Exception {

        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 绑定队列到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 同一时刻服务器只会发一条消息给消费者
        channel.basicQos(1);

        // 定义队列的消费者
        QueueingConsumer consumer = new QueueingConsumer(channel);
        // 监听队列,手动返回完成
        channel.basicConsume(QUEUE_NAME, false, consumer);

        // 获取消息
        while (true) {
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String message = new String(delivery.getBody());
            System.out.println(" [Recv2] Received '" + message + "'");
            Thread.sleep(10);

            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        }
    }
}

结论:

同一个消息被多个消费者获取。一个消费者队列可以有多个消费者实例,只有其中一个消费者实例会消费到消息。
在管理工具中查看队列和交换机的绑定关系:

可以看到我们该队列所绑定的交换机

路由与队列绑定的例子

 

direct路由模式:

路由模式图解:

         发布/订阅模型的时候讲到了四中Exchange:fanout,direct,topic,header,并且使用了fanout实现了发布订阅模式,在发布/订阅模式中所有的消息都会被订阅者接受。这一节我们会修改发布者和订阅者的代码达到消息过滤的功能,使用的是direct Exchange

         direct这种Exchange,这种类型的算法很简单,会将消息转发到绑定健和消息发布指定的选择键完全匹配的队列,简单来说,就是我们使用这种Exchange之后,在发送消息的时候可以指定一个key,然后接受订阅也可以指定一个key,如果这两个key相同,消息就会发送到订阅者哪里。

路由模式:fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中

上图中,生产者发送到Exchange的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。

direct模式:

direct类型的Exchange路由规则:它会把消息路由到binging Key 与 routing Key 完全匹配的Queue中。如图,生产者发送消息的routing key = key1 的时候,只有绑定了 key1的queue才能收到信息

绑定键和选择键

选择键:

绑定键:

生产者:(发送给对应的路由,且选择键为 “delete” 和 “insert”)

package com.routing;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.util.ConnectionUtil;

public class Send {
	private final static String EXCHANGE_NAME = "test_exchange_direct";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        // 声明exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct"); // direct是交换机类型

        // 消息内容
        String message = "Hello Worldhaha!";
        channel.basicPublish(EXCHANGE_NAME, "delete", null, message.getBytes());
        channel.basicPublish(EXCHANGE_NAME, "insert", null, message.getBytes());
        // delete:消息KEY
        System.out.println(" [x] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

console:(由于发送给了两个绑定键,所以会发送两次)

 [x] Sent 'Hello Worldhaha!'

消费者1:(队列接受路由的消息 , 绑定键为 “delete” 和 “update”)

package com.routing;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

public class Recv1 {
	private final static String QUEUE_NAME = "test_queue_direct_1";

	private final static String EXCHANGE_NAME = "test_exchange_direct";

	public static void main(String[] argv) throws Exception {

		// 获取到连接以及mq通道
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();

		// 声明队列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);

		// 绑定队列到交换机
		channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");
		channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");

		// 同一时刻服务器只会发一条消息给消费者
		channel.basicQos(1);

		// 定义队列的消费者
		QueueingConsumer consumer = new QueueingConsumer(channel);
		// 监听队列,手动返回完成
		channel.basicConsume(QUEUE_NAME, false, consumer);

		// 获取消息
		System.out.println("Recv1 start --- ");
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [Recv1] Received '" + message + "'");
			Thread.sleep(10);

			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
		}
	}
}

console:(由于只有一个 “insert” 键的发送,所以只接受到一个)

Recv1 start --- 
 [Recv1] Received 'Hello Worldhaha!'

消费者2:(队列接受路由的消息 , 绑定键为 “insert”  、 “delete” 和 “update”)

package com.routing;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.util.ConnectionUtil;

public class Recv2 {
	private final static String QUEUE_NAME = "test_queue_direct_2";

	private final static String EXCHANGE_NAME = "test_exchange_direct";

	public static void main(String[] argv) throws Exception {

		// 获取到连接以及mq通道
		Connection connection = ConnectionUtil.getConnection();
		Channel channel = connection.createChannel();

		// 声明队列
		channel.queueDeclare(QUEUE_NAME, false, false, false, null);

		// 绑定队列到交换机
		channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "insert");
		channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
		channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

		// 同一时刻服务器只会发一条消息给消费者
		channel.basicQos(1);

		// 定义队列的消费者
		QueueingConsumer consumer = new QueueingConsumer(channel);
		// 监听队列,手动返回完成
		channel.basicConsume(QUEUE_NAME, false, consumer);

		System.out.println("Recv2 start --- ");
		// 获取消息
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println(" [Recv2] Received '" + message + "'");
			Thread.sleep(10);

			channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
		}
	}
}

console:(由于绑定了 “insert” 和 “delete” 键,所以接受了两次)

Recv2 start --- 
 [Recv2] Received 'Hello Worldhaha!'
 [Recv2] Received 'Hello Worldhaha!'

Topic模式

1、topic的要求

 2、topic案例

 

 

实例:

 生产者:

public class SendTopic {
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        Map<String,String> bindingKeyMap =new HashMap<>();
        bindingKeyMap.put("quick.orange.rabbit","被队列Q1Q1接收");
        bindingKeyMap.put("lazy.orange.elephant","被队列Q1Q2接收");
        bindingKeyMap.put("quick.orange.fox","被队列Q1接收");
        bindingKeyMap.put("lazy.brown.fox","被队列Q2接收");
        bindingKeyMap.put("lazy.pink.rabbit","最燃满足两个绑定但只被队列Q2接收一次");
        bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
        bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
        bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配Q2");

        for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
            String routingKey = bindingKeyEntry.getKey();
            String message = bindingKeyEntry.getValue();
            channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
            System.out.println("生产者发出消息: " + message);
        }
        channel.close();
        connection.close();
    }
}

消费者1

public class ReceiveLogsTopic1 {
    // 交换机的名称
    public static final String EXCHANGE_NAME = "topic_logs";
    public static final String QUEUE_NAME = "Q1";

    public static void main(String[] argv) throws Exception {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

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

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.oragne.*");
        System.out.println("等待接收消息--------");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println(new String(message.getBody(), "UTF-8"));
            System.out.println("接收队列:"+ QUEUE_NAME +" , 绑定键: "+ message.getEnvelope().getRoutingKey());
        };

        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

消费者2

public class ReceiveLogsTopic2 {
    // 交换机的名称
    public static final String EXCHANGE_NAME = "topic_logs";
    public static final String QUEUE_NAME = "Q2";

    public static void main(String[] argv) throws Exception {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, "topic");

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

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.*.rabbit");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "lazy.#");

        System.out.println("等待接收消息--------");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println(new String(message.getBody(), "UTF-8"));
            System.out.println("接收队列:"+ QUEUE_NAME +" , 绑定键: "+ message.getEnvelope().getRoutingKey());
        };
        
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

死信队列

      死信,顾名思义就是无法被消费的信息。一般来说,producer将消息投递到broker或者直接到queue里,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信(Dead Letter),所有的死信都会放到死信队列中。

     应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMq的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击支付后在指定时间未支付时自动失效(可以用来做延迟消息)。

1、死信来源

1、消息 TTL 过期。(TTL:存活时间,在这段时间没被消费到)
2、队列达到最大长度(队列满了,无法再添加数据到 mq 中)。
3、消息被拒绝(basic.reject 或 basic.nack)并且 消息不重新放回对垒(requeue=false)。

死信队列来保证消息至少被消费一次以及未被正确处理的消息不会被丢弃

2、实例

代码架构图

 如果出现死信的情况,就会转发给死信交换机,对应的绑定routingKey是lisi

生产者:

public class ProducerDeadQueue {
    private static final String NORMAL_EXCHANGE_NAME = "normal_exchange_name";

    public static void main(String[] argv) throws Exception {
        // 获取到连接以及mq通道
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();


        //延迟消息  设置ttl time to live 10s  如果这个消息超过10s没接收,就会移进死信队列中
        AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
        for (int i=0; i<10;i++){
            String message = "info " + i;
            channel.basicPublish(NORMAL_EXCHANGE_NAME, "zhangsan", properties, message.getBytes("UTF-8"));
            //我们可以在这里设置等待时间
        }
        channel.close();
        connection.close();
        /**
         * 我们可以先运行生产者,等到十秒,就会出现这个问题
         */
    }
}

消费者1:(普通消费者)

public class ReceiveDeadQueue1 {
    // 普通交换机的名称
    public static final String NORMAL_EXCHANGE_NAME = "normal_exchange_name";
    // 死信交换机
    public static final String DEAD_EXCHANGE_NAME = "dead_exchange_name";

    //普通队列
    public static final String NORMAL_QUEUE_NAME = "normal_queue_name";
    //死信队列
    public static final String DEAD_QUEUE_NAME = "dead_queue_name";



    public static void main(String[] argv) throws Exception {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        Map<String, Object> arguments = new HashMap<>();
        //过期时间
        //正常队列设设置死信交换机
        //过期时间  10s=100000ms
//        arguments.put("x-message-ttl",100000); //我们也可以在生产者中设置过期时间,会比较灵活
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key", "lisi");
        //声明普通队列
        channel.queueDeclare(NORMAL_QUEUE_NAME, false, false, false, arguments);

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

        //绑定普通的交换机与队列
        channel.queueBind(NORMAL_QUEUE_NAME, NORMAL_EXCHANGE_NAME, "zhangsan");

        //绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, "lisi");

        System.out.println("等待接收消息--------");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("Consumer1 接受消息 : " + new String(message.getBody(), "UTF-8"));
            System.out.println("接收队列:"+ NORMAL_QUEUE_NAME +" , 绑定键: "+ message.getEnvelope().getRoutingKey());
        };

        channel.basicConsume(NORMAL_QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

消费者2(死信消费者)

public class ReceiveDeadQueue2 {
    // 普通交换机的名称
    public static final String NORMAL_EXCHANGE_NAME = "normal_exchange_name";
    // 死信交换机
    public static final String DEAD_EXCHANGE_NAME = "dead_exchange_name";

    //普通队列
    public static final String NORMAL_QUEUE_NAME = "normal_queue_name";
    //死信队列
    public static final String DEAD_QUEUE_NAME = "dead_queue_name";



    public static void main(String[] argv) throws Exception {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();


        System.out.println("等待接收消息--------");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("Consumer2 接受消息 : " + new String(message.getBody(), "UTF-8"));
            System.out.println("接收队列:"+ DEAD_QUEUE_NAME +" , 绑定键: "+ message.getEnvelope().getRoutingKey());
        };

        channel.basicConsume(DEAD_QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }
}

超过队列长度案例

消费者1修改

public static void main(String[] argv) throws Exception {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        Map<String, Object> arguments = new HashMap<>();
        //过期时间
        //正常队列设设置死信交换机
        //过期时间  10s=100000ms
//        arguments.put("x-message-ttl",100000); //我们也可以在生产者中设置过期时间,会比较灵活
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key", "lisi");
        arguments.put("x-max-length",6);
        //声明普通队列
        channel.queueDeclare(NORMAL_QUEUE_NAME, false, false, false, arguments);

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

        //绑定普通的交换机与队列
        channel.queueBind(NORMAL_QUEUE_NAME, NORMAL_EXCHANGE_NAME, "zhangsan");

        //绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, "lisi");

        System.out.println("等待接收消息--------");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            System.out.println("Consumer1 接受消息 : " + new String(message.getBody(), "UTF-8"));
            System.out.println("接收队列:"+ NORMAL_QUEUE_NAME +" , 绑定键: "+ message.getEnvelope().getRoutingKey());
        };

        channel.basicConsume(NORMAL_QUEUE_NAME, true, deliverCallback, consumerTag -> {});
    }

且需要把之前的队列在界面中删除掉,重新启动即可

 可以看到features有三个参数了

消费者拒绝接收

public class ReceiveDeadQueue1 {
    // 普通交换机的名称
    public static final String NORMAL_EXCHANGE_NAME = "normal_exchange_name";
    // 死信交换机
    public static final String DEAD_EXCHANGE_NAME = "dead_exchange_name";

    //普通队列
    public static final String NORMAL_QUEUE_NAME = "normal_queue_name";
    //死信队列
    public static final String DEAD_QUEUE_NAME = "dead_queue_name";



    public static void main(String[] argv) throws Exception {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();

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

        Map<String, Object> arguments = new HashMap<>();
        //过期时间
        //正常队列设设置死信交换机
        //过期时间  10s=100000ms
//        arguments.put("x-message-ttl",100000); //我们也可以在生产者中设置过期时间,会比较灵活
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE_NAME);
        //设置死信routingKey
        arguments.put("x-dead-letter-routing-key", "lisi");
        //声明普通队列
        channel.queueDeclare(NORMAL_QUEUE_NAME, false, false, false, arguments);

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

        //绑定普通的交换机与队列
        channel.queueBind(NORMAL_QUEUE_NAME, NORMAL_EXCHANGE_NAME, "zhangsan");

        //绑定死信的交换机与死信的队列
        channel.queueBind(DEAD_QUEUE_NAME, DEAD_EXCHANGE_NAME, "lisi");

        System.out.println("等待接收消息--------");

        DeliverCallback deliverCallback = (consumerTag, message)->{
            String str = new String(message.getBody(), "UTF-8");
            if(str.equals("info 5")){
                System.out.println("Consumer1 拒收消息: "+ str);
                channel.basicReject(message.getEnvelope().getDeliveryTag(),false);//false 不放回原先队列,也可以塞回原先队列
            }else {
                System.out.println("Consumer1 接受消息 : " + str);
                System.out.println("接收队列:"+ NORMAL_QUEUE_NAME +" , 绑定键: "+ message.getEnvelope().getRoutingKey());
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);//false 不批量
            }
        };

        //开启手动应答
        channel.basicConsume(NORMAL_QUEUE_NAME, false, deliverCallback, consumerTag -> {});
    }
}

延迟队列

      延时队列,延时队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列

 死信队列其实也是延迟队列,因为正常队列中设置时间就会发送到死信队列中(消息过期)

 

 实践代码:

架构图:

 

 使用Springboot进行开发

public class TtlQueueConfig {
    //普通交换机的名称
    public static final String X_EXCHANGE = "X";
    //死信交换机的名称
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    //普通队列的名称
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";

    //死信队列的名称
    public static final String DEAD_LETTER_QUEUE_D = "QD";

    //声明xExchange  别名
    @Bean("xExchange")
    public DirectExchange xExchange(){
        return new DirectExchange(X_EXCHANGE);
    }

    //声明xExchange   别名
    @Bean("yExchange")
    public DirectExchange yExchange(){
        return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
    }

    //声明 队列
    @Bean("queueA")
    public AMQP.Queue queueA(){
        HashMap<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        arguments.put("x-message-ttl",10000);
        return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
    }

    //声明 队列
    @Bean("queueB")
    public AMQP.Queue queueB(){
        HashMap<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        arguments.put("x-message-ttl",40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
    }

    //声明 死信队列
    @Bean("queueD")
    public AMQP.Queue queueD(){
        return QueueBuilder.durable(DEAD_LETTER_QUEUE_D).withArguments().build();
    }

    //绑定
    @Bean
    public Binding queueABindingX(@Qualifier("queueA")AMQP.Queue queueA, @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }

    @Bean
    public Binding queueBBindingX(@Qualifier("queueB")AMQP.Queue queueB, @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }

    @Bean
    public Binding queueDBindingY(@Qualifier("queueD")AMQP.Queue queueD, @Qualifier("yExchange") DirectExchange yExchange){
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

生产者

/**
 * 发送延迟消息
 * http://localhost:8080/ttl/sendMsg/嘻嘻嘻
 */
@RestController
@RequestMapping("/ttl")
public class SendMsgController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    //开始发消息
    @GetMapping("/dendMsg/{message}")
    public void sendMsg(@PathVariable String message){
        log.info("curtime : " +System.currentTimeMillis() + " , message : " +message );

        rabbitTemplate.convertAndSend("X", "XA", "消息来自ttl为10s的队列 : " +message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自ttl为40s的队列 : " +message);
    }
}

消费者

/**
 * 消费者
 */
@Slf4j
@Component
public class DeadLetterQueueConsumer {
    //接受消息
    @RabbitListener(queues="QD")
    public void receiveD(Message message, Channel channel) throws Exception{
        String msg = new String(message.getBody());
        log.info("当前时间 : {} , 收到死信队列的消息为: {} " , System.currentTimeMillis() , msg);
    }
}

结果图

 为了实现任意时间的

TtlQueueConfig

    //新增一个普通队列
    public static final String QUEUE_C = "QC";
    @Bean("queueC")
    public AMQP.Queue queueC(){
        HashMap<String, Object> arguments = new HashMap<>(3);
        //设置死信交换机
        arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
        //设置死信RoutingKey
        arguments.put("x-dead-letter-routing-key","YD");
        return QueueBuilder.durable(QUEUE_C).withArguments().build();
    }
    @Bean
    public Binding queueCBindingX(@Qualifier("queueC")AMQP.Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange){
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }
SendMsgController
    http://localhost:8080/ttl/sendExpirationMsg/zhangsan/200000
    //开始发消息
    @GetMapping("/sendExpirationMsg/{message}/{ttltime}")
    public void sendMsg(@PathVariable String message, @PathVariable String ttlTime){
        log.info("curtime : " +System.currentTimeMillis() + " , message : " +message +" , ttl : " + ttlTime);

        rabbitTemplate.convertAndSend("X", "XC", "消息来自ttl为10s的队列 : " +message, msg->{
            //发送消息的时候, 设置延时时长
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }

rabbitmq插件

插件安装完成之后当我们新增交换机的时候就可以发现

 

 实例:

1、架构

 配置

@Configuration
public class DelayedQueueConfig {

    //队列
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    //交换机
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    //routingkey
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";


    @Bean
    public AMQP.Queue delayedQueue(){
        return new AMQP.Queue(DELAYED_QUEUE_NAME);
    }
    //声明交换机
    @Bean
    public CustomExchange delayedExchange(){
        Map<String, Object> arguments = new HashMap<>();

        /**
         * 1、交换机的名称
         * 2、交换机的类型
         * 3、是否需要持久化
         * 4、是否需要自动删除
         * 5、其他的参数
         */
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message",true, false, arguments);
    }

    //绑定
    @Bean
    public Binging delayedQueueBindingDelayedExchange(
            @Qualifier("delayedQueue") Queue delayQueue,
            @Qualifier("delayedExchange") CustomExchange delayedExchange){
        return BindingBuilder.bind(delayQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

生产者

    //开始发消息 基于插件的消息 及延迟的时间
    @GetMapping("/sendDelayMsg/{message}/{ttltime}")
    public void sendMsg(@PathVariable String message, @PathVariable String ttlTime){
        log.info("curtime : " +System.currentTimeMillis() + " , message : " +message +" , ttl : " + ttlTime);

        rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,
                DelayedQueueConfig.DELAYED_ROUTING_KEY, "消息来自ttl为10s的队列 : " +message, msg->{
            //发送消息的时候, 设置延时时长
            msg.getMessageProperties().setExpiration(ttlTime);
            return msg;
        });
    }

消费者

@Slf4j
@Component
public class DelayQueueConsumer {
    //监听消息
    @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
    public void receiveDelayQueue(Message message){
        String msg = new String(message.getBody());
        log.info("当前时间 : {} , 收到延迟插件的消息为: {} " , System.currentTimeMillis() , msg);
    }
}

面试题

RabbitMq 如何解决消息不丢失?

topic模式

(详细可看rabbitmq消息队列原理_一群专业码农的笔记本-CSDN博客_rabbitmq原理

摘抄自:RabbitMQ教程(万字教程)_鸟哥哥的专栏-CSDN博客_rabbitmq(详细的RabbitMQ安装和使用)

RabbitMQ 使用 | 第四篇:路由选择_wangjianfengnb-CSDN博客(这哥们写的也会清晰点)

RabbitMq07——主题模式(通配符模式)_青灬火的博客-CSDN博客(看懂路由模式)

RabbitMQ学习(六)——消息确认机制(Confirm模式)_Anumbrella-CSDN博客(确认模式)

必知必会 RabbitMQ面试题 33道(附答案)_田维常-CSDN博客(rabbitMQ的一些面试题)

查看rabbitMq东西

1、只有生产者进行了

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

而消费者还未进行消费的时候,会出现

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值