RabbitMQ中间件

RabbitMQ

MQ

1、什么是MQ

MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据结构。指把要传输的数据(消息)放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。

2、MQ的作用

消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。

应用解耦:一个业务需要多个模块共同实现,或者一条消息有多个系统需要对应处理,只需要主业务完成以后,发送一条MQ,其余模块消费MQ消息,即可实现业务,降低模块之间的耦合。

异步处理:主业务执行结束后从属业务通过MQ,异步执行,减低业务的响应时间,提高用户体验。

流量削峰:高并发情况下,业务异步处理,提供高峰期业务处理能力,避免系统瘫痪。

应用解耦

image-20220722201145553

异步处理

image-20220722201237280

流量削峰

image-20220722200055942

4、MQ的缺点

1、系统可用性降低。依赖服务越多,服务越容易挂掉。需要考虑MQ瘫痪的情况

2、系统复杂性提高。需要考虑消息丢失、消息重复消费、消息传递的顺序性

3、业务一致性。主业务和从属业务一致性的处理

5、主要的MQ产品

主要的MQ产品包括:RabbitMQ、ActiveMQ、RocketMQ、ZeroMQ、Kafka、IBM WebSphere 等。

RabbitMQ

1、介绍

RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。

RabbitMQ服务支持下列操作系统:

  • Linux
  • WindowsNT 到 10
  • Windows Server2003 到 2016
  • macOS
  • Solaris
  • FreeBSD
  • TRU64
  • VxWorks

RabbitMQ支持下列编程语言:

  • Python
  • Java
  • Ruby
  • PHP
  • C#
  • JavaScript
  • Go
  • Elixir
  • Objective-C
  • Swift

2、主要特性

  • 可伸缩性:集群服务
  • 消息持久化:从内存持久化消息到硬盘,再从硬盘加载到内存

3、四大核心概念

image-20220722202456602

生产者

产生数据发送消息的程序是生产者

交换机

交换机是RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定

队列

队列是RabbitMQ内部使用的一种数据结构,尽管消息流经RabbitMQ和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。 许多生产可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式

消费者

消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。 请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

六大核心模式

image-20220722203125829

名词解释

image-20220722203415267

安装RabbitMQ

官网:https://www.rabbitmq.com/

安装步骤:https://blog.csdn.net/shuux666/article/details/124337419

image-20220722223200671

image-20220722223220010

image-20220722205121719

image-20220722223238574

Hello World

简单模式

image-20220722211059013

pom.xml

<!--指定 jdk 编译版本-->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>8</source>
                <target>8</target>
            </configuration>
        </plugin>
    </plugins>
</build>

<dependencies>
    <!--rabbitmq 依赖客户端-->
    <dependency>
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>5.8.0</version>
    </dependency>
    <!--操作文件流的一个依赖-->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
</dependencies>

消息生产者

public class Producer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
		//创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 工厂IP  连接Rabbit.MQ的队列
        factory.setHost("182.92.234.71");
        factory.setUsername("admin");
        factory.setPassword("123");
		// 创建连接
        // channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Connection connection = factory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        /**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享, true表示可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除, true表示自动删除
         * 5.其他参数
         */
          channel.queueDeclare(QUEUE_NAME, false, false, false, null);
          String message = "hello world";
        /**
         * 发送一个消息
         * 1.发送到那个交换机  不写使用默认的交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
           channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
           System.out.println("消息发送完毕");
        }
    }
} 

image-20220722212900237

消息消费者

public class Consumer {
    // 队列名词
    private final static String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("182.92.234.71");
        factory.setUsername("admin");
        factory.setPassword("123");
        // 创建连接
        Connection connection = factory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        // 声明	接收消息
        System.out.println("等待接收消息 ");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println(message);
    	};
        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
		CancelCallback cancelCallback=(consumerTag)->{ 
            System.out.println("消息消费被中断");
		};
        /**
         * 消费者消息
         * 1.消费哪个队列
		 * 2.消费成功之后是否要自动应答  true 代表的自动应答  false 代表手动应答
         * 3.消费者未成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

Work Queues

工作队列模式

image-20220722214836838

抽取连接工具类

public class RabbitMqUtils {
    //得到一个连接的 channel
    public static Channel getChannel() throws Exception {
        //创建一个连接工厂 
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("182.92.234.71");
        factory.setUsername("admin");
        factory.setPassword("123");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        return channel;
  	}
} 

消费者

public class Worker01 {
    // 队列名称
    private static final String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String receivedMessage = new String(delivery.getBody());
            System.out.println("接收到消息:" + receivedMessage);
        };
        // 消息接收被取消时执行下面的内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        // idea运行启动多个线程
        System.out.println("C1 消费者启动等待消费   ");
        // System.out.println("C2 消费者启动等待消费   ");
        // 消费者启动等待消费
        /**
         * 消费者消息
         * 1.消费哪个队列
		 * 2.消费成功之后是否要自动应答  true 代表的自动应答  false 代表手动应答
         * 3.消费者未成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
    }
}

生产者

public class Task01 {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        // 工具类开启信道
		Channel channel = RabbitMqUtils.getChannel();
        // 队列的声明
         /**
         * 生成一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享, true表示可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除, true表示自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //从控制台当中接受信息 
        Scanner scanner = new Scanner(System.in);
		while (scanner.hasNext()){
			String message = scanner.next();
            /**
         * 发送一个消息
         * 1.发送到那个交换机  不写使用默认的交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
            System.out.println("发送消息完成:"+message);
        }
    }
} 

测试

image-20220722221933800 image-20220722221856222 image-20220722221911561

消息应答

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。

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

自动应答

消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。

手动应答

A.Channel.basicAck(用于肯定确认)

​ RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了

B.Channel.basicNack(用于否定确认)

C.Channel.basicReject(用于否定确认)

​ 与 Channel.basicNack 相比少一个参数

​ 不处理该消息了直接拒绝,可以将其丢弃了

Multiple 的解释

手动应答的好处是可以批量应答并且减少网络拥堵

image-20220722223332252

画图解释

image-20220722224016455

消息自动重新入队

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

图解

image-20220723012228969

消息手动应答代码

生产者
public class Task2 {
    
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        // 工具类开启信道
        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("UTF-8"));
            System.out.println("发送消息完成:"+message);
        }
    }
} 
睡眠工具类
// 睡眠工具类
public class SleepUtils {
    public static void sleep(int second) {
        try {
            Thread.sleep(1000 * second);
        } catch (InterruptedException _ignored) {
            Thread.currentThread().interrupt();
        }
    }
}
消费者1
public class Worker03 {
    // 队列名称
    private static final String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1等待接收消息处理时间较短");
        
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // 沉睡一秒钟
            SleepUtils.sleep(1);
            // 开始真正接收消息
            String receivedMessage = new String(message.getBody(),"UTF-8");
            System.out.println("接收到消息:" + receivedMessage);
            // 手动应答
            /**
             * 1.消息标记 tag
             * 2.是否批量应答未应答消息,这里需要选择false,表示不批量应答队列消息
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 消息接收被取消时执行下面的内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        // 消费者启动等待消费
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}
消费者2
public class Worker04 {
    // 队列名称
    private static final String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C2等待接收消息处理时间较长");
        
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // 沉睡30秒钟
            SleepUtils.sleep(30);
            // 开始真正接收消息
            String receivedMessage = new String(message.getBody(),"UTF-8");
            System.out.println("接收到消息:" + receivedMessage);
            // 手动应答
            /**
             * 1.消息标记 tag
             * 2.是否批量应答未应答消息,这里需要选择false,表示不批量应答队列消息
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 消息接收被取消时执行下面的内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        // 消费者启动等待消费
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

队列持久化

生产者

public class Task3 {
    
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        // 工具类开启信道
        Channel channel = RabbitMqUtils.getChannel();
        // 队列的声明
        boolean durable = true;	// 需要让队列QUEUE进行持久化
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
        //从控制台当中接受信息 
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
            System.out.println("发送消息完成:"+message);
        }
    }
} 

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

image-20220723020524588

删除后再次实验

image-20220723020717668

消息持久化

生产者

public class Task3 {
    
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        // 工具类开启信道
        Channel channel = RabbitMqUtils.getChannel();
        // 队列的声明
        boolean durable = true;	// 需要让队列QUEUE进行持久化
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
        //从控制台当中接受信息 
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            // 开始发送消息
            // 设置生产者发送消息为持久化消息(要求保存到磁盘上)
            channel.basicPublish("",
                       			QUEUE_NAME,
                       			MessageProperties.PERSISTENT_TEXT_PLAIN,
                       			message.getBytes("UTF-8"));
            System.out.println("发送消息完成:"+message);
        }
    }
} 

不公平分发

在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是 很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间 处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是 RabbitMQ 并不知道这种情况它依然很公平的进行分发。

为了避免这种情况,我们可以设置参数 channel.basicQos(1);

image-20220723022346089

消费者1

public class Worker03 {
    // 队列名称
    private static final String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1等待接收消息处理时间较短");
        
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // 沉睡一秒钟
            SleepUtils.sleep(1);
            // 开始真正接收消息
            String receivedMessage = new String(message.getBody(),"UTF-8");
            System.out.println("接收到消息:" + receivedMessage);
            // 手动应答
            /**
             * 1.消息标记 tag
             * 2.是否批量应答未应答消息,这里需要选择false,表示不批量应答队列消息
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 消息接收被取消时执行下面的内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        // 设置不公平分发
        int prefetCount = 1;
        channel.basicQos(prefetCount);
        // 消费者启动等待消费
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

消费者2

public class Worker04 {
    // 队列名称
    private static final String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C2等待接收消息处理时间较长");
        
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // 沉睡30秒钟
            SleepUtils.sleep(30);
            // 开始真正接收消息
            String receivedMessage = new String(message.getBody(),"UTF-8");
            System.out.println("接收到消息:" + receivedMessage);
            // 手动应答
            /**
             * 1.消息标记 tag
             * 2.是否批量应答未应答消息,这里需要选择false,表示不批量应答队列消息
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 消息接收被取消时执行下面的内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        // 设置不公平分发
        int prefetCount = 1;
        channel.basicQos(prefetCount);
        // 消费者启动等待消费
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

预取值

image-20220723023703556

这样即使C2干的慢,也必须干完5条,因为给C2指定必须拿5条。如果后面再来数据,就给干的快的C1。

消费者1

public class Worker03 {
    // 队列名称
    private static final String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C1等待接收消息处理时间较短");
        
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // 沉睡一秒钟
            SleepUtils.sleep(1);
            // 开始真正接收消息
            String receivedMessage = new String(message.getBody(),"UTF-8");
            System.out.println("接收到消息:" + receivedMessage);
            // 手动应答
            /**
             * 1.消息标记 tag
             * 2.是否批量应答未应答消息,这里需要选择false,表示不批量应答队列消息
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 消息接收被取消时执行下面的内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        // 设置不公平分发
        int prefetCount = 1;
        // 预取值是2
        int prefetCount = 2;
        channel.basicQos(prefetCount);
        // 消费者启动等待消费
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

消费者2

public class Worker04 {
    // 队列名称
    private static final String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        Channel channel = RabbitMqUtils.getChannel();
        System.out.println("C2等待接收消息处理时间较长");
        
        // 消息的接收
        DeliverCallback deliverCallback = (consumerTag, message) -> {
            // 沉睡30秒钟
            SleepUtils.sleep(30);
            // 开始真正接收消息
            String receivedMessage = new String(message.getBody(),"UTF-8");
            System.out.println("接收到消息:" + receivedMessage);
            // 手动应答
            /**
             * 1.消息标记 tag
             * 2.是否批量应答未应答消息,这里需要选择false,表示不批量应答队列消息
             */
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
        };
        // 消息接收被取消时执行下面的内容
        CancelCallback cancelCallback = (consumerTag) -> {
            System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
        };
        // 设置不公平分发
        int prefetCount = 1;
        // 预取值是5
        int prefetCount = 5;
        channel.basicQos(prefetCount);
        // 消费者启动等待消费
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
    }
}

发布确认

image-20220723025143252

1、开启发布确认方法

发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法

image-20220723025313935

生产者

public class Task3 {
    
    private static final String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
        // 工具类开启信道
        Channel channel = RabbitMqUtils.getChannel();
        // 信道开启发布确认
        channel.confirmSelect();
        // 队列的声明
        boolean durable = true;	// 需要让队列QUEUE进行持久化
        channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
        //从控制台当中接受信息 
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message = scanner.next();
            // 开始发送消息
            // 设置生产者发送消息为持久化消息(要求保存到磁盘上)
            channel.basicPublish("",
                       			QUEUE_NAME,
                       			MessageProperties.PERSISTENT_TEXT_PLAIN,
                       			message.getBytes("UTF-8"));
            System.out.println("发送消息完成:"+message);
        }
    }
} 

2、 单个确认发布

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

这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。

代码

import java.util.UUID;

public class Test {

    // 批量发布确认的个数
    public static final int MESSAGE_COUNT = 1000;

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

        //1、单个确认
        Test.publishMessageIndividually();	//发布1000个单独确认消息,耗时722ms
        //2、批量确认
        //3、异步批量确认

    }
    
 	//1、单个确认
    public static void publishMessageIndividually() throws Exception {
        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");
    }
}

3、批量确认发布

上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。

代码

import java.util.UUID;

public class Test {

    // 批量发布确认的个数
    public static final int MESSAGE_COUNT = 1000;

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

        //1、单个确认
        //Test.publishMessageIndividually();	//发布1000个单独确认消息,耗时722ms
        //2、批量确认
        Test.publishMessageBatch(); //发布1000个批量确认消息,耗时141ms
        //3、异步批量确认

    }
    //2、批量确认
    public static void publishMessageBatch() throws Exception {
        // RabbitMqUtils是自定义的信道工具类
        Channel channel = RabbitMqUtils.getChannel());
        // 队列名称
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName, false, false, false, null);
        //开启发布确认
        channel.confirmSelect();
        // 开始时间
        long begin = System.currentTimeMillis();
        // 批量发送消息,批量确认消息
        // 批量消息确认大小,可以100条为一组这样确认
        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();
            }
        }
        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms");
    }
}

4、异步批量确认

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

image-20220724135325899

代码

import javax.security.auth.callback.ConfirmationCallback;
import java.util.UUID;

public class Test {

    // 批量发布确认的个数
    public static final int MESSAGE_COUNT = 1000;

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

        //1、单个确认
        //Test.publishMessageIndividually();	//发布1000个单独确认消息,耗时722ms
        //2、批量确认
        //Test.publishMessageBatch(); //发布1000个批量确认消息,耗时141ms
        //3、异步批量确认
        Test.publishMessageAsync();//发布1000个异步发布确认消息,耗时62ms
    }
    //3、异步批量确认
    public static void publishMessageAsync() throws Exception {
        // RabbitMqUtils是自定义的信道工具类
        Channel channel = RabbitMqUtils.getChannel());
        // 队列名称
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName, false, false, false, null);
        //开启发布确认
        channel.confirmSelect();
        // 开始时间
        long begin = System.currentTimeMillis();

        // 消息确认成功   回调函数
        ConfirmCallback ackCallback = (deliveryTag,multiple) -> {
            System.out.println("确认的消息:" + deliveryTag);
        };
        // 消息确认失败   回调函数
        // 1. 消息的标记.
        // 2. 是否为批量确认
        ConfirmCallback nackCallback = (deliveryTag,multiple) -> {
            System.out.println("未确认的消息:" + deliveryTag);
        };
        // 准备消息的监听器监听哪些消息成功了哪些消息失败了
        // 1.监听哪些消息成功了
        // 2.监听哪些消息失败了
        channel.addConfirmListener(ackCallback,nackCallback);   // 异步通知
        // 批量发送消息
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String message = "消息" + i;
            // 发布消息
            channel.basicPublish("", queueName, null, message.getBytes());
        }
        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布" + MESSAGE_COUNT + "个异步发布确认消息,耗时" + (end - begin) + "ms");
    }
}

处理异步批量确认未发布的消息

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

代码
public class Test {

    // 批量发布确认的个数
    public static final int MESSAGE_COUNT = 1000;

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

        //1、单个确认
        Test.publishMessageIndividually(); //发布1000个单独确认消息,耗时722ms
        //2、批量确认
        Test.publishMessageBatch(); //发布1000个批量确认消息,耗时141ms
        //3、异步批量确认
        Test.publishMessageAsync();//发布1000个异步发布确认消息,耗时62ms
    }
    //3、异步批量确认
    public static void publishMessageAsync() throws Exception {
        // RabbitMqUtils是自定义的信道工具类
        Channel channel = RabbitMqUtils.getChannel());
        // 队列名称
        String queueName = UUID.randomUUID().toString();
        channel.queueDeclare(queueName, false, false, false, null);
        //开启发布确认
        channel.confirmSelect();
       /*
            线程安全有序的一个哈希表适用于高并发的情况下
            1.轻松的将序号与消息进行关联
            2.轻松批量删除条目只要给到序号
            3.支持高并发(多线程)
        */
        ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();

        // 消息确认成功   回调函数
        ConfirmCallback ackCallback = (deliveryTag,multiple) -> {
            if (multiple) {
                // 删除已经确认的消息 剩下的就是未确认的消息
                ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag);
                confirmed.clear();
            } else {
                outstandingConfirms.remove(deliveryTag);
            }
            System.out.println("确认的消息:" + deliveryTag);
        };
        // 消息确认失败   回调函数
       /*
        1. 消息的标记.
        2. 是否为批量确认
        */
        ConfirmCallback nackCallback = (deliveryTag,multiple) -> {
            // 打印一下未确认的消息都有哪些
            System.out.println("未确认的消息:" + deliveryTag);
        };
        // 准备消息的监听器监听哪些消息成功了哪些消息失败了
        // 1.监听哪些消息成功了
        // 2.监听哪些消息失败了
        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());
            // 此处记录下所有要发送的消息消息的总和
            outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
        }
        // 结束时间
        long end = System.currentTimeMillis();
        System.out.println("发布" + MESSAGE_COUNT + "个异步发布确认消息,耗时" + (end - begin) + "ms");
    }
}

5、以上 3 种发布确认速度对比

  • 单独发布消息
    • 同步等待确认,简单,但吞吐量非常有限。
  • 批量发布消息
    • 批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是哪条消息出现了问题。
  • 异步处理:
    • 最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些

交换机

介绍

在上一节中,我们创建了一个工作队列。我们假设的是工作队列背后,每个任务都恰好交付给一个消费者(工作进程)。在这一部分中,我们将做一些完全不同的事情-我们将消息传达给多个消费者。这种模式称为 ”发布/订阅”.

为了说明这种模式,我们将构建一个简单的日志系统。它将由两个程序组成:第一个程序将发出日志消息,第二个程序是消费者。其中我们会启动两个消费者,其中一个消费者接收到消息后把日志存储在磁盘,另外一个消费者接收到消息后把消息打印在屏幕上,事实上第一个程序发出的日志消息将广播给所有消费者者

image-20220724160249018

交换机类型

总共有以下类型:

  • 直接(direct)----------(路由类型)
  • 主题(topic)
  • 标题(headers)
  • 扇出(fanout)-----------(发布订阅模式)
  • 无名(默认)类型-----(AMQP default)
    • 在本教程的前面部分我们对 exchange 一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换,我们通过空字符串(“”)进行标识。
    • image-20220724160741372
    • 第一个参数是交换机的名称。空字符串表示默认或无名称交换机:消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话

临时队列

临时队列没有持久化

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

创建临时队列的方式如下:

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

创建出来之后长成这样:

image-20220724161040469

绑定

什么是 bingding 呢,binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是 X 与 Q1 和 Q2 进行了绑定

image-20220724161155729

可以通过RoutingKey给不同的队列发消息(direct(直接))

image-20220724161443008

Fanout(扇出)

发布订阅模式

1、介绍

Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。系统中默认有些 exchange 类型

简单来说就是:我想给队列1和队列2都发消息

image-20220724161752215

2、实战

图解

image-20220724161828854

代码
接收方1
public class ReceiveLogs01 {
    // 交换机的名字
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        // 创建(声明)一个交换机
        // channel.exchangeDeclare(交换机名字, "扇出类型");
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        /**
         * 生成一个临时的队列 队列的名称是随机的 
         * 当消费者断开和该队列的连接时 队列自动删除 
         */
        String queueName = channel.queueDeclare().getQueue();
        //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串 
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println("等待接收消息,把接收到的消息打印在屏幕    ");
        // 接收消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("ReceiveLogs01控制台打印接收到的消息" + message);
        };
        // 消费者取消消息时回调接口
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
} 
接收方2
public class ReceiveLogs02 {
    // 交换机的名字
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        // 创建(声明)一个交换机
        // channel.exchangeDeclare(交换机名字, "扇出类型");
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        /**
         * 生成一个临时的队列 队列的名称是随机的 
         * 当消费者断开和该队列的连接时 队列自动删除 
         */
        String queueName = channel.queueDeclare().getQueue();
        //把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串 
        channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println("等待接收消息,把接收到的消息打印在屏幕    ");
        // 接收消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("ReceiveLogs02控制台打印接收到的消息" + message);
        };
        // 消费者取消消息时回调接口
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
} 
发送方
public class EmitLog {
    // 交换机的名字
    private static final String EXCHANGE_NAME = "logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel());
            /**
             * 声明一个 exchange
             * 1.exchange 的名称 
             * 2.exchange 的类型 
             */
            // 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);
         }
    }
} 
运行结果

image-20220724163755104

image-20220724163803527

image-20220724163817134

direct(直接)

路由模式

1、介绍

我们希望将日志消息写入磁盘的程序仅接收严重错误(errros),而不存储哪些警告(warning)或信息(info)日志消息避免浪费磁盘空间。Fanout 这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播,在这里我们将使用 direct 这种类型来进行替换,这种类型的工作方式是,消息只去到它绑定的routingKey 队列中去。

简单来说就是:我想给队列1发消息,但是不给队列2发消息

image-20220724164125531

2、实战

接收方1
public class ReceiveLogsDirect01 {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        // 创建(声明)一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        // 队列名字
        String queueName = "disk";
        // 声明一个队列
        channel.queueDeclare(queueName, false, false, false, null);
        // 交换机绑定队列
        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("C:\\work\\rabbitmq_info.txt");
            FileUtils.writeStringToFile(file, message, "UTF-8");
            System.out.println("错误日志已经接收");
        };
        // 消费者取消消息时回调接口
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
} 
接收方2
public class ReceiveLogsDirect02 {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();

        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
        String queueName = "console";
        channel.queueDeclare(queueName, false, false, false, null);
        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 -> {
        });
    }
} 
发送方
public class EmitLogDirect {
    private static final String EXCHANGE_NAME = "direct_logs";

    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitUtils.getChannel()) {
            channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
            //创建多个 bindingKey
            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);
            }
        }
    }
} 
运行结果

image-20220724165828601

image-20220724165841468

image-20220724165857156

topic(主题)

主题模式

1、介绍

尽管使用direct 交换机改进了我们的系统,但是它仍然存在局限性-比方说我们想接收的日志类型有info.base 和 info.advantage,某个队列只想 info.base 的消息,那这个时候direct 就办不到了。这个时候就只能使用 topic 类型

简单来说就是:我可以给不同的RoutingKey队列同时发消息

注意:直接交换机只能单独给不同的队列发消息

Topic 的要求

发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit".这种类型的。当然这个单词列表最多不能超过 255 个字节。

在这个规则列表中,其中有两个替换符是大家需要注意的

*(星号)可以代替一个单词

#(井号)可以替代零个或多个单词

当队列绑定关系是下列这种情况时需要引起注意

当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了

**如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是direct 了 **

2、实战

接收者1

public class ReceiveLogsTopic01 {
    // 交换机的名称
    private static final String EXCHANGE_NAME = "topic_logs";

    // 接收消息
    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        // 声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        //声明队列Q1
        String queueName = "Q1";
        channel.queueDeclare(queueName, false, false, false, null);
        // 队列和交换机绑定规则
        channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
        System.out.println("等待接收消息.....");
        // 接收消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
             System.out.println(" 接收队列 :" + queueName + " 绑定键:"+delivery.getEnvelope().getRoutingKey()+", 消息:"+message);
        };
        // 消费者取消消息时回调接口
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

接收者2

public class ReceiveLogsTopic02 {
    // 交换机的名称
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        
        Channel channel = RabbitUtils.getChannel();
        // 声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "topic");
        //声明队列Q2
        String queueName = "Q2";
        channel.queueDeclare(queueName, false, false, false, null);
        // 队列和交换机绑定规则
        channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
        channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
        System.out.println("等待接收消息.....");
        // 接收消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" 接收队列 :" + queueName + " 绑定键:"+delivery.getEnvelope().getRoutingKey()+", 消息:"+message);
        };
        // 消费者取消消息时回调接口
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
        });
    }
}

发送者

public class EmitLogTopic {
    // 交换机的名称
    private static final String EXCHANGE_NAME = "topic_logs";

    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitUtils.getChannel()) {
            // 声明交换机
            channel.exchangeDeclare(EXCHANGE_NAME, "topic");
            /**
             * Q1-->绑定的是
             * 中间带 orange 带 3 个单词的字符串(*.orange.*)
             * Q2-->绑定的是
             * 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
             * 第一个单词是 lazy 的多个单词(lazy.#)
             *
             */
            // 创建一个Map
            Map<String, String> bindingKeyMap = new HashMap<>();
            bindingKeyMap.put("quick.orange.rabbit", "被队列 Q1Q2 接收到");
            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 bindingKey = bindingKeyEntry.getKey();
                String message = bindingKeyEntry.getValue();
                // 发送消息
                channel.basicPublish(EXCHANGE_NAME, bindingKey, null,
                        message.getBytes("UTF-8"));
                System.out.println("生产者发出消息" + message);
            }
        }
    }
}

运行结果

生产者

image-20220724201706131

消费者1

image-20220724201719287

消费者2

image-20220724201647173

死信队列

1、介绍

先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。

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

2、死信的来源

  • 消息 TTL 过期

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

  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.

3、实战

图解

image-20220724202546825

消息TTL 过期

消费者1
public class Consumer01 {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";
    // 普通队列的名称
    //private static final String NORMAL_QUEUE = "normal_queue";
    // 死信队列的名称
    //private static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] argv) throws Exception {
        // 获取信道
        Channel channel = RabbitUtils.getChannel();
        //声明死信和普通交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        
        //声明死信队列
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        //死信队列绑定死信交换机与 routingkey
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
        
        //正常队列绑定死信队列信息
        Map<String, Object> params = new HashMap<>();
        // 过期时间	10s=10000ms
        // arguments.put("x-message-ttl",10000);
        //正常队列设置死信交换机 参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //正常队列设置死信 routing-key 参数 key 是固定值
        params.put("x-dead-letter-routing-key", "lisi");
		
        // 普通队列的名称
        String normalQueue = "normal-queue";
        channel.queueDeclare(normalQueue, false, false, false, params);
        //普通队列绑定普通交换机与 routingkey
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
        
       System.out.println("等待接收消息.....");
       
        // 接收消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer01 接收到消息" + message);
        };
        // 消费者取消消息时回调接口
        channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {
        });
    }
}
消费者2
public class Consumer02 {
    // private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        // channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        String deadQueue = "dead-queue";
        // channel.queueDeclare(deadQueue, false, false, false, null);
        // channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
        System.out.println("等待接收死信队列消息.....");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer02 接收死信队列的消息" + message);
        };
        channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
        });
    }
}
生产者
public class Producer {
    // 普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitMqUtils.getChannel())
        // 接收者1已经声明了交换机,这里就不需要再次声明了,否则报错
        //channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        //死信消息	 设置消息的 TTL 时间
        AMQP.BasicProperties properties = new
                AMQP.BasicProperties().builder().expiration("10000").build();
        //该信息是用作演示队列个数限制
        for (int i = 1; i < 11; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties,
                    message.getBytes());
            System.out.println("生产者发送消息:" + message);
        }
    }
}
运行结果

image-20220724221223083

队列达到最大长度

生产者
public class Producer {
    private static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            //该信息是用作演示队列个数限制
            for (int i = 1; i < 11; i++) {
                String message = "info" + i;
                channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
                System.out.println("生产者发送消息:" + message);
            }
        }
    }
}
消费者1
public class Consumer01 {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        //声明死信和普通交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        //声明死信队列
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        //死信队列绑定死信交换机与 routingkey
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
        //正常队列绑定死信队列信息
        Map<String, Object> params = new HashMap<>();
        //正常队列设置死信交换机 参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //正常队列设置死信 routing-key 参数 key 是固定值
        params.put("x-dead-letter-routing-key", "lisi");
        // 设置正常队列的长度的限制
        params.put("x-max-length",6);
        String normalQueue = "normal-queue";
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
        System.out.println("等待接收消息.....");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer01 接收到消息" + message);
        };
        channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {
        });
    }
}
消费者2
public class Consumer02 {
    // private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        // channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        String deadQueue = "dead-queue";
        // channel.queueDeclare(deadQueue, false, false, false, null);
        // channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
        System.out.println("等待接收死信队列消息.....");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer02 接收死信队列的消息" + message);
        };
        channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
        });
    }
}
运行结果

image-20220725000056554

消息被拒

生产者
public class Producer {
    private static final String NORMAL_EXCHANGE = "normal_exchange";

    public static void main(String[] argv) throws Exception {
        try (Channel channel = RabbitMqUtils.getChannel()) {
            channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
            //该信息是用作演示队列个数限制
            for (int i = 1; i < 11; i++) {
                String message = "info" + i;
                channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
                System.out.println("生产者发送消息:" + message);
            }
        }
    }
}
消费者1
public class Consumer01 {
    //普通交换机名称
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    //死信交换机名称
    private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        //声明死信和普通交换机 类型为 direct
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        //声明死信队列
        String deadQueue = "dead-queue";
        channel.queueDeclare(deadQueue, false, false, false, null);
        //死信队列绑定死信交换机与 routingkey
        channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
		//正常队列绑定死信队列信息
        Map<String, Object> params = new HashMap<>();
        //正常队列设置死信交换机 参数 key 是固定值
        params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //正常队列设置死信 routing-key 参数 key 是固定值
        params.put("x-dead-letter-routing-key", "lisi");
        String normalQueue = "normal-queue";
        channel.queueDeclare(normalQueue, false, false, false, params);
        channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
        System.out.println("等待接收消息.....");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            if (message.equals("info5")) {
                System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
                //requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
                channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
            } else {
                System.out.println("Consumer01 接收到消息" + message);
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        boolean autoAck = false;
        channel.basicConsume(normalQueue, autoAck, deliverCallback, consumerTag -> {
        });
    }
}
消费者2
public class Consumer02 {
    // private static final String DEAD_EXCHANGE = "dead_exchange";

    public static void main(String[] argv) throws Exception {
        Channel channel = RabbitUtils.getChannel();
        // channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
        String deadQueue = "dead-queue";
        // channel.queueDeclare(deadQueue, false, false, false, null);
        // channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
        System.out.println("等待接收死信队列消息.....");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println("Consumer02 接收死信队列的消息" + message);
        };
        channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
        });
    }
}
运行结果

image-20220725001201846

延迟队列

  • 两种延迟方法
    • 基于死信(会进入死信队列,导致等待时间短的消息要排队)
    • 基于插件(等待时间短的消息先发送)

1、介绍

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

简单来说。延迟队列就是死信队列中的消息 TTL 过期

2、使用场景

  1. 订单在十分钟之内未支付则自动取消
  2. 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  3. 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  4. 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  5. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

3、延迟队列整合SpringBoot

pom.xml

<dependencies>
    <!--RabbitMQ 依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.47</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <!--swagger-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    <!--RabbitMQ 测试依赖-->
    <dependency>
        <groupId>org.springframework.amqp</groupId>
        <artifactId>spring-rabbit-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

application.properties

# 改成自己的服务器
spring.rabbitmq.host=182.92.234.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123

SwaggerConfig配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
 * @package: com.dahai.config
 */
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket webApiConfig() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApiInfo())
                .select()
                .build();
    }

    private ApiInfo webApiInfo() {
        return new ApiInfoBuilder()
                .title("rabbitmq 接口文档")
                .description("本文档描述了 rabbitmq 微服务接口定义")
                .version("1.0")
                .contact(new Contact("enjoy6288", "http://atguigu.com", "1551388580@qq.com"))
                .build();
    }
}

代码架构图

创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交 换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:

image-20220725160443670

基于死信延迟

配置文件类代码

package com.dahai.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
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 = "QD";

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

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

    //声明普通队列 A ttl 为 10s 并绑定到对应的死信交换机
    @Bean("queueA")
    public Queue queueA() {
        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
        args.put("x-message-ttl", 10000);
        /*
        QueueBuilder.durable    为持久队列创建构建器。
        参数:name – 队列的名称。
        回报:QueueBuilder 实例
        */
        return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
    }

    //声明队列 B ttl 为 40s 并绑定到对应的死信交换机
    @Bean("queueB")
    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
        args.put("x-message-ttl", 40000);
        return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
    }

    //声明死信队列 QD
    @Bean("queueD")
    public Queue queueD() {
        // return new Queue(DEAD_LETTER_QUEUE);
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    // 声明队列 A 绑定 X 交换机
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueA).to(xExchange).with("XA");
    }



    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueB).to(xExchange).with("XB");
    }

    //声明死信队列 QD 绑定死信交换机 Y
    @Bean
    public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,
                                        @Qualifier("yExchange") DirectExchange yExchange) {
        return BindingBuilder.bind(queueD).to(yExchange).with("YD");
    }
}

生产者

package com.dahai.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 {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("/sendMsg/{message}")
    public void sendMsg(@PathVariable String message) {

        log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date().toString(), message);
        rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: " + message);
        rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: " + message);
    }
}

消费者

package com.dahai.consumer;

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 {
    @RabbitListener(queues = "QD")
    public void receiveD(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
    }
}

运行结果

浏览器输入

image-20220725172207611

查看IDEA控制台打印的消息

image-20220725172137612

优化延迟队列

image-20220725172600615

优化配置文件类代码

// 在原来的代码中添加如下代码
// 普通队列C
public static final String QUEUE_C = "QC";
	// 声明QC
    @Bean("queueC")
    public Queue queueC() {
        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");
        /*
        QueueBuilder.durable    为持久队列创建构建器。
        参数:name – 队列的名称。
        回报:QueueBuilder 实例
        */
        return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
    }

    // 声明队列 C 绑定 X 交换机
    @Bean
    public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,
                                  @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }

优化生产者

// 在原来的代码中添加如下代码
//  开始发消息 消息TTL
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
    rabbitTemplate.convertAndSend("X", "XC", message, msg ->{
        //  发送消息的时候     延迟时长
        msg.getMessageProperties().setExpiration(ttlTime);
        return msg;
    });
    log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}

优化结果

image-20220725180630798

基于插件延迟

Rabbitmq 插件实现延迟队列

安装延时队列插件

在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载

rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ

/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

image-20220725181037154

image-20220725181049702

代码架构图

梳理

以前的延迟是基于死信交换机

image-20220725182509715

现在的延迟是基于插件的

image-20220725182759679

逻辑图

image-20220725182859643

配置文件类代码

package com.dahai.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";
    // routingKey
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

    // 说明队列
    @Bean
    public Queue delayedQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

    //自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机的类型
        args.put("x-delayed-type", "direct");
        /**
        * 1.交换机的名称
        * 2.交换机的类型
        * 3.是否需要持久化
        * 4.是否需要自动删除
        * 5.其它的参数
        */
         return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message",
                true, false, args);
    }
    // 队列绑定交换机
     @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue delayedQueue,
                                       @Qualifier("delayedExchange") CustomExchange
                                               delayedExchange) {
        return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

生产者

package com.dahai.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 {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

    //开始发消息 基于插件的   消息  及   延迟的时间
    @GetMapping("sendDelayMsg/{message}/{delayTime}")
    public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
        rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, msg -> {
            // 发送消息的时候 延迟时长 单位:ms
            msg.getMessageProperties().setDelay(delayTime);
            return msg;
        });
        log.info(" 当 前 时 间 : {}, 发送一条延迟 {} 毫秒的信息给队列 delayed.queue:{}",
                new Date(), delayTime, message);
    }
}

消费者

package com.dahai.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;

import java.util.Date;

/**
 * 基于插件的延迟队列
 */
@Slf4j
public class DelayQueueConsumer {

    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);
    }
}

基于插件运行结果

image-20220725211344992

4、总结

  延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用 RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正 确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为 单个节点挂掉导致延时队列不可用或者消息丢失。

  当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz 或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景

发布确认高级

(保证消息不丢失)

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:

正常发布确认

1、添加配置类

package com.dahai.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfirmConfig {
    // 交换机
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    // 队列
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    //声明业务 Exchange
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        return new DirectExchange(CONFIRM_EXCHANGE_NAME);
    }

    // 声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    // 声明确认队列绑定关系
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue confirmQueue,
                                @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(confirmQueue).to(exchange).with("key1");
    }
}

2、生产者

package com.dahai.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
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 javax.annotation.PostConstruct;

@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        String routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message);
        log.info("发送消息内容:{}", message);
    }
}

3、消费者

package com.dahai.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class Consumer {

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    
    @RabbitListener(queues =CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message){
        String msg=new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}",msg);
    }
}

如果交换机宕机

在配置文件当中需要添加

spring.rabbitmq.publisher-confirm-type=correlated

  • NONE

    • 禁用发布确认模式,是默认值
  • CORRELATED

    • 发布消息成功到交换器后会触发回调方法
  • SIMPLE

实现回调接口

简单来说就是:交换机要(无论是否收到消息)反馈消息给生产者,让生产者知道自己发送的东西是否成功

application.properties
spring.rabbitmq.host=182.92.234.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
# 重要!把下面这行代码加上
spring.rabbitmq.publisher-confirm-type=correlated
实现回调接口
package com.dahai.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;

import javax.annotation.PostConstruct;

@Slf4j
// 第一步
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback {

    
    // 第二步
    // 注入
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 第三步
    @PostConstruct
    public void init() {
        // 注入
        rabbitTemplate.setConfirmCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1. 发消息    交换机接收到了    回调
     *    1.1 correlationData 保存回调消息的ID及相关信息
     *    1.2 交换机收到消息 ack=true
     *    1.3 cause null
     * 2. 发消息   交换机接收失败了    回调
     *    2.1 correlationData 保存回调消息的ID及相关信息
     *    2.2 交换机收到消息 ack=false
     *    2.3 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);
        }
    }
}
生产者
package com.dahai.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
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 javax.annotation.PostConstruct;

@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        String routingKey = "key1";
        CorrelationData correlationData = new CorrelationData("1");
        
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message,correlationData);
        log.info("发送消息内容:{}", message);
    }
}
消费者
package com.dahai.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class Consumer {

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
    
    @RabbitListener(queues =CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message){
        String msg=new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}",msg);
    }
}
运行结果
成功

image-20220725225529329

失败

image-20220725225608247

如果队列出问题

回退消息

Mandatory 参数

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由(不可路由:表示进入交换机成功,但是没有从交换机进入队列),那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

实现回退接口
package com.dahai.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;

import javax.annotation.PostConstruct;

@Slf4j
// 第一步
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {

    // 注入
    // 第二步
    @Autowired
    private RabbitTemplate rabbitTemplate;

    // 第三步
    @PostConstruct
    public void init() {
        // 注入
        // 这里要设置回调接口
        rabbitTemplate.setConfirmCallback(this);
        // 这里也要设置回退接口
        rabbitTemplate.setReturnCallback(this);
    }

    /**
     * 交换机确认回调方法
     * 1. 发消息    交换机接收到了    回调
     *    1.1 correlationData 保存回调消息的ID及相关信息
     *    1.2 交换机收到消息 ack=true
     *    1.3 cause null
     * 2. 发消息   交换机接收失败了    回调
     *    2.1 correlationData 保存回调消息的ID及相关信息
     *    2.2 交换机收到消息 ack=false
     *    2.3 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.error(" 消息{},被交换机{}退回,退回原因 :{}, 路由 key:{}",
                new String(message.getBody()),exchange,replyText,routingKey);
    }
}

image-20220725232401145

生产者
package com.dahai.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
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 javax.annotation.PostConstruct;


@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        String routingKey1 = "key1";
        CorrelationData correlationData1 = new CorrelationData("1");

        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey1, message + "key1",correlationData1);
        log.info("发送消息内容:{}", message + "key1");

        // 注意这里是没有routingKey2 = "key2"这个绑定的,这里是想假设消息没有成功从交换机进入队列的情况
        String routingKey2 = "key2";
        CorrelationData correlationData2 = new CorrelationData("2");

        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey2, message + "key2",correlationData2);
        log.info("发送消息内容:{}", message + "key2");
    }
}
消费者
package com.dahai.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class Consumer {

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    @RabbitListener(queues =CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message){
        String msg=new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}",msg);
    }
}
运行结果

image-20220725232818608

备份交换机

图解

image-20220725233042471

application.properties

spring.rabbitmq.host=182.92.234.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
# 设置可以回调
spring.rabbitmq.publisher-confirm-type=correlated
# 设置可以回退
spring.rabbitmq.publisher-returns=true

优化配置类

package com.dahai.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ConfirmConfig {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    // --------------备份交换机,备份队列,报警队列-------------
    // 备份交换机
    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    // 备份队列
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    // 报警队列
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    //  声明备份交换机
    @Bean("backupExchange")
    public FanoutExchange backupExchange(){
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }

    // 声明备份队列
    @Bean("backupQueue")
    public Queue backupQueue(){
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    // 声明警告队列
    @Bean("warningQueue")
    public Queue warningQueue(){
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    // 声明备份队列绑定到备份交换机
    @Bean
    public Binding backupQueueBindingbackupExchange(@Qualifier("backupQueue") Queue backupQueue,
                                 @Qualifier("backupExchange") FanoutExchange backupExchange){
        return BindingBuilder.bind(backupQueue).to(backupExchange);
    }

    // 声明报警队列绑定到备份交换机
    @Bean
    public Binding warningQueueBindingbackupExchange(@Qualifier("warningQueue") Queue warningQueue,
                                  @Qualifier("backupExchange") FanoutExchange backupExchange){
        return BindingBuilder.bind(warningQueue).to(backupExchange);
    }




// ----------------下面代码是没有加备份交换机之前的代码--------------------------

    //声明业务 Exchange
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        //return new DirectExchange(CONFIRM_EXCHANGE_NAME);
        // 修改原来的方法
        ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
                .durable(true)
        //设置该交换机的备份交换机
                 .withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return (DirectExchange)exchangeBuilder.build();
    }

    // 声明确认队列
    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
    }

    // 声明确认队列绑定关系
    @Bean
    public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
                                @Qualifier("confirmExchange") DirectExchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("key1");
    }
}

生产者

package com.dahai.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
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 javax.annotation.PostConstruct;


@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMessage/{message}")
    public void sendMessage(@PathVariable String message) {
        String routingKey1 = "key1";
        CorrelationData correlationData1 = new CorrelationData("1");

        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey1, message + "key1",correlationData1);
        log.info("发送消息内容:{}", message + "key1");

        // 注意这里是没有routingKey2 = "key2"这个绑定的,这里是想假设消息没有成功从交换机进入队列的情况
        String routingKey2 = "key2";
        CorrelationData correlationData2 = new CorrelationData("2");

        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey2, message + "key2",correlationData2);
        log.info("发送消息内容:{}", message + "key2");
    }
}

消费者

package com.dahai.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class Consumer {

    public static final String CONFIRM_QUEUE_NAME = "confirm.queue";

    @RabbitListener(queues =CONFIRM_QUEUE_NAME)
    public void receiveMsg(Message message){
        String msg=new String(message.getBody());
        log.info("接受到队列 confirm.queue 消息:{}",msg);
    }
}

报警消费者

package com.dahai.consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class WarningConsumer {
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    @RabbitListener(queues = WARNING_QUEUE_NAME)
    public void receiveWarningMsg(Message message) {
        String msg = new String(message.getBody());
        log.error("报警发现不可路由消息:{}", msg);
    }
}

运行结果

image-20220726000030037

总结

mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,经过上面结果显示答案是备份交换机优先级高。

RabbitMQ 其他知识点

幂等性

概念

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。 举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常, 此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱 了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误 立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等

消息重复消费

消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断, 故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但 实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

解决思路

MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费 者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消 息时用该 id 先判断该消息是否已消费过。

消费端的幂等性保障

在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性, 这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a. 唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现

唯一 ID+指纹码机制

指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基 本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存 在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数 据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

Redis 原子性

利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

优先级队列

使用场景

在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如 果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall 商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创 造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存 放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级, 否则就是默认优先级。

图解

image-20220726002153742

生产者

package com.dahai.consumer;

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

import java.util.HashMap;
import java.util.Map;

public class Producer {
    private final static String QUEUE_NAME = "hello";

    public static void main(String[] args) throws Exception {
		//创建一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 工厂IP  连接Rabbit.MQ的队列
        factory.setHost("182.92.234.71");
        factory.setUsername("admin");
        factory.setPassword("123");
		// 创建连接
        // channel 实现了自动 close 接口 自动关闭 不需要显示关闭
        Connection connection = factory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();

        Map<String,Object> arguments = new HashMap<>();
        // 官方允许是0 -255之间 此处设置10 允许优化级范围为0-10 不要设置过大浪费CPU与内存
        arguments.put("x-max-priority",10);
        /**
         * 声明一个队列
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享, true表示可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除, true表示自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, arguments);

        for (int i = 1; i < 11; i++) {
            String message = "info" + i;
            if (i == 5) {
                // 给下标5设置优先级为7
                AMQP.BasicProperties properties = new
                        AMQP.BasicProperties().builder().priority(7).build();
                /**
                 * 发送一个消息
                 * 1.发送到那个交换机  不写使用默认的交换机
                 * 2.路由的 key 是哪个
                 * 3.其他的参数信息
                 * 4.发送消息的消息体
                 */
                channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
            } else {
                // 其他不设置,正常发布
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            }
        }
           System.out.println("消息发送完毕");
        }
    }
} 

消费者

public class Consumer {
    // 队列名词
    private final static String QUEUE_NAME = "hello";
	// 接收消息
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("182.92.234.71");
        factory.setUsername("admin");
        factory.setPassword("123");
        // 创建连接
        Connection connection = factory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        System.out.println("等待接收消息 ");
        // 接收消息
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody());
            System.out.println(message);
    	};
        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
		CancelCallback cancelCallback=(consumerTag)->{ 
            System.out.println("消息消费被中断");
		};
        /**
         * 消费者消息
         * 1.消费哪个队列
		 * 2.消费成功之后是否要自动应答  true 代表的自动应答  false 代表手动应答
         * 3.消费者未成功消费的回调
         * 4.消费者取消消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

运行结果

image-20220726120013033

惰性队列

使用场景

RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持 更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中, 这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留 一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的 时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法, 但是效果始终不太理想,尤其是在消息量特别大的时候。

image-20220726120620355

两种模式

队列具备两种模式

  • default
  • lazy

默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy 模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过 Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。 如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。

在队列声明的时候可以通过“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);

内存开销对比

image-20220726121050843

在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅 占用 1.5MB

RabbitMQ 集群

集群原理图解

image-20220726141651652

搭建集群

image-20220726141949063

image-20220726142017634

搭建步骤

1.修改 3 台机器的主机名称
vim /etc/hostname
2.配置各个节点的 hosts 文件,让各个节点都能互相识别对方
vim /etc/hosts
10.211.55.74 node1
10.211.55.75 node2
10.211.55.76 node3

image-20220726142524082

3.以确保各个节点的 cookie 文件使用的是同一个值
在 node1 上执行远程操作命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
4.启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行以下命令)
rabbitmq-server -detached
5.在节点 2 执行
rabbitmqctl stop_app
(rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app(只启动应用服务)
6.在节点 3 执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
7.集群状态
rabbitmqctl cluster_status
8.需要重新设置用户
创建账号
rabbitmqctl add_user admin 123
设置用户角色
rabbitmqctl set_user_tags admin administrator
设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
9.解除集群节点(node2 和 node3 机器分别执行)
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行)

image-20220726143132257

镜像队列

使用镜像的原因

如果 RabbitMQ 集群中只有一个 Broker 节点,那么该节点的失效将导致整体服务的临时性不可用,并 且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable属性也设置为true, 但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在 一个短暂却会产生问题的时间窗。通过 publisherconfirm 机制能够确保客户端知道哪些消息己经存入磁盘, 尽管如此,一般不希望遇到因单点故障导致的服务不可用。

引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中 的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。

搭建步骤

1.启动三台集群节点

2.随便找一个节点添加 policy

image-20220726145314949

3.在 node1 上创建一个队列发送一条消息,队列存在镜像队列

image-20220726145338408

4.停掉 node1 之后发现 node2 成为镜像队列

image-20220726145402860

5.就算整个集群只剩下一台机器了 依然能消费队列里面的消息 说明队列里面的消息被镜像队列传递到相应机器里面了

Haproxy+Keepalive 实现高可用负载均衡

整体架构图

image-20220726175425698

Haproxy 实现负载均衡

HAProxy 提供高可用性、负载均衡及基于 TCPHTTP 应用的代理,支持虚拟主机,它是免费、快速并 且可靠的一种解决方案,包括 Twitter,Reddit,StackOverflow,GitHub 在内的多家知名互联网公司在使用。 HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。

扩展 nginx,lvs,haproxy 之间的区别: http://www.ha97.com/5646.html

搭建步骤

1.下载 haproxy(在 node1 和 node2)
yum -y install haproxy
2.修改 node1 和 node2 的 haproxy.cfg
vim /etc/haproxy/haproxy.cfg
 需要修改红色 IP 为当前机器 IP

image-20220726175646169

3.在两台节点启动 haproxy
haproxy -f /etc/haproxy/haproxy.cfg
ps -ef | grep haproxy
4.访问地址
http://10.211.55.71:8888/stats 

Keepalived 实现双机(主备)热备

试想如果前面配置的 HAProxy 主机突然宕机或者网卡失效,那么虽然 RbbitMQ 集群没有任何故障但是 对于外界的客户端来说所有的连接都会被断开结果将是灾难性的为了确保负载均衡服务的可靠性同样显得 十分重要,这里就要引入 Keepalived 它能够通过自身健康检查、资源接管功能做高可用(双机热备),实现 故障转移.

搭建步骤

1.下载 keepalived
yum -y install keepalived
2.节点 node1 配置文件
vim /etc/keepalived/keepalived.conf
把资料里面的 keepalived.conf 修改之后替换
3.节点 node2 配置文件
需要修改 global_defs 的 router_id,如:nodeB
其次要修改 vrrp_instance_VI 中 state 为"BACKUP";
最后要将 priority 设置为小于 100 的值
4.添加 haproxy_chk.sh
(为了防止 HAProxy 服务挂掉之后 Keepalived 还在正常工作而没有切换到 Backup 上,所以
这里需要编写一个脚本来检测 HAProxy 务的状态,当 HAProxy 服务挂掉之后该脚本会自动重启
HAProxy 的服务,如果不成功则关闭 Keepalived 服务,这样便可以切换到 Backup 继续工作)
vim /etc/keepalived/haproxy_chk.sh(可以直接上传文件)
修改权限 chmod 777 /etc/keepalived/haproxy_chk.sh
5.启动 keepalive 命令(node1 和 node2 启动)
systemctl start keepalived
6.观察 Keepalived 的日志
tail -f /var/log/messages -n 200
7.观察最新添加的 vip
ip add show
8.node1 模拟 keepalived 关闭状态
systemctl stop keepalived 
9.使用 vip 地址来访问 rabbitmq 集群

Federation Exchange(联邦交换机)

使用它的原因

image-20220726180143118

搭建步骤

1.需要保证每台节点单独运行
2.在每台机器上开启 federation 相关插件
    rabbitmq-plugins enable rabbitmq_federation
    rabbitmq-plugins enable rabbitmq_federation_management

image-20220726180250919

3.原理图(先运行 consumer 在 node2 创建 fed_exchange

image-20220726180704170

4.在 downstream(node2)配置 upstream(node1)

image-20220726180734274

4.添加 policy

image-20220726180759035

5.成功的前提

image-20220726180819687

public class Consumer {
    // 队列名词
    private final static String QUEUE_NAME = "hello";
    // 交换机名词
    private final static String FED_EXCHANGE = "fed_exchange";
	// 接收消息
    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("182.92.234.71");
        factory.setUsername("admin");
        factory.setPassword("123");
        // 创建连接
        Connection connection = factory.newConnection();
        // 获取信道
        Channel channel = connection.createChannel();
        // 声明交换机
        channel.exchangeDeclare(FED_EXCHANGE,BuiltinExchangeType.DIRECT);
        // 声明队列
        channel.queueDeclare("node_queue", true, false, false, null);
        // 队列绑定交换机
        channel.queueBind("node_queue", EXCHANGE_NAME, "routeKey");
    }
}

Federation Queue(联邦队列)

联邦队列可以在多个 Broker 节点(或者集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以 连接一个或者多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息 的需求。

搭建步骤

image-20220726184140512

image-20220726184150625

Shovel

使用它的原因

Federation 具备的数据转发功能类似,Shovel 够可靠、持续地从一个 Broker 中的队列(作为源端,即 source)拉取数据并转发至另一个 Broker 中的交换器(作为目的端,即 destination)。作为源端的队列和作 为目的端的交换器可以同时位于同一个 Broker,也可以位于不同的 Broker 上。Shovel 可以翻译为"铲子", 是一种比较形象的比喻,这个"铲子"可以将消息从一方"铲子"另一方。Shovel 行为就像优秀的客户端应用 程序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。

搭建步骤

image-20220726184538360

image-20220726184556940

image-20220726184635256

posted @ 2022-07-26 20:06  海边蓝贝壳  阅读(72)  评论(0编辑  收藏  举报