Rabbit MQ
兔子 消息队列
消息队列
1.1 MQ相关概念
1,什么是MQ
MQ(message queue),从字面意思上看。本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务。
2,为什么用MQ
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服务还能及时的得到异步处理成功的消息。
3,MQ的分类
RabbitMQ
2007年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。
优点:由于erlang语言的高并发特性,性能较好;吞吐量到万级,MQ功能比较完备,健壮、稳定、易用、跨平台、支持多种语言如:Python、Ruby、,NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高
缺点:商业版需要收费,学习成本较高
4,MQ的选择
1.2 RabibitMQ
1,RabbitMQ的概念
RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当作一个快递站,当你要发送一个包裹时,你把它的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ与快递站的主要区别在于,它不处理快件而是接受,存储和转发消息数据。
2,四大核心概念
生产者
产生数据发送消息的程序是生产者
交换机
交换机是RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定
队列
队列是RabbitMQ内部使用的一种数据结构,尽管消息流经Rabbit MQ和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式。
消费者
消费和接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又可以是消费者。
3,RabbitMQ核心部分
4,各个名词介绍
Broker:接收和分发消息的应用,RabbitMQ Server 就是Message Broker
Virtual host:出于多租户和安全因素设计的,把AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等。
Connection:生产者/消费者和 borker 之间的TCP连接
Channel:信道。如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在connection内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了 channe id 帮助客户端和message broker 识别 channel,所有 channel 之间是完全隔离的。 Channel 作为轻量级的****Connection 极大减少了操作系统建立 TCP connection 的开销。
Exchange:交换机。message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key(路由键),分发消息到 queue 中去。常用的类型有: direct(point-to-point),topic(publish-subscribe)and fanout(multicast)
Queue:队列。消息最终被送到这里等待 consumer 取走
Binding:绑定。exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key ,Binding 信息被保存到exchange 中的查询表中,用于 message 的分发依据。
5,安装
官网:Downloading and Installing RabbitMQ — RabbitMQ
下载完安装包之后
将两个包放到Linux /usr/local/software 目录下
运行以下命令:
rpm -ivh erlang-21.3.8.21-1.el7.x86_64.rpm
yum install socat -y
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
启动服务
# 启动服务
systemctl start rabbitmq-server
# 查看服务状态
systemctl status rabbitmq-server
# 开机自启动
systemctl enable rabbitmq-server
# 停止服务
systemctl stop rabbitmq-server
# 重启服务
systemctl restart rabbitmq-server
web管理界面及授权操作
安装
rabbitmq-plugins enable rabbitmq_management
安装完毕以后,重启服务即可
systemctl restart rabbitmq-server
访问 http://192.168.137.135:15672 ,用默认账号密码(guest)登录,出现权限问题。
默认情况只能在 localhost 本机下访问,所以需要添加一个远程登录的用户
# 创建账号和密码
rabbitmqctl add_user admin 123456
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 为用户添加资源权限
# set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
# 添加配置、写、读权限
#当前用户和角色
rabbitmqctl list_users
用户级别:
- administrator:可以登录控制台、查看所有信息、可以对 rabbitmq 进行管理
- monitoring:监控者 登录控制台,查看所有信息
- policymaker:策略制定者 登录控制台,指定策略
- managment:普通管理员 登录控制台
再次登录,用 admin 用户。
重置命令
关闭应用的命令为:rabbitmqctl stop_app
清除的命令为:rabbitmqctl reset
重新启动命令为:rabbitmqctl start_app
防火墙
systemctl status firewalld #查看防火墙状态
systemctl stop firewalld #关闭防火墙
systemctl enable firewalld #开机之后不启动防火墙
2,hello world
在下图中,P 是我们的生产者, C 是我们的消费者,
中间的框是一个队列-rabbitmq代表使用者保留的消息缓冲区。
导入依赖
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
1,消息生产者
publicclassProducer{//队列名称privatestaticfinalStringQUEUE_NAME="hello";//发消息publicstaticvoidmain(String[] args)throwsException{//创建连接工厂ConnectionFactory factory =newConnectionFactory();//配置工厂IP,连接RabbitMQ的队列
factory.setHost("192.168.137.135");//配置用户名
factory.setUsername("root");//配置密码
factory.setPassword("924721");//创建连接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,true,null);//创建消息体String message ="hello world";/*
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪个,本次是队列的名称
* 3,其它参数信息
* 4,发送消息的消息体 (二进制数组)
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());System.out.println("生产者发送消息.....");}}
2,消息消费者
publicclassConsumer{//队列名称privatestaticfinalStringQUEUE_NAME="hello";//消费消息publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{//创建连接工厂ConnectionFactory factory =newConnectionFactory();
factory.setHost("192.168.137.135");
factory.setUsername("root");
factory.setPassword("924721");//创建连接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(QUEUE_NAME,true,deliverCallback,cancelCallback);System.out.println("消费者消费消息.....");}}
3,Work QUeues
工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
3.1,轮训分发消息
1.1 抽取工具类
publicclassRabbitMqUtils{publicstaticChannelgetChannel()throwsIOException,TimeoutException{//创建连接工厂ConnectionFactory factory =newConnectionFactory();//配置工厂IP,连接RabbitMQ的队列
factory.setHost("192.168.137.135");//配置用户名
factory.setUsername("root");//配置密码
factory.setPassword("924721");//创建连接Connection connection = factory.newConnection();//获取信道Channel channel = connection.createChannel();return channel;}}
1.2 工作线程代码
publicclassWorkThread01{privatestaticfinalStringQUEUE_NAME="hello";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();//声明 接收消息DeliverCallback deliverCallback =(consumerTag,message)->{//打印消息的消息体System.out.println(newString(message.getBody()));};//取消消息的回调CancelCallback cancelCallback = consumerTag ->{System.out.println(consumerTag+": 消息消费被中断");};
channel.basicConsume(QUEUE_NAME,true, deliverCallback,cancelCallback);}}
3.2,消息应答
1,概念
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然挂掉了,会发生什么情况?RabbitMQ一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息,以及后续发送给该消费者的消息,因为它无法接收到。
为了保证消息在发送过程中不丢失,Rabbit MQ引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉RabbitMQ,它已经处理了,RabbitMQ可以把该消息删除了。
2,自动应答(尽量少使用)
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者channel关闭,那么消息就会丢失,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样也有可能使得消费者这边由于接受太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所有这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
3,消息应答的方法
A. Channel.basicAck(用于肯定确认)
RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
B. Channel.basicNack(用于否定确认)
C. Channel.basicReject(用于否定确认)
与Channel.basicNack 相比少一个参数(Multiple)
不处理该消息了直接拒绝,可以将其丢弃了。
4,Multiple的解释
手动应答的好处是可以批量应答并且减少网络拥堵
Multiple的true和false代表不同意思
true代表批量应答 channel上未应答的消息
比如说channel上有传送tag的消息 5,6,7,8 当前tag是8,那么此时5-8的这些还未应答的消息都会被确认收到消息应答。
false同上面相比(建议使用)
只会应答 tag =8的消息,5,6,7这三个消息依然不会被确认收到消息应答。
5,消息自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
6,消息手动应答代码
默认消息采用的是自动应答,所以我们要想实现消息消费过程中不丢失,需要把自动应答改为手动应答,消费者在上面代码的基础上增加部分代码。
生产者代码:
publicclassTaskThread02{privatestaticfinalStringQUEUE_NAME="ack_queue";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();//生成队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);//发布消息Scanner sc =newScanner(System.in);while(sc.hasNext()){String message = sc.next();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));System.out.println("发送消息:"+message);}}}
工作线程1:
publicclassWorkThread02{privatestaticfinalStringQUEUE_NAME="ack_queue";//接受消息publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();System.out.println("C1等待接收消息处理时间较短");//声明接收消息回调DeliverCallback deliverCallback =(consumertag,message)->{//沉睡一秒SleepUtils.sleep(1);System.out.println("接收到的消息为:"+newString(message.getBody(),"UTF-8"));/**
* 手动应答
* 1,消息标签 deliveryTag
* 2,是否批量应答 multpile
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);};//取消消息回调CancelCallback cancelCallback = consumertag ->{System.out.println("消费者取消消费");};//采用手动应答boolean autoAck =false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);}}
7,手动应答效果演示
正常情况下消息发送方发送两个消息C1 和 C2 分别接收到消息并进行处理
3.3,RabbitMQ 持久化
1,概念
刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当RabbitMQ服务停掉以后消息生产者发送过来的消息不丢失。默认情况下RabbitMQ推出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。
确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
2,队列如何实现持久化
之前我们创建的队列都是非持久化的,rabbitmq如果重启的话,该队列就会被删除掉,如果要队列实现持久化,需要在声明队列的时候把durable参数设置为持久化。
但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误。
inequivalent arg 'durable' for queue 'hello' in vhost '/': received 'false' but current is 'true'
/*
* 生成一个队列
* 1,队列名称
* 2,队列里面的消息是否持久化(磁盘),默认情况消息存储在内存中 true持久化 false不持久化
* 3,该队列是否只供一个消费者进行消费 是否进行消息共享,true可以多个消费者消费 false:只能一个消费者消费
* 4,是否自动删除,最后一个消费者断开连接以后,该队列是否自动删除,true自动删除 false不自动删除
* 5,其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
设置之前的显示
设置之后的显示:会有D(durable)图标
持久化后即使重启RabbitMQ队列也依然存在。
3,消息实现持久化
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN添加这个属性。
//开启消息持久化
channel.basicPublish("",QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉RabbitMQ将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。后边有更强有力的持久化策略。
4,不公平分发
在最开始的时候我们学习到RabbitMQ分发消息采用的是轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中消费者1处理任务的速度非常快,而另外一个消费者2处理速度却很慢,这个时候我们还是采用轮训分发的话就会导致处理速度快的这个消费者很大一部分时间出于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ并不知道这种情况,它依然很公平的进行分发。
改在消费方,设置在消费消息之前:
设置不公平分发:设置参数channel.basicQos(1);
设置轮训分发: 设置参数channel.basicQos(0);–默认
//设置不公平分发int prefetchCount =1;
channel.basicQos(prefetchCount);
5,预取值
本身消息的发送就是异步发送的,所以在任何时候,channel上肯得不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息的最大数量。这时可以通过使用basicQos方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,Rabbit MQ将停止在通道上传递更多消息,除非有消息未被应答。
4,发布确认
4.1,发布确认原理
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所以在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag(消息标签)域包含了确认消息的序列号,此外broker也可以设置basicAck的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm模式最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果Rabbit MQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。
4.2,发布确认的策略
1,开启发布确认的方法
发布确认默认是没有开启的,如果需要开启可以调用方法channel.confirmSelect,每当你要想使用发布确认,都需要在channel上调用该方法。
Channel channel =RabbitMqUtils.getChannel();//开启发布确认
channel.confirmSelect();
2,单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认,那么它将抛出异常。
这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
3,批量确认发布
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
4,异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,它是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。
/**
* 单个发布确认
* 批量发布确认
* 异步批量确认
*/publicclassMessageConfirm{privatestaticfinalintSIZE=1000;publicstaticvoidmain(String[] args)throwsException{// MessageConfirm.publishOnce(); //发送1000个单次确认消息耗时为426ms// MessageConfirm.publishMultiple(); //发送1000个批量确认消息耗时为94msMessageConfirm.publishAsync();//发送1000个异步确认消息耗时为65ms}publicstaticvoidpublishOnce()throwsException{Channel channel =RabbitMqUtils.getChannel();//队列名称String queueName =UUID.randomUUID().toString();//声明队列
channel.queueDeclare(queueName,true,false,false,null);//开启发布确认
channel.confirmSelect();//开始时间Long begin =System.currentTimeMillis();for(int i =0; i <SIZE; i++){//开启单次确认
channel.waitForConfirms();String message = i +"";/*
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪个,本次是队列的名称
* 3,其它参数信息
* 4,发送消息的消息体 (二进制数组)
*/
channel.basicPublish("",queueName,null,message.getBytes());System.out.println("发送消息:"+message);}//结束时间Long end =System.currentTimeMillis();System.out.println("发送"+SIZE+"个单次确认消息耗时为"+(end - begin)+"ms");}publicstaticvoidpublishMultiple()throwsException{Channel channel =RabbitMqUtils.getChannel();//队列名称String queueName =UUID.randomUUID().toString();//声明队列
channel.queueDeclare(queueName,true,false,false,null);//开启发布确认
channel.confirmSelect();//开始时间Long begin =System.currentTimeMillis();//批量大小int multiple =100;for(int i =0; i <SIZE; i++){String message = i +"";
channel.basicPublish("",queueName,null,message.getBytes());//批量一百发布确认if(i %100==0){
channel.waitForConfirms();}}//结束时间Long end =System.currentTimeMillis();System.out.println("发送"+SIZE+"个批量确认消息耗时为"+(end - begin)+"ms");}publicstaticvoidpublishAsync()throwsException{Channel channel =RabbitMqUtils.getChannel();//队列名称String queueName =UUID.randomUUID().toString();//声明队列
channel.queueDeclare(queueName,true,false,false,null);//开启发布确认
channel.confirmSelect();//开始时间Long begin =System.currentTimeMillis();//已确认的消息回调函数ConfirmCallback ackCallback =(deliveryTag,multiple)->{};//未确认的消息回调函数ConfirmCallback nackCallback =(deliveryTag,multiple)->{System.out.println("未被确认的消息:"+ deliveryTag);};//准备消息的监听器,看看哪些被确认,哪些未被确认
channel.addConfirmListener(ackCallback,nackCallback);for(int i =0; i <SIZE; i++){String message =""+ i;
channel.basicPublish("",queueName,null,message.getBytes());}//结束时间Long end =System.currentTimeMillis();System.out.println("发送"+SIZE+"个异步确认消息耗时为"+(end - begin)+"ms");}}
5,如何处理异步未确认消息
最好的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用ConcurrentLinkedQueue这个队列在confirm callbacks与发布线程之间进行消息的传递。
ConcurrentLinkedQueue会先将所有消息记录起来,等到消息发送完毕之后,会将已经确认的消息删除掉,剩下的就是未确认的消息。
6,以上3种发布确认速度对比
单独发布消息:
同步等待确认,简单,但吞吐量非常有限。
批量发布消息:
批量同步等待确认,简单,合理的吞吐量,一旦出现问题很难推断出是哪条消息出现了问题。
异步处理:
最佳性能和资源使用,再出现错误的情况下可以很好地控制,但是实现起来稍微难些。
5,交换机
在上一节中,我们创建了一个工作队列。我们假设的是工作队列背后,每个任务都恰好交付给一个消费者(工作进程)。在这一部分中,我们将做一些完全不同的事情,我们将消息传达给多个消费者。这种模式成为“发布/订阅”。
为了说明这种模式,我们将构建一个简单的日志系统。它将由两个程序组成:第一个程序将发出日志消息,第二个程序是消费者。其中我们会启动两个消费者,其中一个消费者接收到消息后把日志存储在磁盘,另外一个消费者接收到消息后把消息打印在屏幕上,事实上第一个程序发出的日志消息将广播给所有消费者。
5.1,Exchanges
1,Exchanges 概念
Rabbit MQ消息传递模型的核心思想是:生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接受来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们放到许多队列中还是说应该丢弃它们。这就得由交换机的类型来决定。
2,Exchanges的类型
总共有以下类型
直接(direct),主题(topic),标题(headers),闪出(fanout)
3,无名exchange
我们在发布消息的时候未指定交换机仍然能够将消息发送到队列。之前能实现的原因是我们使用的是默认交换,我们通过空字符串“”来进行标识。
channel.basicPublish("",queueName,null,message.getBytes());
第一个参数是交换机的名称。空字符串表示默认或五名称交换机:消息能路由发送到队列中其实是有routingKey(bindingkey)绑定key指定的,如果它存在的话。
5.2,临时队列
之前的章节我们使用的是具有特定名称的队列(前边的hello和ack_queue)。队列的名称对我们来说至关重要,我们需要指定我们的消费者去消费哪个队列的消息。
每当我们连接到Rabbit时,我们都需要一个全新的空队列,为此我们可以创建一个具有随即名称的队列,或者能让服务器为我们选择一个随即队列名称就更好了,其次我们一旦断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
5.3,绑定(bindings)
什么是binding,binding其实是exchange(交换机)和queue(队列)之间的桥梁,它告诉我们exchange和哪个队列进行了绑定关系。
5.4,Fanout(扇出)
1,Fanout介绍 (发布/订阅)(大喇叭)
Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。系统中默认有写exchange类型。
/**
* @return: fanout扇出
* @Author: ljs
* @Date: 2023/10/9 15:39
*/publicclassReceivelog01{privatestaticfinalStringEXCHANGE_NAME="logs";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();//创建一个交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");//生成一个临时队列String queue = channel.queueDeclare().getQueue();//绑定交换机和队列
channel.queueBind(queue,EXCHANGE_NAME,"");System.out.println("消费者1等待接收消息.....");DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println("消息:"+newString(message.getBody()));};
channel.basicConsume(queue,true,deliverCallback,cancelCallback->{});}}
/**
* @return: 消费者2
* @Author: ljs
* @Date: 2023/10/9 20:48
*/publicclassReceivelog02{privatestaticfinalStringEXCHANGE_NAME="logs";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();//创建一个交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");//生成一个临时队列String queue = channel.queueDeclare().getQueue();//绑定交换机和队列
channel.queueBind(queue,EXCHANGE_NAME,"");System.out.println("消费者2等待接收消息.....");DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println("消息:"+newString(message.getBody()));};
channel.basicConsume(queue,true,deliverCallback,cancelCallback->{});}}
/**
* @return: 生产者
* @Author: ljs
* @Date: 2023/10/9 20:48
*/publicclassEmitlog{privatestaticfinalStringEXCHANGE_NAME="logs";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");System.out.println("生产者开始生产消息.....");Scanner scanner =newScanner(System.in);while(scanner.hasNext()){String message = scanner.next();System.out.println("消息:"+message);/*
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪个,本次是队列的名称
* 3,其它参数信息
* 4,发送消息的消息体 (二进制数组)
*/
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());}}}
5.5,Direct (直接模式)
比方说我们只让某个消费者订阅发布的部分消息。
如果我们希望将日志消息写入磁盘的程序仅接收严重错误(error),而不存储哪些警告(warning)或信息(info)日志消息避免浪费磁盘空间。Fanout这种交换类型并不能给我们带来很大的灵活性-它只能进行无意识的广播,在这里我们将使用direct这种类型来进行替换,这种类型的工作方式是:消息只去到它绑定的routingKey队列中去。
/**
* @return: 消费者1
* @Author: ljs
* @Date: 2023/10/9 20:48
*/publicclassReceiveDirectLog01{privatestaticfinalStringEXCHANGE_NAME="direct_log";publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.DIRECT);//声明队列
channel.queueDeclare("console",false,false,false,null);//绑定队列
channel.queueBind("console",EXCHANGE_NAME,"info");
channel.queueBind("console",EXCHANGE_NAME,"warning");System.out.println("消费者1开始消费消息.....");DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println("消费消息:"+newString(message.getBody()));};
channel.basicConsume("console",true,deliverCallback,cancelCallback->{});}}
/**
* @return: 消费者2
* @Author: ljs
* @Date: 2023/10/9 20:48
*/publicclassReceiveDirectLog02{privatestaticfinalStringEXCHANGE_NAME="direct_log";publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{Channel channel =RabbitMqUtils.getChannel();//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.DIRECT);//声明队列
channel.queueDeclare("disk",false,false,false,null);//绑定队列
channel.queueBind("disk",EXCHANGE_NAME,"error");System.out.println("消费者2开始消费消息.....");DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println("消费消息:"+newString(message.getBody()));};
channel.basicConsume("disk",true,deliverCallback,cancelCallback->{});}}
/**
* @return: 生产者
* @Author: ljs
* @Date: 2023/10/9 20:48
*/publicclassDirect{privatestaticfinalStringEXCHANGE_NAME="direct_log";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.DIRECT);System.out.println("生产者开始生产消息.....");Scanner scanner =newScanner(System.in);while(scanner.hasNext()){String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"error",null,message.getBytes(StandardCharsets.UTF_8));}}}
5.6,Topics(主题类型)
尽管使用direct 交换机改进了我们的系统,但是它仍然存在局限性,比方说我们想接收的日志类型有info.base和info.advantage,某个队列只想info.base的消息,那这个时候direct就办不到了。这个时候就只能使用topic类型。
1,要求
发送到类型是topic交换机的消息的routing_key不能随意写,必须满足一定要求,它必须是一个单词列表,以点号分隔开。这些单词可以是任意单词,比如说:“stock.usd.nyse”,"nyse.vmw"等这种类型的。当然这个单次列表最多不能超过255个字节。
在这个规则列表中,其中有两个替换符需要注意
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单次
当队列绑定关系是下列这种情况时需要引起注意
如果队列绑定键是#,那么这个队列将接收所有的数据,功能类似于fanout
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是direct
2,实战
/**
* @return: 生产者
* @Author: ljs
* @Date: 2023/10/9 20:48
*/publicclassTopics{privatestaticfinalStringEXCHANGE_NAME="topics_log";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.TOPIC);System.out.println("生产者开始生产消息.....");Map<String,String> map =newHashMap<>();
map.put("quick.orange.rabbit","被队列Q1Q2接收到");
map.put("lazy.orange.elephant","被队列Q1Q2接收到");
map.put("quick.orange.fox","被队列Q1接收到");
map.put("lazy.brown.fox","被队列Q2接收到");
map.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列Q2接收一次");
map.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
map.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
map.put("lazy.orange.male.rabbit","是四个单词但匹配Q2");for(Map.Entry<String,String> stringStringEntry : map.entrySet()){String message = stringStringEntry.getValue();String routingKey = stringStringEntry.getKey();
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes(StandardCharsets.UTF_8));System.out.println("生产者发出消息:"+ message);}}}
/**
* @return: 消费者1
* @Author: ljs
* @Date: 2023/10/9 20:47
*/publicclassReceiveTopics01{privatestaticfinalStringEXCHANGE_NAME="topics_log";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.TOPIC);String queueName ="Q1";
channel.queueDeclare(queueName,false,false,false,null);
channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*");System.out.println("消费者1开始消费消息.....");DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println(newString(message.getBody()));System.out.println("接收队列:"+ queueName +"--绑定键:"+ message.getEnvelope().getRoutingKey());};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback ->{});}}
/**
* @return: 消费者2
* @Author: ljs
* @Date: 2023/10/9 20:47
*/publicclassReceiveTopics02{privatestaticfinalStringEXCHANGE_NAME="topics_log";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.TOPIC);String queueName ="Q2";
channel.queueDeclare(queueName,false,false,false,null);
channel.queueBind(queueName,EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(queueName,EXCHANGE_NAME,"lazy.#");System.out.println("消费者2开始消费消息.....");DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println(newString(message.getBody()));System.out.println("接收队列:"+ queueName +"--绑定键:"+ message.getEnvelope().getRoutingKey());};
channel.basicConsume(queueName,true,deliverCallback,cancelCallback ->{});}}
6,死信队列
1,定义
死信,顾名思义就是无法被消费的消息,一般来说,producer将消息投递到broker或者直接到queue里了,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中。还有比如说:用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。
2,死信的来源
消息TTL(存活时间 Time To Live)过期。
队列达到最大长度(队列满了,无法再添加数据到mq中)。
消息被拒绝(basic.reject 或 basic.nack) 并且 requeue = false。
3,实战
6.3.1,消息TTL过期
/**
*
* @Author: ljs
*
* 死信队列
* 消费者-1
*/publicclassConsumer01{//普通交换机名称privatestaticfinalStringNORMAL_EXCHANGE="normal_exchange";//死信交换机名称privatestaticfinalStringDEAD_EXCHANGE="dead_exchange";//普通队列名称privatestaticfinalStringNORMAL_QUEUE="normal_queue";//死信队列名称privatestaticfinalStringDEAD_QUEUE="dead_queue";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();//声明普通和死信交换机
channel.exchangeDeclare(NORMAL_EXCHANGE,BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);//声明普通队列Map<String,Object> arguments =newHashMap<>();//设置过期时间 1s = 1000ms
arguments.put("x-message-ttl",10000);//正常队列设置死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);//设置死信RoutingKey
arguments.put("x-dead-letter-routing-key","lisi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);//普通交换机绑定队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");//死信交换机绑定队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");System.out.println("等待接收消息.....");//消费者1消费消息DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println("Consumer01接收的消息为:"+newString(message.getBody(),"UTF-8"));};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag ->{});}}
/**
*
* @Author: ljs
* 死信队列
* 生产者
*/publicclassProducer{privatestaticfinalStringNORMAL_EXCHANGE="normal_exchange";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();//设置TTL 10sAMQP.BasicProperties properties =newAMQP.BasicProperties().builder().expiration("10000").build();System.out.println("生产者开始发送消息.....");for(int i =1; i <11; i++){String message ="info"+ i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes(StandardCharsets.UTF_8));}}}
关闭消费者线程,打开生产者线程,等待消息过期,发现死信进入死信队列
/**
*
* @Author: ljs
* 死信消费者
*/publicclassConsumer02{//死信队列名称privatestaticfinalStringDEAD_QUEUE="dead_queue";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();System.out.println("等待接收消息.....");//消费者2消费消息DeliverCallback deliverCallback =(consumerTag, message)->{System.out.println("Consumer02接收的消息为:"+newString(message.getBody(),"UTF-8"));};
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumerTag ->{});}}
6.3.2,队列达到最大长度
修改消费者1的代码,设置最大长度
/**
*
* @Author: ljs
*
* 死信队列
* 消费者-1
*/publicclassConsumer01{//普通交换机名称privatestaticfinalStringNORMAL_EXCHANGE="normal_exchange";//死信交换机名称privatestaticfinalStringDEAD_EXCHANGE="dead_exchange";//普通队列名称privatestaticfinalStringNORMAL_QUEUE="normal_queue";//死信队列名称privatestaticfinalStringDEAD_QUEUE="dead_queue";publicstaticvoidmain(String[] args)throwsException{Channel channel =RabbitMqUtils.getChannel();//声明普通和死信交换机
channel.exchangeDeclare(NORMAL_EXCHANGE,BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);//声明普通队列Map<String,Object> arguments =newHashMap<>();//设置队列最大长度
arguments.put("x-max-length",6);//正常队列设置死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);//设置死信RoutingKey
arguments.put("x-dead-letter-routing-key","lisi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);//普通交换机绑定队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");//死信交换机绑定队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");System.out.println("等待接收消息.....");//消费者1消费消息DeliverCallback deliverCallback =(consumerTag,message)->{System.out.println("Consumer01接收的消息为:"+newString(message.getBody(),"UTF-8"));};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag ->{});}}
6.3.3,消息被拒
修改消费者1的代码
/**
*
* @Author: ljs
*
* 死信队列
* 消费者-1
*/publicclassConsumer01{//普通交换机名称privatestaticfinalStringNORMAL_EXCHANGE="normal_exchange";//死信交换机名称privatestaticfinalStringDEAD_EXCHANGE="dead_exchange";//普通队列名称privatestaticfinalStringNORMAL_QUEUE="normal_queue";//死信队列名称privatestaticfinalStringDEAD_QUEUE="dead_queue";publicstaticvoidmain(String[] args)throwsException{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","lisi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);//普通交换机绑定队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");//死信交换机绑定队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");System.out.println("等待接收消息.....");//消费者1消费消息DeliverCallback deliverCallback =(consumerTag,message)->{String msg =newString(message.getBody(),StandardCharsets.UTF_8);if(msg.equals("info5")){System.out.println("Consumer01接收的消息为:"+newString(message.getBody(),"UTF-8")+"此消息是被拒绝的.....");/*
* 拒绝操作
* 参数1:被拒绝的消息标签
* 参数2:拒绝后是否放回队列
*/
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);}else{System.out.println("Consumer01接收的消息为:"+newString(message.getBody(),"UTF-8"));
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);}};//必须设置手动应答,否则不会被拒绝
channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,consumerTag ->{});}}
7,延迟队列
1,延迟队列概念
延时队列,队列内部是有序的,最重要的特性就是体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出或处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
2,延迟队列使用场景
1,订单在十分钟之内未支付则自动取消
2,新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3,用户注册成功后,如果三天内没有登录则进行短信提醒。
4,用户发起退款,如果三天内没有得到处理则通知相关运营人员。
5,预定会议后,需要在预定的时间点前十分钟通知各个人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎使用定时任务,一直轮询数据,每秒查一次,去除需要被处理的数据,然后处理不就完事了吗?如果数据量比较少,确实可以这样做,比如:对于"如果账单一周内未支付则进行自动结算"这样的需求,如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:订单十分钟内未支付则关闭,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
3,整合SpringBoot
3.1,导入相关依赖
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-amqp</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.73</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit-test</artifactId><scope>test</scope></dependency></dependencies>
3.2,配置RabbitMQ
spring.rabbitmq.host=192.168.163,128
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
3.3,导入swagger工具类
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("rabbitmq接口文档")
.description("本文档描述了rabbitmq微服务接口定义")
.version("1.0")
.contact(new Contact("enjoy6288","http://atguigu.com","[email protected]"))
.build();
}
}
4,队列TTL
1,代码架构图
创建两个队列QA和QB,两者队列TTL分别设置为10s和40s,然后再创建一个交换机X和死信交换机Y,它们的类型都是direct,创建一个死信队列QD,它们的绑定关系如下:
2,配置文件类代码
/*
* TTL队列 配置文件类代码
* */@ConfigurationpublicclassTtlQueueConfig{//普通交换机privatestaticfinalStringX_EXCHANGE="X";//死信交换机privatestaticfinalStringY_DEAD_LETTER_EXCHANGE="Y";//普通队列privatestaticfinalStringA_QUEUE="QA";privatestaticfinalStringB_QUEUE="QB";//死信队列privatestaticfinalStringD_DEAD_LETTER_QUEUE="QD";//注册普通交换机@Bean("exchangeX")publicDirectExchangeexchangeX(){returnnewDirectExchange(X_EXCHANGE);}//注册死信交换机@Bean("exchangeY")publicDirectExchangeexchangeY(){returnnewDirectExchange(Y_DEAD_LETTER_EXCHANGE);}//注册普通队列QA@Bean("queueA")publicQueuequeueA(){Map<String,Object> arguments =newHashMap<>();//设置TTL 10s
arguments.put("x-message-ttl",10000);
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");returnQueueBuilder.durable(A_QUEUE).withArguments(arguments).build();}//注册普通队列QB@Bean("queueB")publicQueuequeueB(){Map<String,Object> arguments =newHashMap<>();//设置TTL 40s
arguments.put("x-message-ttl",40000);
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");returnQueueBuilder.durable(B_QUEUE).withArguments(arguments).build();}//注册死信队列QD@Bean("queueD")publicQueuequeueD(){returnQueueBuilder.durable(D_DEAD_LETTER_QUEUE).build();}//绑定X交换机和QA队列@BeanpublicBindingquequeABindingX(@Qualifier("queueA")Queue queueA,@Qualifier("exchangeX")Exchange exchangeX){returnBindingBuilder.bind(queueA).to(exchangeX).with("XA").noargs();}//绑定X交换机和QB队列@BeanpublicBindingquequeBBindingX(@Qualifier("queueB")Queue queueB,@Qualifier("exchangeX")Exchange exchangeX){returnBindingBuilder.bind(queueB).to(exchangeX).with("XB").noargs();}//绑定Y交换机和QD队列@BeanpublicBindingquequeDBindingY(@Qualifier("queueD")Queue queueD,@Qualifier("exchangeY")Exchange exchangeY){returnBindingBuilder.bind(queueD).to(exchangeY).with("YD").noargs();}}
@RestController@RequestMapping("/ttl")@Slf4j/**
* @return: 生产者
* @Author: ljs
* @Date: 2023/10/10 19:40
*/publicclassSendController{@AutowiredprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMsg/{message}")publicvoidsendMsg(@PathVariableString message){
log.info("当前时间:{},发送一条消息给两个队列:{}",LocalDateTime.now(), message);
rabbitTemplate.convertAndSend("X","XA","消息来自TTL为10秒的队列:"+ message);
rabbitTemplate.convertAndSend("X","XB","消息来自TTL为40秒的队列:"+ message);}}
/**
* @return: 消费者
* @Author: ljs
* @Date: 2023/10/10 19:40
*/@Slf4j@ComponentpublicclassDeadLetterQueueConsumer{//接收消息@RabbitListener(queues ="QD")publicvoidreceiveD(Message message,Channel channel)throwsException{String msg =newString(message.getBody());System.out.println(msg);
log.info("当前时间:{},收到死信队列的消息:{}",LocalDateTime.now(), msg);}}
第一条消息在10s后变成了死信消息,然后被消费者消费掉,第二条消息在40s之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。
不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有10s和40s两个时间选项,如果需要一个小时后处理,那么就需要增加TTL为一个小时的队列,如果是预定会议室提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
5,延时队列优化
修改配置工具类
/*
* TTL队列 配置文件类代码
* */@ConfigurationpublicclassTtlQueueConfig{//普通交换机privatestaticfinalStringX_EXCHANGE="X";//死信交换机privatestaticfinalStringY_DEAD_LETTER_EXCHANGE="Y";//普通队列privatestaticfinalStringA_QUEUE="QA";privatestaticfinalStringB_QUEUE="QB";privatestaticfinalStringC_QUEUE="QC";//死信队列privatestaticfinalStringD_DEAD_LETTER_QUEUE="QD";//注册普通交换机@Bean("exchangeX")publicDirectExchangeexchangeX(){returnnewDirectExchange(X_EXCHANGE);}//注册死信交换机@Bean("exchangeY")publicDirectExchangeexchangeY(){returnnewDirectExchange(Y_DEAD_LETTER_EXCHANGE);}//注册普通队列QA@Bean("queueA")publicQueuequeueA(){Map<String,Object> arguments =newHashMap<>();//设置TTL 10s
arguments.put("x-message-ttl",10000);
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");returnQueueBuilder.durable(A_QUEUE).withArguments(arguments).build();}//注册普通队列QB@Bean("queueB")publicQueuequeueB(){Map<String,Object> arguments =newHashMap<>();//设置TTL 40s
arguments.put("x-message-ttl",40000);
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");returnQueueBuilder.durable(B_QUEUE).withArguments(arguments).build();}//注册普通队列QC@Bean("queueC")publicQueuequeuqC(){Map<String,Object> arguments =newHashMap<>();
arguments.put("x-dead-letter-exchange",Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key","YD");returnQueueBuilder.durable(C_QUEUE).withArguments(arguments).build();}//注册死信队列QD@Bean("queueD")publicQueuequeueD(){returnQueueBuilder.durable(D_DEAD_LETTER_QUEUE).build();}//绑定X交换机和QA队列@BeanpublicBindingquequeABindingX(@Qualifier("queueA")Queue queueA,@Qualifier("exchangeX")Exchange exchangeX){returnBindingBuilder.bind(queueA).to(exchangeX).with("XA").noargs();}//绑定X交换机和QB队列@BeanpublicBindingquequeBBindingX(@Qualifier("queueB")Queue queueB,@Qualifier("exchangeX")Exchange exchangeX){returnBindingBuilder.bind(queueB).to(exchangeX).with("XB").noargs();}//绑定Y交换机和QD队列@BeanpublicBindingquequeDBindingY(@Qualifier("queueD")Queue queueD,@Qualifier("exchangeY")Exchange exchangeY){returnBindingBuilder.bind(queueD).to(exchangeY).with("YD").noargs();}//绑定X交换机和QC队列@BeanpublicBindingqueueCBindingY(@Qualifier("queueC")Queue queueC,@Qualifier("exchangeX")Exchange exchangeX){returnBindingBuilder.bind(queueC).to(exchangeX).with("XC").noargs();}}
修改生产者代码
@RestController@RequestMapping("/ttl")@Slf4j/**
* @return: 生产者
* @Author: ljs
* @Date: 2023/10/10 19:40
*/publicclassSendController{@AutowiredprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMsg/{message}")publicvoidsendMsg(@PathVariableString message){
log.info("当前时间:{},发送一条消息给两个队列:{}",LocalDateTime.now(), message);
rabbitTemplate.convertAndSend("X","XA","消息来自TTL为10秒的队列:"+ message);
rabbitTemplate.convertAndSend("X","XB","消息来自TTL为40秒的队列:"+ message);}@GetMapping("/sendTTLMsg/{message}/{ttlTime}")publicvoidsendMsg(@PathVariableString message,@PathVariableString ttlTime){
log.info("当前时间:{},发送一条时长{}消息给queueC队列:{}",LocalDateTime.now(),ttlTime,message);
rabbitTemplate.convertAndSend("X","XC",message, msg ->{//发送消息的时候设置发送时延
msg.getMessageProperties().setExpiration(ttlTime);return msg;});}}
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置TTL的方式,消息可能并不会按时"死亡",因为RabbitMQ只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
6,RabbitMQ插件实现延迟队列
如果不能实现在消息粒度上的TTL,并使其在设置的TTL时间及时死亡,就无法设计成一个通用的延时队列。
1,安装延时队列插件
进入RabbitMQ的安装目录下的plugins目录,执行下面命令让该插件生效,然后重启RabbitMQ 。
#移动
cp rabbitmq_rtopic_exchange-3.11.1.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.12.6/plugins
#安装
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
#重启服务
systemctl restart rabbitmq-server
使用插件前:延时是延时的队列
使用插件后:延时设置在交换机
2,实战开发
/**
* @return: 延迟队列配置类
* @Author: ljs
* @Date: 2023/10/12 16:08
*/@ConfigurationpublicclassDelayedQueueConfig{//延迟交换机publicstaticfinalStringDELAYED_EXCHANGE_NAME="delayed.exchange";//延迟队列publicstaticfinalStringDELAYED_QUEUE_NAME="delayed.queue";//RoutingKeypublicstaticfinalStringDELAYED_ROUTINGKEY="delayed.routingkey";//声明延迟交换机@BeanpublicCustomExchangedelayedExchange(){Map<String,Object> arguments =newHashMap<>();
arguments.put("x-delayed-type","direct");/*
* 1,名字
* 2,类型
* 3,持久化
* 4,自动删除
* 5,参数设置
* */returnnewCustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",false,false,arguments);}//声明延迟队列@BeanpublicQueuedelayedQueue(){returnnewQueue(DELAYED_QUEUE_NAME);}@Bean//绑定延迟交换机和延迟队列publicBindingdelayedQueueBingdingDelayedExchange(@Qualifier("delayedExchange")CustomExchange delayedExchange,@Qualifier("delayedQueue")Queue delayedQueue){returnBindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTINGKEY).noargs();}}
@RestController@RequestMapping("/ttl")@Slf4j/**
* @return: 生产者
* @Author: ljs
* @Date: 2023/10/10 19:40
*/publicclassSendController{@AutowiredprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMsg/{message}")publicvoidsendMsg(@PathVariableString message){
log.info("当前时间:{},发送一条消息给两个队列:{}",LocalDateTime.now(), message);
rabbitTemplate.convertAndSend("X","XA","消息来自TTL为10秒的队列:"+ message);
rabbitTemplate.convertAndSend("X","XB","消息来自TTL为40秒的队列:"+ message);}@GetMapping("/sendTTLMsg/{message}/{ttlTime}")publicvoidsendMsg(@PathVariableString message,@PathVariableString ttlTime){
log.info("当前时间:{},发送一条时长{}消息给queueC队列:{}",LocalDateTime.now(),ttlTime,message);
rabbitTemplate.convertAndSend("X","XC",message, msg ->{//发送消息的时候设置发送时延
msg.getMessageProperties().setExpiration(ttlTime);return msg;});}@GetMapping("/delayedMsg/{message}/{delayedTime}")publicvoidsendMsg(@PathVariableString message,@PathVariableInteger delayedTime){
log.info("当前时间:{},发送一条时长:{}的消息给延迟队列:{}",LocalDateTime.now(),delayedTime,message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTINGKEY,message, msg ->{//发送消息的时候设置发送时延
msg.getMessageProperties().setDelay(delayedTime);return msg;});}}
/**
* @return: 延迟队列消费者
* @Author: ljs
* @Date: 2023/10/12 16:09
*/@Slf4j@ComponentpublicclassDelayedQueueConsumer{@RabbitListener(queues =DelayedQueueConfig.DELAYED_QUEUE_NAME)publicvoidreceiveDelayed(Message message){String msg =newString(message.getBody());System.out.println(msg);
log.info("当前时间:{},收到延时队列的消息:{}",LocalDateTime.now(), msg);}}
从结果中可以看出,队列优先消费延时短的消息,达到预期目标。
7,总结
延时队列在需要延时处理的场景下非常有用,使用RabbitMQ来实现延时队列可以很好的利用RabbitMQ的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过RabbitMQ集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用java的DelayQueue,利用redis的zset,利用Quartz或者利用kafka的时间轮,这些方式各有特点,看需要适用的场景。
8,发布确认高级
在生产环境中由于一些不明原因,导致rabbitmq重启,在rabbitMQ重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行RabbitMQ的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ集群不可用的时候,无法处理的消息该如何处理呢?
1,发布确认SpringBoot版本
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
#发布消息成功到交换机后会触发回调方法
NONE
禁止发布确认模式,是默认值
CORRELATED
发布消息成功到交换机后会触发回调方法
SIMPLE(发一条就确认一次)
精测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker。
/**
* @return: 发布确认高级版
* @Author: ljs
* @Date: 2023/10/12 17:19
*/@ConfigurationpublicclassConfirmConfig{//交换机publicstaticfinalStringCONFIRM_EXCHANGE_NAME="confirm.exchange";//队列publicstaticfinalStringCONFIRM_QUEUE_NAME="confirm.queue";//routingkeypublicstaticfinalStringCONFIRM_ROUTING_KEY="key1";//声明交换机@BeanpublicDirectExchangeexchangeConfirm(){returnnewDirectExchange(CONFIRM_EXCHANGE_NAME);}//声明队列@BeanpublicQueuequeueConfirm(){returnnewQueue(CONFIRM_QUEUE_NAME);}//绑定关系@BeanpublicBindingqueueConfirmBindExchangeConfirm(@Qualifier("exchangeConfirm")DirectExchange directExchange,@Qualifier("queueConfirm")Queue queue){returnBindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);}}
/**
* @return: 生产者发送消息
* @Author: ljs
* @Date: 2023/10/12 19:44
*/@RestController@Slf4j@RequestMapping("/confirm")publicclassConfirmController{@AutowiredprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMessage/{message}")publicvoidconfirmP(@PathVariableString message){
log.info("确认队列发送消息:{}", message);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message);}}
/**
* @return: 消费消息
* @Author: ljs
* @Date: 2023/10/12 19:44
*/@Component@Slf4jpublicclassConfirmConsumer{@RabbitListener(queues =ConfirmConfig.CONFIRM_QUEUE_NAME)publicvoidrecived(Message message){String msg =newString(message.getBody());
log.info("收到来自确认队列的消息:{}",msg);}}
2,回调接口
当如果出现交换机宕机或者其它情况无法工作而使消息发送失败,可以使用回调接口将未正常发送的消息回调起来。
/**
* @return: 回调接口
* @Author: ljs
* @Date: 2023/10/12 20:20
*/@Component@Slf4jpublicclassMyCallBackimplementsRabbitTemplate.ConfirmCallback{@AutowiredprivateRabbitTemplate rabbitTemplate;//注入@PostConstructpublicvoidinit(){
rabbitTemplate.setConfirmCallback(this);}/*
* 交换机确认回调方法
* 1,发消息 交换机接收到了 回调
* 1.1 correlationData 保存回调消息的ID及相关消息
* 1.2 交换机收到消息 ask = true
* 1.3 reason null
* 2,发消息 交换机接受失败了 回调
* 2.1,correlationData 保存回调消息的ID及相关信息
* 2.2 交换机收到消息 ask = false
* */@Overridepublicvoidconfirm(CorrelationData correlationData,boolean ask,String reason){String id = correlationData !=null? correlationData.getId():"";if(ask){
log.info("交换机已经收到id为:--{}--的消息",id);}else{
log.info("交换机没有收到id为:{}的消息,未收到的原因为:{}",id,reason);}}}
修改生产者交换机代码
/**
* @return: 生产者发送消息
* @Author: ljs
* @Date: 2023/10/12 19:44
*/@RestController@Slf4j@RequestMapping("/confirm")publicclassConfirmController{@AutowiredprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMessage/{message}")publicvoidconfirmP(@PathVariableString message){CorrelationData correlationData =newCorrelationData("1");// correlationData.setId("1");
log.info("确认队列发送消息:{}", message);
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+"123",ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);}}
修改生产者路由代码
/**
* @return: 生产者发送消息
* @Author: ljs
* @Date: 2023/10/12 19:44
*/@RestController@Slf4j@RequestMapping("/confirm")publicclassConfirmController{@AutowiredprivateRabbitTemplate rabbitTemplate;@GetMapping("/sendMessage/{message}")publicvoidconfirmP(@PathVariableString message){CorrelationData correlationData =newCorrelationData("1");// correlationData.setId("1");
log.info("确认队列发送消息:{}", message +"key1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message +"key1",correlationData);CorrelationData correlationData2 =newCorrelationData("2");
log.info("确认队列发送消息:{}", message +"key2");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY+"2",message +"key2",correlationData2);}}
3,回退消息
3.1,Mandatory参数
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?通过设置mandatory参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
在配置文件中添加
spring.rabbitmq.publisher-returns=true
#表示开启回退消息给生产者
修改回调接口实现类代码
/**
* @return: 回调接口
* @Author: ljs
* @Date: 2023/10/12 20:20
*/@Component@Slf4jpublicclassMyCallBackimplementsRabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback{@AutowiredprivateRabbitTemplate rabbitTemplate;//注入@PostConstructpublicvoidinit(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);}/*
* 交换机确认回调方法
* 1,发消息 交换机接收到了 回调
* 1.1 correlationData 保存回调消息的ID及相关消息
* 1.2 交换机收到消息 ask = true
* 1.3 reason null
* 2,发消息 交换机接受失败了 回调
* 2.1,correlationData 保存回调消息的ID及相关信息
* 2.2 交换机收到消息 ask = false
* */@Overridepublicvoidconfirm(CorrelationData correlationData,boolean ask,String reason){String id = correlationData !=null? correlationData.getId():"";if(ask){
log.info("交换机已经收到id为:--{}--的消息",id);}else{
log.info("交换机没有收到id为:{}的消息,未收到的原因为:{}",id,reason);}}//当消息传递过程中不可达目的地时将消息返回给生产者/*private final Message message;
private final int replyCode;
private final String replyText;
private final String exchange;
private final String routingKey;*/@OverridepublicvoidreturnedMessage(ReturnedMessage returnedMessage){
log.error("由:{}交换机,路由:{}退回的消息为:{},退回原因为:{}",
returnedMessage.getExchange(),
returnedMessage.getRoutingKey(),
returnedMessage.getMessage(),
returnedMessage.getReplyText());}}
4,备份交换机
有了mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。在RabbitMQ.中,有一种备份交换机的机制存在,可以很好的应对这个问题。
什么是备份交换机呢?备份交换机可以理解为 RabbitMQ中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
修改配置类代码
/**
* @return: 发布确认高级版
* @Author: ljs
* @Date: 2023/10/12 17:19
*/@ConfigurationpublicclassConfirmConfig{//交换机publicstaticfinalStringCONFIRM_EXCHANGE_NAME="confirm.exchange";//队列publicstaticfinalStringCONFIRM_QUEUE_NAME="confirm.queue";//routingkeypublicstaticfinalStringCONFIRM_ROUTING_KEY="key1";//备份交换机publicstaticfinalStringBACKUP_EXCHANGE_NAME="backup.exchange";//报警队列publicstaticfinalStringWARNING_QUEUE_NAME="warning.queue";//声明交换机@BeanpublicDirectExchangeexchangeConfirm(){returnExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(false).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();}//声明队列@BeanpublicQueuequeueConfirm(){returnnewQueue(CONFIRM_QUEUE_NAME);}//声明备份交换机@BeanpublicFanoutExchangebackupExchange(){returnnewFanoutExchange(BACKUP_EXCHANGE_NAME);}//声明报警队列@BeanpublicQueuewarningQueue(){returnnewQueue(WARNING_QUEUE_NAME);}//绑定关系@BeanpublicBindingqueueConfirmBindExchangeConfirm(@Qualifier("exchangeConfirm")DirectExchange directExchange,@Qualifier("queueConfirm")Queue queue){returnBindingBuilder.bind(queue).to(directExchange).with(CONFIRM_ROUTING_KEY);}//绑定备用交换机和报警队列@BeanpublicBindingwarningQueueBindBackupExchange(@Qualifier("backupExchange")FanoutExchange fanoutExchange,@Qualifier("warningQueue")Queue warningQueue){returnBindingBuilder.bind(warningQueue).to(fanoutExchange);}}
添加警报队列消费者
/**
* @return: 警告队列
* @Author: ljs
* @Date: 2023/10/13 9:53
*/@Slf4j@ComponentpublicclassWarningConsumer{@RabbitListener(queues =ConfirmConfig.WARNING_QUEUE_NAME)publicvoidrecevice(Message message){
log.error("收到的警报消息为:{}",newString(message.getBody()));}}
当运行生产者时,上述代码中由于RoutingKey不正确导致消息发送失败,消息就会进入备用交换机分配的警告队列中。
mandatory参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先级高,警告上面结果显示答案是备份交换机优先级高。
9,RabbitMQ其它知识点
1,幂等性
1.1,概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是在响应客户端的时候也有可能出现网络中断或者异常等等
1.2,解决思路
MQ消费者的幂等性的解决一般使用全局ID或者写个唯一标识比如时间戳或者UUID或者订单消费者消费MQ中的消息也可利用MQ的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过。
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作**:a.唯一ID+指纹码机制,利用数据库主键去重, b.利用redis.的原子性去实现**
唯一ID+指纹码机制:
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个id是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
Redis原子性:
利用redis执行SETNX命令,天然具有幂等性,从而实现不重复消费(建议使用!)
2,优先级队列
2.1,使用场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用redis.,来存放的定时轮询,大家都知道redis,只能用List做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用RabbitMQ进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。
要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费,这样才有机会对消息进行排序。
设置最大优先级参数(“x-max-priority”)
publicclassProducer{// 队列名称publicstaticfinalStringQUEUE_NAME="hello";// 发消息publicstaticvoidmain(String[] args)throwsIOException,TimeoutException{// 创建一个连接工厂ConnectionFactory factory =newConnectionFactory();// 工厂IP连接RabbitMQ的队列
factory.setHost("192.168.163.128");// 用户名
factory.setUsername("admin");// 密码
factory.setPassword("123");// 创建连接Connection connection = factory.newConnection();// 获取信道Channel channel = connection.createChannel();Map<String,Object> arguments =newHashMap<>();//官方允许是0-255之间,此处设置10,允许优化级范围为0-10,不要设置过大,浪费CPU与内存
arguments.put("x-max-priority",10);
channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);// 发消息for(int i =0; i <10; i++){String message ="info"+ i;if(i ==5){AMQP.BasicProperties properties =newAMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes(StandardCharsets.UTF_8));}else{
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));}}System.out.println("消息发送完毕!");}}
3,惰性队列
RabbitMQ从 3.6.0版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。
队列具备两种模式: default和lazy.默认的为default模式,在3.6.0之前的版本无需做任何变更。lazy_模式即为惰性队列的模式,可以通过调用channel.queueDecare方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用这两种方式设置的话,那么Policy的方式具备更高的优先级。如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。
在队列声明的时候**可以通过"x-queue-mode"参数来设置队列的模式,取值为"default"和"lazy”"**。下面示例中演示了一个惰性队列的声明细节:
Map<String,Object> args =newHashMap<String,Object>();
args.put("x-queue-mode","lazy");
channel.queueDeclare("myqueue",false,false,false,args);
在发送1百万务消息,每条消息大概占1KB的情况下,普通队列占用内存是1.2GB,而惰性队列仅仅占用1.5MB。
10,集群
1,搭配集群
克隆三台虚拟机 ,修改三台机器的名称为node1、node2、node3
vim /etc/hosts
node1
node2
node3
修改三台机器的hosts文件中的名称,让各个节点都能互相识别对象
vim /etc/hosts
192.168.163.128 node1
192.168.163.132 node2
193.168.163.131 node3
以确保各个节点的cookie文件使用的是同一个值:在node1上执行远程操作命令
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
启动RabbitMQ服务,顺带启动Erlang虚拟机和RabbitMQ应用服务,三台节点下执行:
rabbitmq-server -detached
在节点2执行
# rabbitmqctl stop会将Erlang虚拟机关闭 rabbitmqctl stop_app 只关闭rabbitmq服务
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node1
# 只启动rabbitmq服务
rabbitmqctl start_app
在节点3执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
查看集群状态
rabbitmqctl cluster_status
需要重新设置用户
# 创建账号
rabbitmqctl add_user admin 123
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
# 之后在三个集群节点的任意一个可视化界面登录均可
解除集群节点,node2和node3分别执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
# 此项命令均在node1上执行
rabbitmqctl forget_cluster_node rabbit@node2
2,镜像队列
如果RabbitMQ集群中只有一个Broker节点,那么该节点的失效将导致整体服务的临时性不可用,并且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable属性也设置为true,但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过publisherconfirm机制能够确保客户端知道哪些消息己经存入磁盘,尽管如此,一般不希望遇到因单点故障导致的服务不可用。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他Broker节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。
- 启动三台集群节点
- 随便找一个节点添加policy^mirrior 表示以mirrior为前缀的交换机或队列Apply to:应用范围ha:表示备份ha-mode:备机模式 exactly:指定模式ha-params:备份份数 2(本身一份,备份一份)ha-sync-mode:同步模式 automatic:自动备份
就算整个集群只剩下一台机器了,依然能消费队列里面的消息。说明队列里面的消息被镜像队列传递到相应机器里面了。
3,实现高可用负载均衡
之前的连接工厂配置只连接了一台MQ,因为固定写死了。
HAProxy.提供高可用性、负载均衡及基于TCPHTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub.在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。
扩展nginx,lvs,haproxx.p间的区别: http://www.ha97.com/5646.html
4,Federation Exchange
联邦 交换机
(broker北京),(broker深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京的业务(Client北京)需要连接(broker北京),向其中的交换器exchangeA.发送消息,此时的网络延迟很小,(Client北京)可以迅速将消息发送至exchangeA.中,就算在开启了publisherconfirm.机制或者事务机制的情况下,也可以迅速收到确认信息。此时又有个在深圳的业务(Client深圳)需要向exchangeA发送消息,那么(Client深圳)(broker北京)之间有很大的网络延迟,(Client深圳)将发送消息至exchangeA会经历一定的延迟,尤其是在开启了publisherconfirm.机制或者事务机制的情况下,(Client深圳)会等待很长的延迟时间来接收(broker北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。
将业务(Client深圳)部署到北京的机房可以解决这个问题,但是如果(Client深圳)调用的另些服务都部署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现?这里使用Federation插件就可以很好地解决这个问题.
搭建步骤
- 需要保证每台节点单独运行
- 在每台机器上开启federation相关插件
# 每台节点均需执行以下命令rabbitmq-plugins enable rabbitmq_federationrabbitmq-plugins enable rabbitmq_federation_management
tmqctl join_cluster rabbit@node2
rabbitmqctl start_app
查看集群状态
rabbitmqctl cluster_status
需要重新设置用户
```xml
# 创建账号
rabbitmqctl add_user admin 123
# 设置用户角色
rabbitmqctl set_user_tags admin administrator
# 设置用户权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
# 之后在三个集群节点的任意一个可视化界面登录均可
解除集群节点,node2和node3分别执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
# 此项命令均在node1上执行
rabbitmqctl forget_cluster_node rabbit@node2
2,镜像队列
如果RabbitMQ集群中只有一个Broker节点,那么该节点的失效将导致整体服务的临时性不可用,并且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable属性也设置为true,但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过publisherconfirm机制能够确保客户端知道哪些消息己经存入磁盘,尽管如此,一般不希望遇到因单点故障导致的服务不可用。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他Broker节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。
[外链图片转存中…(img-pJWbzpb4-1715068862752)]
- 启动三台集群节点
- 随便找一个节点添加policy^mirrior 表示以mirrior为前缀的交换机或队列Apply to:应用范围ha:表示备份ha-mode:备机模式 exactly:指定模式ha-params:备份份数 2(本身一份,备份一份)ha-sync-mode:同步模式 automatic:自动备份
[外链图片转存中…(img-5Izfev22-1715068862752)]
就算整个集群只剩下一台机器了,依然能消费队列里面的消息。说明队列里面的消息被镜像队列传递到相应机器里面了。
3,实现高可用负载均衡
[外链图片转存中…(img-LFw70z8n-1715068862753)]
之前的连接工厂配置只连接了一台MQ,因为固定写死了。
HAProxy.提供高可用性、负载均衡及基于TCPHTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub.在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。
扩展nginx,lvs,haproxx.p间的区别: http://www.ha97.com/5646.html
4,Federation Exchange
联邦 交换机
(broker北京),(broker深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京的业务(Client北京)需要连接(broker北京),向其中的交换器exchangeA.发送消息,此时的网络延迟很小,(Client北京)可以迅速将消息发送至exchangeA.中,就算在开启了publisherconfirm.机制或者事务机制的情况下,也可以迅速收到确认信息。此时又有个在深圳的业务(Client深圳)需要向exchangeA发送消息,那么(Client深圳)(broker北京)之间有很大的网络延迟,(Client深圳)将发送消息至exchangeA会经历一定的延迟,尤其是在开启了publisherconfirm.机制或者事务机制的情况下,(Client深圳)会等待很长的延迟时间来接收(broker北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。
将业务(Client深圳)部署到北京的机房可以解决这个问题,但是如果(Client深圳)调用的另些服务都部署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现?这里使用Federation插件就可以很好地解决这个问题.
搭建步骤
- 需要保证每台节点单独运行
- 在每台机器上开启federation相关插件
# 每台节点均需执行以下命令rabbitmq-plugins enable rabbitmq_federationrabbitmq-plugins enable rabbitmq_federation_management
版权归原作者 天天吃饭丶 所有, 如有侵权,请联系我们删除。