本笔记代码【16】8】
本笔记代码【7
说明:你好,此篇文章是我在学习尚硅谷RabbitMQ时做的笔记,如果存在问题的话欢迎指出。
视频链接
1 MQ的相关概念
1.1.1 什么是MQ
MQ(message queue),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务。
1.1.2 为什么需要使用MQ
1、流量消峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验更好。
2、应用解耦
以电商应用为例,应用中有订单系统,库存系统,物流系统,支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统处理故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。
3、异步处理
有些服务间调用是异步的, 例如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服务还能及时的得到异步处理成功的消息。
1.2.1 RabbitMQ的概念
RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
1.2.2 RabbitMQ的四大核心
生产者
产生数据发送消息的程序是生产者
交互机
交互机是RabbiMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交互机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,或者是把消息丢弃,这个得有交换机类型决定。
队列
队列是RabbitMQ内部使用的一种数据结构,尽管消息流进RabbitMQ和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接受数据。这就是我们使用队列的方式。
消费者
消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。
1.2.3 RabbitMQ核心
1、简单模式 2、工作模式 3、发布/订阅模式 4、路由模式 5、主题模式 6、发布确认模式
RabbitMQ的工作原理
Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
Exchange:交换机
Queue:队列
Producer:生产者
Consumer:消费者
Connection:连接,publisher/consumer和broker之间的TCP连接
Channel:通道,如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时间建立TCP Connection的开销巨大的,效率也较低。Channel是在Connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id帮助客户端和message broker识别channel,所以channel之间是完全隔离的。channel作为轻量级的
Connection极大减少了操作系统建立TCO connection 的开销。
Binding:exchange和queue之间的虚拟连接,binding中可以包含 routing key,Binding信息被保存到exchange中的查询表中,用于message的分发依据。
一个交换机(Exchange)对应多个队列(Queue),一个消息实体(Broker)中也可以有多个交换机
1.2.4 安装RabbitMQ
Docker安装教程
在浏览器中访问
http://ip地址:15672/
2 HelloWorld
首先新建一个空模块
创建一个Maven模块
Maven配置
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>rabbitmq</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties><dependencies><!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client --><dependency><groupId>com.rabbitmq</groupId><artifactId>amqp-client</artifactId><version>5.15.0</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.11.0</version></dependency><dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>RELEASE</version><scope>test</scope></dependency></dependencies></project>
生产者
先创建一个“生产者”
packagecom.zhoujing.rabbltmq.one;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.Connection;importcom.rabbitmq.client.ConnectionFactory;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/6/30-21:00-星期四
*
* 生产者发送消息
*/publicclassProducer{/**
* 队列名称
*/publicstaticfinalString QUEUE_NAME ="hello";/**
* 发送消息
* @param args
*/publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{// 创建一个连接工厂ConnectionFactory factory =newConnectionFactory();// 工厂IP连接RabbitMQ的队列
factory.setHost("服务器IP地址");// 15672是web访问的端口
factory.setPort(5672);// 设置超时时间
factory.setHandshakeTimeout(60000);// 用户名
factory.setUsername("admin");// 设置密码
factory.setPassword("admin");// 建立连接Connection connection = factory.newConnection();// 获取信道Channel channel = connection.createChannel();/*
* 生成一个队列
* 1、队列名称
* 2、队列里面的消息是否持久化(磁盘),默认情况消息存储到内存中,true存储到磁盘中,false存储到内存当中
* 3、该队列是否只供一个消费者进行消费。是否进行消息共享,true只供一个,false多个消费者
* 4、是否自动删除,最后一个消费者断开连接后,该队一句是否自动删除,true自动删除,false不自动删除
* 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("消息发送完毕");}}
使用服务器的同学记得在阿里云安全组中打开相应的端口
访问Web端
http://IP地址:15672
消费者
消费者用于接收消息
packagecom.zhoujing.rabbltmq.one;importcom.rabbitmq.client.*;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/1-16:23-星期五
*
* 消费者接收消息
*/publicclassConsumer{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{ConnectionFactory factory =newConnectionFactory();
factory.setHost("IP地址");
factory.setPort(5672);
factory.setHandshakeTimeout(60000);
factory.setUsername("admin");
factory.setPassword("admin");Connection connection = factory.newConnection();Channel channel = connection.createChannel();// 声明 接收消息DeliverCallback deliverCallback =(consumerTag,message)->{// 消息分为很多,如:消息头,消息体,……,我们只需要获取消息体System.out.println(newString(message.getBody()));};// 取消消息时的回调CancelCallback cancelCallback = consumerTag->{System.out.println("消息消费被中断");};/*
* 消费者消费消息
* 1、消费哪个队列
* 2、消费成功之后是否自动应答,true代表的自动应答,false代表手动应答
* 3、当消息传达后回调
* 4、消费者取消消费的回调
* */
channel.basicConsume(Producer.QUEUE_NAME,true,deliverCallback,cancelCallback);}}
将生产者和消费者都运行,再将生产者重新运行几次
生产者
消费者
每当生产者重新运行,消费者都会接收到生产者发送的消息
3 Work Queues
3.1 概述
工作队列(又称为任务队列)的主要思想是避免立即执行资源密集型任务,而不是不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
3.1.1 轮询分发消息
一个生产者发送消息由多个消费者(工作线程)去接收,而多个工作线程是轮询关系而且一个消息只能被处理一次不能处理多次,而每个工作线程接收消息的情况是轮询的。
我们接下来创建两个消费者C1和C2来模拟轮询效果。
3.1.2 创建工具类
将创建连接的代码封装成工具类
packagecom.zhoujing.rabbltmq.utils;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.Connection;importcom.rabbitmq.client.ConnectionFactory;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/1-22:17-星期五
*
* RabbitMQ工具类
*/publicclassRabbitMqUtils{/**
* 获取信道
* @return 信道
* @throws IOException
* @throws TimeoutException
*/publicstaticChannelgetChannel()throwsIOException,TimeoutException{ConnectionFactory factory =newConnectionFactory();
factory.setHost("IP地址");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setHandshakeTimeout(60000);Connection connection = factory.newConnection();Channel channel = connection.createChannel();return channel;}}
3.1.3 创建两个工作线程
这次我们先创建工作线程
packagecom.zhoujing.rabbltmq.two;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/1-22:40-星期五
*
* 工作线程
*/publicclassWorker01{privatestaticfinalString QUEUE_NAME ="hello";publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println(consumerTag+"接收到的消息:"+newString(message.getBody()));};CancelCallback cancelCallback = consumerTag->{System.out.println(consumerTag+":取消回调");};System.out.println("C1正在等待……");Channel channel =RabbitMqUtils.getChannel();
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);}}
第一个工作线程完成,先将其启动起来,接下创建第二个工作线程,使用IDEA添加新的进程,免去了复制代码的操作
将输出语句C1修改成C2
再次点击运行,出现两个打印窗口,一个是C1,一个是C2
3.1.4 创建消费者
创建生产者
packagecom.zhoujing.rabbltmq.two;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.Scanner;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/3-13:37-星期日
* <p>
* 生产者,发送大量的消息
*/publicclassTask01{publicstaticfinalString QUEUE_NAME ="hello";/**
* 发送大量的信息
*
* @param args
*/publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 因为消费者有多个所以第三个参数必须为false
channel.queueDeclare(QUEUE_NAME,false,false,false,null);Scanner input =newScanner(System.in);while(input.hasNext()){String message = input.next();
channel.basicPublish("", QUEUE_NAME,null, message.getBytes());System.out.println("消息发送完成:"+message);}}}
3.1.5 运行
将生产者运行,连续输入值
按照轮询效果,结果应该是这样的
打开控制台查看,和预期一样
C1:
C2:
3.2 消息应答
3.2.1 概念
消费者完成一个任务可能需要一段时间,如其中一个消费者处理一个长的任务仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ一旦消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费者的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,RabbitMQ引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉RabbitMQ它已经处理了,RabbitMQ可以把消息删除了。
3.2.3 自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者Chanel关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
3.2.4 手动应答
1、Channel.basicAck:用于肯定确认
RabbitMQ已知道该消息并且成功的处理消息,可以将其丢弃了
2、Channel.basicNack:用于否定确认
3、Channel.basicReject:用于否定确认
与Channel.basicNack相比少一个参数,少了一个Multiple(批量)处理的参数
不处理该消息了直接拒绝,可以将其丢弃了
3.2.5 Multiple的解释
手动应答的好处是可以批量应答并且减少网络拥堵
multiple的true和false代表不同意思
true:代表批量应答channel上未应答的消息
比如说channel上有传送tag的消息5,6,7,8当前tag是8那么此时5~8的这些还未应答的消息都会被确认收到消息应答
false:只会应答tag=8的消息5,6,7这三个消息依然不会被确认收到消息应答
工作中建议使用“false”,因为其他的队列不知道是否处理成功。
3.2.5 消息自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
接下来通过下面这张图帮助理解↓↓↓
第一张图,生产者(P)生成多个消息给两个消费者(C1,C2)接收,C1接收的是消息1,C2是消息2
第二张图:C1在接收消息1后断开了连接,并没有消息应答
第三张图:消息1重新入队,消息2已被C2处理完
第四张图:因为C1断开了连接,消息1被分配到了另一个消费者(C2)
3.2.6 代码实现
生产者代码,还是和之前差不多
packagecom.zhoujing.rabbltmq.three;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.Scanner;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/9-15:31-星期六
* <p>
* 消息在手动应答时是不丢失的,放回队列中重新消费
*/publicclassTask02{/**
* 队列名称
*/publicstaticfinalString TASK_QUEUE_NAME ="ack_queue";/**
* 生产者发送消息
* @param args
* @throws IOException
* @throws TimeoutException
*/publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 声明队列//queueDeclare(队列名称,是否持久化,是否共享,是否删除,其他参数);
channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);// 获取控制台消息Scanner input =newScanner(System.in);// 判断是否有下一个消息while(input.hasNext()){String message = input.next();//basicPublish(交互机名称空字符串为默认,队里名称,其他参数,消息必须为二进制)
channel.basicPublish("", TASK_QUEUE_NAME,null, message.getBytes("UTF-8"));System.out.println("发送的消息为:"+ message);}}}
消费者
两个消费者唯一的区别就是C1线程睡眠1秒,C2睡眠15秒,之所以使用线程睡眠是为了模拟当消费者C2在线程睡眠的15秒内挂掉后,消费者C2的消息重新回到队列当中由消费者C1去处理。
消费者C1
packagecom.zhoujing.rabbltmq.three;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/9-17:56-星期六
* <p>
* 消息在手动应答时不丢失,放回队列中重新消费
*/publicclassWork02{/**
* 队列名称
*/publicstaticfinalString TASK_QUEUE_NAME ="ack_queue";/**
* 消费者接收消息
*
* @param args
* @throws IOException
* @throws TimeoutException
*/publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();System.out.println("C1等待接收消息处理时间较短");// 传递回调DeliverCallback deliverCallback =(tag,message)->{// 让线程睡眠1秒try{Thread.sleep(1*1000);}catch(InterruptedException e){
e.printStackTrace();}System.out.println("接收到的消息为:"+newString(message.getBody(),"UTF-8"));// 手动应答// basicAck(消息标识,是否批量应答)// 消息标识:用于每个消息,是否批量应答:批量应答会导致消息丢失
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);};// 取消回调CancelCallback cancelCallback =(tag)->{System.out.println(tag +"消费者取消回调");};boolean autoAck =false;// basicConsume(队列名称,是否自动应答,回调,取消回调)
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);}}
消费者C2
packagecom.zhoujing.rabbltmq.three;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/9-17:56-星期六
* <p>
* 消息在手动应答时不丢失,放回队列中重新消费
*/publicclassWork03{/**
* 队列名称
*/publicstaticfinalString TASK_QUEUE_NAME ="ack_queue";/**
* 消费者接收消息
*
* @param args
* @throws IOException
* @throws TimeoutException
*/publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();System.out.println("C2等待接收消息处理时间较长");// 传递回调DeliverCallback deliverCallback =(tag,message)->{// 让线程睡眠15秒try{Thread.sleep(15*1000);}catch(InterruptedException e){
e.printStackTrace();}System.out.println("接收到的消息为:"+newString(message.getBody(),"UTF-8"));// 手动应答// basicAck(消息标识,是否批量应答)// 消息标识:用于每个消息,是否批量应答:批量应答会导致消息丢失
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);};// 取消回调CancelCallback cancelCallback =(tag)->{System.out.println(tag +"消费者取消回调");};boolean autoAck =false;// basicConsume(队列名称,是否自动应答,回调,取消回调)
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);}}
如果这时候先启动两个消费者会出现错误,原因是RabbitMQ的Queue中还没有“ack_queue”这个队列,所以需要先将生产者启动一次。
向控制台输入数据,C1一秒后接收到消息,C2一十五秒后接收到消息
可以将上面的例子理解为,C1处理的消息比较快,而C2较慢
接下来模拟一下,当C2接收到消息后中途失去连接将消息放回到队列中由C1处理
我们向C2发送了一个FF的消息,C2会在15秒后接收到此消息
在这15秒未到的时间将C2停止,消息重新回到了队列中并由另一个消费者(C1)去处理了
3.3 RabbitMQ持久化
3.3.1 概念
刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当RabbitMQ服务停止以后消息生产者发送过来的消息不丢失。默认情况下RabbitMQ退出或由于某种原因崩溃时,它忽然队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
3.3.2 队列持久化
RabbitMQ Web端 **Features **不是D说明不是持久化的(D是durable(持久化)的缩写)
在生产者声明队列时,修改为支持持久化
修改完后执行出现异常
Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'ack_queue' in vhost '/': received 'true' but current is 'false', class-id=50, method-id=10)
at com.rabbitmq.utility.ValueOrException.getValue(ValueOrException.java:66)
at com.rabbitmq.utility.BlockingValueOrException.uninterruptibleGetValue(BlockingValueOrException.java:36)
at com.rabbitmq.client.impl.AMQChannel$BlockingRpcContinuation.getReply(AMQChannel.java:502)
at com.rabbitmq.client.impl.AMQChannel.privateRpc(AMQChannel.java:293)
at com.rabbitmq.client.impl.AMQChannel.exnWrappingRpc(AMQChannel.java:141)
... 3 more
原因:如果修改了Queue(队列)的持久化方式需将原来的Queue(队列)删除
打开RabbitMQ Web端,找到ack_queue点击进入详细页面,点击Delete(删除)
再次运行,访问Web端
3.3.3 消息持久化
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN添加这个属性。
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略,参考第四章的“** 发布确认 **”章节。
3.3.4 不公平分发
在最开始的时候我们学习到RabbitMQ分发消息采用的轮询分发,但是在某种场景下这种策略并不是很好,比如说有两个消费者在处理任务,其中有个消费者1处理任务的速度非常快,而另为一个消费者2处理速度非常慢,这个时间我们还是采用轮询分发的话就会使得处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ并不知道这种情况它依然很公平的进行分发。
在消费者中添加以下代码↓↓↓
// 在消息接收之前设置
channel.basicQos(1);
basicQos默认为0,1表示不公平分发
生产几个消息,因为C1睡眠1秒所以处理速度快,而C2较慢,当C2的消息还没被处理完(应答)时C2是接收不到消息的
意思就是如果这个任务我还没有处理完或者我没有应答你,你先别分配给我,我目前只能处理一个任务,然后RabbitMQ就会把任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完成手上的任务,队列还不停的添加新的任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的worker或者改变其他存储任务的策略。
3.3.5 预取值
假如我们有七个消息我们想让C1分配两个,C2分配5个,那么可以使用预取值实现。
预取值非常类似于权重,将C1的睡眠时间从1秒调整到5秒(为了保证C1只处理2条,我们要在5秒内能发送7条数据,这样保证后面的消息全部发送给C2,避免快的C1处理完了消息,又将发送后续消息),再将basicQos设置为2,将C2的basicQos设置为5(basicQos为0表示使用公平分发,1表示不公平分发,其它数字表示预取值)
启动生产者在五秒内输入7条数据
C1接收到2条数据
因为C2睡眠时间较长所以暂时只接受到了1条消息,可以通过Web端查看还有多少个消息未完成
因为Web端显示存在一定的延迟,所以这里真实Unacked的值为4
最终C1接收到2条消息,C2接收到了5条消息,和预期的结果一样。
4 发布确认
当消息或者队列在保存(持久化)到磁盘的过程中出现了宕机导致持久化失败,所以必须使用发布确认,当消息或者队列保存到磁盘后RabbitMQ要和生产者说一声已经保存完毕了。
4.1 原理
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所以在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就是使得生产者知道消息已经正确到达目的地的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序 同样可以在回调方法中处理nack消息。
4.2 发布默认的策略
4.2.1 开启发布确认的方法
发布确认默认是没有开启的,如果要开启在生产者中需要调用方法confirmSelect,每当你要想使用发布确认,都需要在channel上调用该方法
4.2.2 单个发布确认
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是每发布一条消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续的消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
我们验证下单个发布所需要消耗的时间,我们进行批量1000个操作
packagecom.zhoujing.rabbltmq.publish;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.UUID;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/10-21:13-星期日
* <p>
* 发布确认
*/publicclassConfirmMessage{/**
* 队列名称
*/publicstaticfinalString QUEUE_NAME = UUID.randomUUID().toString();/**
* 批量发送消息的数量
*/publicstaticfinalInteger MESSAGE_COUNT =1000;publicstaticvoidmain(String[] args)throwsIOException,InterruptedException,TimeoutException{// 单个确认singleConfirm();}/**
* 单个确认
*
* @throws IOException
* @throws TimeoutException
* @throws InterruptedException
*/publicstaticvoidsingleConfirm()throwsIOException,TimeoutException,InterruptedException{Channel channel =RabbitMqUtils.getChannel();// 开启发布确认
channel.confirmSelect();// 队列声明
channel.queueDeclare(QUEUE_NAME,true,false,false,null);// 记录开始时间long startTime =System.currentTimeMillis();for(Integer i =0; i < MESSAGE_COUNT; i++){
channel.basicPublish("", QUEUE_NAME,null, i.toString().getBytes());// 消息发送完立即消息确认boolean flag = channel.waitForConfirms();if(!flag){System.out.println("消息发送失败");}}// 结束时间long endTime =System.currentTimeMillis();System.out.println("耗时秒为:"+(endTime - startTime)/1000);}}
在运行的过程中可以到Web端查看状态
State:running(运行)/ idle(空闲)
total:消息数量
最终结果为
4.2.3 批量确认发布
上面那种方式非常慢,与单个等到确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发送故障导致发布出现问题时,不知道是哪个消息出现问题,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
/**
* 批量确认
* 每循环100次进行发布确认
* @throws IOException
* @throws TimeoutException
* @throws InterruptedException
*/publicstaticvoidbatchConfirm()throwsIOException,TimeoutException,InterruptedException{Channel channel =RabbitMqUtils.getChannel();// 开启发布确认
channel.confirmSelect();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);// 记录开始时间long startTime =System.currentTimeMillis();// 每100次进行消息确认int batchLength =100;for(Integer i =0; i < MESSAGE_COUNT; i++){
channel.basicPublish("", QUEUE_NAME,null, i.toString().getBytes());if(i%batchLength ==0){boolean flag = channel.waitForConfirms();}}// 结束时间long endTime =System.currentTimeMillis();System.out.println("耗时秒为:"+(endTime - startTime)/1000);}
publicstaticvoidmain(String[] args)throwsIOException,InterruptedException,TimeoutException{// 单个确认 耗时秒为:98// singleConfirm();// 批量确认 耗时为:1batchConfirm();}
批量确认所消耗的时间为:
4.2.4 异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,它是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。
/**
* 异步确认
* @throws IOException
* @throws TimeoutException
*/publicstaticvoidpublishMessageAsync()throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 开启发布确认
channel.confirmSelect();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);// 记录开始时间long startTime =System.currentTimeMillis();// 监听成功的回调ConfirmCallback ackCallback =(tag,multiple)->{System.out.println("确认的消息:"+tag);};// 监听失败的回调ConfirmCallback nackCallback =(tag,multiple)->{System.out.println("未确认的消息:"+ tag);};/*
* 准备消息监听器,监听哪些消息成功了,哪些消息失败了
* 参数1:成功回调
* 参数2:失败回调
* */
channel.addConfirmListener(ackCallback,nackCallback);// 批量发送消息for(Integer i =0; i < MESSAGE_COUNT; i++){
channel.basicPublish("",QUEUE_NAME,null,i.toString().getBytes());}// 结束时间long endTime =System.currentTimeMillis();System.err.println("耗时秒为:"+(endTime - startTime)/1000);}
单个参数:只监听成功,双个参数:成功/失败都监听
publicstaticvoidmain(String[] args)throwsIOException,InterruptedException,TimeoutException{// 单个确认 耗时秒为:98// singleConfirm();// 批量确认 耗时为:1// batchConfirm();// 异步确认 耗时为:0publishMessageAsync();}
4.2.5 处理异步未确认的消息
最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用ConcurrentLinkedQueue这个队列在confirm callbacks与发布线程之间进行消息的传递。
/**
* 异步确认
* @throws IOException
* @throws TimeoutException
*/publicstaticvoidpublishMessageAsync()throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 开启发布确认
channel.confirmSelect();
channel.queueDeclare(QUEUE_NAME,true,false,false,null);/*
* 线程安全有序的一个跳表,适用于高并发的情况下
* 1、轻松的将序号与消息进行关联
* 2、轻松批量删除条目,只要给到序号
* 3、支持高并发(多线程)
*/ConcurrentNavigableMap<Long,String> outstandingConfirms =newConcurrentSkipListMap<>();// 监听成功回调ConfirmCallback ackCallback =(tag,multiple)->{// 如果是批量发消息就将其全部删除掉if(multiple){// 2、总消息减去发送成功的消息剩下来就是发送失败的消息ConcurrentNavigableMap<Long,String> headMap = outstandingConfirms.headMap(tag,true);
headMap.clear();}else{// 删除单个
outstandingConfirms.remove(tag);}System.out.println("确认的消息:"+tag);};// 监听失败的回调ConfirmCallback nackCallback =(tag,multiple)->{System.out.println("未确认的消息:"+ tag);};/*
* 准备消息监听器,监听哪些消息成功了,哪些消息失败了
* 参数1:成功回调
* 参数2:失败回调
* */
channel.addConfirmListener(ackCallback,nackCallback);// 记录开始时间long startTime =System.currentTimeMillis();// 批量发送消息for(Integer i =0; i < MESSAGE_COUNT; i++){String message = i.toString();// 1、记录所有要发的消息,消息的总和
outstandingConfirms.put(channel.getNextPublishSeqNo()-1,message);
channel.basicPublish("",QUEUE_NAME,null,i.toString().getBytes());}// 结束时间long endTime =System.currentTimeMillis();System.err.println("耗时秒为:"+(endTime - startTime)/1000);}
4.2.6 以上3种发布确认速度对比
1、单独发布消息
同步等待确认,简单,但吞吐量非常有限。
2、批量发布消息
批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
3、异步处理:
最佳性能和资源使用,在出现错误的情况下可以很好的控制,但是实现起来稍微难些。
5 交换机
在上一章中,我们创建了一个工作队列。我们假设的是工作队列背后,每个任务都恰好交付一个消费者(工作线程)。在这一部分中,我们将做一个完全不同的事情,我们将消息传达给多个消费者。这种模式称为“发布/订阅”。
为了说明这种模式,我们将创建一个简单的日志系统。它将由两个程序组成:第一个程序将发出日志消息,第二个程序是消费者。其中我们会启动两个消费者,其中一个消费者接收到消息后把日志存储在磁盘,另外一个消费者接收到消息后把消息打印在屏幕上,事实上第一个程序发出的日志消息将广播给所有消费者。
我们想生产者发送一个消息让多个消费者接收到,按照之前的示例是实现不了,因为多个消费者是轮询的效果。
我们使用交换机将一个消息转发给多个队列,然后通过队列发给消费者
![未命名文件 (1).png](https://img-blog.csdnimg.cn/img_convert/76b95b39b567d2a34438a9f1f5bec879.png#clientId=u9da9e284-e542-4&crop=0&crop=0&crop=1&crop=1&from=drop&id=u56c92050&margin=%5Bobject%20Object%5D&name=未命名文件 (1).png&originHeight=488&originWidth=876&originalType=binary&ratio=1&rotation=0&showTitle=false&size=57621&status=done&style=none&taskId=u4ce58350-323a-4ead-bbe9-0d4832d1403&title=)
5.1 Exchanges概念
RabbitMQ 消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递到了哪些队列中。
相反,生产者只能将消息发送到交互机(exchange),交换机工作的内容非常简单,一反面它接收来自生产
者的消息,另一方面将它推入队列,交互机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把它们推到许多队列中还是说应该丢弃它们。这就是由交互机的类型来决定。
5.1.1 Exchanges的类型
直接(direct)路由类型主题(topic)主题类型标题(headers)头类型扇出(fanout)发布订阅类型
5.1.2 Exchange无名类型
在本教程的前面部分我们对exchange一无所知,但仍然能够将消息发送到队列。之前能实现的原因是因为我们使用的是默认交换机,我们通过空字符串(“”)进行表示
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
第一个参数是交换机的名称。空字符串表示默认或无名交换机:消息能路由发送到队列中其实是由RoutingKey(bindingkey)绑定key指定的,如果它存在的话
5.2 临时队列
之前的章节我们使用的是具有特定名称的队列(hello,ack_queue)。队列的名称对我们来说至关重要,我们需要指定我们的消费者去消费哪个队列的消息。
每当我们连接RabbitMQ时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:
String tempQueue = channel.queueDeclare().getQueue();
publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();
channel.confirmSelect();String tempQueue = channel.queueDeclare().getQueue();System.out.println("临时队列名称为:"+tempQueue);}
AD Excl表示临时的意思
5.3 绑定(bindings)
bindings是exchange和queue之间的桥梁,它告诉我们exchange和哪个队列进行了绑定关系。
使用RabbitMQ Web操作页面可视化创建一个队列,名称为:QueueBinDing
再创建一个交互机
使交互机对队列进行绑定
5.4 发布订阅(Fanout/扇出交换机)模式
Fanout(扇出)这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列的中。系统中默认有些exchange类型,fanout(扇出)不会去判断路由Key,也就是说不管交换机和队列中的路由Key是否匹配队列都接收到消息
5.4.1 Fanout实战
我们先将这两个消费者实现下
队列和交换机在消费者和生产者都可以声明
packagecom.zhoujing.rabbltmq.five;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/17-20:32-星期日
* <p>
* 消费者01
*/publicclassReceiveLogs01{/**
* 交互机名称
*/publicstaticfinalString EXCHANGE_NAME ="logs";publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();/*
* 声明一个交换机
* exchangeDeclare(交换机名称,交换机类型);
* */
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");/*
* 声明一个临时的队列
* 临时队列名称为随机
* 当消费者断开与队列连接的时候,队列就会删除
* */String queueName = channel.queueDeclare().getQueue();/*
* 交互机与队列进行绑定
* exchangeBind(队列名称,交换机名称,路由Key);
* fanout交换机路由Key无效
* */
channel.queueBind(queueName, EXCHANGE_NAME,"");System.out.println("消费者01,等待接收消息,把接收的消息打印到屏幕上……");// 接收消息DeliverCallback deliverCallback =(tag, message)->{System.out.println("接收到消息为:"+newString(message.getBody(),"UTF-8"));};// 取消接收CancelCallback cancelCallback =(tag)->{System.out.println("用户取消消息的接收,消息的标签为:"+tag);};// 接收消息
channel.basicConsume(queueName,true, deliverCallback,cancelCallback);}}
消费者02也差不多,将消费者01复制粘贴下将名称及相应的提示内容进行修改下
接下来实现下生产者
packagecom.zhoujing.rabbltmq.five;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.Scanner;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/17-21:00-星期日
*
* 生产者
* 发送消息给交互机
*/publicclassEmitLog{publicstaticfinalString EXCHANGE_NAME ="logs";publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();Scanner input =newScanner(System.in);while(input.hasNext()){String message = input.next();/*
* basicPublish(交互机名称,路由Key,其他参数,消息)
* 因为之前没有接触到交换机所以之前一直是为空字符串的(默认交互机)
* 因为使用的是fanout交换机所以路由Key无关
* */
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes("UTF-8"));System.out.println("生产者发送的消息为:"+ message);}}}
因为交互机声明在消费者,所以先启动两个消费者后启动生产者
Web页面可以查看到logs交互机绑定了两个队列
5.5 路由(direct/直接交换机)模式
fanout(扇出)交换机不会去判定key,直接将接收到的消息发送给所有绑定这个交换机的队列,direct(直接交)换机才会根据key把消息给指定的队列
packagecom.zhoujing.rabbltmq.six;importcom.rabbitmq.client.BuiltinExchangeType;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.Scanner;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-14:32-星期一
* <p>
* 生产者,direct模式
* 发送消息
*/publicclassDirectLog{/**
* 交换机名称
*/publicstaticfinalString EXCHANGE_NAME ="DirectLog";/**
* 队列01名称
*/publicstaticfinalString QUEUE_MAME01 ="ReceiveLogs01";/**
* 队列02名称
*/publicstaticfinalString QUEUE_MAME02 ="ReceiveLogs02";publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.DIRECT);// 交换机与队列进行绑定
channel.queueBind(QUEUE_MAME01, EXCHANGE_NAME,"123");
channel.queueBind(QUEUE_MAME02, EXCHANGE_NAME,"321");Scanner input =newScanner(System.in);while(input.hasNext()){String message = input.next();// 123是ReceiveLogs01的路由Key,只有ReceiveLogs01才能接收到消息
channel.basicPublish(EXCHANGE_NAME,"123",null, message.getBytes("UTF-8"));System.out.println("发送的消息为:"+ message);}}}
packagecom.zhoujing.rabbltmq.six;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-14:53-星期一
*
* 消费者01
*/publicclassReceiveLogs01{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 声明队列01
channel.queueDeclare(DirectLog.QUEUE_MAME01,false,false,false,null);DeliverCallback deliverCallback =(tag,message)->{System.out.println(tag+"接收到的消息为:"+newString(message.getBody(),"UTF-8"));};CancelCallback cancelCallback =(tag)->{System.out.println(tag +"取消了消息的回调");};System.out.println("消费者01正在等待接收消息……");
channel.basicConsume(DirectLog.QUEUE_MAME01,true,deliverCallback,cancelCallback);}}
packagecom.zhoujing.rabbltmq.six;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-14:53-星期一
* <p>
* 消费者02
*/publicclassReceiveLogs02{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();
channel.queueDeclare(DirectLog.QUEUE_MAME02,false,false,false,null);DeliverCallback deliverCallback =(tag, message)->{System.out.println(tag +"接收到的消息为:"+newString(message.getBody(),"UTF-8"));};CancelCallback cancelCallback =(tag)->{System.out.println(tag +"取消了消息的回调");};System.out.println("消费者02正在等待接收消息……");
channel.basicConsume(DirectLog.QUEUE_MAME02, deliverCallback, cancelCallback);}}
先启动两个消费者(没有队列交换机会出现错误)
向控制台中输入数据,因为消息在发送时指定了消费者01的路由Key,所以只有消费者01接收到了数据
加个条件又回到了轮询效果了
int count =0;String routingKey ="123";while(input.hasNext()){
count++;if(count %2==0){
routingKey ="123";}else{
routingKey ="321";}String message = input.next();// 123是ReceiveLogs01的路由Key,只有ReceiveLogs01才能接收到消息
channel.basicPublish(EXCHANGE_NAME, routingKey,null, message.getBytes("UTF-8"));System.out.println("发送的消息为:"+ message);}
5.6 Topsics(主题交换机)
5.6.1 之前类型的问题
在上一章中,改进了日志记录系统。我们没有使用只能进行随意广播的fanout交换机,而是使用了direct交换机,从而有能实现有选择性的接收日志。
尽管使用direct交换机改进了我们的系统,但是它仍然存在局限性,比如说我们想接收的日志类型有123,321和987,654,某个队列只想123,321的消息,那个这个时候direct就办不到了。这个时候就只能使用topic类型
5.6.2 Topic的要求
发送到类型是topic交换机的消息的routing_key不能随意写,必须满足一定的要求,它必须是有个单词列表,以点号分割开。这些单词可以是任意单词,但是单词列表最多不能超过255个字节。
这个规则列表中,其中有两个替换符是需要注意的
*(星号)可以代替一个单词#(井号)可以代替零个或者多个单词
packagecom.zhoujing.rabbltmq.seven;importcom.rabbitmq.client.BuiltinExchangeType;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.ArrayList;importjava.util.List;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-17:32-星期一
* <p>
* 生产者
* 主题类型
*/publicclassTopictLog{/**
* 交换机名称
*/publicstaticfinalString EXCHANGE_NAME ="TopictLog";/**
* 队列名称
*/publicstaticfinalString[] QUEUE_NAME ={"Queue01","Queue02","Queue03"};publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.TOPIC);// 交换机与队列进行绑定
channel.queueBind(QUEUE_NAME[0], EXCHANGE_NAME,"*.orange.*");
channel.queueBind(QUEUE_NAME[1], EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(QUEUE_NAME[2], EXCHANGE_NAME,"laz.#");// 绑定数据List<String> routingKey =newArrayList<>();
routingKey.add("test.orange.end");
routingKey.add("hello.world.rabbit");
routingKey.add("laz.yes.yes.ok.end");
routingKey.add("hello.java.yes");
routingKey.add("hello.world");
routingKey.add("laz.end");for(String key : routingKey){String message = key;
channel.basicPublish(EXCHANGE_NAME,key,null,message.getBytes("UTF-8"));}}}
packagecom.zhoujing.rabbltmq.seven;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.six.DirectLog;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-14:53-星期一
*
* 消费者01
*/publicclassReceiveLogs01{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 声明队列
channel.queueDeclare(TopictLog.QUEUE_NAME[0],false,false,false,null);DeliverCallback deliverCallback =(tag,message)->{System.out.println(tag+"接收到的消息为:"+newString(message.getBody(),"UTF-8"));System.out.println("routingKey为:"+message.getEnvelope().getRoutingKey());};CancelCallback cancelCallback =(tag)->{System.out.println(tag +"取消了消息的回调");};System.out.println("ReceiveLogs01正在等待消息……");// 接收消息
channel.basicConsume(TopictLog.QUEUE_NAME[0],true,deliverCallback,cancelCallback);}}
其它两个消费者也差不多,将消费者01复制两份把“TopictLog.QUEUE_NAME[0]”改成相应的下标即可
路由Key说明接收到的消费者test.orange.end满足消费者01,列表中存在3个并且第2位为“orange”消费者01hello.world.rabbit满足消费者02,列表中存在3个并且第3位为为“rabbit”消费者02laz.yes.yes.ok.end满足消费者03,列表以“laz”开头,后面无关消费者03hello.java.yes不满足任何消费者无hello.world不满足任何消费者无laz.end满足消费者03,列表以“laz”开头,后面无关消费者03
6 死信队列
6.1 死信的概念
先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue中里了,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费。这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异
常时,将消息投入死信队列中,还有比如说:用户商城下单成功并点击支付后在指定时间未支付时自动失效。
6.2 死信的来源
1、消息TTL(缓存时间)过期
2、队列达到最大长度(队列满了,无法再添加数据到RabbitMQ中)
3、消息被拒绝(basic.reject或basic.nack)并且requeue=false
6.3 死信实战
6.3.1 代码架构图
6.3.2 消息TTL过期
因为消费者01比较复杂,所以先写消费者01
Broker:接收和分发消息的应用
packagecom.zhoujing.rabbltmq.eight;importcom.rabbitmq.client.BuiltinExchangeType;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.HashMap;importjava.util.Map;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-22:45-星期一
*
* 声明交换机和队列
*/publicclassBroker{/**
* 正常交换机
*/publicstaticfinalString NORMAL_EXCHANGE ="normal_exchange";/**
* 死信交换机
*/publicstaticfinalString DEAD_EXCHANGE ="dead_exchange";/**
* 正常队列
*/publicstaticfinalString NORMAL_QUEUE ="normal_queue";/**
* 死信队列
*/publicstaticfinalString DEAD_QUEUE ="dead_queue";publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 声明正常交换机
channel.exchangeDeclare(NORMAL_EXCHANGE,BuiltinExchangeType.DIRECT);// 声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);Map<String,Object> arguments =newHashMap<>();// 正常队列对应的死信队列
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);// 设置死信 RoutingKey
arguments.put("x-dead-letter-routing-key","dead");// 设置过期时间,一般由生产者设置过期时间// arguments.put("x-message-ttl",10000);// 声明正常队列
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);// 声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);/*
* 交换机与队列进行绑定
* */
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"normal");
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"dead");}}
packagecom.zhoujing.rabbltmq.eight;importcom.rabbitmq.client.AMQP;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-23:19-星期一
*
* 生产者
*/publicclassProducer{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();String message ="Hello RabbitMQ!";// 消息有效时间为10秒,超出将进入死信队列AMQP.BasicProperties properties =newAMQP.BasicProperties().builder().expiration("10000").build();
channel.basicPublish(Broker.NORMAL_EXCHANGE,"normal",properties,message.getBytes("UTF-8"));}}
packagecom.zhoujing.rabbltmq.eight;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-22:22-星期一
*
* 消费者01
*/publicclassConsumer01{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();DeliverCallback deliverCallback =(tag,message)->{System.out.println(tag +"接收到的消息为:"+newString(message.getBody(),"UTF-8"));};CancelCallback cancelCallback =(tag)->{System.out.println(tag+"取消了消息回调");};System.out.println("消费者01正在等待消息接收……");
channel.basicConsume(Broker.NORMAL_QUEUE,true,deliverCallback,cancelCallback);}}
消费者02和消费者01差不多,将消费者01复制粘贴一份把 “channel.basicConsume(Broker.NORMAL_QUEUE,true,deliverCallback,cancelCallback);”
换成
“channel.basicConsume(Broker.DEAD_QUEUE,true,deliverCallback,cancelCallback);”
先启动 Broker ,再启动 两个消费者,最后启动生产者
启动生产者后,消费者01接收到了消息,说明这是正常情况并没有进入死信队列,将消费者01关闭再次启动生产者,等待10秒后消费者02接收到了消息说明消息进入了死信队列
6.3.3 队列达到最大的长度
把上面的创建的normal队列删除,并对上面的代码稍微进行改动
packagecom.zhoujing.rabbltmq.eight;importcom.rabbitmq.client.AMQP;importcom.rabbitmq.client.Channel;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-23:19-星期一
* <p>
* 生产者
*/publicclassProducer{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// String message = "Hello RabbitMQ!";// 消息有效时间为10秒,超出将进入死信队列// AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();// channel.basicPublish(Broker.NORMAL_EXCHANGE,"normal",null,message.getBytes("UTF-8"));for(int i =0; i <10; i++){String message = i+"";
channel.basicPublish(Broker.NORMAL_EXCHANGE,"normal",null,message.getBytes("UTF-8"));}}}
在 Broker 中添加以下代码
// 设置队列最大只能存储6个消息
arguments.put("x-max-length",6);
先启动 Broker ,在启动两个消费者,最后启动生产者,会发现消费者01依然接收到了10条数据,将消费者01关闭,再次启动生产者,消费者02接收到了4条数据,其余6条被消费者01接收到了(可在Web端进行查看)
6.3.4 消息被拒绝
把上面的创建的normal队列删除,并对上面的代码稍微进行改动
生产者我们连续发送了10条消息,分别是0~9,我们对其中的消息5进行拒绝
对上面 Broker 的队列最大长度6进行注释,对消费者01进行修改(02不需要进行修改)
basicReject:对消息进行拒绝,是否放回普通队列为false,那么就只能去死信队列了
packagecom.zhoujing.rabbltmq.eight;importcom.rabbitmq.client.CancelCallback;importcom.rabbitmq.client.Channel;importcom.rabbitmq.client.DeliverCallback;importcom.zhoujing.rabbltmq.utils.RabbitMqUtils;importjava.io.IOException;importjava.util.concurrent.TimeoutException;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/18-22:22-星期一
*
* 消费者01
*/publicclassConsumer01{publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();// 消息5finalString FIVE ="5";DeliverCallback deliverCallback =(tag,message)->{String msg =newString(message.getBody(),"UTF-8");if(FIVE.equals(msg)){// 拒绝消息// basicReject(消息标签,是否重新放回普通队列);
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);}else{System.out.println(tag +"接收到的消息为:"+newString(message.getBody(),"UTF-8"));// 手动确认// basicAck(消息标签,是否是批量);
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);}};CancelCallback cancelCallback =(tag)->{System.out.println(tag+"取消了消息回调");};System.out.println("消费者01正在等待消息接收……");
channel.basicConsume(Broker.NORMAL_QUEUE,false,deliverCallback,cancelCallback);}}
7 延迟队列
7.1 延迟队列概念
延迟队列,队列内部是有序的,最重要的特性就体现在它的延迟属性上,延迟队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延迟队列就是用来存放需要在指定时间被处理的元素队列。
7.2 延迟队列使用场景
1、订单在10分钟之内未支付则自动取消
2、新创建的店铺,如果在10天内都没有上传过商品,则自动发送消息提醒
3、用户注册成功后,如果三天内没有登录则进行短信提醒
4、用户发起退款,如果三天内没有得到处理则通知相关运营人员
5、预定会议后,需要在预定的时间点前10分钟通知各个与会议相关人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在10分钟之后检查该订单支付状态,然后将未支付的订单进行关闭。看起来似乎使用定时任务,一直轮询数据,每秒查询一次,取出需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,如果对于时间不是很严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但是对于数据量比较大,并且时效性较强的场景,如:“订单10分钟内未完成支付则关闭”,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对于这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很有可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大的压力,无法满足业务要求而且性能低下。
7.3 整合SpringBoot
创建SpringBoot项目
修改配置文件
spring:rabbitmq:host: IP地址
port:5672username: admin
password: admin
7.4 队列TTL
7.4.1 代码架构图
创建两个队列QA和QB,两者队列TTL分别设置为10妙和40秒,然后创建一个交换机X和死信交换机Y,它们的类型都是direct(直接),创建一个死信队列QD,它们的绑定关系如下:
7.4.2 实现
新建config,对队列、交换机进行声明并进行绑定
packagecom.zhoujing.myrabbitmq.config;importorg.springframework.amqp.core.*;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/19-11:07-星期二
* <p>
* TTL队列
*/@ConfigurationpublicclassTtlQueueConfig{/**
* 普通交换机名称
*/publicstaticfinalString EXCHANGE_NAME ="X";/**
* 死信交换机名称
*/publicstaticfinalString DEAD_EXCHANGE_NAME ="Y";/**
* 普通队列名称
*/publicstaticfinalString[] QUEUE_NAME ={"QA","QB"};/**
* 死信队列名称
*/publicstaticfinalString DEAD_QUEUE_NAME ="QD";/**
* 声明 X 交换机
*
* @return
*/@Bean("xExchange")publicDirectExchangexExchange(){returnnewDirectExchange(EXCHANGE_NAME);}/**
* 声明 Y 死信交换机
*
* @return
*/@Bean("yExchange")publicDirectExchangeyExchange(){returnnewDirectExchange(DEAD_EXCHANGE_NAME);}/**
* 声明 普通队列QA
*
* @return
*/@Bean("aQueue")publicQueueaQueue(){// 不用实例化,可以直接构建队列/*
* durable:持久化队列
* ttl:设置缓存过期时间,毫秒为单位
* deadLetterExchange:设置对应的死信交换机
* deadLetterRoutingKey:死信交换机RoutingKey
* */returnQueueBuilder.durable(QUEUE_NAME[0]).ttl(10000).deadLetterExchange(DEAD_EXCHANGE_NAME).deadLetterRoutingKey("YD").build();}/**
* 声明 普通队列QB
*
* @return
*/@Bean("bQueue")publicQueuebQueue(){returnQueueBuilder.durable(QUEUE_NAME[1]).ttl(40000).deadLetterExchange(DEAD_EXCHANGE_NAME).deadLetterRoutingKey("YD").build();}/**
* 声明 死信队列QD
*
* @return
*/@Bean("dQueue")publicQueuedQueue(){returnQueueBuilder.durable(DEAD_QUEUE_NAME).build();}/**
* OA队列绑定 X 交换机
*
* @param aQueue QA队列
* @param xExchange X 交换机
* @return
*/@BeanpublicBindingqueueQABinDingX(@Qualifier("aQueue")Queue aQueue,@Qualifier("xExchange")DirectExchange xExchange){// BindingBuilder.bind(队列名称).to(交换机名称).with(路由Key);returnBindingBuilder.bind(aQueue).to(xExchange).with("XA");}/**
* QB队列绑定 X 交换机
*
* @param bQueue QB队列
* @param xExchange X 交换机
* @return
*/@BeanpublicBindingqueueQBBinDingX(@Qualifier("bQueue")Queue bQueue,@Qualifier("xExchange")DirectExchange xExchange){returnBindingBuilder.bind(bQueue).to(xExchange).with("XB");}/**
* QD队列绑定 Y 交换机
*
* @param dQueue QD队列
* @param yExchange Y 交换机
* @return
*/@BeanpublicBindingqueueDBinDingY(@Qualifier("dQueue")Queue dQueue,@Qualifier("yExchange")DirectExchange yExchange){returnBindingBuilder.bind(dQueue).to(yExchange).with("YD");}}
新建Controller
packagecom.zhoujing.myrabbitmq.controller;importlombok.extern.slf4j.Slf4j;importorg.springframework.amqp.rabbit.core.RabbitTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.Date;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/19-12:01-星期二
*
* 生产者
* 发送消息
*/@Slf4j@RestController@RequestMapping("/ttl")publicclassSendMsgController{@AutowiredprivateRabbitTemplate rabbitTemplate;/**
* 开始发送消息
* @param message
*/@GetMapping("/sendMsg/{message}")publicvoidsendMsg(@PathVariableString message){
log.info("当前时间:{},发送一条消息给两个TTL队列:{}",newDate().toString(),message);
rabbitTemplate.convertAndSend("X","XA","消息来自TTL为10s的队列:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自TTL为40s的队列:"+message);}}
新建消费者接收消息
packagecom.zhoujing.myrabbitmq.consumer;importcom.rabbitmq.client.Channel;importlombok.extern.slf4j.Slf4j;importorg.springframework.amqp.core.Message;importorg.springframework.amqp.rabbit.annotation.RabbitListener;importorg.springframework.stereotype.Component;importjava.util.Date;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/19-18:15-星期二
*
* TTL队里 消费者
*/@Slf4j@ComponentpublicclassDeadLetterQueueConsumer{@RabbitListener(queues ="QD")publicvoidreceiveD(Message message,Channel channel){String msg =newString(message.getBody());
log.info("当前时间:{},接收到的消息为:{}",newDate(),msg);}}
启动项目,打开Postman或者浏览器输入
http://localhost:8080/ttl/sendMsg/hello
2022-07-19 19:33:30.267 INFO 15436 --- [nio-8080-exec-2] c.z.m.controller.SendMsgController : 当前时间:Tue Jul 19 19:33:30 CST 2022,发送一条消息给两个TTL队列:hello
2022-07-19 19:33:40.449 INFO 15436 --- [ntContainer#0-1] c.z.m.consumer.DeadLetterQueueConsumer : 当前时间:Tue Jul 19 19:33:40 CST 2022,接收到的消息为:消息来自TTL为10s的队列:hello
2022-07-19 19:34:10.441 INFO 15436 --- [ntContainer#0-1] c.z.m.consumer.DeadLetterQueueConsumer : 当前时间:Tue Jul 19 19:34:10 CST 2022,接收到的消息为:消息来自TTL为40s的队列:hello
第一条消息在10秒之后变成了死信消息,然后被消费者消费掉,第二条消息在40秒之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。
不过,如果这样使用的话,岂不是每添加一个新的时间需求,就要新增一个队列,这里只有10秒和40秒两个时间选项,如需要一个小时候处理,那么就需要添加TTL为一个小时的队列,如果是预定会议然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
7.5 延时队列优化
7.5.1 代码架构图
在这里新增了一个队列QC,绑定关系如下,该队列不设置TTL(缓存过期)时间
7.5.2 实现
我们可以将TTL设置时间由生产者进行设置,这样就不用一直创建队列了。
在config “QUEUE_NAME ” 中添加QC,并创建相应的队列及绑定相应的交换机
/**
* 普通队列名称
*/publicstaticfinalString[] QUEUE_NAME ={"QA","QB","QC"};
声明队列不设置过期时间由生产者去设置
/**
* 声明 普通队列QC
* 不进行过期时间设置
*
* @return
*/@Bean("cQueue")publicQueuecQueue(){returnQueueBuilder.durable(QUEUE_NAME[2]).deadLetterExchange(DEAD_EXCHANGE_NAME).deadLetterRoutingKey("YD").build();}
/**
* QC队列绑定 X 交换机
*
* @param cQueue QC队列
* @param xExchange X 交换机
* @return
*/@BeanpublicBindingqueueQCBinDingX(@Qualifier("cQueue")Queue cQueue,@Qualifier("xExchange")DirectExchange xExchange){returnBindingBuilder.bind(cQueue).to(xExchange).with("XC");}
controller(生产者),对sendMsg方法进行重载
/**
* 发送消息并设置过期时间
* @param message 消息
* @param expirationTime 过期时间
*/@GetMapping("/sendMsg/{message}/{expirationTime}")publicvoidsendMsg(@PathVariableString message,@PathVariableInteger expirationTime){
log.info("当前时间:{},发送一条消息给一个队列:{}",newDate().toString(),message);// 设置过期时间MessagePostProcessor messagePostProcessor = msg ->{
msg.getMessageProperties().setExpiration((expirationTime*1000)+"");return msg;};
rabbitTemplate.convertAndSend("X","XC",message,messagePostProcessor);}
和预期一样相差5秒
生产者设置过期时间存在一个问题,发送一个消息并过期时间设置为20秒,在这20秒内继续发送一个5秒的消息
过期时间为5秒的world并没有出现在hello的前面,而且发现接收时间和hello一样。
因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。队列的特性就是先进先出。
7.6 RabbitMQ插件实现延迟队列
如果不能实现在消息粒度上的TTL,并使其在设置的TTL时间及时死亡,就无法设计成一个通用的延迟队列。那如何解决呢?
7.6.1 安装延迟队列插件
Github插件下载
在安装插件时出现错误
rabbitmq_delayed_message_exchange: Plugin doesn’t support current server version. Actual broker version: “3.9.11”, supported by the plugin: [“3.10.0-3.10.x”]
原来是我的RabbitMQ为 3.9.11 版本的 rabbitmq_delayed_message_exchange 3.10版本的不支持,所以我又重新下载了3.9版本的
1、将插件上传服务器
2、将插件复制到 RabbitMQ 容器中
dockercp rabbitmq_delayed_message_exchange-3.9.0.ez RabbitMQ容器ID:/plugins
3、进入到 RabbitMQ 容器中
dockerexec -it RabbitMQ容器ID /bin/bash
4、进入 plugins 目录
cd /plugins
5、赋予权限
chmod777 rabbitmq_delayed_message_exchange-3.9.0.ez
6、启动延时插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
在Web查看Exchange(交换机),添加交换机查看类型多了一项 “x-delayed-message”说明插件安装成功
7.6.2 代码架构图
在这里新增了一个队列 delayed.queue,一个自定义交换机,delayed.exchange,绑定关系如下:
7.6.3 实现
新建config,创建延时队列及交换机
packagecom.zhoujing.myrabbitmq.config;importorg.springframework.amqp.core.*;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importjava.util.HashMap;importjava.util.Map;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/19-22:40-星期二
* <p>
* 延时队列
*/@ConfigurationpublicclassDelayedQueueConfig{/**
* 队列名称
*/publicstaticfinalString DELAYED_QUEUE_NAME ="delayed.queue";/**
* 交换机名称
*/publicstaticfinalString DELAYED_EXCHANGE_NAME ="delayed.exchange";/**
* 路由Key
*/publicstaticfinalString DELAYED_ROUTING_KEY ="delayed.routingKey";/**
* 声明延时队列
*
* @return
*/@BeanpublicQueuedelayedQueue(){returnQueueBuilder.durable(DELAYED_QUEUE_NAME).build();}/**
* 声明交换机
* 因为API没有提供“x-delayed-message”类型所以返回为类型为自定义交换机
*
* @return
*/@BeanpublicCustomExchangedelayedExchange(){// 消息确实是延迟了,但是怎么传播到队列呢,是要扇出呢还是直连呢,所以这里还得设置类型Map<String,Object> arguments =newHashMap<>();
arguments.put("x-delayed-type","direct");/*
* 1、交换机名称
* 2、交换机类型
* 3、是否需要持久化
* 4、是否需要自动删除
* 5、其他参数
* */returnnewCustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false, arguments);}/**
* 延时队列与延时交换机进行绑定
* @param delayedQueue 延时队列
* @param delayedExchange 延时交换机
* @return
*/@BeanpublicBindingdelayedQueueBinDingExchange(@Qualifier("delayedQueue")Queue delayedQueue,@Qualifier("delayedExchange")CustomExchange delayedExchange){// noargs:自定义交换机需要构建returnBindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();}}
生产者
/**
* 基于插件的方式实现延时队列
* @param message 消息
* @param expirationTime 过期时间
*/@GetMapping("/sendMsgDelayed/{message}/{expirationTime}")publicvoidsendMsgDelayed(@PathVariableString message,@PathVariableInteger expirationTime){
log.info("当前时间:{},发送一条消息至延时队列:{},过期时间为:{} 秒",newDate(),message,(expirationTime*1000));MessagePostProcessor messagePostProcessor =(msg)->{
msg.getMessageProperties().setDelay(expirationTime*1000);return msg;};
rabbitTemplate.convertAndSend("delayed.exchange","delayed.routingKey",message,messagePostProcessor);};
消费者,监听消息
/**
* 监听消息
* @param message 消息
* @throws UnsupportedEncodingException
*/@RabbitListener(queues ="delayed.queue")publicvoidreceiveDelayQueue(Message message)throwsUnsupportedEncodingException{String msg =newString(message.getBody(),"UTF-8");
log.info("当前时间为:{},收到的延时消息为:{}",newDate(),msg);}
进行测试,发送一个消息延时时间为20秒,然后在这20秒之内再次发送一条消息延时时间为5秒
输出结果符合预期,延时时间为5秒的先接收。
8 发布确认SpringBoot
在生成环境由于一些不明原因,导致RabbitMQ重启,在RabbitMQ重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行RabbitMQ的消息可开投递?特别是这样比较极端的情况,RabbitMQ集群不可用的时候,无法投递的消息该如何处理?
8.1 发布确认SpringBoot版本
8.1.1 确认机制方案
8.1.2 代码架构图
我们先新建一个普通的示例
config
packagecom.zhoujing.myrabbitmq.config;importorg.springframework.amqp.core.*;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/20-10:45-星期三
*
* 配置类
* 发布确认(高级)
*/@ConfigurationpublicclassConfirmConfig{/**
* 交换机名称
*/publicstaticfinalString CONFIRM_EXCHANGE ="confirm.exchange";/**
* 队列名称
*/publicstaticfinalString CONFIRM_QUEUE ="confirm.queue";/**
* RoutingKey
*/publicstaticfinalString CONFIRM_ROUTING_KEY ="key1";/**
* 声明交换机
* @return
*/@BeanpublicDirectExchangeconfirmExchange(){returnnewDirectExchange(CONFIRM_EXCHANGE);}/**
* 声明队列
* @return
*/@BeanpublicQueueconfirmQueue(){returnQueueBuilder.durable(CONFIRM_QUEUE).build();}/**
* 队列绑定交换机
* @param cExchange 交换机
* @param cQueue 队列
* @return
*/@BeanpublicBindingcExchangeBinDingCQueue(@Qualifier("confirmExchange")DirectExchange cExchange,@Qualifier("confirmQueue")Queue cQueue){returnBindingBuilder.bind(cQueue).to(cExchange).with(CONFIRM_ROUTING_KEY);}}
消费者
@Slf4j@ComponentpublicclassConfirmConsumer{@RabbitListener(queues =ConfirmConfig.CONFIRM_QUEUE)publicvoidreceiveConfirmMessage(Message message)throwsUnsupportedEncodingException{String msg =newString(message.getBody(),"UTF-8");
log.info("接收到的消息为:{}", msg);}}
生产者
@Slf4j@RestController@RequestMapping("/confirm")publicclassConfirmController{@ResourceprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMessage/{message}")publicvoidsendMessage(@PathVariableString message){// 发送消息
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,ConfirmConfig.CONFIRM_ROUTING_KEY,message);
log.info("当前时间为:{},发送的消息内容为:{}",newDate(),message);}}
进行测试
普通示例成功运行。
假如RabbitMQ因为网线断了或者RabbitMQ服务器关闭了,导致交换机接收不到消息该这么办?
在config中新建回调接类,对ConfirmCallback接口进行重写并重新注入进去。
packagecom.zhoujing.myrabbitmq.config;importlombok.extern.slf4j.Slf4j;importorg.springframework.amqp.rabbit.connection.CorrelationData;importorg.springframework.amqp.rabbit.core.RabbitTemplate;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Component;importjavax.annotation.PostConstruct;importjavax.annotation.Resource;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/20-11:41-星期三
*
* 回调接口
*/@Slf4j@ComponentpublicclassMyCallBackimplementsRabbitTemplate.ConfirmCallback{@ResourceprivateRabbitTemplate rabbitTemplate;/**
* @PostConstruct注解:在对象加载完依赖注入后执行它,通常都是一些初始化的操作,但初始化可能依赖于注入的其他组件,所以要等依赖全部加载完再执行
*/@PostConstructpublicvoidinit(){// 注入
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 失败的相关原因
*
* @param correlationData 保存回调消息的ID及相关消息
* @param ack 成功或失败
* @param cause 失败的相关信息
*/@Overridepublicvoidconfirm(CorrelationData correlationData,boolean ack,String cause){String id = correlationData !=null? correlationData.getId():"";if(ack){
log.info("交换机已经接收到了ID为:{}的消息",id);}else{
log.error("交换机还未收到ID为:{}的消息,由于原因:{}",id,cause);}}}
对 ConfirmController 进行修改
@Slf4j@RestController@RequestMapping("/confirm")publicclassConfirmController{@ResourceprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMessage/{message}")publicvoidsendMessage(@PathVariableString message){// 设置IDCorrelationData correlationData =newCorrelationData(UUID.randomUUID().toString());// 发送消息
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
log.info("当前时间为:{},发送的消息内容为:{}",newDate(),message);}}
8.1.3 配置文件
在配置文件当中需要添加
spring:rabbitmq:host: IP地址
port:5672username: admin
password: admin
# 开启发布确认,当消息发送给交换机时进行回调publisher-confirm-type: correlated
server:port:8080
none禁用发布确认模式,是默认值correlated发布消息成功到交换机后会触发回调方法simple有两种效果
1、其一效果和correlated值一样会触发回调方法。
2、其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果判定下一步的逻辑,需要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker
进行测试
测试结果成功,接下来测试失败的结果。
输入一个错误的交换机名称
异常信息为:
: 当前时间为:Wed Jul 20 12:38:00 CST 2022,发送的消息内容为:发布确认
Shutdown Signal: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange ‘confirm.exchange2’ in vhost ‘/’, class-id=60, method-id=40)
交换机还未收到ID为:889bcf8b-b475-4bdc-bb8c-51630745a0d5的消息,由于原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange ‘confirm.exchange2’ in vhost ‘/’, class-id=60, method-id=40)
消费发送到交换机失败也得到了相关的错误的信息,接下来试下消息发送到队列。
测试结果发现消费者并没有接收到消息,并且也没有错误消息提示,在8.2章节中将此问题进行修复。
8.2 回退消息
8.2.1 returns参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理。通过设置returns参数可以在当消息传递过程中不可达目的地时将消息返回这个生产者。
添加配置参数
spring:rabbitmq:host: IP地址
port:5672username: admin
password: admin
# 开启发布确认,当消息发送给交换机时进行回调publisher-confirm-type: correlated
# 当不可路由时将消息回退给生产者publisher-returns:trueserver:port:8080
在 MyCallBack 中实现 ReturnsCallback 返回回调
/**
* 消息传递过程中不可路由时将消息返回给消费者
* @param returnedMessage 返回的消息
*/@SneakyThrows@OverridepublicvoidreturnedMessage(ReturnedMessage returnedMessage){
log.error("消息:{},被交换机:{}退回,退回原因:{},路由Key:{}",newString(returnedMessage.getMessage().getBody(),"UTF-8"),returnedMessage.getExchange(),returnedMessage.getReplyText(),returnedMessage.getRoutingKey());}
也要将其注入
@PostConstructpublicvoidinit(){// 注入
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);}
测试结果
8.3 备份交换机
有了returns参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但是有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置returns参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在RabbitMQ中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么备份交换机呢?备份交换可以理解为RabbitMQ中交换机的“备胎”,当我们为某个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为:Fanout,这样就能把所有消息都投递到其绑定队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
8.3.1 代码架构图
在 ConfirmConfig 中新建
backup.queue(备份队列)、warning.queue(警告队列)、backup.exchange(备份交换机)
对普通交换机声明时进行设置备用交换机
ConfirmConfig 完整示例
packagecom.zhoujing.myrabbitmq.config;importorg.springframework.amqp.core.*;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/**
* @author zhoujing
* @version 1.0
* @createTime 2022/7/20-10:45-星期三
*
* 配置类
* 发布确认(高级)
*/@ConfigurationpublicclassConfirmConfig{/**
* 交换机名称
*/publicstaticfinalString CONFIRM_EXCHANGE ="confirm.exchange";/**
* 备份交换机
*/publicstaticfinalString BACKUP_EXCHANGE ="backup.exchange";/**
* 队列名称
*/publicstaticfinalString CONFIRM_QUEUE ="confirm.queue";/**
* 备份队列
*/publicstaticfinalString BACKUP_QUEUE ="back.queue";/**
* 警告队列
*/publicstaticfinalString WARNING_QUEUE ="warning.queue";/**
* RoutingKey
*/publicstaticfinalString CONFIRM_ROUTING_KEY ="key1";/**
* 声明交换机
* @return
*/@BeanpublicDirectExchangeconfirmExchange(){// 普通交换机设置备份交换机returnExchangeBuilder.directExchange(CONFIRM_EXCHANGE).durable(true).alternate(BACKUP_EXCHANGE).build();}/**
* 备份交换机
* @return
*/@BeanpublicFanoutExchangebackupExchange(){returnnewFanoutExchange(BACKUP_EXCHANGE);}/**
* 声明队列
* @return
*/@BeanpublicQueueconfirmQueue(){returnQueueBuilder.durable(CONFIRM_QUEUE).build();}/**
* 备份队列
* @return
*/@BeanpublicQueuebackupQueue(){returnQueueBuilder.durable(BACKUP_QUEUE).build();}/**
* 警告队列
* @return
*/@BeanpublicQueuewarningQueue(){returnQueueBuilder.durable(WARNING_QUEUE).build();}/**
* 队列绑定交换机
* @param cExchange 交换机
* @param cQueue 队列
* @return
*/@BeanpublicBindingcExchangeBinDingCQueue(@Qualifier("confirmExchange")DirectExchange cExchange,@Qualifier("confirmQueue")Queue cQueue){returnBindingBuilder.bind(cQueue).to(cExchange).with(CONFIRM_ROUTING_KEY);}/**
* 备用队里绑定备用交换机
* @param backupQueue 备用队里
* @param backupExchange 备用交换机
* @return
*/@BeanpublicBindingbackupQueueBinDingBackExchange(@Qualifier("backupQueue")Queue backupQueue,@Qualifier("backupExchange")FanoutExchange backupExchange){returnBindingBuilder.bind(backupQueue).to(backupExchange);}/**
* 警告队列绑定备用交换机
* @param warningQueue 警告队列
* @param backupExchange 备用交换机
* @return
*/@BeanpublicBindingwarningQueueBinDingBackExchange(@Qualifier("warningQueue")Queue warningQueue,@Qualifier("backupExchange")FanoutExchange backupExchange){returnBindingBuilder.bind(warningQueue).to(backupExchange);}}
因为备用消费者和警告消费者是一样的,所以本次测试只创建警告消费者
@Slf4j@ComponentpublicclassWarningConsumer{@RabbitListener(queues =ConfirmConfig.BACKUP_QUEUE)publicvoidreceiveBackupMsg(Message message)throwsUnsupportedEncodingException{String msg =newString(message.getBody(),"UTF-8");
log.info("警告消费者接收到的消息为:{}",msg);}}
RoutingKey依然是输入错的
重启启动时需要将 confirm_Exchange 交换机删除
测试结果:因为RoutingKey是错的,所以进入了 备用 交换机/队列。
returns参数与备份交换机可以一起使用的时候,如果两者同时启动,消息究竟去何从?谁优先级高,经过上面的结果显示答案是备份交换机优先级高。
9 RabbitMQ的其他问题
9.1 幂等性
9.1.1 概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生副作用。举个简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发送错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等。
9.1.2 消息重复消费
消费者在消费RabbitMQ中的消息时,RabbitMQ已把消息发送给消费者,消费者在给RabbitMQ返回ack时网络中断,故RabbitMQ未收到确认消息,该条消息会重新发送给其他的消费者,或者在网络重连后再次发送给消费者,但是实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
9.1.3 解决思路
RabbitMQ消费者的幂等性的解决一般使用全局ID或者写个唯一标识比如时间戳或者UUID或者订单消费者消费RabbitMQ中的消息也可以利用RabbitMQ的该ID来判断,或者可按照自己的规则生成一个全局唯一的ID,每次消费消息时用ID先判断该消息是否已消费过。
9.1.4 消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能会重复发生消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:
1、唯一ID+指纹码机制,利用数据库主键去重。
2、利用Redis的原子性去实现。
9.1.5 唯一ID+指纹码机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个ID是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
9.1.6 Redis原子性
利用Redis执行setnx命令,天然具有幂等性。从而实现不重复消费
10 RabbitMQ集群
10.1 为什么使用集群
最开始我们介绍了如何安装及运行RabbitMQ服务,不过这些都是单机版,无法满足目前真实应用的要求。如果RabbitMQ服务器遇到内存崩溃,机器断电或者主板故障等情况,该肿么办?单台RabbitMQ服务器可以满足每1000条消息的吞吐量,那么如果应用需要RabbitMQ服务满足每秒10万条消息的吞吐量呢?购买昂贵的服务器来增强单机RabbitMQ服务器的性能显得捉襟见肘,搭建一个RabbitMQ集群才是解决实际问题的关键。
现在这台RabbitMQ作为node01,接下来我们再启动两个RabbitMQ,分别是node02,node03。
Docker搭建集群
版权归原作者 週进 所有, 如有侵权,请联系我们删除。