RabbitMQ核心功能
本文对RabbitMQ核心功能的介绍,没有介绍RabbitMQ的安装与集群,案列代码采用原生的Java代码和springboot两种形式,内容参考自RabbitMQ中文文档黑马程序员和尚硅谷RabbitMQ教程的笔记,还有一些自己的理解。
一.MQ的概念与功能介绍
1.MQ是什么?
🐘🐘 MQ(message queue),从字面意思上看就是消息·队列,满足队列的FIFO 先入先出特点,只不过队列中存放的内容是message,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ 是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了 MQ 之后,消息发送上游只需要依赖 MQ,不用依赖其他服务。
什么叫上下游传递消息:就好比老板给我发微信让我加班,老板就是上游,我就是下游,我和老板之间的通信就可以称为上下游传递消息。又好比两个分布式系统之间进行通信A系统(又称为生产者)给B系统(消费者)发送消息只需要依赖 MQ,不用依赖其他服务。
2.为什么要用MQ呢?
🐸🐸MQ优势1.应用解耦:因为系统的耦合性越高,容错性就越低,可维护性就越低,那么我们就要进行应用解耦。
假如有如下分布式系统用户购买完商品去订单系统下单,订单系统去操作库存系统修改订单,操作支付系统扣钱,提醒物流系统发货,如果其中有任何一个系统挂了那么跟着订单系统也挂了。再比如又添加了一个X系统那么又要去修改订单系统的代码操作X系统。
解决办法:使用MQ,万物皆可中间加一层,没有加一层解决不了的问题如果有那就再加一层。
此时如果用户再去下订单,订单系统只需要发一条消息给MQ就可以,MQ再去操作其他系统,订单系统就可以发一条消息告诉客户下单成功,其他的事情都交给MQ解决。假如此时库存系统又挂了,MQ可以等到库存系统修复了再发消息给库存系统让他减库存即可。此时订单系统与其他的系统之间耦合度便降低了,如果再添加X系统那么也还是通过MQ发送消息给X系统,此时系统的可扩展性就提高了。
🐻🐻MQ优势2.异步提速:同样用上面用户下单的例子来说明问题。
用户购买商品通过订单系统下单,此时订单系统要去操作库存系统支付系统物流系统然后再去操作自己的数据库,一系列操作完成后再去告诉用户下单成功。此时过去了920ms,十分影响用户体验。
加入MQ此时订单系统把订单消息发送给MQ然后就可以去操作数据库和返回消息给用户了。其他事情都交给MQ去做。用户点击完下单按钮后,只需等待25ms就能得到下单响应 (20 + 5 = 25ms)。
提升用户体验和系统吞吐量(单位时间内处理请求的数目)。
🐼🐼MQ优势三:流量削峰或者称为削峰填谷:A系统每秒最大处理1000个请求突然来了5000个请求,此时系统扛不住就要挂了。
解决方案:把请求发往MQ,然后A系统每秒从MQ中拉取1000个请求就行处理。使用了 MQ 之后,限制消费消息的速度为1000,这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。使用MQ后,可以提高系统稳定性。
MQ的劣势
💀💀系统可用性降低
系统引入的外部依赖越多,系统稳定性越差。一旦 MQ 宕机,就会对业务造成影响。需要考虑如何保证MQ的高可用?
💀💀 系统复杂度提高
MQ 的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。存在一下问题:
如何保证消息没有被重复消费?怎么处理消息丢失情况?怎么保证消息传递的顺序性?
💀💀一致性问题
A 系统处理完业务,通过 MQ 给B、C、D三个系统发消息数据,如果 B 系统、C 系统处理成功,D 系统处理失败。如何保证消息数据处理的一致性?
3.MQ有优势有劣势,那MQ什么时候可以使用?
- 生产者不需要从消费者处获得反馈。
- 容许短暂的不一致性。
- 确实是用了有效果。能够有解耦、提速、削峰这些方面的收益,并且带来的收益超过加入MQ后,管理MQ的成本。
4. 几种常见的MQ
MQ的选择
-
Kafka:
主要特点是基于 Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能,肯定是首选 kafka 了。 -
RocketMQ:
天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ 在稳定性上可能更值得信赖,这些业务场景在阿里双 11 已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。 -
RabbitMQ
结合 erlang 语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ。
二.RabbitMQ的介绍和入门案例
1.RabbitMQ的介绍
RabbitMQ 是Rabbit 技术公司基于 AMQP 标准开发的,采用 Erlang 语言开发,是一个消息中间件它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
2. AMQP是什么
AMQP(高级消息队列协议)是一个网络协议。它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信,模型图如下。
消息(message)被发布者(publisher)发送给交换机(exchange),交换机常常被比喻成邮局或者邮箱。然后交换机将收到的消息根据路由规则分发给绑定的队列(queue)。最后AMQP代理会将消息投递给订阅了此队列的消费者,或者消费者按照需求自行获取。
3.RabbitMQ的工作原理图和相关概念解释
因为RabbitMQ是基于 AMQP 标准开发的所以其原理图与AMQP模型图相差无几。
🐴🐴1.Producer: 产生数据发送消息的程序是生产者
🐑🐑2.Broker:接收和分发消息的应用,RabbitMQ Server 就是 Message Broker
🐍🐍3.Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等
🐥🐥4.Connection:publisher/consumer 和 broker 之间的 TCP 连接
🐧🐧5.Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP ,Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销
🐝🐝6.Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个由交换机类型决定
🐌🐌7.Queue:队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式
🐀🐀8.Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据
🐁🐁8.Consumer:消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。
注意:生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。
4.RabbitMQ入门程序
🐳🐳1.普通jiava程序,引入相关依赖
<dependencies>
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
生产者
public class Producer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("8.142.89.135");
factory.setUsername("guest");
factory.setPassword("guest");
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
try(Connection connection = factory.newConnection();Channel channel =
connection.createChannel()) {
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String message="hello world";
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
}
消费者
public class Consumer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("8.142.89.135");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
System.out.println("等待接收消息....");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String message= new String(delivery.getBody());
System.out.println(message);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback=(consumerTag)->{
System.out.println("消息消费被中断");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
🐬🐬2.SpringBoot入门程序
生产者消费者都引入一下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
生产者
@Configuration
public class HelloWorldConfig {
//交换机名称
public static final String DIRECT_EXCHANGE_NAME="direct_exchange";
//队列名称
public static final String QUEUE_NAME="hello_world";
//声明交换机
@Bean("helloExchange")
public DirectExchange helloExchange(){
return new DirectExchange(DIRECT_EXCHANGE_NAME);
}
//声明队列
@Bean("consoleQueue")
public Queue consoleQueue(){
return QueueBuilder.durable(QUEUE_NAME).build();
}
//绑定关系
@Bean
public Binding helloBinding(@Qualifier("helloExchange") DirectExchange exchange,@Qualifier("consoleQueue") Queue queue){
return BindingBuilder.bind(queue).to(exchange).with("hello");
}
}
@SpringBootTest
public class HelloWorldTest {
@Resource
private RabbitTemplate rabbitTemplate;
@Test
public void TestSendMsg(){
rabbitTemplate.convertAndSend(HelloWorldConfig.DIRECT_EXCHANGE_NAME,"hello","hello world");
}
}
消费者
@Component
public class HelloConsumer {
@RabbitListener(queues = "hello_world")
public void ListenerQueue(Message message){
String msg = new String(message.getBody());
System.out.println(msg);
}
}
启动消费者的主程序控制台下输出:
三.RabbitMQ的工作队列
1.工作队列的概念
WorkQueues工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。
相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
案例分析:我们启动两个消息消费者线程,一个消息生产者线程,我们来看看两个工作线程是如何工作的。
两个工作线程
public class Worker01 {
private static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String receivedMessage = new String(delivery.getBody());
System.out.println("接收到消息:"+receivedMessage);
};
CancelCallback cancelCallback=(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
};
System.out.println("C2 消费者启动等待消费......");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
public class Worker02 {
private static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String receivedMessage = new String(delivery.getBody());
System.out.println("接收到消息:"+receivedMessage);
};
CancelCallback cancelCallback=(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
};
System.out.println("C1 消费者启动等待消费......");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
生产者
public class Task01 {
private static final String QUEUE_NAME="hello";
public static void main(String[] args) throws Exception {
try(Channel channel=RabbitMqUtils.getChannel();) {
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//从控制台当中接受信息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送消息完成:"+message);
}
}
}
}
通过程序执行发现生产者总共发送 4 个消息,消费者 1 和消费者 2 分别分得两个消息,并且是按照有序的一个接收一次消息,是通过轮询的方式发送消息。
不公平分发机制
🐆🐆RabbitMQ默认情况下是通过轮询的方式发送消息。但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然很公平的进行分发。
为了避免这种情况,我们可以设置参数 channel.basicQos(1);
1为不公平分发,默认为0是公平分发。
消息预取值
🐩🐩本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息,另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。
这个时候就可以通过使用 如下方法设置“预取计数”值来完成的
void basicQos(int prefetchCount);
该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认。
例如,假设在通道上有未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何
消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知这个情况到并再发送一条消息。
🐪🐪消息应答和 QoS 预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。
预取值为 1 是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的。
2.消息应答机制
🐠🐠 问题分析 消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费者的消息,因为它无法接收到。
🐋🐋 为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
3.消息自动应答模式
🐏🐏消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。
使用方式只需要在发送消息时将 autoAck参数设置为true即可。
String basicConsume(String queue,
boolean autoAck, DeliverCallback deliverCallback,
CancelCallback cancelCallback)
4. 消息手动应答模式 可以批量应答并且减少网络拥堵
在发送消息时将 autoAck参数设置为false,并且需要指定应答策略
三种应答策略
//用于肯定确认 RabbitMQ 已知道该消息并且成功的处理消息,
//可以将其丢弃了 第一个参数代表的是消息的标记,
//第二个参数是代表是否批量应答
1. void basicAck(long deliveryTag, boolean multiple);
//用于否定确认 参数1:消息的标记
//参数2:是否批量应答
//参数3:是否重新入队
2.void basicNack(long deliveryTag, boolean multiple, boolean requeue)
//用于否定确认不处理该消息了直接拒绝,可以将其丢弃
// 参数含义同上
3.void basicReject(long deliveryTag, boolean requeue)
Multiple 的解释
multiple 的 true 和 false 代表不同意思
🐐🐐true 代表批量应答 channel 上未应答的消息
🐖🐖false 同上面相比:只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答
手动应答案例演示:
消费者:
public class TaskConsumerOne {
public static final String TASK_QUEUE_NAME="task";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqChannel.getChannel();
//是否自动应答
boolean autoAck=false;
//是否公平分发
//channel.basicQos(1);
channel.basicConsume(TASK_QUEUE_NAME,autoAck,(consumerTag,delivery)->{
try {
String message= new String(delivery.getBody());
//睡眠一秒钟
TimeUnit.SECONDS.sleep(1);
System.out.println("接受到的消息是:"+message);
//手动应答 arg1:消息标记,arg2:是否可以批处理
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
} catch (InterruptedException e) {
e.printStackTrace();
}
},consumerTag->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
public class TaskConsumerTwo {
public static final String TASK_QUEUE_NAME="task";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqChannel.getChannel();
//是否自动应答
boolean autoAck=false;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,(consumerTag,delivery)->{
try {
String message= new String(delivery.getBody());
//睡眠一分钟
TimeUnit.SECONDS.sleep(60);
System.out.println("接受到的消息是:"+message);
//手动应答 arg1:消息标记,arg2:是否可以批处理
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
} catch (InterruptedException e) {
e.printStackTrace();
}
},consumerTag->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
生产者:
public class ProductThree {
public static final String TASK_QUEUE_NAME="task";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqChannel.getChannel();
Scanner scanner = new Scanner(System.in);
//申明一个队列
channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);
System.out.println("请输入要发送的消息");
while (scanner.hasNextLine()){
String message = scanner.nextLine();
channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes("UTF-8"));
System.out.println("消息发送完毕");
}
}
}
正常情况下消息发送方发送两个消息 C1 和 C2 分别接收到消息并进行处理。
如果此时多输入几条消息则会被C1消费,因为C2还在睡眠
5.消息自动重新入队
🐊🐊 如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
比如P发送了两个消息一个发送给C1一个发送给C2,C1未确认信息,此时信息可以入队,队列可以把消息发送给其他消费者消费。
6. RabbitMQ 持久化
🐆🐆如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它会忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
队列如何实现持久化
之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化
queueDeclare(String queue, boolean durable,
boolean exclusive, boolean autoDelete,
Map<String, Object> arguments)
消息如何实现持久化
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。
channel.basicPublish("",QUEUE_NAME,
MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
🐕🐕将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。如果此时服务宕机了,则仍然不能持久化。
四.RabbitMQ的工作模式
在了解RabbitMQ的工作模式之前我们先来了解一下交换机的概念和类型。
Exchanges 概念
🐀🐀RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中。
相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
总共有以下类型:
直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)
无名 exchange
第一个参数是交换机的名称。空字符串表示默认或无名称交换机。
临时队列
系统会为我们创建一个随机名字的临时队列
String queueName = channel.queueDeclare().getQueue();
绑定(bindings)
🐞🐞什么是 bingding 呢,binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是 X 与 Q1 和 Q2 进行了绑定
多重绑定
如上图所示exchange 的绑定类型是 direct,它绑定的多个队列的 key 都相同,在这种情况下虽然绑定类型是 direct 但是它表现的就和 fanout 有点类似了,就跟广播差不多。
🐨🐨1. 简单模式 HelloWorld
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)。我们的入门程序就是一个简单模式。
🐵🐵2. 工作队列模式 Work Queue
一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。上面的工作队列就是工作队列模式。
🐎🐎 3. 发布订阅模式 Publish/subscribe
需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。
Fanout介绍
Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的所有队列中。
我们根据如上图实例来实现发布订阅模式
//ReceiveLogs01 将接收到的消息打印在控制台
public class ReceiveLogs01 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/**
* 生成一个临时的队列 队列的名称是随机的
* 当消费者断开和该队列的连接时 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息打印在屏幕.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("控制台打印接收到的消息"+message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
//ReceiveLogs02 将接收到的消息存储在磁盘
public class ReceiveLogs02 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/**
* 生成一个临时的队列 队列的名称是随机的
* 当消费者断开和该队列的连接时 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
//把该临时队列绑定我们的 exchange 其中 routingkey(也称之为 binding key)为空字符串
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息写到文件.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
File file = new File("C:\\work\\rabbitmq_info.txt");
FileUtils.writeStringToFile(file,message,"UTF-8");
System.out.println("数据写入文件成功");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
EmitLog 发送消息给两个消费者接收
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitUtils.getChannel()) {
/**
* 声明一个 exchange
* 1.exchange 的名称
* 2.exchange 的类型
*/
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
}
🐒🐒 4. 路由模式 Routing
需要设置类型为 direct 的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
Direct exchange 介绍
Direct exchange的工作方式是,消息只去到它绑定的routingKey 队列中去
在这种绑定情况下,生产者发布消息到 exchange 上,绑定键为 orange 的消息会被发布到队列Q1。绑定键为 blackgreen 和的消息会被发布到队列 Q2,其他消息类型的消息将被丢弃。
案例根据如下图示来发送消息:
public class ReceiveLogsDirect01 {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = "disk";
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "error");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
message="接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message;
File file = new File("C:\\work\\rabbitmq_info.txt");
FileUtils.writeStringToFile(file,message,"UTF-8");
System.out.println("错误日志已经接收");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
public class ReceiveLogsDirect02 {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = "console";
channel.queueDeclare(queueName, false, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "info");
channel.queueBind(queueName, EXCHANGE_NAME, "warning");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收绑定键 :"+delivery.getEnvelope().getRoutingKey()+", 消
息:"+message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
public class EmitLogDirect {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitUtils.getChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//创建多个 bindingKey
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("info","普通 info 信息");
bindingKeyMap.put("warning","警告 warning 信息");
bindingKeyMap.put("error","错误 error 信息");
//debug 没有消费这接收这个消息 所有就丢失了
bindingKeyMap.put("debug","调试 debug 信息");
for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,bindingKey, null,
message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
}
🐮🐮 5. 通配符模式 Topic
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert
需要设置类型为 topic 的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
public class TopicConsumerOne {
public static final String EXCHANGE_NAME="topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqChannel.getChannel();
//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String queue="Q1";
channel.queueDeclare(queue,false,false,false,null);
//绑定队列
channel.queueBind(queue,EXCHANGE_NAME,"*.orange.*");
System.out.println("绑定完成,Q1准备接收消息");
channel.basicConsume(queue,true,((consumerTag, message) -> {
String msg =new String(message.getBody(),"UTF-8");
System.out.println("路由key是"+message.getEnvelope().getRoutingKey()+"接受到的消息是:"+msg);
}),consumerTag -> {});
}
}
public class TopicConsumerTwo {
public static final String EXCHANGE_NAME="topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqChannel.getChannel();
//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
String queue="Q2";
channel.queueDeclare(queue,false,false,false,null);
//绑定队列
channel.queueBind(queue,EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(queue,EXCHANGE_NAME,"lazy.#");
System.out.println("绑定完成,Q2准备接收消息");
channel.basicConsume(queue,true,((consumerTag, message) -> {
String msg =new String(message.getBody(),"UTF-8");
System.out.println("路由key是"+message.getEnvelope().getRoutingKey()+"接受到的消息是:"+msg);
}),consumerTag -> {});
}
}
public class TopicProducter {
public static final String EXCHANGE_NAME="topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqChannel.getChannel();
//创建交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put(" quick.orange.rabbit","被队列 Q1Q2 接收到");
bindingKeyMap.put(" lazy.orange.elephant","被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox"," 被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit"," 是四个单词但匹配 Q2");
//发送消息
for (Map.Entry<String, String> entry : bindingKeyMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
channel.basicPublish(EXCHANGE_NAME,key,null,value.getBytes("UTF-8"));
}
System.out.println("消息发送完毕");
}
}
五.RabbitMQ的发布确认
1.发布确认原理
🐜🐜生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
🐃🐃confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
2.发布确认的策略
发布确认默认是没有开启的,如果要开启需要调用方法confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法。
1) 单个确认发布
🐆🐆这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。
🐌🐌这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
2)批量确认发布
上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量。
🐘🐘当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
3)异步确认发布
🐴🐴异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功。
三种发布确认代码如下:
//三种发布确认消息的耗时情况
public class ProductMessageAck {
public static final int message_number =200;
public static void main(String[] args) throws Exception {
//单个发布确认 发布200个消息完成所花费的时间是:8257ms
//SinglePublishAck();
//批量发布确认 发布200个消息完成所花费的时间是:227ms
// BatchPublishAck();
//异步发布确认 发布200个消息完成所花费的时间是:45ms
AsyncPublishAck();
}
//单个发布确认
public static void SinglePublishAck() throws Exception{
//获取信道
Channel channel=RabbitMqChannel.getChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
String queue_name= UUID.randomUUID().toString();
channel.queueDeclare(queue_name,false,false,false,null);
//设置发布确认
channel.confirmSelect();
//开始时间
long startTime=System.currentTimeMillis();
for (int i = 0; i < message_number; i++) {
String message=i+"";
channel.basicPublish("",queue_name,null,message.getBytes());
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
if(flag){
System.out.println("消息发送成功");
}
}
//结束时间
long endTime=System.currentTimeMillis();
System.out.println("发布"+message_number+"个消息完成所花费的时间是:"+(endTime-startTime)+"ms");
}
//批量发布确认
public static void BatchPublishAck() throws Exception{
//获取信道
Channel channel=RabbitMqChannel.getChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
String queue_name= UUID.randomUUID().toString();
channel.queueDeclare(queue_name,false,false,false,null);
//设置发布确认
channel.confirmSelect();
//开始时间
long startTime=System.currentTimeMillis();
for (int i = 0; i < message_number; i++) {
String message=i+"";
channel.basicPublish("",queue_name,null,message.getBytes());
if(i%50==0){
//发送50条之后确认一次
channel.waitForConfirms();
}
}
//结束时间
long endTime=System.currentTimeMillis();
System.out.println("发布"+message_number+"个消息完成所花费的时间是:"+(endTime-startTime)+"ms");
}
//异步发布确认
public static void AsyncPublishAck() throws Exception{
//获取信道
Channel channel=RabbitMqChannel.getChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
String queue_name= UUID.randomUUID().toString();
channel.queueDeclare(queue_name,false,false,false,null);
//设置发布确认
channel.confirmSelect();
//开始时间
long startTime=System.currentTimeMillis();
/**
* 线程安全有序的一个哈希表,适用于高并发的情况
* 1.轻松的将序号与消息进行关联
* 2.轻松批量删除条目 只要给到序列号
* 3.支持并发访问
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms = new
ConcurrentSkipListMap<>();
/**
* 确认收到消息的一个回调
* 1.消息序列号
* 2.true 可以确认小于等于当前序列号的消息
* false 确认当前序列号消息
*/
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
//返回的是小于等于当前序列号的未确认消息 是一个 map
ConcurrentNavigableMap<Long, String> confirmed =
outstandingConfirms.headMap(sequenceNumber, true);
//清除该部分未确认消息
confirmed.clear();
}else{
//只清除当前序列号的消息
outstandingConfirms.remove(sequenceNumber);
}
};
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
String message = outstandingConfirms.get(sequenceNumber);
System.out.println("发布的消息"+message+"未被确认,序列号"+sequenceNumber);
};
/**
* 添加一个异步确认的监听器
* 1.确认收到消息的回调
* 2.未收到消息的回调
*/
channel.addConfirmListener(ackCallback, nackCallback);
long begin = System.currentTimeMillis();
for (int i = 0; i < message_number; i++) {
String message = "消息" + i;
/**
* channel.getNextPublishSeqNo()获取下一个消息的序列号
* 通过序列号与消息体进行一个关联
* 全部都是未确认的消息体
*/
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queue_name, null, message.getBytes());
}
//结束时间
long endTime=System.currentTimeMillis();
System.out.println("发布"+message_number+"个消息完成所花费的时间是:"+(endTime-startTime)+"ms");
}
}
3.发布确认 springboot 版本
代码架构图
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
⚫ NONE
禁用发布确认模式,是默认值
⚫ CORRELATED
发布消息成功到交换器后会触发回调方法
⚫ SIMPLE
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法
等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是
waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
配置类
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//声明业务 Exchange
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
// 声明确认队列绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
}
回调接口
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
/**
* 交换机不管是否收到消息的一个回调方法
* CorrelationData
* 消息相关数据
* ack
* 交换机是否收到消息
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}else{
log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
}
}
}
@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private MyCallBack myCallBack;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(myCallBack);
}
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable String message){
//指定消息 id 为 1
CorrelationData correlationData1=new CorrelationData("1");
String routingKey="key1";
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message+routingKey,correlationData1);
CorrelationData correlationData2=new CorrelationData("2");
routingKey="key2";
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message+routingKey,correlationData2);
log.info("发送消息内容:{}",message);
}
}
消费者
@Component
@Slf4j
public class ConfirmConsumer {
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
@RabbitListener(queues =CONFIRM_QUEUE_NAME)
public void receiveMsg(Message message){
String msg=new String(message.getBody());
log.info("接受到队列 confirm.queue 消息:{}",msg);
}
}
🐥🐥可以看到,发送了两条消息,第一条消息的 RoutingKey 为 “key1”,第二条消息的 RoutingKey 为"key2",两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。
回退消息
🐧🐧在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。
🐇🐇通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
@Slf4j
@Component
public class MessageProducer implements RabbitTemplate.ConfirmCallback ,
RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//rabbitTemplate 注入之后就设置该值
@PostConstruct
private void init() {
rabbitTemplate.setConfirmCallback(this);
/**
* true:
* 交换机无法将消息进行路由时,会将该消息返回给生产者
* false:
* 如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
//设置回退消息交给谁处理
rabbitTemplate.setReturnCallback(this);
}
@GetMapping("sendMessage")
public void sendMessage(String message){
//让消息绑定一个 id 值
CorrelationData correlationData1 = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("confirm.exchange","key1",message+"key1",correlationData1)
;
log.info("发送消息 id 为:{}内容为{}",correlationData1.getId(),message+"key1");
CorrelationData correlationData2 = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("confirm.exchange","key2",message+"key2",correlationData2)
;
log.info("发送消息 id 为:{}内容为{}",correlationData2.getId(),message+"key2");
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id = correlationData != null ? correlationData.getId() : "";
if (ack) {
log.info("交换机收到消息确认成功, id:{}", id);
} else {
log.error("消息 id:{}未成功投递到交换机,原因是:{}", id, cause);
}
}
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String
exchange, String routingKey) {
log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
new String(message.getBody()),replyText, exchange, routingKey);
}
}
回调接口
@Component
@Slf4j
public class MyCallBack implements
RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
/**
* 交换机不管是否收到消息的一个回调方法
* CorrelationData
* 消息相关数据
* ack
* 交换机是否收到消息
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}else{
log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
}
}
//当消息无法路由的时候的回调方法
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String
exchange, String routingKey) {
log.error(" 消 息 {}, 被交换机 {} 退回,退回原因 :{}, 路 由 key:{}",new
String(message.getBody()),exchange,replyText,routingKey);
}
}
六.RabbitMQ的死性队列
死信的概念
🐰🐰先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
🐯🐯应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时自动失效。
Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。
消息成为死信的三种情况:
- 队列消息长度到达限制;
- 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
- 原队列存在消息过期设置,消息到达超时时间未被消费;
死性队列实战架构图
🐗🐗1.消息 TTL 过期
队列中第五个参数map中可以设置的参数
参数 | 作用 |
---|---|
x-message-ttl | 设置ttl |
x-expires | 设置消息过期时间 |
x-max-length | 设置队列接收消息的长度 |
x-max-length-bytes | 设置消息的最大字节长度 |
x-overflow | |
x-dead-letter-exchange | 设置死性队列名 |
x-dead-letter-routing-key | 设置死性队列路由名 |
x-max-priority | 设置队列优先级 |
x-queue-mode | 设置队列模式 |
x-queue-type | 设置队列类型 |
x-delivery-limit | delivery的最大长度限制 |
x-single-active-consumer |
我们将C1开启之后然后关闭再开启C2此时消息会发送到C2中
public class DeadQueueConsumerOne {
public static final String NORMAL_EXCHANGE="normal_exchange";
public static final String NORMAL_QUEUE="normal_queue";
public static final String DEAD_EXCHANGE="dead_exchange";
public static final String DEAD_QUEUE="dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqChannel.getChannel();
//创建交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明队列
//设置参数
//正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "wf");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,params);
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"wlf");
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"wf");
//消费消息
channel.basicConsume(NORMAL_QUEUE,true,(consumerTag, message) -> {
String msg=new String(message.getBody(), StandardCharsets.UTF_8);
System.out.println("接收到的消息是:"+msg);
},consumerTag -> {});
}
}
public class DeadQueueConsumerTwo {
public static final String DEAD_EXCHANGE="dead_exchange";
public static final String DEAD_QUEUE="dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqChannel.getChannel();
//创建交换机
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"wf");
//消费消息
channel.basicConsume(DEAD_QUEUE,true,(consumerTag, message) -> {
String msg=new String(message.getBody(), StandardCharsets.UTF_8);
System.out.println("死信队列接收到的消息是:"+msg);
},consumerTag -> {});
}
}
public class DeadQueueProduce {
private static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws Exception{
try(Channel channel= RabbitMqChannel.getChannel()){
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置消息的 TTL 时间
AMQP.BasicProperties properties = new
AMQP.BasicProperties().builder().expiration("10000").build();
for (int i = 1; i < 11; i++) {
//发送消息
String msg="info"+i;
channel.basicPublish(NORMAL_EXCHANGE,"wlf",properties,msg.getBytes(StandardCharsets.UTF_8));
}
System.out.println("生产者消息发送完毕");
}
}
}
🐌🐌 2.队列达到最大长度
将正常队列收到的消息设置为6,向队列中发送10条消息,死性队列中收到4条。
@Configuration
public class DeadQueueConfig {
public static final String NORMAL_EXCHANGE="normal_exchange";
public static final String NORMAL_QUEUE="normal_queue";
public static final String DEAD_EXCHANGE="dead_exchange";
public static final String DEAD_QUEUE="dead_queue";
@Bean("normalExchange")
public DirectExchange normalExchange(){
return new DirectExchange(NORMAL_EXCHANGE);
}
@Bean("deadExchange")
public DirectExchange deadExchange(){
return new DirectExchange(DEAD_EXCHANGE);
}
@Bean("normalQueue")
public Queue normalQueue(
){
return QueueBuilder
.durable(NORMAL_QUEUE)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey("lishi")
.maxLength(6)
.build();
}
@Bean("deadQueue")
public Queue deadQueue(){
return QueueBuilder.durable(DEAD_QUEUE).build();
}
@Bean
public Binding normalQueueBinding(
@Qualifier("normalQueue") Queue queue,
@Qualifier("normalExchange")DirectExchange exchange
){
return BindingBuilder.bind(queue).to(exchange).with("zhangsan");
}
@Bean
public Binding deadQueueBinding(
@Qualifier("deadQueue") Queue queue,
@Qualifier("deadExchange")DirectExchange exchange
){
return BindingBuilder.bind(queue).to(exchange).with("lishi");
}
}
@Test
public void TestDeadMsg(){
for (int i = 1; i < 11; i++) {
String msg="info"+i;
rabbitTemplate.convertAndSend(DeadQueueConfig.NORMAL_QUEUE,"zhangsan",msg);
}
}
@Component
@RabbitListener(queues = "normalQueue")
public class DeadConsumerTwo {
@RabbitHandler
public void ReceiveMessage(String message){
System.out.println("normalQueue---->receive message is:"+message);
}
}
@Component
@RabbitListener(queues = "deadQueue")
public class DeadConsumerTwo {
@RabbitHandler
public void ReceiveMessage(String message){
System.out.println("deadQueue---->receive message is:"+message);
}
}
🐡🐡 3.消费者拒接消费消息:设置拒绝接受第五条消息
public class Consumer01 {
//普通交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitUtils.getChannel();
//声明死信和普通交换机 类型为 direct
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明死信队列
String deadQueue = "dead-queue";
channel.queueDeclare(deadQueue, false, false, false, null);
//死信队列绑定死信交换机与 routingkey
channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
//正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
String normalQueue = "normal-queue";
channel.queueDeclare(normalQueue, false, false, false, params);
channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
if(message.equals("info5")){
System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
//requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
}else {
System.out.println("Consumer01 接收到消息"+message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
boolean autoAck = false;
channel.basicConsume(normalQueue, autoAck, deliverCallback, consumerTag -> {
});
}
}
七.RabbitMQ的延迟队列
1. 延迟队列概念
🐂🐂延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的元素的队列。
2.延迟队列相关使用场景
- 订单在十分钟之内未支付则自动取消
- 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
- 用户注册成功后,如果三天内没有登陆则进行短信提醒。
- 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
- 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
3. RabbitMQ 中的 TTL
- TTL 全称 Time To Live(存活时间/过期时间,单位是毫秒)。
- RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
- 如果在 TTL 设置的时间内没有被消费,则会成为"死信"。
- 如果同时配置了队列的 TTL 和消息的
TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
消息声明TTL
队列声明TTL
🐆🐆如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;
💐💐另外,还需要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
延时队列=TTL+死性队列+一点点调味品
延时队列,不就是想要消息延迟多久被处理吗,TTL 则刚好能让消息在延迟多久之后成为死信,另一方面,成为死信的消息都会被投递到死信队列里,这样只需要消费者一直消费死信队列里的消息就完事了,因为里面的消息都是希望被立即处理的消息。
队列 TTL实战
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后在创建一个交换机 X 和死信交换机 Y,它们的类型都是 direct,创建一个死信队列 QD,它们的绑定关系如下:
@Configuration
public class TTLQueueConfig {
public static final String X_EXCHANGE = "X";
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String DEAD_LETTER_QUEUE = "QD";
// 声明 xExchange
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明 xExchange
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明队列 A ttl 为 10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
// 声明队列 A 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明队列 B ttl 为 40s 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD(){
return new Queue(DEAD_LETTER_QUEUE);
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
@Test
void sendMsgToQD(){
String message="hello world";
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
}
消息消费者代码
@Slf4j
@Component
public class DeadLetterQueueConsumer {
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
😹😹第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。
问题分析
💀💀如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
优化后的架构图:添加了一个不设置时间的队列,可以在发送消息时设置信息的发送时间,这样更加的灵活。
添加一个配置
@Configuration
public class MsgTtlQueueConfig {
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
public static final String QUEUE_C = "QC";
//声明队列 C 死信交换机
@Bean("queueC")
public Queue queueB(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//没有声明 TTL 属性
return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
}
@Test
void contextLoads() {
String message="hello world";
String ttlTime="20000";
rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}
控制台输出:
当前时间:Thu Aug 19 09:55:29 CST 2021,发送一条时长20000毫秒
TTL 信息给队列 C:hello world
当前时间:Thu Aug 19 09:55:49 CST 2021,收到死信队列信息hello world
问题分析
🙈🙈看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
😸😸解决办法:安装延时队列插件
在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载
rabbitmq_delayed_message_exchange
插件,然后解压放置到 RabbitMQ 的插件目录。进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ。
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:
配置如下
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//自定义交换机 我们在这里定义的是一个延迟交换机
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
//自定义交换机的类型
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
args);
}
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
@Qualifier("delayedExchange") CustomExchange
delayedExchange) {
return
BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}