在介绍RabbitMQ之前,我们先来看下面一个电商项目的场景:
-
商品的原始数据保存在数据库中,增删改查都在数据库中完成。
-
搜索服务数据来源是索引库(Elasticsearch),如果数据库商品发生变化,索引库数据不能及时更新。
-
商品详情做了页面静态化处理,静态页面数据也不会随着数据库商品更新而变化。
如果我们在后台修改了商品的价格,搜索页面和商品详情页显示的依然是旧的价格,这样显然不对。该如何解决?
我们可能会想到这么做:
-
方案1:每当后台对商品做增删改操作,同时修改索引库数据及更新静态页面。
-
方案2:搜索服务和商品页面静态化服务对外提供操作接口,后台在商品增删改后,调用接口。
这两种方案都有个严重的问题:就是代码耦合,后台服务中需要嵌入搜索和商品页面服务,违背了微服务的独立
原则。
这时,我们就会采用另外一种解决办法,那就是消息队列!
商品服务对商品增删改以后,无需去操作索引库和静态页面,只需向MQ发送一条消息(比如包含商品id的消息),也不关心消息被谁接收。 搜索服务和静态页面服务监听MQ,接收消息,然后分别去处理索引库和静态页面(根据商品id去更新索引库和商品详情静态页面)。
什么是消息队列
MQ全称为Message Queue,即消息队列。“消息队列”是在消息的传输过程中保存消息的容器。它是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。
开发中消息队列通常有如下应用场景:
1、任务异步处理:
高并发环境下,由于来不及同步处理,请求往往会发生堵塞,比如说,大量的insert,update之类的请求同时到达MySQL,直接导致无数的行锁表锁,甚至最后请求会堆积过多,从而触发too many connections错误。通过使用消息队列,我们可以异步处理请求,从而缓解系统的压力。将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。减少了应用程序的响应时间。
2、应用程序解耦合:
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
AMQP和JMS
MQ是消息通信的模型,并发具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。
两者间的区别和联系:
-
JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
-
JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
-
JMS规定了两种消息模型;而AMQP的消息模型更加丰富
常见MQ产品
-
ActiveMQ:基于JMS
-
RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
-
RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
-
Kafka:分布式消息系统,高吞吐量
RabbitMQ快速入门
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。RabbitMQ官方地址:http://www.rabbitmq.com
下载与安装
RabbitMQ由Erlang语言开发,需要安装与RabbitMQ版本对应的Erlang语言环境,具体的就不解释了,自行搜索教程。RabbitMQ官网下载地址:Downloading and Installing RabbitMQ — RabbitMQ
RabbitMQ的工作原理
下图是RabbitMQ的基本结构:
组成部分说明:
- Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue
- Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。
- Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的
- Producer:消息生产者,即生产方客户端,生产方客户端将消息发送
- Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。
生产者发送消息流程:
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)
消费者接收消息流程:
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。
6、ack回复
六种消息模型
①基本消息模型:
在上图的模型中,有以下概念:
-
P:生产者,也就是要发送消息的程序
-
C:消费者:消息的接受者,会一直等待消息到来。
-
queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
生产者
新建一个maven工程,添加amqp-client依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.1</version>
</dependency>
连接工具类:
public class ConnectionUtil {
/**
* 建立与RabbitMQ的连接
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("192.168.226.128");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码
factory.setUsername("admin");
factory.setPassword("123");
// 通过工厂获取连接
Connection connection = factory.newConnection();
return connection;
}
}
生产者发消息
public class Producer {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 1、获取到连接
Connection connection = ConnectionUtil.getConnection();
// 2、从连接中创建通道,使用通道才能完成消息相关的操作
Channel channel = connection.createChannel();
// 3、声明(创建)队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 4、消息内容
String message = "Hello World!";
// 向指定的队列中发送消息
//参数:String exchange, String routingKey, BasicProperties props, byte[] body
/**
* 参数明细:
* 1、exchange,交换机,如果不指定将使用mq的默认交换机(设置为"")
* 2、routingKey,路由key,交换机根据路由key来将消息转发到指定的队列,如果使用默认交换机,routingKey设置为队列的名称
* 3、props,消息的属性
* 4、body,消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//关闭通道和连接(资源关闭最好用try-catch-finally语句处理)
channel.close();
connection.close();
}
}
控制台:
web管理页面:服务器地址/端口号 (本地:127.0.0.1:15672,默认用户及密码:admin 123)
消费者接收消息
public class Consumer {
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
//创建会话通道,生产者和mq服务所有通信都在channel通道中完成
Channel channel = connection.createChannel();
// 声明队列
//参数:String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
/**
* 参数明细
* 1、queue 队列名称
* 2、durable 是否持久化,如果持久化,mq重启后队列还在
* 3、exclusive 是否独占连接,队列只允许在该连接中访问,如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建
* 4、autoDelete 自动删除,队列不再使用时是否自动删除此队列,如果将此参数和exclusive参数设置为true就可以实现临时队列(队列不用了就自动删除)
* 5、arguments 参数,可以设置一个队列的扩展参数,比如:可设置存活时间
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//实现消费方法
DefaultConsumer consumer = new DefaultConsumer(channel){
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
/**
* 当接收到消息后此方法将被调用
* @param consumerTag 消费者标签,用来标识消费者的,在监听队列时设置channel.basicConsume
* @param envelope 信封,通过envelope
* @param properties 消息属性
* @param body 消息内容
* @throws IOException
*/
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
//消息id,mq在channel中用来标识消息的id,可用于确认消息已接收
long deliveryTag = envelope.getDeliveryTag();
// body 即消息体
String msg = new String(body,"utf-8");
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认。
//参数:String queue, boolean autoAck, Consumer callback
/**
* 参数明细:
* 1、queue 队列名称
* 2、autoAck 自动回复,当消费者接收到消息后要告诉mq消息已接收,如果将此参数设置为tru表示会自动回复mq,如果设置为false要通过编程实现回复
* 3、callback,消费方法,当消费者接收到消息要执行的方法
*/
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
控制台打印:
再看看队列的消息,已经被消费了
我们发现,消费者已经获取了消息,但是程序没有停止,一直在监听队列中是否有新的消息。一旦有新的消息进入队列,就会立即打印.
②work消息模型
工作队列或者竞争消费者模式
work queues与入门程序相比,多了一个消费端,两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。
这个消息模型在Web应用程序中特别有用,可以处理短的HTTP请求窗口中无法处理复杂的任务。
接下来我们来模拟这个流程:
P:生产者:任务的发布者
C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时)
C2:消费者2:领取任务并且完成任务,假设完成速度较快
生产者
public class Send {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 循环发布任务
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "task .. " + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(i * 2);
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
消费者1
public class Consumer {
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);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//交换机
String exchange = envelope.getExchange();
long deliveryTag = envelope.getDeliveryTag();
String msg = new String(body,"utf-8");
System.out.println(" [消费者一] received : " + msg + "!");
}
};
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
消费者2
运行消费者一
然后使用
将原本的消费者一改成消费者二,便于辨别
然后在运行
发现有两个Consumer
最后写上Producer
public class Producer {
private final static String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 循环发布任务
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "task .. " + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
}
}
}
运行结果
可见workqueue是轮询的!
消息确认机制(ACK)
通过刚才的案例可以看出,消息一旦被消费者接收,队列中的消息就会被删除。
那么问题来了:RabbitMQ怎么知道消息被接收了呢?
如果消费者领取消息后,还没执行操作就挂掉了呢?或者抛出了异常?消息消费失败,但是RabbitMQ无从得知,这样消息就丢失了!
因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被接收。不过这种回执ACK分两种情况:
-
自动ACK:消息一旦被接收,消费者自动发送ACK。优点:适用在消费者可以高效并 以某种速率能够处理这些消息的情况下使用;缺点就是一旦接收就发ACK,并不保证消息能被消费
-
手动ACK:消息接收后,不会发送ACK,需要手动调用
大家觉得哪种更好呢?
这需要看消息的重要性:
-
如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
-
如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。
如何设置手动ACK,先上代码
生产者-同上
public class Producer {
public static final String QUEUE_NAME="auto_ack_queue";
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);
while (scanner.hasNext()){
String message = scanner.nextLine();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息"+message);
}
}
}
其中RabbitMqUtils
public class RabbitMqUtils {
//得到一个连接的 channel
public static Channel getChannel() throws Exception{
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.226.128");
factory.setUsername("admin");
factory.setPassword("123");
factory.setHandshakeTimeout(999999);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
消费者一
public class Consumer03 {
public static final String QUEUE_NAME="auto_ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("消费者一收到消息较慢");
try {
TimeUnit.SECONDS.sleep(5);//沉睡5s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者一收到消息"+message);
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
boolean autoACK=false;
channel.basicConsume(QUEUE_NAME,autoACK,consumer);
}
}
消费者二
public class Consumer03 {
public static final String QUEUE_NAME="auto_ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("消费者二收到消息较快");
try {
TimeUnit.SECONDS.sleep(1);//沉睡1s
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者二收到消息"+message);
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
boolean autoACK=false;
channel.basicConsume(QUEUE_NAME,autoACK,consumer);
}
}
说明:
生产者没有变化;
消费者主要是两个地方:
对于①
Multiple 的解释
multiple 的 true 和 false 代表不同意思:
true 代表批量应答 channel 上未应答的消息 比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答
false 同上面相比 只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答
解读:有四个消息A,B,C,D,他们的tag分别是5,6,7,8(这里应该理解了
envelope.getDeliveryTag()
是什么意思),如果multiple=true,如果8被接受且处理,那么5-7都会被ACK;multiple=false,8处理好了单独ack,7处理好了单独ack......
消息自动重新入队机制
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息 未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者 可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确 保不会丢失任何消息。
代码测试
生产者发消息:
此时消费者一和二都运行良好
现在生产者发消息FF
消费者一接收,但是处理过程中,消费者一宕机了
MQ采取重新入队机制,将消息FF重新放到消息队列中,消费者二成功接收处理
RabbitMQ 持久化
概念
刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉以后消 息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列 和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标 记为持久化。
队列持久化
之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果 要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化
但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新 创建一个持久化的队列,不然就会出现错误
以下为控制台中持久化与非持久化队列的 UI 显示区
持久化之后即使重启 rabbitmq 队列也依然存在
tips:如何删除一个消息队列
如果队列持久化,但是消息没有持久化,仍然无法保证消息不丢失,因为队列持久化只是保证队列不丢失,消息可能会寄
消息持久化
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添 加这个属性。
消息不持久化:
消息持久化
注意:队列持久化,消息持久化都是在生产者中设置的
能者多劳
如果将消费者一设置睡眠时间5s,消费者二睡眠时间设置为1s,那么还是轮询
有问题吗?
-
消费者1比消费者2的效率要低,一次任务的耗时较长
-
然而两人最终消费的消息数量是一样的
-
消费者2大量时间处于空闲状态,消费者1一直忙碌
现在的状态属于是把任务平均分配,正确的做法应该是消费越快的人,消费的越多。
怎么实现呢?
通过 BasicQos 方法设置prefetchCount = 1。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理1个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。相反,它会将其分派给不忙碌的下一个Consumer。
不设置 basicQos的话是一次性平均分发给所有的队列,设置之后限制了一次分发消息的数量,在设置手动确认机制,这样在你没有提交已经处理好的消息的时候是不会给你分发消息的。实现的不公平分发。
总结:prefetchCount =0就是轮询分发,你一条消息我一条消息;
prefetchCount =n(n>0),就是信道最大容纳消息的数量,也包括正在处理的(没有发送ACK的)。
这样就应该可以理解为什么等于1的时候是能者多劳了,C1处理慢,C2处理快。消息A发给C1处理,此时C1的信道里的消息就达到最大值1,只要不处理完返回ACK,那么队列就不会给C1再发消息,C2处理快,处理完就接收新的。
值得注意的是:prefetchCount在手动ack的情况下才生效,自动ack不生效。
预取值
其实就是prefecthCount
本身消息的发送就是异步发送的,所以在任何时候,channel 上肯定不止只有一个消息另外来自消费 者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此 缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos 方法设 置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量, RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有 未确认的消息 5、6、7,8,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何 消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK,RabbitMQ 将会感知 这个情况到并再发送一条消息。消息应答和 QoS 预取值对用户吞吐量有重大影响。通常,增加预取将提高 向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理 的消息的数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器)应该小心使用具有无限预处理 的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的 内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范 围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的。当然这 将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境 中。对于大多数应用来说,稍微高一点的值将是最佳的。
发布确认
确认发布是在生产者中设置的,ack是在消费之中设置的
为什么要用发布确认?
前面我们学习了消息持久化和队列持久化,但是存在一个问题就是,可能在将消息存到磁盘的过程中宕机了,那消息不还是丢失了吗,所以我们需要发布确认;
什么是发布确认?
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的 消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队 列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传 给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置 basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
发布确认分为单个确认发布,批量确认发布,异步确认发布
单个确认发布
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它 被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认 的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。 这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会 阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某 些应用程序来说这可能已经足够了。
public class Producer04 {
public static final String QUEUE_NAME="hello";
public static final int MESSAGE_COUNT=1000;
public static void main(String[] args) throws Exception {
publishMessageIndividually();//发布1000个单独确认消息,耗时503ms
}
public static void publishMessageIndividually() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//开始单个确认
channel.confirmSelect();
long begin=System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message=i+"";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
boolean flag=channel.waitForConfirms();
if(flag){
System.out.println("发送成功");
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) +
"ms");
}
}
批量确认发布
public static void publishMessageBatch() throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//批量确认消息大小
int batchSize = 100;
//未确认消息个数
int outstandingMessageCount = 0;
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
outstandingMessageCount++;
if (outstandingMessageCount == batchSize) {
channel.waitForConfirms();
outstandingMessageCount = 0;
}
}
//为了确保还有剩余没有确认消息 再次确认
if (outstandingMessageCount > 0) {
channel.waitForConfirms();
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) +
"ms");
}
}
异步确认发布
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说, 他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功, 下面就让我们来详细讲解异步确认是怎么实现的。
public class Producer05 {
public static final String QUEUE_NAME="async";
public static final int MESSAGE_COUNT=1000;
public static void main(String[] args) throws Exception {
publishMessageAsync();
}
public static void publishMessageAsync() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//开始确认发布
channel.confirmSelect();
long begin=System.currentTimeMillis();
//异步确认:发布者只管发,回调函数是broker调用的
/*
long deliveryTag:消息编号
boolean multiple:是否批量确认
ConfirmCallback ackCallback = (deliveryTag,multiple)->{};
ConfirmCallback nackCallback = (deliveryTag,multiple)->{};
channel.addConfirmListener(ackCallback,nackCallback);
合并
*/
channel.addConfirmListener((deliveryTag,multiple)->{
System.out.println("ack message:"+deliveryTag);
},(deliveryTag, multiple)->{
System.out.println("nack message:"+deliveryTag);
});
//一直发消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message="message:"+i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
long end=System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) +
"ms");
}
}
其中监听器中的参数可以写两种,单参监听器只监听发送成功的消息,双参即监听成功,也监听失败
控制台显示:
为什么不是连续的1-1000
为什么显示完耗时?还在打印确认消息多少多少呢?因为那是监听器再磨磨叽叽的执行呢
完善异步确认-如何处理异步未确认消息
//对于异步确认进行完善,记录未接受的消息
public class Producer06 {
public static final String QUEUE_NAME="async";
public static final int MESSAGE_COUNT=1000;
public static void main(String[] args) throws Exception {
publishMessageAsyncPro();
}
public static void publishMessageAsyncPro() throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.confirmSelect();
long begin=System.currentTimeMillis();
//最好地解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,
//比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传
//递。
/**
* 线程安全有序的一个跳跃表ConcurrentSkipListMap,适用于高并发的情况
* 优点:
* 1.轻松的将序号与消息进行关联
* 2.轻松批量删除条目 只要给到序列号
* 3.支持并发访问
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
channel.addConfirmListener((deliverTag,multiple)->{
//确认回调函数
//步骤二:删除已经确认的消息,剩下的就是未确认的消息
if(multiple){ //是否批量确认,一半false
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliverTag);
confirmed.clear();
}else {
outstandingConfirms.remove(deliverTag);
}
},(deliverTag,multiple)->{
//未确认回调函数
//步骤三:处理未确认的消息
String message = outstandingConfirms.get(deliverTag);
System.out.println("发布的消息"+message+"未被确认,序列号"+deliverTag);
});
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message= i+"";
//步骤一:记录下所有要发送的消息
// 其中 channel.getNextPublishSeqNo():获取下一个消息的序列号,如何理解呢?比如已经发了两条消息,对应的序列号是1,2,那么下一个序列号是不是就是3,那么这一条消息的序列号是不是应该是3
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
long end=System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) +
"ms");
}
}
问题总结:
Q:为什么不直接记录下未确认的消息,而是在map中删除已确认的消息,剩下的就是未确认
A:nackCallback只会告诉你是发送失败的编号,不能给你发送失败的具体数据,所以只能把所有数据都存一边,删除发送成功的数据,这样剩下的都是发送失败的数据
Q:multiple什么意思?批量确实跟单个有什么区别?如果是批量确认1-10,但是9没有收到,那不是丢失数据了吗
channel.addConfirmListener((deliverTag,multiple)->{
//确认回调函数
//步骤二:删除已经确认的消息,剩下的就是未确认的消息
if(multiple){ //是否批量确认,一半false
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliverTag);
confirmed.clear();
}else {
outstandingConfirms.remove(deliverTag);
}
}
交换机
从使用交换机开始就需要多个队列,因为一个队列中的消息只能被消费一次,我们需要多个消费者能接收到相同的消息那就要使用多个队列;或者我们希望指定某个消费者获得某个消息,也要使用多个队列
1.
X(Exchanges):交换机一方面:接收生产者发送的消息。另一方面:知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange类型有以下几种:
Fanout:广播,将消息交给所有绑定到交换机的队列-所有队列收到的消息是一样的
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Header:header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配队列。
Header模式不展开了,感兴趣可以参考这篇文章RabbitMQ学习之Headers交换类型(java)_slimina的博客-CSDN博客
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!所以每个交换机需要使用RoutingKey绑定指定的队列
2.
使用过空字符串(“”),则表明使用默认交换机,不是不使用交换机
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
3.临时队列
临时队列就是没有持久化的队列,我们可以使用一个简单的方法创建临时队列,名字是随机的
String queueName = channel.queueDeclare().getQueue();
效果:
③Publish/subscribe(交换机类型:Fanout,也称为广播 )
Fanout 这种类型非常简单。正如从名称中猜到的那样,它是将接收到的所有消息广播到它知道的 所有队列中。系统中默认有些 exchange 类型
在Fanout类型下,routingkey已经无所谓了,反正每个消费者都会收到,管他什么对应信道
官方文档写的很清楚 fanout类型的交换机可以忽略routingkey 只要队列绑定了交换机都能收到
实战:
生产者-只要声明交换机名和RountingKey就行,也可以声明交换机类型和绑定队列
public class Producer1 {
public static final String EXCHANGE_NAME = "logs" ;
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.nextLine();
//注意这里的第二个参数,如果定义了交换机是RountingKey,如果使用默认交换机""那么这里应该填队列名
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes(StandardCharsets.UTF_8));
}
}
}
消费者一(这里消费者的写法,跟上面写的都可以用)需要声明交换机类型和绑定队列
public class Consumer1 {
public static final String EXCHANGE_NAME = "logs" ;
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
String queueName = channel.queueDeclare().getQueue();
//声明交换机类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
//使用RountingKey绑定队列
channel.queueBind(queueName,EXCHANGE_NAME,"");
DeliverCallback deliverCallback=(consumerTag,message)->{
System.out.println("consumer1 receive :"+new String(message.getBody()));
};
channel.basicConsume(queueName,deliverCallback,consumerTag -> {});
}
}
消费者二
public class Consumer2 {
public static final String EXCHANGE_NAME = "logs" ;
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
String queueName = channel.queueDeclare().getQueue();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
channel.queueBind(queueName,EXCHANGE_NAME,"");
DeliverCallback deliverCallback=(consumerTag,message)->{
System.out.println("consumer2 receive :"+new String(message.getBody()));
};
channel.basicConsume(queueName,deliverCallback,consumerTag -> {});
}
}
效果:两个消费者都接受到了相同的消息
④Routing 路由模型(交换机类型:direct)
Routing模型示意图:
P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
C1:消费者,其所在队列指定了需要routing key 为 error 的消息
C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
生产者
public class Producer {
public static final String EXCHANGE_NAME = "direct_logs" ;
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.nextLine();
//basicPublish,第二个参数的意思就是RountingKey的值,指定发送到哪个队列
channel.basicPublish(EXCHANGE_NAME,"error",null,message.getBytes(StandardCharsets.UTF_8));
}
}
}
消费者二
public class Consumer2 {
public static final String EXCHANGE_NAME = "direct_logs" ;
public static final String QUEUE_NAME="disk";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"error");
DeliverCallback deliverCallback=(consumerTag,message)->{
System.out.println("disk receive :"+new String(message.getBody()));
};
channel.basicConsume(QUEUE_NAME,deliverCallback,consumerTag -> {});
}
}
消费者一
public class Consumer1 {
public static final String EXCHANGE_NAME = "direct_logs" ;
public static final String QUEUE_NAME="console";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"info");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"warning");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"error");
DeliverCallback deliverCallback=(consumerTag,message)->{
System.out.println("console receive :"+new String(message.getBody()));
};
channel.basicConsume(QUEUE_NAME,deliverCallback,consumerTag -> {});
}
}
效果:指定Rounting=error,发送消息"error",两个消费者都接收到消息
⑤Topics 通配符模式(交换机类型:topics)
之前的问题
在上一个小节中,我们改进了日志记录系统。我们没有使用只能进行随意广播的 fanout 交换机,而是 使用了 direct 交换机,从而有能实现有选择性地接收日志;
尽管使用 direct 交换机改进了我们的系统,但是它仍然存在局限性-比方说我们想接收的日志类型有 info.base 和 info.advantage,某个队列只想 info.base 的消息,那这个时候 direct 就办不到了。这个时候 就只能使用 topic 类型。——尚硅谷文档
Although using the direct exchange improved our system, it still has limitations - it can't do routing based on multiple criteria.
路由模式 (exchange:direct) 和发布/订阅模式(exchange:fanout)基本是绑定死的,路由模式绑定rountingkey无法更改,发布/订阅就是每个人都得发
所有需要一个模式灵活变通,我想广播就广播,我想给指定一个人发就给一个人发,我想给两个人发就是两个人——便有了topic模式!
Topic 的要求
发送到类型是 topic 交换机的消息的 routing_key 不能随意写,必须满足一定的要求,它必须是一个单 词列表,以点号分隔开。这些单词可以是任意单词,比如说:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit".这种类型的。当然这个单词列表最多不能超过 255 个字节。 在这个规则列表中,其中有两个替换符是大家需要注意的
*(星号)可以代替一个单词
#(井号)可以替代零个或多个单词
Topic 匹配案例
Q1-->绑定的是 中间带 orange 带 3 个单词的字符串(*.orange.*)
Q2-->绑定的是 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
第一个单词是 lazy 的多个单词(lazy.#)
上图是一个队列绑定关系图,我们来看看他们之间数据接收情况是怎么样的
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 了
生产者
public class EmitLogTopic {
public static final String EXCHANGE_NAME="topic_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
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> bindingKeyEntry : bindingKeyMap.entrySet()) {
System.out.println("emit.....");
String message = bindingKeyEntry.getValue();
String routingKey = bindingKeyEntry.getKey();
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes(StandardCharsets.UTF_8));
}
}
}
消费者一
public class ReceiveLogTopic1 {
public static final String QUEUE_NAME="Q1";
public static final String EXCHANGE_NAME="topic_exchange";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//交换机绑定队列
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.orange.*");
System.out.println("等待接收消息.....");
//接收消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message=new String(body);
System.out.println("queueName:"+QUEUE_NAME+" RoutingKey:"+envelope.getRoutingKey()+" message:"+message);
}
};
channel.basicConsume(QUEUE_NAME,consumer);
}
}
消费者二
public class ReceiveLogTopic1 {
public static final String QUEUE_NAME="Q1";
public static final String EXCHANGE_NAME="topic_exchange";
public static void main(String[] args) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//声明交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
//交换机绑定队列
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.orange.*");
System.out.println("等待接收消息.....");
//接收消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message=new String(body);
System.out.println("queueName:"+QUEUE_NAME+" RoutingKey:"+envelope.getRoutingKey()+" message:"+message);
}
};
channel.basicConsume(QUEUE_NAME,consumer);
}
}
效果:
死信队列
死信的概念
先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理 解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息 进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有 后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息 消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时 间未支付时自动失效。
死信产生的原因
①消息 TTL(Time To Live) 过期
②队列达到最大长度(队列满了,无法再添加数据到 mq 中)
③消息被拒绝(basic.reject :拒绝应答 或 basic.nack:否定应答)并且 requeue=false :不重新入队
实战
生产者(①通过设置TTL产生死信)
测试的时候先把消费者一二都启动,是为了创建死信队列/交换机和普通队列/交换机,然后在都关闭,开启生产者就能看到效果
public class EmitDl {
public static final String NORMAL_EXCHANGE="normal_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//再生产者中设置消息的TTL
AMQP.BasicProperties properties=new AMQP.BasicProperties().builder().expiration("10000").build();
for (int i = 0; i < 10; i++) {
String message="info"+i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangSan",properties,message.getBytes(StandardCharsets.UTF_8));
System.out.println("emit "+message);
}
}
}
也可以再消费者一中设置TTL,是在hashmap中,arguments.put("x-dead-ttl",10000) 过期时间为10s
②除了设置TTL 也可以设置队列最大长度,方法在消费者一中设置 arguments.put("x-max-length",7);超过7个的消息就好变成死信
注意:对队列的属性进行修改,需要先删除队列
③也可以通过拒绝消息(代码见下)
消费者一
问题;
Q:到底消费者一中是否要声明死信队列,生产者中是否要声明普通队列;
A:其实就是看谁先启动,你想不管启动谁都能生效,就都写,如果有的写有的不写,那就要保证先启动的里面先声明了交换机/队列
Q:怎么知道他是死信交换机和死信队列
A:死信交换机:在普通队列绑定某个交换机时候,在queueDeclare中设置(详见下),如果最后一个参数hashmap中有x-head-letter-exchange的key那就是死信交换机
死信队列:就是普通的队列,配置上面没有特别的
public class ReceiveLogDl1 {
public static final String NORMAL_QUEUE="normal_queue";
public static final String NORMAL_EXCHANGE="normal_exchange";
public static final String DEAD_QUEUE="dead_queue";
public static final String DEAD_EXCHANGE="dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
/* //声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);*/
//声明队列,并将队列绑定死性交换机
Map<String, Object> arguments=new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
arguments.put("x-dead-letter-routing-key", "liSi");
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//普通队列绑定普通交换机
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangSan");
//consumer
channel.basicConsume(NORMAL_QUEUE, true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("receive message:"+new String(body));
}
});
}
}
消费者二
public class ReceiveLogDl2 {
public static final String DEAD_QUEUE="dead_queue";
public static final String DEAD_EXCHANGE="dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//声明交换机
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//binding
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"liSi");
//consumer
channel.basicConsume(DEAD_QUEUE, true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("receive message:"+new String(body));
}
});
}
}
补充:通过拒绝消息来产生死信-一定要手动ack
消费者二
public class ReceiveLogDl1 {
public static final String NORMAL_QUEUE="normal_queue";
public static final String NORMAL_EXCHANGE="normal_exchange";
public static final String DEAD_QUEUE="dead_queue";
public static final String DEAD_EXCHANGE="dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
//声明普通交换机
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
/* //声明死信交换机
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);*/
//声明队列,并将队列绑定死性交换机
Map<String, Object> arguments=new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
arguments.put("x-dead-letter-routing-key", "liSi");
/*//设置最长队列
arguments.put("x-max-length",7);*/
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//普通队列绑定普通交换机
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangSan");
//consumer,如果是拒绝消息,一定是手动ack
channel.basicConsume(NORMAL_QUEUE, false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body);
if(message.equals("info5")){
/*
* long deliveryTag 消息的tag
* boolean requeue 拒绝之后是否放回队列
* */
channel.basicReject(envelope.getDeliveryTag(), false);
}else {
channel.basicAck(envelope.getDeliveryTag(), false);
System.out.println("receive message:"+ message);
}
}
});
}
}
注意点:
效果:
延迟队列
什么是延迟队列?
如果因为TTL而产生了死信队列,那么这个普通队列就称为延迟队列
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望 在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的 元素的队列。
延迟队列使用场景
1.订单在十分钟之内未支付则自动取消
2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。
5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如: 发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎 使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果 数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求, 如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支 付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十 分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万 级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单 的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
RabbitMQ 中的 TTL
TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有 消息的最大存活时间
单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置 TTL 属性的队列,那么这 条消息如果在 TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的 TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
方式一:在普通队列绑定死信交换机时设置
方法二:在消息中设置ttl
区别:如果设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队 列中),而第二种方式,消息即使过期,也不一定会被马上丢弃,因为消息是否过期是在即将投递到消费者 之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间;另外,还需 要注意的一点是,如果不设置 TTL,表示消息永远不会过期,如果将 TTL 设置为 0,则表示除非此时可以 直接投递该消息到消费者,否则该消息将会被丢弃。
看不懂,自己的概括:
在队列中设置ttl,一旦设定,就很麻烦,更改的话还要删掉队列(还是交换机?)
在消息中设置ttl,灵活,缺点:队列遵循先进先出,如果队首有一个ttl=10s的消息,进队一个ttl=1s的消息,那这个ttl=1s的消息必须等到ttl=10s的消息走了,他才能出队。为了解决这个问题
整合 springboot
创建一个springboot
<dependencies>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
配置文件
spring.rabbitmq.host=182.92.234.71
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123
添加swagger配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("rabbitmq 接口文档")
.description("本文档描述了 rabbitmq 微服务接口定义")
.version("1.0")
.contact(new Contact("enjoy6288", "http://atguigu.com",
"1551388580@qq.com"))
.build();
}
}
代码架构图
配置文件类代码-声明普通交换机/死信交换机,普通队列/死信队列,并将它们绑定
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//配置各个交换机,队列,已经对他们进行绑定
@Configuration
public class TtlQueueConfig {
public static final String NORMAL_QUEUE_A = "QA";
public static final String NORMAL_QUEUE_B = "QB";
public static final String NORMAL_EXCHANGE = "X";
public static final String DEAD_LETTER_QUEUE = "QD";
public static final String DEAD_LETTER_EXCHANGE="Y";
//declare 普通交换机
@Bean(NORMAL_EXCHANGE)
public DirectExchange normalExchange(){
return new DirectExchange(NORMAL_EXCHANGE,false,true);
}
//declare dead letter exchange
@Bean(DEAD_LETTER_EXCHANGE)
public DirectExchange deadLetterExchange(){
return new DirectExchange(DEAD_LETTER_EXCHANGE,false,true);
}
//declare normal queue QA and QA binging dead letter exchange Y and set ttl=10s
@Bean(NORMAL_QUEUE_A)
public Queue queueQA(){
return QueueBuilder.nonDurable(NORMAL_QUEUE_A).autoDelete().ttl(10000).deadLetterExchange(DEAD_LETTER_EXCHANGE).deadLetterRoutingKey("YD").build();
}
//declare normal queue QB and QB binging dead letter exchange Y and set ttl=40s
@Bean(NORMAL_QUEUE_B)
public Queue queueQB(){
return QueueBuilder.nonDurable(NORMAL_QUEUE_B).autoDelete().ttl(40000).deadLetterExchange(DEAD_LETTER_EXCHANGE).deadLetterRoutingKey("YD").build();
}
// normal queue QA binging normal exchange X
@Bean
public Binding queueQABindingX(@Qualifier(NORMAL_QUEUE_A)Queue queueQA,
@Qualifier(NORMAL_EXCHANGE)DirectExchange normalExchange){
return BindingBuilder.bind(queueQA).to(normalExchange).with("XA");
}
// normal queue QB binging normal exchange X
@Bean
public Binding queueQBBindingX(@Qualifier(NORMAL_QUEUE_B)Queue queueQB,
@Qualifier(NORMAL_EXCHANGE)DirectExchange normalExchange){
return BindingBuilder.bind(queueQB).to(normalExchange).with("XB");
}
//declare dead queue QD and QD binding exchange Y
@Bean(DEAD_LETTER_QUEUE)
public Queue queueQD(){
return QueueBuilder.nonDurable(DEAD_LETTER_QUEUE).build();
}
//dead queue QD binding dead letter exchange Y
@Bean
public Binding queueQDBindingY(@Qualifier(DEAD_LETTER_QUEUE)Queue queueQD,
@Qualifier(DEAD_LETTER_EXCHANGE)DirectExchange deadLetterExchange){
return BindingBuilder.bind(queueQD).to(deadLetterExchange).with("YD");
}
}
消费者
@Slf4j
@RequestMapping("ttl")
@RestController
public class EmitMSGController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个 TTL 队列:{}", new Date(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
}
}
生产者
@Slf4j
@Component
public class ReceiveMSGController {
@RabbitListener(queues = "QD")
public void receive(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
局限:不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S 两个时间选项,如果需要一个小时后处理,那么就需要增加 TTL 为一个小时的队列,如果是预定会议室然 后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
延时队列优化
代码架构图
在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间,而是在消息里设置ttl,更加灵活
配置文件添加
//declare normal queue QC and QC binging dead letter exchange Y
@Bean(NORMAL_QUEUE_C)
public Queue queueQC(){
return QueueBuilder.nonDurable(NORMAL_QUEUE_C).autoDelete().deadLetterExchange(DEAD_LETTER_EXCHANGE).deadLetterRoutingKey("YD").build();
}
// normal queue QC binging normal exchange X
@Bean
public Binding queueQCBindingX(@Qualifier(NORMAL_QUEUE_C)Queue queueQC,
@Qualifier(NORMAL_EXCHANGE)DirectExchange normalExchange){
return BindingBuilder.bind(queueQC).to(normalExchange).with("XC");
}
生产者修改
@GetMapping("/sendExpirationMsg/{message}/{ttl}")
public void sendMSHPro(@PathVariable String message,
@PathVariable String ttl){
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttl, message);
rabbitTemplate.convertAndSend("X","XC",message,msg->{
msg.getMessageProperties().setExpiration(ttl);
return msg;
});
}
发起请求
http://localhost:8080/ttl/sendExpirationMsg/你好 1/20000
http://localhost:8080/ttl/sendExpirationMsg/你好 2/2000
效果
Q:消息 你好2 明明只是延迟2s为什么跟 你好1 一样延迟了20s呢?
A:因为队列是FIFO,你好1 先进去到了队头,等待20s才能出去, 你好2 ,虽然2s就能出去,但是他要先等队头元素出去了他才能出去,是个缺点
为了解决这个问题有了Rabbitmq 插件实现延迟队列
Rabbitmq 插件实现延迟队列
具体操作就是安装延时队列插件
安装延迟插件前
安装插件后
真正延迟的位置就到了交换机这里,就不会出现上述问题
实战代码架构图
在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:
delay是延迟,TTL是 Time To Live的缩写,生存时间值,注意这里跟上面的延迟队列的区别
配置文件类代码
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DelayQueueConfig {
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";
//declare queue
@Bean(DELAYED_QUEUE_NAME)
public Queue delayedQueue(){
return QueueBuilder.nonDurable(DELAYED_QUEUE_NAME).build();
}
//declare exchange
@Bean(DELAYED_EXCHANGE_NAME)
public CustomExchange delayedExchange(){
Map<String, Object> arguments = new HashMap<>();
//设置交换机的分配模式
arguments.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",false,true,arguments);
}
//queue binging exchange
@Bean
public Binding bindingDelayedQueue(@Qualifier(DELAYED_QUEUE_NAME)Queue delayedQueue,
@Qualifier(DELAYED_EXCHANGE_NAME)CustomExchange delayedExchange){
return BindingBuilder.bind(delayedQueue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
生产者
@Slf4j
@RestController
@RequestMapping("/ttl")
public class DelayedEmitMSGController {
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMSG(@PathVariable int delayTime,
@PathVariable String message){
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME,DELAYED_ROUTING_KEY,message,msg->{
msg.getMessageProperties().setDelay(delayTime);
return msg;
});
log.info("当前时间:{},发来一条延迟为{}毫秒的消息给队列{}:{}",new Date(),delayTime,"delayed.queue",message);
}
}
消费者
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;
@Slf4j
@Component
public class DelayedReceiveMSG {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receive(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
延迟队列总结
前一种是基于死信的,另一种是基于插件的
尚硅谷RabbitMQ教程丨快速掌握MQ消息中间件_哔哩哔哩_bilibili
发布确认高级
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:
前面讲的发布确认是消费者确认,高级部分是交换机/队列也可能宕机,需要交换机和队列都确认
确认机制方案
代码架构图
交换机的确认 的具体操作
步骤一:在配置文件中开启确认模式
spring.rabbitmq.publisher-confirm-type=correlated
关于confirm-type,可以写三个值分别是
NONE 禁用发布确认模式,是默认值
CORRELATED 发布消息成功到交换器后会触发回调方法
SIMPLE 类似于之前讲的发布确认的单个确认
第二步骤:加入回调接口
回调接口是当交换机没有确认消息或者确认的消息是失败的时候调用
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback{
/*
* 设置开启了回调函数,那么springboot就会去RabbitTemplate中找ConfirmCallback,
* 但api提供的只是接口,具体实现需要我们自己写
* 写完了,我们应该将他保存到api中,这样springboot调用的才是我们自己实现的回调函数
* @PostConstruct确保下面的init是在@Autowired之后运行
* */
@Autowired
RabbitTemplate rabbitTemplate;
//依赖注入 rabbitTemplate 之后再设置它的回调对象
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
/**
* true:
* 交换机无法将消息进行路由时,会将该消息返回给生产者
* false:
* 如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnsCallback(this);
}
/*
*
* 开启确认回调方法之后,无法交换机是否接收到消息都会调用这个方法
* 1.如果交换机收到消息触发回调
* CorrelationData correlationData 保存回调消息的ID及相关信息
* boolean ack 交换机确认收到消息,ack=true
* String cause 确认收到,cause=null,只有接收失败才 !=null
* 2.如果交换机接收消息失败触发回调
* CorrelationData correlationData 保存回调消息的ID及相关信息
* boolean ack 交换机收到消息失败,ack=false
* String cause 失败的原因
* */
@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(ReturnedMessage returned) {
String message=new String(returned.getMessage().getBody());
String exchange = returned.getExchange();
String cause = returned.getReplyText();
String routingKey =returned.getRoutingKey();
log.error(" 消 息 {}, 被交换机 {} 退回,退回原因 :{}, 路 由 key:{}",
message,exchange,cause,routingKey);
}
}
配置文件
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_ROUTINGKEY="key1";
//声明业务 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(CONFIRM_ROUTINGKEY);
}
}
生产者
@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmEmitMSGController {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable String message){
//因为在回调函数中有一个参数CorrelationData,所以我们传消息的时候也要附带这个,但是不知道为什么
//指定消息 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 ConfirmReceiveMSG {
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);
}
}
回退消息机制
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如 果发现该消息不可路由(就是队列没有正常接收消息),那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何 让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参 数可以在当消息传递过程中不可达目的地时将消息返回给生产者。
队列的的确认 的具体操作-就是在回调接口中加上回退,代码写在回调接口中
接口的具体实现,注意几个api
效果
备份交换机-比回退更好的方法
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息 无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然 后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者 所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增 加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的 复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些 处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。 在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份 交换机可以理解为 RabbitMQ 中交换机的“备胎”,当我们为某一个交换机声明一个对应的备份交换机时, 就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由 备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,这样就能把所有消息都投递到与其绑 定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都 进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
代码架构图
代码
修改配置类
@Configuration
public class ConfirmConfig {
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
public static final String CONFIRM_ROUTINGKEY="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";
/* //声明业务 Exchange
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}*/
//声明确认 Exchange 交换机的备份交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME)
.withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME)
.build();
}
// 声明确认队列
@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(CONFIRM_ROUTINGKEY);
}
//声明备份 Exchange ,注意是fanout类型
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
// 声明警告队列
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
// 声明报警队列绑定关系
@Bean
public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange
backupExchange){
return BindingBuilder.bind(queue).to(backupExchange);
}
// 声明备份队列
@Bean("backQueue")
public Queue backQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
// 声明备份队列绑定关系
@Bean
public Binding backupBinding(@Qualifier("backQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(queue).to(backupExchange);
}
}
报警消费者
@Component
@Slf4j
public class WarningConsumer {
public static final String WARNING_QUEUE_NAME = "warning.queue";
@RabbitListener(queues = WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message) {
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}", msg);
}
}
效果
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先 级高,经过上面结果显示答案是备份交换机优先级高。
RabbitMQ 其他知识点
幂等性
概念
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。 举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常, 此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱 了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误 立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等
消息重复消费
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断, 故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但 实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
解决思路
MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费 者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消 息时用该 id 先判断该消息是否已消费过。
消费端的幂等性保障
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性, 这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a. 唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现
唯一 ID+指纹码机制
指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基 本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存 在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数 据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
Redis 原子性
利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费
优先级队列
使用场景
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如 果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall 商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创 造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存 放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景, 所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级, 否则就是默认优先级。
代码
@RestController
@Slf4j
@RequestMapping("ttl")
public class PriorityQueueController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/123")
public void seanMSG(){
for (int i = 0; i < 10; i++) {
String message="info"+i;
if(i==5){
rabbitTemplate.convertAndSend("", PriorityQueueConfig.PRIORITY_QUEUE,message,msg->{
msg.getMessageProperties().setPriority(10);
return msg;
});
}else{
rabbitTemplate.convertAndSend("",PriorityQueueConfig.PRIORITY_QUEUE,message);
}
}
}
}
@Slf4j
@Configuration
public class PriorityQueueConfig {
public static final String PRIORITY_QUEUE="priority_queue";
@Bean
public Queue priorityQueue(){
return QueueBuilder.nonDurable(PRIORITY_QUEUE).autoDelete().maxPriority(10).build();
}
}
注意,如果生产者,消费者都是开启,那么优先队列是没有用的,必须是队列是堵塞的时候优先队列才起作用
惰性队列
RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消 费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持 更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致 使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。 默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中, 这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留 一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的 时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法, 但是效果始终不太理想,尤其是在消息量特别大的时候。
方法就是在声明队列时候
优点就是内存消耗少
最后除了使用代码声明交换机队列,也可在可视化界面声明