@TOC
MQ相关概念
MQ(message queue),从字面意思上看,本质是个队列,FIFO先入先出,只不过队列中存放的内容是message而已,还是一种跨进程的通信机制,用于上下游传递消息。在互联网架构中,MQ是一种非常常见的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务。
作用
- 流量消峰
- 应用解耦
- 异步处理
MQ的分类
| 名称 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ActiveMQ | 单机吞吐量万级,时效性ms级,可用性高,基于主从架构实现高可用性,消息可靠性较低的概率丢失数据 | 维护越来越少,高吞吐量场景较少使用 | ActiveMQ支持任何消息传递用例的能力和灵活性,比较适合小型吞吐量比较小的公司进行使用 |
| Kafka | 大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开Kafka,这款为大数据而生的消息中间件,以其百万级TPS的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输存储的过程中发挥着举足轻重的作用。目前已经被LinkedIn,Uber,Twitter,Netflix等大公司所采纳。性能卓越,单机写入TPS约在百万条/秒,最大的优点,就是吞吐量高。时效性ms级可用性非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用,消费者采用Pull方式获取消息,消息有序,通过控制能够保证所有消息被消费且仅被消费一次;有优秀的第三方Kafka Web管理界面Kafka-Manager;在日志领域比较成熟,被多家公司和多个开源项目使用;功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用 | Kafka单机超过64个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长,使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试;支持消息顺序,但是一台代理宕机后,就会产生消息乱序,社区更新较慢; | 适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能,肯定是首选kafka了 |
| RocketMQ | 出自阿里巴巴的开源产品,用Java语言实现,在设计时参考了Kafka,并做出了自己的一些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理binglog分发等场景。 单机吞吐量十万级,可用性非常高,分布式架构,消息可以做到0丢失,MQ功能较为完善,还是分布式的,扩展性好,支持10亿级别的消息堆积,不会因为堆积导致性能下降,源码是java我们可以自己阅读源码,定制自己公司的MQ | 支持的客户端语言不多,目前是java及c++,其中c++不成熟;社区活跃度一般,没有在MQ核心中去实现JMS等接口,有些系统要迁移需要修改大量代码 | 天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择RocketMQ。 |
| RabbitMQ | 2007年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。由于erlang语言的高并发特性,性能较好;吞吐量到万级,MQ功能比较完备,健壮、稳定、易用、跨平台、支持多种语言如: Python、Ruby、.NET、 Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高 | 商业版需要收费,学习成本较高 | 结合erlang语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的RabbitMO |
RabbitMQ
RabbitMQ是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑RabbitMQ是一个快递站,一个快递员帮你传递快件。RabbitMQ.与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。
概念
生产者:产生数据发送消息的程序是生产者交换机:交换机是RabbitMQ非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推关到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定队列:队列是RabbitMQ内部使用的一种数据结构,尽管消息流经RabbitMQ和应用程序但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式消费者与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。 核心部分Borker: 接受和分发消息的应用,RabbitMQ Server就是Message BrokerVirtual host: 出于多租户和安全因素设计的,把AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的namespace.概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange / queue 等Connection: publisher / consumer和broker之间的TCP连接Channel: 如果每一次访问 RabbitMQ 都建立一个Connection,在消息量大的时候建立TCPConnection的开销将是巨大的,效率也较低。Channel是在connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread 创建单独的channel进行通讯,AMQP method包含了channel 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的分发依据
安装
初识
- 引入如下依赖
<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>
- 编写生产者
注意:此处需要防火墙开启5672端口
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
/**
* @Description: 生产者
*/
public class Producer {
// 队列名称
public static final String QUEUE_NAME="hello";
// 发消息
public static void main(String[] args) throws IOException, TimeoutException {
// 创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 工厂IP连接RabbitMQ的队列
factory.setHost("192.18.31.89");
// 用户名
factory.setUsername("admin");
// 密码
factory.setPassword("admin");
factory.setPort(5672);
// 创建连接
Connection connection = factory.newConnection();
// 获取信道
Channel channel = connection.createChannel();
/*
* 生成一个队列
* 参数1:队列名称
* 参数2:是否将队列里面的消息持久化到磁盘,默认情况下,消息存储在内存中
* 参数3:该队列是否只供一个消费者进行消费,是否进行消费共享,true可以多个消费者消费,
* false只能一个消费者消费
* 参数4:是否自动删除:最后一个消费者断开连接之后,该队列是否自动删除,true则自动删除,
* false不自动删除
* 参数5:传递一些队列的参数
* */
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 发消息
String message = "hello world";
/*
* 发送一个消息
* 参数1:发送到哪个交换机
* 参数2:路由的key值是那个,本次是队列的名称
* 参数3:其他参数信息
* 参数4:发送消息的消息体
* */
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("消息发送完毕!");
}
}
控制台查看就会有一条没有被消费的消息
- 编写消费者
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @Description: 消费者
*/
public class Consumer {
// 队列名称,需要和生产者保持一致
public static final String QUEUE_NAME = "hello";
// 接受消息
public static void main(String[] args) throws IOException, TimeoutException {
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 工厂IP连接RabbitMQ的队列
factory.setHost("192.168.31.89");
// 用户名
factory.setUsername("admin");
// 密码
factory.setPassword("admin");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//消息接收成功的回调方法
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody()));
};
// 消息接收失败的回调方法
CancelCallback cancelCallback = consumer -> {
System.out.println("消息接收失败");
};
/*
* 消费者接收消息
* 参数1:表示消费哪个UI列
* 参数2:消费成功之后,是否需要自动应答,true表示自动应答,false表示手动应答
* 参数3:消费者成功消费的回调
* 参数4:消费者接收消息失败的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
任务队列
任务队列(又称工作队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个工作线程时,这些工作线程将一起处理这些任务。
- 实现任务队列 1.抽取连接工厂工具类
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
/**
* @Description: rabittmq连接工具类
*/
public class RabbitMqUtils {
/**
* 获取信道
* @return
* @throws Exception
*/
public static Channel getChannel() throws Exception{
// 创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 工厂IP连接RabbitMQ的队列
factory.setHost("192.168.31.89");
// 用户名
factory.setUsername("admin");
// 密码
factory.setPassword("admin");
factory.setPort(5672);
Connection connection = factory.newConnection();
return connection.createChannel();
}
}
2.编写消费者
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者
*/
public class Worker01 {
// 队列名称,需要和生产者保持一致
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//消息接收成功的回调方法
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("消费者接收到的消息-->"+new String(message.getBody()));
};
// 消息接收失败的回调方法
CancelCallback cancelCallback = consumer -> {
System.out.println("消费者接收消息失败");
};
/*
* 消费者接收消息
* 参数1:表示消费哪个UI列
* 参数2:消费成功之后,是否需要自动应答,true表示自动应答,false表示手动应答
* 参数3:消费者成功消费的回调
* 参数4:消费者接收消息失败的回调
*/
System.out.println("c1启动成功,等待接收消息");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
3.设置IDE允许并行运行
4.启动代码运行,将此处的打印,分别改为
c2,点击运行,改为c3,点击运行
System.out.println("c2启动成功,等待接收消息");
运行效果:
5.编写生产者
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* @Description: 生产者
*/
public class Task01 {
// 队列名称
public static final String QUEUE_NAME = "hello";
// 发送大量消息
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
// 队列的声明
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 从控制台中输入消息
Scanner scanner = new Scanner(System.in);
System.out.println("请输入需要推送的消息:");
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("发送消息完成:"+ message);
}
}
}
6.启动生产者,发送消息
发送:
接收:
第一条消息由
c2接收:
第二条消息由
c1接收:
c3没有接收到消息
消息应答
消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会发生什么情况。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
- 自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用
- 手动应答
手动应答的好处是可以批量应答并且减少网络拥堵
手动应答的方法:
channel.basicAck();:用于肯定确认告知RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
channel.basicNack():用于批量消息否定确认
channel.basicReject():用于单个消息否定确认
- 消息自动重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP连接丢失),导致消息未发送ACK确认,RabbitMQ将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
实现手动应答:
生产者:
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* @Description: 生产者----消息手动应答
*/
public class Task2 {
// 队列名称
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
// 声明队列
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
Scanner scanner = new Scanner(System.in);
System.out.println("请输入需要推送的消息:");
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish("", TASK_QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发出消息:" + message);
}
}
}
消费者1:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者----消息手动应答
*/
/*
* 消费者
* */
public class Worker03 {
// 队列名称
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C1等待接受消息处理时间较短");
DeliverCallback deliverCallback = (consumerTag, message) -> {
//模拟其他业务处理,需要1s的执行时间
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接受到的消息是:"+new String(message.getBody(),"UTF-8"));
//进行手动应答
/*
* 参数1:消息的标记 tag
* 参数2:是否批量应答,false:不批量应答 true:批量
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
// 采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag) -> {
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
消费者2:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者----消息手动应答
*/
/*
* 消费者
* */
public class Worker03 {
// 队列名称
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C2等待接受消息处理时间30s");
DeliverCallback deliverCallback = (consumerTag, message) -> {
//模拟其他业务处理,需要30s的执行时间
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接受到的消息是:"+new String(message.getBody(),"UTF-8"));
//进行手动应答
/*
* 参数1:消息的标记 tag
* 参数2:是否批量应答,false:不批量应答 true:批量
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
// 采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag) -> {
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
测试效果:
正常情况下在Task2中发送消息C1 和 C2会按照轮询的机制分别接收到消息,并进行处理,在上述代码中我先发送一个消息aa,可以看到由C1接收了,在发送一个bb由C2接收了,只是因为C2我们设定的时间为30s所以等等一会才能看见消息bb,当我们再发送cc,此时按照轮询机制消息再次被C1所接收,再发送dd后,我们模拟C2宕机,关闭C2,此处会发现这条本来应该由C2接收的消息就会因为C2无法接收消息,消息 dd 被重新入队,然后分配给能处理消息的 C1 处理了
生产者发送的消息:
C1接收到的消息:
C2接收到的消息:
队列和消息的持久化
默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要将队列和消息都标记为持久化。
- 队列持久化
实现持久化需要在声明队列的时候把 durable 参数设置为
true,开启持久化
注意:如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现如下错误
channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'ack_queue' in vhost '/': received 'true' but current is 'false', class-id=50, method-id=10)
删除队列:
开启队列持久化
Channel channel = RabbitMqUtils.getChannel();
//开启队列持久化
boolean durable=true;
// 声明队列
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
控制台查看持久化结果
这个时候即使重启 ack_queue队列也依然存在
- 消息持久化
实现消息持久化需要在消息生产者修改代码, 添加
MessageProperties.PERSISTENT_TEXT_PLAIN这个属性。
注意:将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 ,还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。后续还需要添加更强有力的持久化策略。
开启消息持久化
Channel channel = RabbitMqUtils.getChannel();
//开启队列持久化
boolean durable=true;
// 声明队列
channel.queueDeclare(TASK_QUEUE_NAME, durable, false, false, null);
String message = "我是一条消息";
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发出消息:" + message);
不公平分发
RabbitMQ
默认分发消息采用的是轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2 处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然很公平的进行分发。所谓不公平分发就是,通过对RabbitMQ 的一些设置,让他在对消费者进行消息分发时,采用“能者多劳”的模式进行分发,消费快的,多分发一些,消费慢的少分发一些
在消费者端开启不公平分发
// 设置不公平分发,默认设置的是0
int prefetchCount = 1;
channel.basicQos(prefetchCount);
消费者:
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者----消息手动应答
*/
/*
* 消费者
* */
public class Worker03 {
// 队列名称
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C2等待接受消息处理时间30s");
DeliverCallback deliverCallback = (consumerTag, message) -> {
//模拟其他业务处理,需要30s的执行时间
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接受到的消息是:"+new String(message.getBody(),"UTF-8"));
//进行手动应答
/*
* 参数1:消息的标记 tag
* 参数2:是否批量应答,false:不批量应答 true:批量
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
// 设置预取值为1,从而实现不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
// 采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag) -> {
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
预取值
本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息,另外来自消费 者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此 缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用
channel.basicQos()方法设 置“预取计数”值来完成。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有未确认的消息 1、2、3,4,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被确认。比方说 tag=1 这个消息刚刚被确认 ,RabbitMQ 将会感知这个情况到并再发送一条消息。消息应答和预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同, 100 到 300 范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的,也就是上文中的不公平分发。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,或者是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的。 通俗一点来说预取值就是,该消费者在消费消息时可达到的最大峰值,超过这个峰值,该消费者将暂停接收消息,等通道中有一个消息被确认,峰值达到空缺,那么他才会再次接收一个消息
- 在消费者端设置预取值
// 设置预取值为5
int prefetchCount =5;
channel.basicQos(prefetchCount);
发布确认
- 原理
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。
- 开启发布确认
发布确认默认是没有开启的,如果要开启需要在生产者端调用方法
channel.confirmSelect()
Channel channel = RabbitMqUtils.getChannel();
//开启发布确认
channel.confirmSelect();
- 发布确认的策略
单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,
waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* @Description: 生产者----发布确认
*/
public class ReleaseConfirmationMessage {
// 批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
// 1、单个确认
// 发布1000个单独确认消息,耗时46121ms
publishMessageIndividually();
}
/**
* 单个确认发布
* @throws Exception
*/
public static void publishMessageIndividually() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
// 开启发布确认
channel.confirmSelect();
// 开始时间
long begin = System.currentTimeMillis();
// 批量发消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
// 单个消息马上进行发布确认
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发送成功-->"+i);
}
}
// 结束时间
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个单独确认消息,耗时"+ (end - begin) + "ms");
}
}
批量确认发布
与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
package com.wf.rabittmqstudy.four;
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* @Description: 生产者----发布确认
*/
public class ReleaseConfirmationMessage {
// 批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
// 发布1000个批量确认发布消息,耗时781ms
publishMessageBatch();
}
/**
* 批量确认发布
* @throws Exception
*/
public static void publishMessageBatch() throws Exception{
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
// 开启发布确认
channel.confirmSelect();
// 开始时间
long begin = System.currentTimeMillis();
// 批量确认消息大小
int batchSize = 100;
// 批量发送 批量确认
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
// 判断达到100条消息的时候,批量确认一次
if (i%batchSize == 0){
// 确认发布
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发送成功--》"+i);
}
}
}
// 结束时间
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个批量确认消息,耗时"+ (end - begin) + "ms");
}
}
异步发布确认
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功
package com.wf.rabittmqstudy.four;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* @Description: 生产者----发布确认
*/
public class ReleaseConfirmationMessage {
// 批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
// 发布1000个异步确认消息,耗时141ms
publicMessageAsync();
}
/**
* 异步确认
* @throws Exception
*/
public static void publicMessageAsync() throws Exception{
Channel channel = RabbitMqUtils.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName,false,false,false,null);
// 开启发布确认
channel.confirmSelect();
/*
* 创建一个线程安全的Map
* */
ConcurrentSkipListMap<Long,String> outstandingConfirms = new ConcurrentSkipListMap<>();
// 开始时间
long begin = System.currentTimeMillis();
// 消息确认成功回调函数
ConfirmCallback ackCallback = (deliveryTag, multiply) -> {
//判断是批量确认时
if(multiply){
// 删除到已经确认的消息,剩下的就是未确认的消息
ConcurrentNavigableMap<Long, String> confiremed = outstandingConfirms.headMap(deliveryTag);
confiremed.clear();
}else {
//删除单个确认的消息
outstandingConfirms.remove(deliveryTag);
}
System.out.println("确认的消息标记:"+deliveryTag);
};
// 消息确认失败回调函数
/*
* 参数1:消息的标记
* 参数2:是否为批量确认
* */
ConfirmCallback nackCallback = (deliveryTag,multiply) -> {
// 获取为确认的消息
String message = outstandingConfirms.get(deliveryTag);
System.out.println("未确认的消息是:" + message +"未确认的消息tag:" + deliveryTag);
};
// 准备消息的监听器,监听哪些消息成功,哪些消息失败
/*
* 参数1:监听哪些消息成功
* 参数2:监听哪些消息失败
* */
channel.addConfirmListener(ackCallback,nackCallback);
// 批量发送消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = "消息" + i;
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
// 此处记录下所有要发送的消息
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
}
// 结束时间
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个异步确认消息,耗时"+ (end - begin) + "ms");
}
}
交换机
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产 者甚至都不知道这些消息传递传递到了哪些队列中。 相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来 自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消 息放到特定队列还是说把他们放到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定。
交换机的类型 直接(direct), 主题(topic) ,标题(headers) , 扇出(fanout)
无名交换机(AMQP)
之前能实现将消息发送到队列的原因是因为我们使用的是默认交换机(AMQP),我们通过空字符串(“”)进行标识。
//第一个参数是交换机的名称。空字符串表示默认或无名称交换机
//消息能路由发送到队列中其实是由 routingKey(bindingkey)绑定 key 指定的,如果它存在的话
channel.basiPublish("","hello",null,message.getBytes());
临时队列
队列名称随机,其次一旦我们断开了消费者的连接,队列将被自动删除
创建方式
Channel channel = RabbitMqUtils.getChannel();
String queueName = channel.queueDeclare().getQueue();
System.out.println("临时队列名称:"+queueName);
临时队列名称:amq.gen-8FZ42X3k2K1eVpOxd25meg
绑定(Bindings)
绑定其实是交换机和队列之间的桥梁,它告诉我们交换机和那个队列进行了绑定关系。比如说下面这张图告诉我们的就是X与Q1和Q2进行了绑定

通过控制台进行绑定
1.创建队列
2.创建交换机
3.添加绑定关系
绑定结果
扇出(Fanout)-发布订阅模式
它是将接收到的所有消息广播到它知道的 所有队列中。
系统中默认有 fanout类型
发布订阅的代码实现 1.编写生产者
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* @Description: 生产者-发布订阅
*/
public class Emitlog {
/**
* 交换机名称
*/
private static final String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机,及交换机的类型
channel.exchangeDeclare(EXCHANGE_NAME,BuiltinExchangeType.FANOUT);
Scanner scanner = new Scanner(System.in);
System.out.println("请输入需要发送的消息:");
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发出的消息:"+ message);
}
}
}
2.编写消费者一
package com.wf.rabittmqstudy.five;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者-发布订阅
*/
public class ReceiveLogs01 {
/**
* 交换机名称
*/
private static final String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//获取一个临时队列,连接关闭后就会自动删除
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("ReceiveLogs01等待接收消息..");
//消息接收成功的回调方法
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogs01接收到的消息--》》"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(queueName,true,deliverCallback,(consumerTag) -> {
System.out.println("消息接收失败:"+consumerTag);
});
}
}
3.编写消费者二
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者-发布订阅
*/
public class ReceiveLogs02 {
/**
* 交换机名称
*/
private static final String EXCHANGE_NAME="logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//获取一个临时队列,连接关闭后就会自动删除
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
System.out.println("ReceiveLogs02等待接收消息..");
//消息接收成功的回调方法
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogs02接收到的消息--》》"+new String(message.getBody(),"UTF-8"));
};
channel.basicConsume(queueName,true,deliverCallback,(consumerTag) -> {
System.out.println("消息接收失败:"+consumerTag);
});
}
}
4.测试效果
生产者发送消息,aa, bb
消费者一接收到aa,bb
消费者二也接收到aa,bb
直接 (direct)-路由模式
fanout 这种交换类型并不能带来很大的灵活性,它只能进行广播, direct 这种类型的工作方式是,消息只去到它绑定的routingKey 队列中去。
在上面这张图中,可以看到交换机X 绑定了Q1,Q2,两个队列,绑定类型是 direct。队列 Q1 绑定键为orange,
队列 Q2 绑定键有两个,一个绑定键为 black,另一个绑定键为 green,在这种绑定情况下,生产者发布消息到 交换机X上,绑定键为 orange 的消息会被发布到队列Q1。绑定键为 black和 green 的消息会被发布到队列 Q2,其他消息类型的消息将被丢弃。
多重绑定
如上图所示,如果交换机
X 的绑定类型是 direct,但是它绑定的多个队列的 key 如果都相同,在这种情况下虽然绑定类型是 direct 但是它表现的就和 fanout 有点类似了,就跟广播差不多。
直接交换机的代码实现
1.实现的效果图
2.编写生产者
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* @Description: 生产者-直接交换机类型
*/
public class DirectLogs {
/**
* 交换机名称
*/
private static final String EXCHANGE_NAME="direct_logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机,使用直接模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Scanner scanner = new Scanner(System.in);
System.out.println("请输入需要发送的消息:");
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"info",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发出的消息:"+ message);
}
}
}
3.编写消费者一
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者-直接交换机模式
*/
public class ReceiveLogsDirect01 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个队列
channel.queueDeclare("console",false,false,false,null);
//绑定交换机与队列
channel.queueBind("console",EXCHANGE_NAME,"info");
channel.queueBind("console",EXCHANGE_NAME,"warning");
System.out.println("-------------ReceiveLogsDirect01准备接收消息-----------");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogsDirect01控制台打印接受到的消息:" + new String(message.getBody()));
};
channel.basicConsume("console",true,deliverCallback,consumerTag -> {});
}
}
4.编写消费者二
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者-直接交换机模式
*/
public class ReceiveLogsDirect02 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明一个队列
channel.queueDeclare("disk",false,false,false,null);
//绑定交换机与队列
channel.queueBind("disk",EXCHANGE_NAME,"error");
System.out.println("-------------ReceiveLogsDirect02准备接收消息-----------");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("ReceiveLogsDirect02控制台打印接受到的消息:" + new String(message.getBody()));
};
channel.basicConsume("disk",true,deliverCallback,consumerTag -> {});
}
}
5.测试效果
主题(topic)- 通配符模式
topic和direct交换机非常类似,根据消息的路由键
routing key,将消息以模糊匹配的方式路由到指定的队列中。
topic 交换机的 routing_key
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单词列表,以点号(.)分隔开。这些单词可以是任意单词,比如说:stock.usd.nyse, nyse.vmw, quick.orange.rabbit这种类的,当然这个单词列表最多不能超过 255 个字节。
在这个规则列表中,*(星号)可以代替一个单词,#(井号)可以替代零个或多个单词
例:
假设存在如下绑定关系
队列
Q1的匹配规则是routing_key中间带 orange 这个单词,且单词的长度为3 个单词的字符串,比如(a.orange.b)
队列Q2的第一个匹配规则是routing_key最后一个单词是 rabbit ,且单词的长度为3 个单词的字符串,比如(a.b.rabbit)
队列Q2的第二个匹配规则是routing_key以单词 lazy 开头,单词的长度至少为1,比如(lazy,或者lazy.a,lazy.a.b)
例如:
quick.orange.rabbit : 被队列 Q1Q2 接收到
lazy.orange.elephant :被队列 Q1Q2 接收到
quick.orange.fox :被队列 Q1 接收到
lazy.brown.fox :被队列 Q2 接收到
lazy.pink.rabbit :虽然满足两个绑定但只被队列 Q2 接收一次
quick.brown.fox :不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit :是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit :是四个单词但匹配 Q2
注:
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像fanout了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是direct了
主题交换机的代码实现 1.编写生产者
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 生产者-主题交换机
*/
public class EmitLogTopic {
//交换机的名称
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception{
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
HashMap<String, String> map = new HashMap<>();
map.put("quick.orange.rabbit","被队列Q1,Q2接收到");
map.put("quick.orange.fox","被队列Q1接收到");
map.put("lazy.brown.fox","被队列Q2接收到 ");
map.put("lazy.pink.rabbit","虽然满足队列Q2的两个绑定但是只会被接收一次");
map.put("quick.orange.male.rabbit","四个单词不匹配任何绑定会被丢弃");
for (Map.Entry<String, String> bindingKeyEntry : map.entrySet()) {
String routingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发送消息:"+ message );
}
}
}
2.编写消费者一
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者-主题交换机
*/
public class ReceiveLogsTopic01 {
//交换机名称
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//声明队列
String queueName = "Q1";
channel.queueDeclare(queueName,false,false,false,null);
//队列捆绑
channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*");
System.out.println("Q1等待接收消息......");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("Q1接收消息到的消息为--》"+new String(message.getBody()));
System.out.println("接收队列--》"+ queueName + "\n绑定键--》" + message.getEnvelope().getRoutingKey());
};
//接收消息
channel.basicConsume(queueName,true,deliverCallback,consumerTag -> {});
}
}
3.编写消费者二
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者-主题交换机
*/
public class ReceiveLogsTopic02 {
//交换机名称
public static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//声明队列
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("Q2等待接收消息......");
DeliverCallback deliverCallback = (consumerTag,message) -> {
System.out.println("Q2接收消息到的消息为--》"+new String(message.getBody()));
System.out.println("接收队列--》"+ queueName + "\n绑定键--》" + message.getEnvelope().getRoutingKey());
};
//接收消息
channel.basicConsume(queueName,true,deliverCallback,consumerTag -> {});
}
}
4.测试效果
死信队列
死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,生产者将消息投递到交换机或者直接到队列里了,消费者从 队列取出消息进行消费,但某些时候由于特定的原因导致队列中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
-
应用场景 1.为了保证订单业务的消息数据不丢失,需要使用到RabbitMQ的死信队列机制,当消息消费发生异常时,将消息投入死信队列中. 2.用户在商城下单成功并点击去支付后在指定时间未支付时自动失效
-
死信的来源 1.消息 TTL(存活时间) 过期 2.队列达到最大长度(队列满了,无法再添加数据到 mq 中) 3.消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false
-
TTL的含义
TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
设置指定消息过期时间(前提是整合了MQ整合了SpringBoot):
/**
* RabbitTemplate
* 供了接收,发送等方法
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 推送消息
*/
public void sendDirectMessage() {
rabbitTemplate.convertAndSend("交换机名称", routeKey, "需要发送的消息",correlationData->{
//设置过期时间为4s
correlationData.getMessageProperties().setExpiration("4000");
return correlationData;
});
}
设置指定队列过期时间(前提是整合了MQ整合了SpringBoot):
//声明队列
@Bean
public Queue queueA(){
Map<String, Object> arguments = new HashMap<>(3);
//设置死信交换机
arguments.put("x-dead-letter-exchange","A");
//设置死信Routing-key
arguments.put("x-dead-letter-routing-key","B");
//设置队列TTL为10s
arguments.put("x-message-ttl",10000);
return QueueBuilder.durable("queue").withArguments(arguments).build();
}
两种设置方式的区别:
如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。
- 代码实现死信队列
实现如下架构图,正常消息由消费者
C1消费,死信消息将会有消费者C2消费
消息TTL过期
1.编写生产者
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
/**
* @Description: 生产者-死信队列
*/
public class Producer {
//普通交换机的名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//死信消息,设置TTL时间 单位是ms 10000ms是10s
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
for(int i = 0; i < 10; i++) {
String message = "消息:" + i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes(StandardCharsets.UTF_8));
}
}
}
2.编写消费者一
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.util.HashMap;
/**
* @Description: 消费者-死信队列
*/
/*
* 死信队列实战
* 消费者01
* */
public class Consumer01 {
//普通交换机名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
public static final String NORMAL_QUEUE = "normal_queue";
//死信队列名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
//声明死信和普通的交换机类型为direct
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明普通队列
HashMap<String, Object> arguments = new HashMap<>();
//设置死信过期时间
arguments.put("x-message-ttl",1000);
//设置产生死信后需要转发的死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置产生死信后需要转发的RoutingKey
arguments.put("x-dead-letter-routing-key","lisi");
//声明普通队列,传入的arguments就是当产生死信以后,转发到私信队列中的参数
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("Consumer01等待接收消息......");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("Consumer01接收的消息是:" + new String(message.getBody()));
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,consumerTag->{});
}
}
3.编写消费者二
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
/**
* @Description: 消费者-死信队列
*/
/*
* 死信队列实战
* 消费者02
* */
public class Consumer02 {
//死信队列名称
public static final String DEAD_QUEUE = "dead_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
System.out.println("Consumer02等待接收消息......");
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("Consumer02接收的消息是:" + new String(message.getBody()));
};
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumerTag->{});
}
}
4.测试
启动消费者一,启动生产者发送消息,消费者一能接收到,关闭消费者一模拟其接收不到消息,启动消费者二,从新启动生产者,消费者二接收到消息
队列达到最大长度
1.修改生产者
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.wf.rabittmqstudy.utils.RabbitMqUtils;
import java.nio.charset.StandardCharsets;
/**
* @Description: 生产者-死信队列
*/
public class Producer {
//普通交换机的名称
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMqUtils.getChannel();
// //死信消息,设置TTL时间 单位是ms 10000ms是10s
// AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
for(int i = 0; i < 10; i++) {
String message = "消息:" + i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,message.getBytes(StandardCharsets.UTF_8));
}
}
}
2.修改消费者一
添加如下参数
//设置队列的最大长度
arguments.put("x-max-length",6);
3.测试
删除队列normal_queue,启动消费者一,建立绑定关系,然后关闭消费者一,模拟接收不到消息,启动消费者二,启动生产者,查看消费者二就会接收到4条消息,再次启动消费者一,就会收到6条消息
消息被拒绝 1.修改消费者一
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
if(message.equals("消息:5")){
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);
}
};
channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,consumerTag->{});
2.测试
删除队列normal_queue,启动消费者一,启动消费者二,启动生产者
整合SpringBoot
延迟队列
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的 元素的队列。
-
使用场景 1.订单在十分钟之内未支付则自动取消 2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。 3.用户注册成功后,如果三天内没有登陆则进行短信提醒。 4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。 5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
-
队列实现 创建两个队列
QA和QB,两者队列TTL分别设置为10S和40S,然后在创建一个交换机X和死信交换机Y,它们的类型都是direct,创建一个死信队列QD,它们的绑定关系如下:
前提:
参见上文
整合SpringBoot内容搭建一个项目
以设置队列过期的方式实现死信队列
1.编写direct模式的配置类
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 通过TTL的设置使用direct模式,实现死信队列
*/
@Configuration
public class TtlConfig {
//正常队列的交换机
private static final String X_EXCHANGE="X";
//正常routeKey一
private static final String XA_ROUTE_KEY="XA";
//正常routeKey二
private static final String XB_ROUTE_KEY="XB";
//正常队列一
private static final String QA_QUEUE="QA";
//正常队列二
private static final String QB_QUEUE="QB";
//死信routeKey
private static final String YD_ROUTE_KEY="YD";
//死信队列的交换机
private static final String Y_EXCHANGE="Y";
//死信队列
private static final String QD_QUEUE="QD";
/**
* 创建正常交换机,X
* @return
*/
@Bean
public DirectExchange createXExchange() {
return new DirectExchange(X_EXCHANGE);
}
/**
* 创建死信交换机,Y
* @return
*/
@Bean
public DirectExchange createYExchange() {
return new DirectExchange(Y_EXCHANGE);
}
/**
* 创建正常队列QA
* @return
*/
@Bean
public Queue createQAQueue() {
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", YD_ROUTE_KEY);
//声明队列的 TTL 为10S
args.put("x-message-ttl", 10000);
return QueueBuilder.durable(QA_QUEUE).withArguments(args).build();
}
/**
* 队列QA绑定交换机X
* @return
*/
@Bean
public Binding QABindingX() {
return BindingBuilder.bind(createQAQueue()).to(createXExchange()).with(XA_ROUTE_KEY);
}
/**
* 创建正常队列QB
* @return
*/
@Bean
public Queue createQBQueue() {
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", YD_ROUTE_KEY);
//声明队列的 TTL 为40S
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QB_QUEUE).withArguments(args).build();
}
/**
* 队列QB绑定交换机X
* @return
*/
@Bean
public Binding QBBindingX() {
return BindingBuilder.bind(createQBQueue()).to(createXExchange()).with(XB_ROUTE_KEY);
}
/**
*创建死信队列 QD
* @return
*/
@Bean()
public Queue createQDQueue(){
return new Queue(QD_QUEUE);
}
/**
* 死信队列QD绑定死信交换机Y
* @return
*/
@Bean
public Binding QDBindingY() {
return BindingBuilder.bind(createQDQueue()).to(createYExchange()).with(YD_ROUTE_KEY);
}
}
2.编写生产者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* @Description: 死信队列-生产者
*/
@RestController
@RequestMapping("/ttl")
@Slf4j
public class MsgProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 通过设置队列过期时间的形式,实现死信队列
* @param message 消息
* @return
*/
@GetMapping("/send/{message}")
public String sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个TTL队列:{}",new Date().toString(),message);
//给队列QA发送消息
rabbitTemplate.convertAndSend("X","XA","消息来自TTL为10s的队列:" + message);
//给队列QB发送消息
rabbitTemplate.convertAndSend("X","XB","消息来自TTL为40s的队列:" + message);
return message+"推送成功";
}
}
3.编写消费者
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @Description: 死信队列-消费者
*/
@Slf4j
@Component
public class MsgConsumer {
/**
* 监听队列QD,接收消息
* @param message
* @param channel
* @throws Exception
*/
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws Exception {
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列的消息:{}",new Date().toString(),msg);
}
}
4.测试
http://localhost:8081/ttl/send/121
推送效果
以设置消息过期的方式实现死信队列
在上述代码的基础上,新增了一个队列 QC,该队列不设置 TTL 时间,而是在推送消息的时候设置过期时间,绑定关系如下:
1.在TtlConfig类中新增如下代码
//正常routeKey三
private static final String XC_ROUTE_KEY="XC";
//正常队列三
private static final String QC_QUEUE="QC";
/**
* 创建正常队列QC
* @return
*/
@Bean
public Queue createQCQueue() {
Map<String, Object> args = new HashMap<>(2);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", YD_ROUTE_KEY);
return QueueBuilder.durable(QC_QUEUE).withArguments(args).build();
}
/**
* 队列QC绑定交换机X
* @return
*/
@Bean
public Binding QCBindingX() {
return BindingBuilder.bind(createQCQueue()).to(createXExchange()).with(XC_ROUTE_KEY);
}
2.在MsgProducer类中新增如下代码
/**
* 通过设置消息过期时间的形式,实现死信队列
* @param message 消息
* @param ttlTime 消息过期时间
* @return
*/
@GetMapping("sendExpiration/{message}/{ttlTime}")
public String sendExpirationMsg(@PathVariable String message,@PathVariable String ttlTime){
log.info("当前时间:{},发送一条时长{}毫秒TTL信息给队列QC:{}",
new Date().toString(),ttlTime,message);
rabbitTemplate.convertAndSend("X","XC",message,msg->{
//发送消息的时候 延迟时长
msg.getMessageProperties().setExpiration(ttlTime);
return msg;
});
return message+"推送成功,过期时长为:"+ttlTime+"毫秒";
}
3.测试
http://localhost:8081/ttl/sendExpiration/666/20000
http://localhost:8081/ttl/sendExpiration/777/3000
如上所示,消息666的存活时间是20s,消息777的存活时间是3s,按理说,虽然666先被发送,但也应该是777先被接收打印,但效果并非如此,
这是因为使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。如果需要解决这个问题,就需要使用到延时队列插件
延时队列插件实现延迟队列(解决消息无法按时死亡)
上文中提到的消息777无法按照设置的时间,先行死亡的问题,导致不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间及时死亡,无法设计成一个通用的延时队列。所以需要安装延时队列插件来解决这个问题
-
代码实现
在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:
1.编写direct模式的配置类
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 使用direct模式使用延迟队列插件交换机实现死信队列
*/
@Configuration
public class DelayedQueueConfig {
//交换机
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
//队列
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
//routingKey
public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";
/**
* 声明队列
* @return
*/
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
/**
* 声明一个自定义交换机
* @return
*/
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> arguments = new HashMap<>();
//声明模式为direct
arguments.put("x-delayed-type", "direct");
/**
* 参数解析:
* name:交换机名称
* type:交换机的类型
* durable:是否需要持久化
* autoDelete:是否需要自动删除
* arguments:其他参数
*
*/
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message",true, false, arguments);
}
/**
* 绑定交换机和队列
* @return
*/
@Bean
public Binding delayedQueueBindingDelayedExchange() {
return BindingBuilder.bind(delayedQueue()).to(delayedExchange()).with(DELAYED_ROUTING_KEY).noargs();
}
}
2.编写生产者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* @Description: 死信队列-生产者-基于插件的延迟消息
*/
@RestController
@RequestMapping("/ttl")
@Slf4j
public class DelayedMsgProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
* @param message 消息
* @param delayTime 过期时间
*/
@GetMapping("/sendDelayMsg/{message}/{delayTime}")
public String sendMsg(@PathVariable String message,@PathVariable Integer delayTime){
log.info("当前时间:{},发送一条时长{}毫秒信息给延迟队列delayed.queue:{}",
new Date().toString(),delayTime,message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME
,DelayedQueueConfig.DELAYED_ROUTING_KEY,message,msg -> {
// 发送消息的时候 延迟时长 单位ms
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
return message+"推送成功,过期时间为:"+delayTime+"毫秒";
}
}
3.编写消费者
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @Description: 死信队列-消费者-基于插件的延迟消息
*/
@Slf4j
@Component
public class DelayedMsgConsumer {
/**
* 监听消息
* @param message
*/
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
public void recieveDelayQueue(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到延迟队列的消息:{}",new Date().toString(),msg);
}
}
4.测试
http://localhost:8081/ttl/sendDelayMsg/111/50000
http://localhost:8081/ttl/sendDelayMsg/222/10000
消息222虽然后发,但是因为它的过期时间比111短,所以222先接收,符合预期
- 总结
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。
当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景
消息发布确认
- yml文件新增配置
application.yml
publisher-confirm-type: correlated #确认消息已发送到交换机(Exchange)
publisher-returns: true #确认消息已发送到队列(Queue)
publisher-confirm-type配置的可选值:
none:表示禁用发布确认模式,是默认值
correlated:表示发布消息后,交换机接收成功或交换机接受失败都会回调方法
simple:有两种效果,其一效果和correlated值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms,或 waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDiea方法如果返回false则会关闭channel,则接下来无法发送消息到broker
完整版application.yml:
server:
port: 8081
spring:
rabbitmq:
host: 192.168.31.89
port: 5672
username: admin
password: admin
#确认消息已发送到交换机(Exchange)
publisher-confirm-type: correlated
#确认消息已发送到队列(Queue)
publisher-returns: true
- 编写配置类
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: 消息确认回调配置
*/
@Configuration
public class RabbitMQConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//设置消息推送结果失败或成功都强制调用回调函数
rabbitTemplate.setMandatory(true);
/*
确认消息送到交换机(Exchange)回调
* 交换机确认回调方法,发消息后,交换机接收成功或交换机接受失败都会回调
* correlationData:保存回调消息的ID及相关信息
* ack:交换机收到消息,为true,交换机没收到消息 为false
* cause:失败原因,成功为null,失败返回失败的原因
* */
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
//判断是否接收成功
if(ack){
System.out.println("----------交换机已成功接收到消息---------");
System.out.println("相关信息:"+correlationData);
System.out.println("接收情况:"+ack);
System.out.println("错误原因:"+cause);
}else{
System.out.println("----------交换机接收消息失败---------");
System.out.println("相关信息:"+correlationData);
System.out.println("接收情况:"+ack);
System.out.println("错误原因:"+cause);
}
});
/**
* 确认消息送到队列(Queue)回调
* message:推送的消息
* replyCode:返回的状态码
* replyText:错误信息
* exchange:交换机
* routingKey:路由键
*/
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
System.out.println("----------消息推送失败---------");
System.out.println("消息:"+message);
System.out.println("返回的状态码:"+replyCode);
System.out.println("错误信息:"+replyText);
System.out.println("交换机:"+exchange);
System.out.println("路由键:"+routingKey);
});
return rabbitTemplate;
}
}
备份交换机
有了消息确认发布,就有机会在生产者的消息无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置mandatory参数会增加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。在RabbitMQ.中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份交换机可以理解为 RabbitMQ中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为Fanout,这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
- 代码实现
1.编写配置类
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: 备份交换机配置类
*/
@Configuration
public class ConfirmConfig {
//交换机
public static final String CONFIRM_EXCHANGE_NAME = "confirm_exchange";
//队列
public static final String CONFIRM_QUEUE_NAME = "confirm_queue";
//RoutingKey
public static final String CONFIRM_routing_key = "key1";
//备份交换机
public static final String BACKUP_EXCHANGE_NAME = "backup_exchange";
//备份队列
public static final String BACKUP_QUEUE_NAME = "backup_queue";
//报警队列
public static final String WARNING_QUEUE_NAME = "warning_queue";
/**
* 声明交换机
*/
@Bean
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
//设置该交换机的备份机
.withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
/**
* 声明队列
* @return
*/
@Bean
public Queue confirmQueue(){
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
}
/**
* 绑定交换机和队列
*/
@Bean
public Binding queueBindingExchange(){
return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with(CONFIRM_routing_key);
}
/**
* 声明备份交换机
* @return
*/
@Bean
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
/**
* 声明备份队列
* @return
*/
@Bean
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
/**
* 声明报警队列
* @return
*/
@Bean
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
/**
* 绑定备份交换机和备份队列
* @return
*/
@Bean
public Binding backupQueueBindingBackupExchange(){
return BindingBuilder.bind(backupQueue()).to(backupExchange());
}
/**
* 绑定备份交换机和报警队列
* @return
*/
@Bean
public Binding warningQueueBindingBackupExchange(){
return BindingBuilder.bind(warningQueue()).to(backupExchange());
}
}
2.编写生产者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description: 备份交换机-生产者
*/
@RestController
@Slf4j
@RequestMapping("/confirm")
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 通过正确的路由key发送消息,不会触发报警
* @param message
* @return
*/
@GetMapping("/sendMessage/{message}")
public String sendMessage(@PathVariable String message){
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_routing_key ,message);
log.info("发送的第一条消息内容:{}",message);
return message+"推送成功";
}
/**
* 通过错误的路由key发送消息,会触发报警
* @param message
* @return
*/
@GetMapping("/sendErrorMessage/{message}")
public String sendMessageError(@PathVariable String message){
//此处设置错误的路由key从而,触发报警
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,"aa" ,message);
log.info("发送的第二条消息内容:{}",message);
return message+"推送成功";
}
}
3.编写正常消息的消费者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Description: 消费者
*/
// 接收消息
@Slf4j
@Component
public class Consumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receiveConfirmMessage(Message message){
String msg = new String(message.getBody());
log.info("接受到的队列confirm.queue消息:{}",msg);
}
}
4.编写报警的消费者
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Description: 备份交换机-报警-消费者
*/
@Component
@Slf4j
public class WarningConsumer {
//接受报警消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message){
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}",msg);
}
}
5.测试
http://localhost:8081/confirm/sendMessage/111
http://localhost:8081/confirm/sendErrorMessage/222
注:当同时配置了消息确认发布的回调函数,也配置了备份交换机,二者同时使用时,推送消息失败触发的将会是备份机,备份交换机优先级高。
幂等性
幂等性的实质是:对于一个资源,不管你请求一次还是请求多次,对该资源本身造成的影响应该是相同的,不能因为重复相同的请求而对该资源重复造成影响。注意关注的是请求操作对资源本身造成的影响,而不是请求资源返回的结果。就是保证同一条消息不会重复或者重复消费了也不会对系统数据造成异常。
- RabbitMQ的幂等性
拿RabbitMQ来说的话,消费者在消费完成一条消息之后会向MQ回复一个ACK(可以配置自动ACK或者手动ACK) 来告诉MQ这条消息已经消费了。假如当消费者消费完数据后,准备回执ACK时,系统挂掉了或者网络中断,MQ 未收到确认信息,MQ是不知道该条消息已经被消费了。所以该条消息会重新发给其他的消费者或者重启之后MQ会再次发送给该消费者,导致消息被重复消费,如果此时没有做幂等性处理,可能就会导致数据错误等问题。
- 解决方案
| 方案 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 唯一 ID+指纹码机制 | 指纹码指的是我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中。 | 实现简单就一个拼接,然后查询判断是否重复 | 高并发时,如果是单个数据库就会有写入性能瓶颈,在消费者中如果调用了第三方接口,第三方接口中也有插入,而本地ID并非第三方接口数据的主键,就会造成本地数据不会重复,但是第三方接口数据大量重复。 |
| Redis 原子性 | 利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费 | 不存在第三个方接口调用问题,性能较高 | 实现较为负杂,且需要集成,成本较高 |
- 代码实现
唯一 ID+指纹码机制1.生产者
/**
* RabbitTemplate
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 推送消息
* @return
*/
public String sendDirectMessage(String msg) {
Map<String,Object> map=new HashMap<>();
map.put("messageData",msg);
String newMsg=JSONObject.toJSONString(map);
Message message = MessageBuilder
.withBody(newMsg.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON)
.setContentEncoding("utf-8")
//请求头设置消息id(messageId)
.setMessageId(String.valueOf(UUID.randomUUID()))
.build();
rabbitTemplate.convertAndSend("fanout_test_exchange", "", message);
return msg+"-fanout模式推送成功";
}
2.消费者
/**
* 接收消息
* @param message
* @throws Exception
*/
@RabbitListener(queues = "queueOne.fanout.queue")
@RabbitHandler
public void process(Message message) throws Exception {
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId();
//获取消息
String msg = new String(message.getBody(), "UTF-8");
//判断唯一Id是否被消费,消息消费成功后将id和状态保存在日志表中
//此处模拟从数据库查询ID为messageId的数据,或者也可以从redis中获取messageId的value
// String start=logMapper.queryLog(messageId);
String start="0";
//判断是否已经消费
if(start!=null&&start.equals("1") ){
return;
}
JSONObject jsonObject = JSONObject.parseObject(msg);
System.out.println("接收到的messageId:" + messageId);
System.out.println("接收到的消息内容:" + msg);
System.out.println("转换后的:" + jsonObject);
// 此处编写自己对消息的处理业务逻辑
// 此处消费成功,修改messageId的状态,并存入数据库日志表或者存到redis中,key为消息Id、value为状态
}
Redis 原子性
1.生产者代码同上
2.消费者
@Resource
private RedisTemplate redisTemplate;
/**
* 接收消息
* @param message
* @throws Exception
*/
@RabbitListener(queues = "queueOne.fanout.queue")
@RabbitHandler
public void process(Message message) throws Exception {
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId();
//获取消息
String msg = new String(message.getBody(), "UTF-8");
/**
* setNT()
* 在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0
* redisTemplate中返回的是true和false,即成功 true,失败false,
*
*/
boolean target= (boolean)redisTemplate.execute((RedisCallback) action->{
return action.setNX(messageId.getBytes(),messageId.getBytes());
});
//判断是否已经消费
if(!target){
return;
}
JSONObject jsonObject = JSONObject.parseObject(msg);
System.out.println("接收到的messageId:" + messageId);
System.out.println("接收到的消息内容:" + msg);
System.out.println("转换后的:" + jsonObject);
// 此处编写自己对消息的处理业务逻辑
// 此处消费成功,后设置消息的过期时间为60s,此处只做演示,根据实际情况填写时间
redisTemplate.expire(messageId.getBytes(), 60, TimeUnit.SECONDS);
}
注:如果MQ设置的手动ACK模式,那么报错或者TTL过时,那么需要在死信队列删除该key。
优先级队列
优先级队列指的是具有更高优先级的队列具有较高的优先权,优先级高的消息具备优先被消费的特权
- 实现优先级队列的前提 1.队列需要设置为优先级队列 2.消息需要设置消息的优先级 3.消费者需要等待消息已经发送到队列中才去消费,因为这样才有机会对消息进行排序
- 设置优先级队列
/**
* 定义优先级队列
*/
@Bean
Queue queue() {
Map<String, Object> args= new HashMap<>();
//设置优先级
args.put("x-max-priority", 10);
return new Queue(QUEUE, false, false, false, args);
}
注:使用 x-max-priority 参数设置优先级,此参数应为介于 1 ~ 255 之间的正整数,指示队列应支持的最大优先级,数字越大,优先级越高,推荐1 ~ 10。当前使用更多优先级将消耗更多的 CPU 资源,通过使用更多的 Erlang 进程。运行时调度也会受到影响。
- 设置消息的优先级
@Autowired
private RabbitTemplate rabbitTemplate;
rabbitTemplate.convertAndSend("EXCHANGE_NAME","ROUTING_KEY",message,msg -> {
// 设置消息的优先级
msg.getMessageProperties().setPriority(1).setDelay(delayTime);
return msg;
});
- 代码实现
1.在项目A中编写配置类
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: 优先级队列配置
*/
@Slf4j
@Configuration
public class PriorityConfig {
public static final String EXCHANGE = "priority-exchange";
public static final String QUEUE = "priority-queue";
public static final String ROUTING_KEY = "priority_key";
/**
* 定义优先级队列
*/
@Bean
public Queue priorityQueue() {
Map<String, Object> args= new HashMap<>();
//设置优先级
args.put("x-max-priority", 10);
return new Queue(QUEUE, false, false, false, args);
}
/**
* 定义交换器
*/
@Bean
public DirectExchange priorityExchange() {
return new DirectExchange(EXCHANGE);
}
/**
* 绑定交换机与队列
* @return
*/
@Bean
public Binding priorityBinding() {
return BindingBuilder.bind(priorityQueue()).to(priorityExchange()).with(ROUTING_KEY);
}
}
2.在项目A中编写生产者类
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description: 优先级队列-生产者
*/
@RestController
@RequestMapping("/priority")
public class PriorityProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 模拟发送多条数据
*/
@GetMapping("/sendPriorityMessage")
public String sendPriorityMessage(){
String message = "";
for (int i = 0; i < 10;i++){
message = "info" + i;
if(i%2!=0){ // 设置奇数的优先级为10 ,优先级也可以作为形参接受
rabbitTemplate.convertAndSend(PriorityConfig.EXCHANGE,PriorityConfig.ROUTING_KEY,message,msg -> {
msg.getMessageProperties().setPriority(10);
return msg;
});
}else {
rabbitTemplate.convertAndSend(PriorityConfig.EXCHANGE,PriorityConfig.ROUTING_KEY,message,msg -> {
msg.getMessageProperties().setPriority(5);
return msg;
});
}
}
return "消息推送成功";
}
}
3.在项目B中编写消费者类
package com.wf.rabittmqspringbootconsumer.priority;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.Date;
/**
* @Description: 优先级队列-消费者
*/
@Slf4j
@Service
public class PriorityConsumers {
/**
* 监听队列,接收消息
* @param message
* @throws Exception
*/
@RabbitListener(queues = "priority-queue")
@RabbitHandler
public void receiveD(String message) throws Exception {
log.info("接收到优先级队列的消息:{}",message);
}
}
4.测试 先启动项目A,不启动项目B,访问接口
http://localhost:8081/priority/sendPriorityMessage
启动项目B
注:消费端速度大于生产端速度,且broker中没有消息堆积的话,对发送的消息设置优先级也没什么实际意义,因为发送端刚发送完一条消息就被消费端消费了,那么就相当于broker至多只有一条消息,那么对于单条消息来说优先级是没有什么意义的,
也就是说,假如启动生产者后,不推送消息,先启动消费者,此时推送消息,推送一条就会被消费者消费一条,所以是无法实现排序效果的,解决方案就是在消费者端使用 channel.basicQos()方法,设置预取值,以限制随时可以发送的消息数,从而允许对消息进行优先级排序。
惰性队列
RabbitMQ从 3.6.0版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。 默认情况下,当生产者将消息发送到RabbitMQ的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当RabbitMQ需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。
- 模式
default:默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。lazy:lazy模式即为惰性队列的模式,可以通过调用channel.queueDeclare()方法的时候在参数中设置,也可以通过Policy的方式设置,如果一个队列同时使用这两种方式设置的话,那么Policy的方式具备更高的优先级。 如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。 - 设置
在队列声明的时候可以通过
x-queue-mode参数来设置队列的模式,取值为default和lazy下面示例中演示了一个惰性队列的声明细节:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
- 对比
在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅占用 1.5MB