MQ
mq安装 https://blog.csdn.net/hzw19920329/article/details/53156015
mq安装:https://blog.csdn.net/2401_84911346/article/details/138773135
1.Windows下安装RabbitMQ需要以下几个步骤
(1):下载erlang,原因在于RabbitMQ服务端代码是使用并发式语言erlang编写的,下载地址:http://www.erlang.org/downloads,双击.exe文件进行安装就好,安装完成之后创建一个名为ERLANG_HOME的环境变量,其值指向erlang的安装目录,同时将%ERLANG_HOME%\bin加入到Path中,最后打开命令行,输入erl,如果出现erlang的版本信息就表示erlang语言环境安装成功;
(2):下载RabbitMQ,下载地址:http://www.rabbitmq.com/,同样双击.exe进行安装就好(这里需要注意一点,默认的安装目录是C:/Program Files/....,这个目录中是存在空格符的,我们需要改变安装目录,貌似RabbitMQ安装目录中是不允许有空格的,我之前踩过这个大坑);
(3):安装RabbitMQ-Plugins,这个相当于是一个管理界面,方便我们在浏览器界面查看RabbitMQ各个消息队列以及exchange的工作情况,安装方法是:打开命令行cd进入rabbitmq的sbin目录(我的目录是:E:\software\rabbitmq\rabbitmq_server-3.6.5\sbin),输入:rabbitmq-plugins enable rabbitmq_management命令,稍等会会发现出现plugins安装成功的提示,默认是安装6个插件。
1、如果你在安装插件的过程中出现了下面的错误:
解决方法是:首先在命令行输入:rabbitmq-service stop,接着输入rabbitmq-service remove,再接着输入rabbitmq-service install,接着输入rabbitmq-service start,最后重新输入rabbitmq-plugins enable rabbitmq_management试试,我是这样解决的;
2、如果你在安装插件的过程中出现了下面的错误(插件安装失败):
参考:安装RabbitMQ出现Plugin configuration unchanged.问题-CSDN博客
插件安装成功提示如下:
(4):插件安装完之后,在浏览器输入http://localhost:15672进行验证,你会看到下面界面,输入用户名:guest,密码:guest你就可以进入管理界面,当然用户名密码你都可以变的;
安装死信延迟插件(交换机类型)
1、下载插件
https://www.rabbitmq.com/community-plugins.html
往下翻
注意下载与Rabbit MQ对应版本;(最开始使用的是3.9版本,24年7月时重新安装发现当前版本插件启动时异常,因此更换成3.8版本插件,可以正常启动)
2、将下载好的插件放到Rabbit MQ安装目录的插件目录下,例如 C:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.14\plugins
3、进入当前Rabbit MQ安装目录的 sbin目录下
4、打开cmd执行命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
5、重启 Rabbit MQ Server
rabbitmqctl stop
rabbitmq-server start
安装RabbitMQ之后,有相关的命令执行快捷方式(也可输入命令执行)
6、查看Rabbit MQ控制台的交换机
MQ基础
1、概念
什么是MQ?
MQ( message queue),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已.,还是一种跨进程的通信机制,用于上下游传递消息,在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务。
流量消峰?
当一万个请求直接发送给系统,可能造成系统宕机,但发送给MQ,MQ有排队等待机制。
优点:防止系统宕机;
缺点:访问延长处理;
应用解耦?
以电商应用为例,应用中有订单系统库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统岀了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成,当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
异步处理?
有些服务间调用是异步的,例如A调用B,B需要花费很长时间执行,但是A需要知道B什么时候可以执行完,以前一般有两种方式,A过一段时间去调用B的查询api查询。或者A提供一个 callback api,B执行完之后调用api,通知A服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题A调用B服务后,只需要监听B处理完成的消息,当B处理完成后,会发送一条消息给MQ,MQ会将此消息转发给A服务。这样A服务既不用循环调用B的查询api,也不用提供 callback api。同样B服务也不用做这些操作。A服务还能及时的得到异步处理成功的消息。
2、分类
1、Activemq
优点:单机吞吐量万级,时效性ms(毫秒)级,可用性高,基于主从架构实现高可用性,低的概率丢失数据;
缺点:官方社区现在对AtⅳveMQ5x维护越来越少,高吞吐量场景较少使用。
尚硅谷官网视频:htp://www.gulixueyuan.com/course/322
2. Kafka
大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开Kafka,这款为大数据而生的消息中间件以其百万级TPS的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输、存储的过程中发挥着举足轻重的作用。目前已经被 Linkedin,Uber, Twitter, Netflix等大公司所采纳;
优点:性能卓越,单机写入TPS约在百万条/秒,最大的优点,就是吞吐量高。时效性ms级可用性非常高, kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用消费者采用Pull方式获取消息,消息有序,通过控制能够保证所有消息被消费且仅被消费一次;有优秀的第三方Kafka Web管理界面 Kafka-manager;在日志领域比较成熟,被多家公司源项目使用;功能支持功能较为简单,主要支持简单的MQ功能,在大数据领或的实时计算以及日志釆集被大规模使用;
缺点:Kafka单机超过64个队讽高现象,队列越多,送消息响应时间变长,使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试;支持消息顶序但是一台代理宕机后,就会产生消息乱序,社区更新较慢;
3. RocketMQ
RocketMQ出自阿里巴巴的开源产品,用Java语言实现,在设计时参考了 Kafka,并做出了自己的一些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理, binglog分发等场景;
优点:单机吞吐量十万级可用性非常高,分布式架构消息可以做到0丟失,MQ功能较为完善,还是分布式的,扩展性好支持10亿级別的消息堆积,不会因为堆积导致性能下降,源码是java我们同可以自己阅读源码,定制自己公司的MQ;
缺点:支持的客户端语言不多,目前是java及c++,其中c++不成熟;社区活跃度一般,没有在MQ核心中去实现JMS等接口有些系统要迁移需要修改大量代码;
4. RabbitMQ
2007年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一;
优点:由于 erlang语言的高并发特性,性能较好;吞吐量到万级,№Q功能比较完备,健壮、稳定、易用跨平台、支持多种语言如: Python、Ruby、,NET、Java、JMS、C、PHP、 ActionScript. XMPP、 STOMP等,支持AJAX文档齐全;开源提供的管理界面非常棒,用起来很好用社区活跃度高;更新频率相当高https://www.rabbitmg.com/news.html
缺点:商业版需要收费学习成本较高;
3、RabbitMQ四大概念
1、生产者
产生数据发送消息的程序;
2、交换机
交换机是 RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定;
3、队列
队列是 RabbitMQ内部使用的一种数据结构,尽管消息流经 RabbitMQ和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式
4、消费者
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者;
Broker:接收和分发消息的应用,RabbitMQ Server就是Message Broker;
Virtual host:出于多租户和安全因素设计的,把AMQP的基本组件划分到个虚拟的分组中,类似于网络中的 namespace概念。当多个不同的用户使用同一个 RabbitMQ Server提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost刨建 exchange/ queue等
(Broker中可以存在多个 Virtual host,每个Virtual host可以存在多个 Exchange(交换机))
Connection:publisher/consumer和broker之间的TCP连接;
Channel:
如果毎一次访问 RabbitMQ都建立一个 Connection,在消息量大的时候建立TCP Connection的开销将是巨大的,效率也较低。 Channel是在 connection内剖建立的逻辑连接,如果应用程序支持多线程,通常每个 thread刨建单独的 channel进行通讯, AMQP method包含了 channel id帮助客户端和 message broker识别 channel,所以 channel之间是完全隔离的 Channel作为轻量级的Connection极大减少了操作系统立 TCP connection的开销;
Exchange:message到达 broker的第一站,根据分发规则,匹配查询表中的 routing keγ,分发消息到 queue中去。常用的类型有: direct( point-to- point), topic( publish- subscribe) and fanout(multicast)
queue:消息最终被送到这里等待 consumer取走
Binding:exchange和 queue之间的虚拟连接, binding中可以包含 routing key, Binding信息被保存到 exchange中的查询表中,用于 message的分发依据
(交换机与队列直接的连接)
4、入门程序(简单模式)
生产者代码

import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * Rabbit MQ入门程序 * 生产者 */ public class Producer { //队列名称 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws Exception{ //创建一个连接工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setUsername("admin"); factory.setPassword("123456"); //创建连接 Connection connection = factory.newConnection(); //获取通道 Channel channel = connection.createChannel(); /** * 生成一个队列 * queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5) throws IOException; * var1(String):队列名称 * var2(boolean):队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中 * var3(boolean):该队列是否进行消息共享,true代表多个消费者消费,false只能一个消费者消费 * var4(boolean):最后一个消费者断开连接后,该队列是否自动删除 * var5(Map<String, Object>):其他参数 */ channel.queueDeclare(QUEUE_NAME,false,false,false,null); //发消息 String message = "hello world"; /** * 发送一个消息 * basicPublish(String var1, String var2, BasicProperties var3, byte[] var4) throws IOException; * var1:发送到哪个交换机 * var2:路由的 Key是哪个 * var3:其他参数信息 * var4:发送的消息体 */ channel.basicPublish("",QUEUE_NAME,null,message.getBytes()); } }
执行之后查看Rabbit MQ
消费者代码

/** * Rabbit MQ入门程序 * 消费者 */ public class Consumer { //队列名称 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws Exception{ //创建一个连接工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setUsername("admin"); factory.setPassword("123456"); //创建连接 Connection connection = factory.newConnection(); //获取通道 Channel channel = connection.createChannel(); //声明 接收消息的回调 DeliverCallback deliverCallback = (consumerTag,message) -> { System.out.println(new String(message.getBody())); }; //声明 取消消息的回调 CancelCallback cancelCallback = consumerTag -> { System.out.println("消息消费被中断"); }; /** * 消费者消费消息 * basicConsume(String var1, boolean var2, DeliverCallback var3, CancelCallback var4) throws IOException; * var1(String):消费哪个队列 * var2(boolean):消费成功之后是否自动应答 * var3(DeliverCallback):接收消息的回调方法 * var4(CancelCallback):取消消息的回调方法 */ channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback); } }
5、工作模式(自动应答)(多个消费者之间消息轮询消费)
工具类

import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; /** * 连接工厂创建通信的工具类 */ public class RabbitMQUtil { public static Channel getChannel() throws Exception { //创建一个连接工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setUsername("admin"); factory.setPassword("123456"); //创建连接 Connection connection = factory.newConnection(); //获取通道 Channel channel = connection.createChannel(); return channel; } }
生产者

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.Channel; import java.util.Scanner; /** * 工作模式 * 生产者 */ public class Producer { //队列名称 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.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()); } } }
消费者01

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 工作模式 * 工作线程01(消费者) */ public class Consumer01 { //队列名称 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtil.getChannel(); //声明 接收消息的回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody())); }; //声明 取消消息的回调 CancelCallback cancelCallback = consumerTag -> { System.out.println(consumerTag + "消息消费被中断"); }; System.out.println("C1等待消息接收..."); channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback); } }
消费者02

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 工作模式 * 工作线程02(消费者) */ public class Consumer02 { //队列名称 public static final String QUEUE_NAME = "hello"; public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtil.getChannel(); //声明 接收消息的回调 DeliverCallback deliverCallback = (consumerTag, message) -> { System.out.println(new String(message.getBody())); }; //声明 取消消息的回调 CancelCallback cancelCallback = consumerTag -> { System.out.println(consumerTag + "消息消费被中断"); }; System.out.println("C2等待消息接收..."); channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback); } }
消息应答
概念:
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情況。 Rabbitmq一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情況下,突然有个消贵者挂掉了,
我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到;
为了保证消息在发送过程中不丢失, RabbitMQ引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq它已经处理了, rabbitmq可以把该消息删除了;
自动应答:
消息发送后立即被认为己经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
手动应答:
其中Channel接口的某个实现类
basicReject与basicNack比较起来,少了一个参数 multiple:是否批量应答(true/false);
消息自动重新入队
如果消费者由于某些原因失去连接(其通道己关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认, RabbitMQ将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
工作模式(手动应答)(多个消费者之间轮询消费)代码
生产者:

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.Channel; import java.util.Scanner; /** * 工作模式 * 生产者 * 多个消费者之间消费消息采用轮询的规则 */ public class Producer { //队列名称 public static final String QUEUE_NAME = "ack_queue"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.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); } } }
消费者01:

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.ithailin.framework.utils.ThreadUtils; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 工作模式 * 工作线程01(消费者)(手动应答) */ public class Consumer01 { //队列名称 public static final String QUEUE_NAME = "ack_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtil.getChannel(); //声明 接收消息的回调 DeliverCallback deliverCallback = (consumerTag, message) -> { //沉睡一秒 ThreadUtils.sleep(1); System.out.println("消费者01消费的消息:" + new String(message.getBody(),"UTF-8")); /** * 开始手动应答 * 参数1:消息的标记 tag * 参数2:是否批量应答 */ channel.basicAck(message.getEnvelope().getDeliveryTag(),false); }; //声明 取消消息的回调 CancelCallback cancelCallback = consumerTag -> { System.out.println(consumerTag + "消费者01消费消息被中断"); }; System.out.println("C1等待消息接收...时间较短..."); //false:手动应答 channel.basicConsume(QUEUE_NAME, false,deliverCallback,cancelCallback); } }
消费者02:

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.ithailin.framework.utils.ThreadUtils; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 工作模式 * 工作线程02(消费者)(手动应答) */ public class Consumer02 { //队列名称 public static final String QUEUE_NAME = "ack_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtil.getChannel(); //声明 接收消息的回调 DeliverCallback deliverCallback = (consumerTag, message) -> { //沉睡30秒 ThreadUtils.sleep(30); System.out.println("消费者02消费的消息:" + new String(message.getBody(),"UTF-8")); /** * 开始手动应答 * 参数1:消息的标记 tag * 参数2:是否批量应答 */ channel.basicAck(message.getEnvelope().getDeliveryTag(),false); }; //声明 取消消息的回调 CancelCallback cancelCallback = consumerTag -> { System.out.println(consumerTag + "消费者02消费消息被中断"); }; System.out.println("C2等待消息接收...时间较长..."); //false:手动应答 channel.basicConsume(QUEUE_NAME, false,deliverCallback,cancelCallback); } }
Rabbit MQ持久化
概念:
刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化;
队列如何实现持久化?
之前我们创建的队列都是非持久化的, Rabbit MQ如果重启,该队列就会被删除掉,如果要队列实现持久化需要在声明队列的时候把 durable参数设置为持久化(true);
但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,若之间将该非持久化队列,更改为持久化队列,运行代码时,会出现错误;
/** * 生成一个队列 * queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5) throws IOException; * var1(String):队列名称 * var2(boolean):队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中 * var3(boolean):该队列是否进行消息共享,true代表多个消费者消费,false只能一个消费者消费 * var4(boolean):最后一个消费者断开连接后,该队列是否自动删除 * var5(Map<String, Object>):其他参数 */ //var2 true:表示当前创建的队列持久化到Rabbit MQ中,即使MQ关闭或者宕机,该队列也不会丢失; channel.queueDeclare(QUEUE_NAME,true,false,false,null);
消息如何实现持久化?
要想让消息实现持久化需要在消息生产者修改代码 添加属性:MessageProperties.PERSISTENT_TEXT_PLAIN
/** * 发送一个消息 * basicPublish(String var1, String var2, BasicProperties var3, byte[] var4) throws IOException; * var1:发送到哪个交换机 * var2:路由的 Key是哪个 * var3:其他参数信息 * var4:发送的消息体 */ //MessageProperties.PERSISTENT_TEXT_PLAIN:表示将当前发送的消息进行持久化 channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
多消费者不公平分发模式(默认是轮询模式)
在多个消费者之间设置代码
/** * prefetchCount:默认为 0,采用轮询规则 * 设置为 1时,采用不公平分发(能者多劳) */ channel.basicQos(1); //false:手动应答 channel.basicConsume(QUEUE_NAME, false,deliverCallback,cancelCallback);
预取值
本身消息的发送就是异步发送的,所以在任何时候, channel上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缀冲区里面无限制的未确认消息问题。这个时候就可以通过使用basicqQs方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量Rabbitmq将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认;
/** * prefetchCount:默认为 0,采用轮询规则 * 设置为 1时,采用不公平分发(能者多劳) * 设置为 >1 时,代表预取值(当前消费者堆积未处理消息的缓冲区大小) */ channel.basicQos(1); //false:手动应答 channel.basicConsume(QUEUE_NAME, false,deliverCallback,cancelCallback);
6、发布、确认模式
单个发布
批量发布
异步确认发布
如何处理异步未确认消息
最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列比如说用 ConcurrentLinkedQueue这个队列在 confirm callbacks与发布线程之间进行消息的传递;

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.Channel; import com.rabbitmq.client.ConfirmCallback; import com.rabbitmq.client.MessageProperties; import java.util.UUID; /** * 发布确认模式 * 生产者 */ public class Producer { //测试消息发送条数 public static final int MESSAGE_COUNT = 1000; public static void main(String[] args) throws Exception { //单个确认 publishMessageIndividually();//发布1000条消息,单独确认总耗时:833ms //批量确认 publicMessageBatch();//发布1000条消息,批量确认总耗时:37ms //异步确认 publishMessageAsync();//发布1000条消息,异步确认总耗时:11ms } //单个发布确认 public static void publishMessageIndividually() throws Exception{ Channel channel = RabbitMQUtil.getChannel(); //开启发布确认模式 channel.confirmSelect(); //队列名称 String queueName = UUID.randomUUID().toString(); /** * 声明队列 * queueDeclare(String var1, boolean var2, boolean var3, boolean var4, Map<String, Object> var5) throws IOException; * var1(String):队列名称 * var2(boolean):队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中 * var3(boolean):该队列是否进行消息共享,true代表多个消费者消费,false只能一个消费者消费 * var4(boolean):最后一个消费者断开连接后,该队列是否自动删除 * var5(Map<String, Object>):其他参数 */ //var2 true:表示当前创建的队列持久化到Rabbit MQ中,即使MQ关闭或者宕机,该队列也不会丢失; channel.queueDeclare(queueName,true,false,false,null); //开始时间 long begin = System.currentTimeMillis(); //批量发送消息 for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; /** * 发送一个消息 * basicPublish(String var1, String var2, BasicProperties var3, byte[] var4) throws IOException; * var1:发送到哪个交换机 * var2:路由的 Key是哪个 * var3:其他参数信息 * var4:发送的消息体 */ //MessageProperties.PERSISTENT_TEXT_PLAIN:表示将当前发送的消息进行持久化 channel.basicPublish("",queueName, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8")); //单个消息进行确认 channel.waitForConfirms(); } //结束时间 long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "条消息,单独确认总耗时:" + (end - begin) + "ms"); } //批量发布确认 public static void publicMessageBatch() throws Exception{ Channel channel = RabbitMQUtil.getChannel(); //开启发布确认模式 channel.confirmSelect(); //队列名称 String queueName = UUID.randomUUID().toString(); //声明队列 channel.queueDeclare(queueName,true,false,false,null); //开始时间 long begin = System.currentTimeMillis(); for (int i = 0; i < MESSAGE_COUNT; i++) { String message = i + ""; channel.basicPublish("",queueName, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8")); } //批量发布确认 channel.waitForConfirms(); //结束时间 long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "条消息,批量确认总耗时:" + (end - begin) + "ms"); } //异步发布确认 public static void publishMessageAsync() throws Exception{ Channel channel = RabbitMQUtil.getChannel(); //开启发布确认模式 channel.confirmSelect(); //队列名称 String queueName = UUID.randomUUID().toString(); //声明队列 channel.queueDeclare(queueName,true,false,false,null); //开始时间 long begin = System.currentTimeMillis(); /** * 消息确认成功 回调函数 * 参数1:消息的标记 * 参数2:是否批量确认 */ ConfirmCallback ackCallback = (deliveryTag,multiple) ->{ }; /** * 消息确认失败 回调函数 * 参数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, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8")); } //结束时间 long end = System.currentTimeMillis(); System.out.println("发布" + MESSAGE_COUNT + "条消息,异步确认总耗时:" + (end - begin) + "ms"); } }
临时队列
每次连接 Rabbit MQ时都需要一个全新的空队列,为此可以创建一个具有随机名称的队列,一旦断开消费者连接,队列就会自动删除;
String queue = channel.queueDeclare().getQueue();
以下模式(发布订阅,路由,主题)都将引入一个新概念:交换机
交换机(Exchanges)概念
RabbitMQ消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。相反,生产者只能将消息发送到交换机 (exchange),交换机一方面接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们推入到许多队列中还是说应该丢弃它们。这都由交换机的类型来决定;
交换机类型
直接(direct)、主题(topic)、标题(headers)、扇出(fanout)、默认(默认类型,通常用空字符串表示);
绑定(bindings)
binding通常使用一个标识(routing Key)来表示;
7、Fanout(扇出)交换机(发布、订阅模式)
将接收到的所有消息广播到它所知道的所有队列中,系统中默认有一个默认的扇出交换机(fanout)
生产者绑定一个交换机,多个消费者都绑定这个交换机,并且多个消费者(队列)与交换机绑定的关系(标识/routing Key)相同时,则当前为扇出(fanout);
生产者代码(扇出)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.Channel; import java.util.Scanner; /** * 发布、订阅模式(交换机类型:扇出 fanout) * 生产者 */ public class Producer { //交换机名称 public static final String EXCHANGE_NAME = "logs"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.getChannel(); channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()){ String message = scanner.next(); channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8")); System.out.println("生产者生产消息:"+ message); } } }
消费者01代码(扇出)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 发布、订阅模式(交换机类型:扇出 fanout) * 消费者 */ public class Consumer01 { //交换机名称 public static final String EXCHANGE_NAME = "logs"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.getChannel(); /** * 声明一个交换机 * DeclareOk exchangeDeclare(String exchange, String type) throws IOException * exchange:交换机名称 * type:交换机类型 */ channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT); //声明一个临时队列 String queue = channel.queueDeclare().getQueue(); /** * 绑定交换机和队列 * BindOk queueBind(String queue, String exchange, String routingKey) throws IOException * queue:队列 * exchange:交换机 * routingKey:交换机与队列绑定的标识名称 */ channel.queueBind(queue,EXCHANGE_NAME,""); System.out.println("C1等待接收消息...把接收到的消息打印在控制台上..."); //消费者消费消息回调接口 DeliverCallback deliverCallback = (consumerTag,message) ->{ System.out.println("C1接收到消息是:" + new String(message.getBody(),"UTF-8")); }; //消费者取消消费消息回调接口 CancelCallback cancelCallback = (consumerTag) -> { System.out.println("C1消息消费被中断"); }; //消费消息 channel.basicConsume(queue,true,deliverCallback,cancelCallback); } }
消费者02代码(扇出):与消费者01代码相同,此处就不贴了;
用于区别消费者01与消费者02在于队列的不同,而队列与交换机绑定关系 routing Key相同则交换机类型为“扇出(fanout)”,也称为“发布、订阅模式”,
若routing Key不同,则交换机类型为“直接(direct)”,也称为“路由模式”;
8、direct(直接)交换机(路由模式)
将交换机收到的消息,通过队列与交换机的绑定关系routing Key来指定发送到队列;
生产者代码(直接)

/** * 路由模式(交换机类型:直接 direct) * 生产者 */ public class Producer { //交换机名称 public static final String EXCHANGE_NAME = "direct_logs"; public static String routingKey = "error"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.getChannel(); channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); Scanner scanner = new Scanner(System.in); while (scanner.hasNext()){ String message = scanner.next(); channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8")); System.out.println("生产者生产消息:"+ message); } } }
消费者01代码(直接)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 路由模式(交换机类型:直接 direct) * 消费者 */ public class Consumer01 { //交换机名称 public static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.getChannel(); //绑定交换机(BuiltinExchangeType.DIRECT:直接交换机) channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //声明一个队列 channel.queueDeclare("console",false,false,false,null); //绑定队列 info:表示当前queue(“console”)与交换机(“direct_logs”)绑定的关系 routing Key channel.queueBind("console",EXCHANGE_NAME,"info"); //此处一个交换机与一个队列存在多个绑定关系(routing Key) channel.queueBind("console",EXCHANGE_NAME,"warning"); System.out.println("C1等待接收消息...把接收到的消息打印在控制台上..."); //消费者消费消息回调接口 DeliverCallback deliverCallback = (consumerTag, message) ->{ System.out.println("C1接收到消息是:" + new String(message.getBody(),"UTF-8")); }; //消费者取消消费消息回调接口 CancelCallback cancelCallback = (consumerTag) -> { System.out.println("C1消息消费被中断"); }; //消费消息 channel.basicConsume("console",true,deliverCallback,cancelCallback); } }
消费者02代码(直接)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 路由模式(交换机类型:直接 direct) * 消费者 */ public class Consumer02 { //交换机名称 public static final String EXCHANGE_NAME = "direct_logs"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.getChannel(); //绑定交换机(BuiltinExchangeType.DIRECT:直接交换机) channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //声明一个队列 channel.queueDeclare("disk",false,false,false,null); //绑定队列 error:表示当前queue(“disk”)与交换机(“direct_logs”)绑定的关系 routing Key channel.queueBind("disk",EXCHANGE_NAME,"error"); System.out.println("C2等待接收消息...把接收到的消息打印在控制台上..."); //消费者消费消息回调接口 DeliverCallback deliverCallback = (consumerTag, message) ->{ System.out.println("C2接收到消息是:" + new String(message.getBody(),"UTF-8")); }; //消费者取消消费消息回调接口 CancelCallback cancelCallback = (consumerTag) -> { System.out.println("C2消息消费被中断"); }; //消费消息 channel.basicConsume("disk",true,deliverCallback,cancelCallback); } }
9、Topic(主题)交换机(主题模式)
生产者代码(主题)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.Channel; import java.util.HashMap; import java.util.Map; /** * 主题模式(交换机类型:主题 direct) * 生产者 */ public class Producer { //交换机名称 public static final String EXCHANGE_NAME = "topic_logs"; public static void main(String[] args) throws Exception{ //创建一个集合,便于发送消息 Map<String,String> map = new HashMap(); map.put("quick.orange.rabbit","被队列Q1、Q2接收到"); map.put("lazy.orange.elephant","被队列Q1、Q2接收到"); map.put("quick.orange.fox","被队列Q1接收到"); map.put("lazy.brown.fox","被队列Q2接收到"); map.put("lazy.pink.rabbit","虽然满足Q2两个绑定关系,但只会被Q2接收一次"); map.put("quick.brown.fox","不匹配任何绑定关系,会被丢弃"); map.put("quick.orange.male.rabbit","不匹配任何绑定关系,会被丢弃"); map.put("lazy.orange.male.rabbit","被队列Q2接收到"); Channel channel = RabbitMQUtil.getChannel(); //声明交换机 //channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //遍历集合发送消息 for (Map.Entry<String,String> entry :map.entrySet()) { channel.basicPublish(EXCHANGE_NAME,entry.getKey(),null,entry.getValue().getBytes("UTF-8")); System.out.println("生产者发送消息:" + entry.getValue()); } } }
消费者01代码(主题)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 主题模式(交换机类型:主题 direct) * 消费者 */ public class Consumer01 { //交换机名称 public static final String EXCHANGE_NAME = "topic_logs"; //队列名称 public static final String QUEUE_NAME = "Q1"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.getChannel(); //声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC); //声明队列 channel.queueDeclare(QUEUE_NAME,false,false,false,null); //队列绑定交换机 channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.orange.*"); System.out.println("C1等待接收消息..."); //消费者消费消息回调接口 DeliverCallback deliverCallback = (consumerTag, message) ->{ System.out.println("C1接收到消息是:" + new String(message.getBody(),"UTF-8") + " 接收队列:" + QUEUE_NAME + " 绑定的 routing Key:" + message.getEnvelope().getRoutingKey() ); }; //消费者取消消费消息回调接口 CancelCallback cancelCallback = (consumerTag) -> { System.out.println("C1消息消费被中断"); }; //消费消息 channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback); } }
消费者02代码(主题)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 主题模式(交换机类型:主题 direct) * 消费者 */ public class Consumer02 { //交换机名称 public static final String EXCHANGE_NAME = "topic_logs"; //队列名称 public static final String QUEUE_NAME = "Q2"; public static void main(String[] args) throws Exception{ Channel channel = RabbitMQUtil.getChannel(); //声明交换机 channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.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("C2等待接收消息..."); //消费者消费消息回调接口 DeliverCallback deliverCallback = (consumerTag, message) ->{ System.out.println("C2接收到消息是:" + new String(message.getBody(),"UTF-8") + " 接收队列:" + QUEUE_NAME + " 绑定的 routing Key:" + message.getEnvelope().getRoutingKey() ); }; //消费者取消消费消息回调接口 CancelCallback cancelCallback = (consumerTag) -> { System.out.println("C2消息消费被中断"); }; //消费消息 channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback); } }
10、死信队列
死信的概念:
无法被消费的消息;有死信必然有死信队列;
当信息无法被消费,会成为死信,存储到死信队列中;
死信的来源
消息被拒绝;
消息TTL过期;
队列达到最大长度;
消费者01(死信队列案例)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.BuiltinExchangeType; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; import java.util.HashMap; import java.util.Map; /** * 死信队列 * 消费者 */ public class Consumer01 { //普通交换机名称 public static final String NORMAL_EXCHANGE = "normal_exchange"; //死信交换机名称 public static final String DEAD_EXCHANGE = "dead_exchange"; //普通队列的名称 public static final String NORMAL_QUEUE = "normal_queue"; //死信队列的名称 public static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtil.getChannel(); //声明普通和死信交换机 类型为direct channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT); Map<String, Object> arguments = new HashMap<>(); //过期时间 设置 10s == 10000ms //arguments.put("x-message-ttl",10000); //正常队列设置死信交换机 arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE); //设置死信routing Key arguments.put("x-dead-letter-routing-key","lisi"); //设置普通队列的长度限制 超过的部分会存入死信队列中,被C2接收 arguments.put("x-max-length",6); /** * 声明普通队列 * NORMAL_EXCHANGE:普通交换机 * arguments:其它参数 */ channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments); //绑定普通队列和普通交换机 routing Key为 “zhangsan” channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan"); //声明死信队列 channel.queueDeclare(DEAD_QUEUE,false,false,false,null); //绑定死信队列和死信交换机 routing Key为 “lisi” channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi"); System.out.println("C1等待接收消息..."); //消费者消费消息回调接口 DeliverCallback deliverCallback = (consumerTag, message) ->{ String msg = new String(message.getBody(),"UTF-8"); if (msg.equals("5")){ //拒绝改消息,当前被拒绝的消息会直接成为死信,被C2接收 channel.basicReject(message.getEnvelope().getDeliveryTag(),false); }else { System.out.println("C1接收到消息是:" + msg); } }; //消费者取消消费消息回调接口 CancelCallback cancelCallback = (consumerTag) -> { System.out.println("C1消息消费被中断"); }; //开启手动应答,若是自动应答,则拒绝接收消息无效 channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,cancelCallback); } }
消费者02(死信队列案例)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.CancelCallback; import com.rabbitmq.client.Channel; import com.rabbitmq.client.DeliverCallback; /** * 死信队列 * 消费者 */ public class Consumer02 { //死信队列的名称 public static final String DEAD_QUEUE = "dead_queue"; public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtil.getChannel(); System.out.println("C2等待接收消息..."); //消费者消费消息回调接口 DeliverCallback deliverCallback = (consumerTag, message) ->{ System.out.println("C2接收到消息是:" + new String(message.getBody(),"UTF-8")); }; //消费者取消消费消息回调接口 CancelCallback cancelCallback = (consumerTag) -> { System.out.println("C2消息消费被中断"); }; channel.basicConsume(DEAD_QUEUE,true,deliverCallback,cancelCallback); } }
生产者(死信队列案例)

import com.ithailin.framework.message.utils.RabbitMQUtil; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; /** * 死信队列 * 生产者 */ public class Producer { //普通交换机名称 public static final String NORMAL_EXCHANGE = "normal_exchange"; public static void main(String[] args) throws Exception { Channel channel = RabbitMQUtil.getChannel(); /** * 设置过期时间(TTL) 单位是 ms * 当前消息若在指定时间内未被消费,会加入死信,被C2接收 */ AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build(); for (int i = 1; i <= 10; i++) { String message = i + ""; /** * basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException * exchange:交换机 * routingKey * props:其它参数 * body:消息体 */ channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes()); } } }
测试:
1、当启动消费者01、消费者02时,两个消费者将随时准备消费消息,此时启动生产者(生产10条信息)时,C1将打印1,2,3,4,6,7,8,9,10,消息 5 将被C2打印,表明消息5被拒绝,进入死信队列,被C2消费;
验证:消息被拒绝会进入死信队列
//拒绝改消息,当前被拒绝的消息会直接成为死信,被C2接收 channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
2、当只启动消费者02时,未启动消费者01,只有进入死信队列的消息会被C2打印,此时打开生产者(生产10条信息)时,C2会瞬间打印四条消息,表明队列达到最大长度,超过的消息会进入死信队列,被C2消费;
验证:队列达到最大长度,超过的部分会直接进入死信队列
//设置普通队列的长度限制 超过的部分会存入死信队列中,被C2接收 arguments.put("x-max-length",6);
3、如上所示,当前生产的10条消息,有四条因为超出队列长度直接进入死信队列,被C2直接消费,有六条信息会暂存在普通队列中,等待C1消费,超过10秒后会进入死信队列,若在10秒前,将消费者01启动,则会被C1消费;
验证:消息TTL过期,会进入死信队列
/** * 设置过期时间(TTL) 单位是 ms * 当前消息若在指定时间内未被消费,会加入死信,被C2接收 */ AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
11、延迟队列(即上述三种之一 (TTL):当消息在指定时间内未被消费,会被存入死信队列中)(下方是基于死信队列来实现延迟消息)
用来存放需要在指定时间内被处理的元素的队列;
当前延迟队列 QA、QB都有通病,就是延迟时间写死了,将进行延迟队列优化,新建QC
整合springboot
application-dev.yml
# 配置 Rabbit MQ
rabbitmq:
host: 127.0.0.1
# 当前 5672端口为 Rabbit MQ内部访问端口,15672是web浏览器访问Rabbit MQ的端口
port: 5672
username: guest
password: guest
virtual-host: /
# 开启交换机确认消息
publisher-confirm-type: correlated
# 开启交换机退回消息
publisher-returns: true
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>2.5.6</version> </dependency>
声明交换机、队列相关配置代码

import org.springframework.amqp.core.*; 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 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"; //声明X普通交换机 @Bean("xExchange") public DirectExchange xExchange(){ //创建一个直接交换机 X return new DirectExchange(X_EXCHANGE); } //声明Y死信交换机 @Bean("yDeadLetterExchange") public DirectExchange yDeadLetterExchange(){ return new DirectExchange(Y_DEAD_LETTER_EXCHANGE); } //声明普通队列 TTL为 10S @Bean("queueA") public Queue queueA(){ //其它参数 Map<String,Object> arguments = new HashMap<>(); //设置死信交换机 arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE); //设置死信routingKey arguments.put("x-dead-letter-routing-key","YD"); //设置 TTL 单位是 ms arguments.put("x-message-ttl",10000); /** * QueueBuilder:队列构建工具 * durable:持久化(名称) * withArguments:多个参数(Map map) * withArgument:单个参数(String key,Object value) */ return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build(); } //声明普通队列 TTL为 40S @Bean("queueB") public Queue queueB(){ Map<String,Object> arguments = new HashMap<>(); //设置死信交换机 arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE); //设置死信routingKey arguments.put("x-dead-letter-routing-key","YD"); //设置 TTL 单位是 ms arguments.put("x-message-ttl",40000); return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build(); } //声明死信队列D @Bean("queueDeadLetterQueueD") public Queue queueDeadLetterQueueD(){ return QueueBuilder.durable(DEAD_LETTER_QUEUE_D).build(); } //绑定:将普通队列A(queueA)绑定普通直接交换机(xExchange),并设置routingKey:XA //@Qualifier:将名称为queueA的Queue注入进来,用queueA接收,也就是上方注入的queueA @Bean public Binding queueABindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange){ /** * 将普通队列A(queueA)绑定普通直接交换机X(xExchange),并设置routingKey:XA */ return BindingBuilder.bind(queueA).to(xExchange).with("XA"); } //绑定 @Bean public Binding queueBBindingX(@Qualifier("queueB") Queue queueB, @Qualifier("xExchange") DirectExchange xExchange){ /** * 将普通队列B(queueB)绑定普通直接交换机X(xExchange),并设置routingKey:XB */ return BindingBuilder.bind(queueB).to(xExchange).with("XB"); } //绑定 @Bean public Binding queueDBindingY(@Qualifier("queueDeadLetterQueueD") Queue queueDeadLetterQueueD, @Qualifier("yDeadLetterExchange") DirectExchange yDeadLetterExchange){ /** * 将死信队列D(queueDeadLetterQueueD)绑定死信直接交换机Y(yDeadLetterExchange),并设置routingKey:YD */ return BindingBuilder.bind(queueDeadLetterQueueD).to(yDeadLetterExchange).with("YD"); } //新增 自定义 TTL 时间的队列 QC //普通队列的名称 public static final String QUEUE_C = "QC"; @Bean("queueC") public Queue queueC(){ Map<String,Object> arguments = new HashMap<>(); //设置死信交换机 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(arguments).build(); } @Bean public Binding queueCBindingX(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange xExchange){ /** * 将普通队列C(queueC)绑定普通直接交换机X(xExchange),并设置routingKey:XC */ return BindingBuilder.bind(queueC).to(xExchange).with("XC"); } }
Controller

import com.rabbitmq.client.Channel; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.text.SimpleDateFormat; import java.util.Date; @Slf4j @Api(tags = "消息模块(Rabbit MQ)") @RestController @RequestMapping("/ttl") public class RabbitMQMsgController { static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); @Autowired RabbitTemplate rabbitTemplate; /** * 开始发送消息 * @PathVariable 可以将URL中占位符参数{xxx}绑定到处理器类的方法形参中@PathVariable(“xxx“) * @param message:接收的消息 */ @ApiOperation("发送消息给QA、QB") @GetMapping("/sendMsg/{message}") public void sendMsg(@PathVariable("message") String message){ log.info("当前时间:{},发送一条消息给两个TTL队列QA、QB:{}",sdf.format(new Date()),message); /** * 发送消息:convertAndSend(String exchange, String routingKey, Object object) throws AmqpException * exchange:交换机 * routingKey:交换机与队列的绑定关系:routing Key * object:消息内容 */ rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10S的队列:" + message); rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40S的队列:" + message); } @ApiOperation("发送消息给QC") @PostMapping("/sendExpirationMsg") public void sendMsg(String message,String ttlTime){ log.info("当前时间:{},发送一条时长{}毫秒TTL消息给队列QC:{}",sdf.format(new Date()),ttlTime,message); /** * 发送消息:convertAndSend(String exchange, String routingKey, Object message, MessagePostProcessor messagePostProcessor) throws AmqpException * exchange:交换机 * routingKey:交换机与队列的绑定关系:routing Key * message:消息内容 * messagePostProcessor:设置消息其它参数 */ rabbitTemplate.convertAndSend("X","XC","消息来自ttl队列QC:" + message,msg -> { //设置延迟时间 msg.getMessageProperties().setExpiration(ttlTime); return msg; }); } /** * 监听消息(接收消息) * @RabbitListener(queues = "QD") * @RabbitListener:表示当前方法是一个监听器,监听Rabbit MQ中队列QD的消息 */ @RabbitListener(queues = "QD") public void receiveD(Message message,Channel channel){ String msg = new String(message.getBody()); log.info("当前时间:{},收到的死信队列的消息:{}",sdf.format(new Date()),msg); } }
测试结果如下:
问题:
可以使用 Rabbit MQ插件的方式解决该问题:环境准备如下所示:
1、下载插件:https://www.rabbitmq.com/community-plugins.html
2、将下载好的插件放到Rabbit MQ安装目录的插件目录下,例如 C:\Program Files\RabbitMQ Server\rabbitmq_server-3.7.14\plugins
3、进入当前Rabbit MQ安装目录的 sbin目录下
4、打开cmd执行命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
5、重启 Rabbit MQ Server
rabbitmqctl stop
rabbitmq-server start
6、查看Rabbit MQ控制台的交换机
开始编写基于插件实现延迟队列应用场景
声明交换机、队列相关配置代码

//以下新增 基于插件实现延迟队列 //交换机 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.routing.key"; //声明队列 @Bean public Queue delayedQueue(){ return new Queue(DELAYED_QUEUE_NAME); } /** * 声明自定义交换机 * CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments) * name:交换机名称 * type:交换机类型 * durable:是否需要持久化 * autoDelete:是否自动删除 * arguments:其它参数 * @return */ @Bean public CustomExchange delayedExchange(){ Map<String, Object> arguments = new HashMap<>(); arguments.put("x-delayed-type", "direct"); return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments); } /** * 将当前队列绑定到交换机上 * @return */ @Bean public Binding delayedQueueBindingDelayedExchange(@Qualifier("delayedQueue") Queue queue, @Qualifier("delayedExchange") CustomExchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY).noargs(); }
Controller

@ApiOperation("基于插件的方式发送消息给delayed.queue") @PostMapping("/sendDelayMsg") public void sendMsg(String message,Integer delayTime){ log.info("当前时间:{},发送一条时长{}毫秒消息给延迟队列delayed.queue:{}",sdf.format(new Date()),delayTime,message); rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, message, msg -> { //设置延迟时间 单位 ms msg.getMessageProperties().setDelay(delayTime); return msg; }); } /** * 监听消息(接收消息) * 监听Rabbit MQ中队列 delayed.queue的消息 */ @RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME) public void receiveDelayQueue(Message message){ String msg = new String(message.getBody()); log.info("当前时间:{},收到延迟队列的消息:{}",sdf.format(new Date()),msg); }
测试:
拓展:MQ根据正常队列、死信队列来实现延迟队列的场景
12、发布确认高级篇
在生产环境中由于一些不明原因,导致 RabbitMQ重启,在 RabbitMQ重启期间生产者消息投递失败导致消息丢失,需要手动处理和恢复,于是,我们开始思考,如何才进行 RabbitMQ的消息可靠投递呢?特别是在这样比较极端的情况, RabbitMQ集群不可用的时候,无法投递的消息该如何处理呢?
新增Rabbit MQ参数配置
# 开启交换机确认消息
spring.rabbitmq.publisher-confirm-type=correlated
# 开启交换机退回消息
spring.rabbitmq.publisher-returns=true
交换机确认消息
spring.rabbitmq.publisher-confirm-type=correlated
交换机确认模式有三种方式:当前使用 spring.rabbitmq.publisher-confirm-type=correlated
NONE
禁用发布确认模式,是默认值;
CORRELATED
发布消息成功到交换器后会触发回调方法;
SIMPLE
经测试有两种效果,其一效果和 CORRELATED值一样会触发回调方法
,其二在发布消息成功后使用 rabbitTemplate调用 waitForConfirms或 waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回 false则会关闭 channel,则接下来无法发送消息到 broker
交换机退回消息
在仅开启了生产者确认机制的情况下,交換机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢奔,此时生产者是不知道消息被丟弃这个事件的。通过设置spring.rabbitmq.publisher-returns=true参数可以在当消息传递过程中不可达目的地时将消息返回给生产者;
Controller

//3、以下是发布确认高级篇 @ApiOperation("发布确认高级篇发送消息") @PostMapping("/confirm") public void sendConfirmMessage(String message){ //发送一条正确数据 CorrelationData correlationData = new CorrelationData("1"); rabbitTemplate.convertAndSend(RabbitMQConfig.CONFIRM_EXCHANGE_NAME,RabbitMQConfig.CONFIRM_ROUTING_KEY,message + " add info message",correlationData); //发送一条错误数据 更改routing Key导致数据发送失败 CorrelationData correlationData2 = new CorrelationData("2"); rabbitTemplate.convertAndSend(RabbitMQConfig.CONFIRM_EXCHANGE_NAME,RabbitMQConfig.CONFIRM_ROUTING_KEY + "2",message + " add error message",correlationData2); log.info("发送的确认高级篇的内容:{}",message); } //发布确认高级篇 监听消息(接收消息) @RabbitListener(queues = RabbitMQConfig.CONFIRM_QUEUE_NAME) public void receiveConfirmMessage(Message message){ String msg = new String(message.getBody()); log.info("当前时间:{},收到发布确认高级篇的消息:{}",sdf.format(new Date()),msg); }
回调类(在声明交换机、队列相关代码下)

/** * 发布确认高级篇 * 回调接口 * * 实现 RabbitTemplate类下的ConfirmCallback接口 * * @FunctionalInterface * public interface ConfirmCallback { * void confirm(@Nullable CorrelationData var1, boolean var2, @Nullable String var3); * } */ @Component @Slf4j class ConfirmCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback { @Autowired RabbitTemplate rabbitTemplate; /** * 注解:@PostConstruct 是Java自己的注解。 * Java中该注解的说明:@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。 * 通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序: * Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法) * * 当前实现的ConfirmCallback接口是RabbitTemplate下的内部接口,因此需要将自定义的ConfirmCallBack注入到RabbitTemplate实例中; * 若想将自定义ConfirmCallBack注入到RabbitTemplate.ConfirmCallback中,则需要一个 RabbitTemplate实例; * 当前自定义方法init()使用了注解:@PostConstruct,会在 @Autowired执行完之后再执行; */ @PostConstruct public void init(){ //将当前自定义的ConfirmCallBack注入到RabbitTemplate.ConfirmCallback中 rabbitTemplate.setConfirmCallback(this); //将当前自定义的ConfirmCallBack注入到RabbitTemplate.ReturnsCallback中 rabbitTemplate.setReturnsCallback(this); } /** * 交换机确认回调方法(implements RabbitTemplate.ConfirmCallback即实现confirm) * @param correlationData 保存回调消息的 id以及相关信息 * @param b 是否发送成功(true/false) * @param s 若失败,则代表失败的原因 */ @Override public void confirm(CorrelationData correlationData, boolean b, String s) { String id = correlationData != null ? correlationData.getId() : ""; if (b){ log.info("交换机已经收到了id为{}的消息",id); }else { log.info("交换机还未收到id为{}的消息,原因是{}",id,s); } } /** * 可以在当消息传递过程中不可达目的地时将消息返回给生产者(implements RabbitTemplate.ReturnsCallback即实现 returnedMessage) * @param returnedMessage */ @Override public void returnedMessage(ReturnedMessage returnedMessage) { log.error("消息被退回,消息内容为:{}",returnedMessage); } }
测试结果
13、备份交换机
概念:
在 RabbitMQ中,有一种备份交换机的机制存在,当我们为某一个交换机声明个对应的备份交换机时,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交換机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交換机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警;
声明交换机、队列相关代码

//4、以下是新增的备份交换机相关内容 //备份交换机 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 public FanoutExchange backupExchange(){ return new FanoutExchange(BACKUP_EXCHANGE_NAME); } //声明备份队列 @Bean public Queue backupQueue(){ return new Queue(BACKUP_QUEUE_NAME); } //声明报警队列 @Bean public Queue warningQueue(){ return new Queue(WARNING_QUEUE_NAME); } //备份队列绑定备份交换机上 @Bean public Binding backupBindingBackupExchange(@Qualifier("backupQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange exchange){ return BindingBuilder.bind(queue).to(exchange); } //报警队列绑定备份交换机上 @Bean public Binding warningBindingBackupExchange(@Qualifier("warningQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange exchange){ return BindingBuilder.bind(queue).to(exchange); }
更改发布、确认模式高级篇中,交换机代码
//声明交换机 @Bean public DirectExchange confirmExchange(){ //return new DirectExchange(CONFIRM_EXCHANGE_NAME); return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true) //当前交换机指向备份交换机 .withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME) .build(); }
Controller新增代码
//4、以下是备份交换机相关代码 //监听报警队列 @RabbitListener(queues = RabbitMQConfig.WARNING_QUEUE_NAME) public void receiveWarningMessage(Message message){ String msg = new String(message.getBody()); log.info("当前时间:{},收到报警队列的消息:{}",sdf.format(new Date()),msg); }
注意:更改了交换机信息,需要将Rabbit MQ中当前以存在的交换机删除,在重新创建,否则会报错!
测试结果
结论:
当前同时配置了交换机的消息回退、备份交换机,在备份交换机生效时,消息回退returnedMessage 未接收到回退消息,而是被备份交换机接收到,并发送给了报警队列;表示备份交换机优先级要高于消息回退returnedMessage
14、幂等性
概念:
用户对于同一操作发起的一次或者多次请求的结果是一致的,不会因为多次点击而产生副作用;
解决思路:
Rabbit MQ消费者的幂等性的解决方式一般写一个唯一标识,例如使用全局ID,利用该唯一标识进行判断来避免重复消费;
15、优先级队列
设置队列最大优先级
注意事项:
要让队列实现优先级需要以下两点:
1、队列需要设置为优先级队列;
2、消息需要设置优先级;
3、消费者需要等待需要消费的消息已经全部发送到队列中,才去消费;
新增优先级队列与交换机相关配置

//5、以下是优先级队列相关内容 //优先级队列名称 public static final String PRIORITY_QUEUE_NAME = "priority_queue"; //交换机 public static final String PRIORITY_EXCHANGE_NAME = "priority_exchange"; //routing key public static final String PRIORITY_ROUTING_KEY = "priority_key"; //声明交换机 @Bean public DirectExchange priorityExchange(){ return new DirectExchange(PRIORITY_EXCHANGE_NAME); } //声明队列,设置最大优先级10,也可通过 withArgument/withArguments去设置 @Bean public Queue priorityQueue(){ return QueueBuilder.durable(PRIORITY_QUEUE_NAME).maxPriority(10).build(); } //绑定 @Bean public Binding priorityQueueBindingPriorityExchange(@Qualifier("priorityQueue") Queue queue, @Qualifier("priorityExchange") DirectExchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(PRIORITY_ROUTING_KEY); }
新增优先级队列生产者与消费者代码

//5、以下是优先级队列相关代码 @ApiOperation("发送多条消息给优先级队列") @PostMapping("/priority") public void sendPriorityMessage(String message){ for (int i = 1; i <= 10; i++) { String msg = message + " " + i; if (i == 2){ MessagePostProcessor messagePostProcessor = mess ->{ MessageProperties messageProperties = mess.getMessageProperties(); //设置消息优先级为 2,优先级越高,越先消费 messageProperties.setPriority(2); return mess; }; rabbitTemplate.convertAndSend(RabbitMQConfig.PRIORITY_EXCHANGE_NAME,RabbitMQConfig.PRIORITY_ROUTING_KEY,msg,messagePostProcessor); }else if (i == 8){ MessagePostProcessor messagePostProcessor = mess ->{ MessageProperties messageProperties = mess.getMessageProperties(); messageProperties.setPriority(8); return mess; }; rabbitTemplate.convertAndSend(RabbitMQConfig.PRIORITY_EXCHANGE_NAME,RabbitMQConfig.PRIORITY_ROUTING_KEY,msg,messagePostProcessor); }else { rabbitTemplate.convertAndSend(RabbitMQConfig.PRIORITY_EXCHANGE_NAME,RabbitMQConfig.PRIORITY_ROUTING_KEY,msg); } } } /** * 当前监听器(消费者)会在队列收到一条消息时,直接进行监听(消费),在测试时需要将当前监听器注释,让生产者将多条消息全部发送到队列中, * 使队列中积攒多条待消费的消息,此时再启动该监听器(消费者),同时消费多条消息,就会看到消息根据优先级进行排序消费; * @param message */ //监听优先级队列 @RabbitListener(queues = RabbitMQConfig.PRIORITY_QUEUE_NAME) public void receivePriorityMessage(Message message){ String msg = new String(message.getBody()); log.info("当前时间:{},收到优先级队列的消息:{}",sdf.format(new Date()),msg); }
测试结果
当前消费者(监听器)会在队列收到一条消息时,直接消费,在测试时若直接启动生产者和消费者,看不到优先级排序消费的效果;
先注释掉消费者(监听器)代码,生产者生产多条消息到队列中,再启动消费者(监听器),同时消费多条消息,即可看到消费者对消息根据优先级进行排序消费;
16、惰性队列
正常队列:消息保存在内存上;
惰性队列:消息会尽可能的将下辖保存在磁盘上;而在消费者消费到相应的消息时才会被加载到内存中,它的设计目标是能够支持更长的队列;即支持更多的消息存储;
使用场景:
在消费者宕机等不可用时,此时正常情况下,大量消息会堆积在内存中,此时惰性队列就很有必要了;
两种模式:
default
lazy
MQ集群
https://www.bilibili.com/video/BV1cb4y1o7zz?p=83
未完、待续
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?