狂神:https://www.kuangstudy.com/zl/rabbitmq
乐优项目:https://www.yuque.com/lisha-wosnh/or1xy5/rzk38o
erlang 23.2- 24.X RabbitMq:3.9.X
https://beta.erlang.org/patches/otp-23.2
RabbitMQ安装成功!
使用Docker安装
MQ:消息队列(message queue),本质是一个队列,在队列中存放消息,是一种跨进程的通信机制,用于上下游传递消息
两者间的区别和联系:
JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
JMS规定了两种消息模型;而AMQP的消息模型更加丰富
常见MQ产品
ActiveMQ:基于JMS
RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会
Kafka:分布式消息系统,高吞吐量
是一个消息中间件,负责接收存储并转发消息。
生产者:产生数据发送消息的程序
交换机:一方面接收来自生产者的消息,另一方面将消息推送到队列中。根据交换机的类型,处理接收到的消息,是推送到特定队列还是推送到多个队列,亦或者丢到消息
队列:RabbitMQ内部的数据结构,用于存储消息,受主机的内存和磁盘限制的约束,本质上是一个消息缓冲区。
消费者:消费者大多时候是一个等待接收消息的程序。
注意:消费者、生产者、中间件很多时候并不在一个机器上,同一个应用程序既可以是生产者又可以是消费者。
Server:又称Broker ,接收和分发消息的应用,接收客户端的连接,实现AMQP实体服务。 安装rabbitmq-server
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel(信道)::如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销
Message :消息,服务与应用程序之间传送的数据,由Properties
和body
组成,Properties可是对消息进行修饰,比如消息的优先级,延迟等高级特性,Body则就是消息体的内容。
package org.springframework.amqp.core;
public class Message implements Serializable {
private final MessageProperties messageProperties;
private final byte[] body;
public byte[] getBody() {
return this.body; //NOSONAR
}
public MessageProperties getMessageProperties() {
return this.messageProperties;
}
。。。。。。。。。。。。。。。。
}
package org.springframework.amqp.core;
/**
* Message Properties for an AMQP message.
*/
public class MessageProperties implements Serializable {
public static final Integer DEFAULT_PRIORITY = 0;
private Integer priority = DEFAULT_PRIORITY;
public void setPriority(Integer priority) {
this.priority = priority;
}
public Integer getPriority() {
return this.priority;
}
。。。。。。。。。。。。。。。。。。。。。。
}
Virtual Host :虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个虚拟主机理由可以有若干个Exhange和Queueu,同一个虚拟主机里面不能有相同名字的Exchange
Exchange:交换机,message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发
消息到 queue 中去。常用的类型有:direct (point-to-point)
点对点,topic (publish-subscribe)
发布订阅and fanout(multicast)
广播。(交换机不具备消息存储的能力)
Bindings:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保
存到 exchange 中的查询表中,用于 message 的分发依据
Routing key:是一个路由规则,虚拟机可以用它来确定如何路由一个特定消息。
Queue:队列:也成为Message Queue,消息队列,保存消息并将它们转发给消费者。消息最终被送到这里等待 consumer 取走
RabbitMQ提供了6种消息模型,但是第6种其实是RPC,并不是MQ,因此不予学习。那么也就剩下5种。
但是其实3、4、5这三种都属于订阅模型,只不过进行路由的方式不同。
使用Docker安装
P(producer/ publisher)
:生产者,一个发送消息的用户应用程序。
C(consumer)
:消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序
队列(红色区域)
:rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>8source>
<target>8target>
configuration>
plugin>
plugins>
build>
<dependencies>
<dependency>
<groupId>com.rabbitmqgroupId>
<artifactId>amqp-clientartifactId>
<version>5.10.0version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.6version>
dependency>
dependencies>
package com.lisa.rabitmq.one;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 生产者
*/
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
Channel channel = connection.createChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String message = "hello Lisa";
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕!");
}
}
启动后看到有一个消息Ready 状态
点击队列获取消息的内容:
package com.lisa.rabitmq.one;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 消费者
*/
public class Customer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
Channel channel = connection.createChannel();
System.out.println("等待接收消息!");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
System.out.println(new String(message.getBody()).toString());
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
启动,接收到消息
已经显示消息被消费了
工作队列或者竞争消费者模式
工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。
相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取。
P:生产者:任务的发布者
C1:消费者,领取任务并且完成任务,假设完成速度较快
C2:消费者2:领取任务并完成任务,假设完成速度慢
面试题:避免消息堆积?
1)采用workqueue,多个消费者监听同一队列。
2)接收到消息以后,而是通过线程池,异步消费。
当有多个消费者时,我们的消息会被哪个消费者消费呢,我们又该如何均衡消费者消费信息的多少呢?
主要有两种模式:
1、轮询模式的分发:一个消费者一条,按均分配;
2、公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少;按劳分配;
package com.lisa.rabitmq.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 抽取工具类:使用连接工厂创建连接的信道channel
*/
public class RabbitMQUtils {
public static Channel getChannel() throws Exception{
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
Channel channel = connection.createChannel();
return channel;
}
}
package com.lisa.rabitmq.two;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 第一个工作线程(消费者)
*/
public class Work01 {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
System.out.println("接收到的消息:"+new String(message.getBody()).toString());
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
System.out.println("C2消费者启动消息等待。。。");
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
启动两个工作线程,需要IDEA打开[Allow multiple instances],然后再次启动工作线程
启动后效果:
package com.lisa.rabitmq.two;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* 生产者
*/
public class Task01 {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//从控制台中接收消息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送消息完成:"+message);
}
}
}
启动后,在控制台输入
同时C1和C2消费者接收到信息:
通过程序执行发现生产者总共发送 4 个消息,消费者 1 和消费者 2 分别分得两个消息,并且
是按照有序的一个接收一次消息
在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但RabbitMQ 并不知道这种情况,它依然很公平的进行分发。
为了避免这种情况,我们可以设置参数channel.basicQos(1);
在消费者消费消息之前设置不公平分发:
package com.lisa.rabitmq.three;
/**
* 消费者1
*
*/
public class Work02 {
public static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
System.out.println("C1等待接收消息处理时间较短");
//消息消费的时候如何处理消息 模拟处理速度:沉睡1秒
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
SleepUtils.sleep(1);
System.out.println("接收到的消息:"+new String(message.getBody()).toString());
/**
* 消息应答:
* 1、消息标记tag
* 2、是否批量应答未应答的消息
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消费者取消消费接口回调逻辑!");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
System.out.println("C1消费者启动消息等待。。。");
//设置不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
//采用手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
package com.lisa.rabitmq.three;
/**
* 消费者2
*
*/
public class Work03 {
public static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
System.out.println("C2等待接收消息处理时间较长");
//消息消费的时候如何处理消息 模拟处理速度:沉睡30秒
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
SleepUtils.sleep(30);
System.out.println("接收到的消息:"+new String(message.getBody()).toString());
/**
* 消息应答:
* 1、消息标记tag
* 2、是否批量应答未应答的消息
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消费者取消消费接口回调逻辑!");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
System.out.println("C2消费者启动消息等待。。。");
//设置不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
//采用手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
测试:
生产者连续发送5条消息:
消费者C1:
消费者C2:
实现了不公平分发!
设置预取值C1为2,C2为5
//设置不公平分发
//int prefetchCount = 1;
//预取值
int prefetchCount = 2;
channel.basicQos(prefetchCount);
//采用手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答
就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
因此,RabbitMQ有一个ACK机制。当消费者获取消息后,会向RabbitMQ发送回执ACK,告知消息已经被
接收。不过这种回执ACK分两种情况:
自动ACK
:消息一旦被接收,消费者自动发送ACK
手动ACK
:消息接收后,不会发送ACK,需要手动调用
大家觉得哪种更好呢?
这需要看消息的重要性:
如果消息不太重要,丢失也没有影响,那么自动ACK会比较方便
如果消息非常重要,不容丢失。那么最好在消费完成后手动ACK,否则接收消息后就自动ACK,RabbitMQ就会把消息从队列中删除。如果此时消费者宕机,那么消息就丢失了。
之前的应答方式都是自动应答,现在来测试手动应答
Channel.basicAck
(用于肯定确认)
RabbitMQ已知道该消息并且成功的处理消息,可以将其丢弃了
Channel.basicNack
(用于否定确认)
Channel.basicReject
(用于否定确认)
与 Channel.basicNack 相比少一个参数
不处理该消息了直接拒绝,可以将其丢弃了
手动应答的好处是可以批量应答并且减少网络拥堵
multiple 的 true 和 false 代表不同意思
true :代表批量应答 channel 上未应答的消息
比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时 5-8 的这些还未应答的消息都会被确认收到消息应答
false: 只应答当前的消息。同上面相比只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
图一:消费者C1处理消息1,消费者C2处理消息2
图二:当消费者C1在处理消息时,突然失去连接,这时消息还未被处理完成,没有确认
图三:RabbitMQ 将了解到消息未完全处理,并将对其重新排队
图四:MQ 将消息分发给消费者C2进行处理
消费者
package com.lisa.rabitmq.three;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* 生产者
* 消息手动应答
*/
public class Task02 {
public static final String QUEUE_NAME = "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.next();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes("UTF-8"));
System.out.println("发送消息完成:"+message);
}
}
}
消费者C1
package com.lisa.rabitmq.three;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.lisa.rabitmq.utils.SleepUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 消费者
*
*/
public class Work02 {
public static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
System.out.println("C1等待接收消息处理时间较短");
//消息消费的时候如何处理消息
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
SleepUtils.sleep(1);
System.out.println("接收到的消息:"+new String(message.getBody()).toString());
/**
* 消息应答:
* 1、消息标记tag
* 2、是否批量应答未应答的消息
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消费者取消消费接口回调逻辑!");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
System.out.println("C1消费者启动消息等待。。。");
//采用手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
消费者C2
package com.lisa.rabitmq.three;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.lisa.rabitmq.utils.SleepUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 消费者
*
*/
public class Work03 {
public static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
System.out.println("C2等待接收消息处理时间较长");
//消息消费的时候如何处理消息
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
SleepUtils.sleep(30);
System.out.println("接收到的消息:"+new String(message.getBody()).toString());
/**
* 消息应答:
* 1、消息标记tag
* 2、是否批量应答未应答的消息
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消费者取消消费接口回调逻辑!");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
System.out.println("C2消费者启动消息等待。。。");
//采用手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
工具类:
package com.lisa.rabitmq.utils;
/**
* 睡眠工具类
*/
public class SleepUtils {
public static void sleep(int second){
try {
Thread.sleep(1000 * second);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
启动测试:
启动生产者,会看到队列
然后启动消费者C1和C2:
我们已经知道处理任务不丢失的情况,但是当RabbitMQ服务停掉以后但是如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
之前我们创建的队列都是非持久化的,rabbitmq 如果重启的化,该队列就会被删除掉,如果要队列实现持久化需要在声明队列的时候把 durable 参数设置为持久化
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
//让消息队列持久化
boolean durable = true;
channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments) throws IOException;
但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误
#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)
删除当前的队列ack_queue
然后再启动生产者,会看到该队列持久化的特性:D(持久化)
以下为控制台中持久化与非持久化队列的 UI 显示区、
这个时候即使重启 rabbitmq 队列也依然存在
要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN
添加这个属性。
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略,参考后边课件发布确认章节。
交换机持久化:
队列持久化:
消息持久化:
//1、单个确认发布 发布1000个单个确认消息,耗时178ms
public static void messageConfirmSingle() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列名称
String queueName = UUID.randomUUID().toString();
//声明队列
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
long begin = System.currentTimeMillis();
//批量发消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes());
//单个消息发送后就马上进行发布确认
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发布成功!");
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单个确认消息,耗时" + (end - begin) + "ms");
}
//2、批量确认发布
public static void messageConfirmBatch() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列名称
String queueName = UUID.randomUUID().toString();
//声明队列
channel.queueDeclare(queueName,true,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());
//每100个消息就批量确认发布
if(i % batchSize == 0){
//发布确认
channel.waitForConfirms();
}
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个批量确认发布消息,耗时" + (end - begin) + "ms");
}
异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面就让我们来详细讲解异步确认是怎么实现的。
发消息之前准备消息监听器,监听哪些消息成功了,哪些消息失败了
//消息确认成功 回调函数
ConfirmCallback ackCallback = (deliveryTag , multiple) -> {
System.out.println("确认的消息的标记:" + deliveryTag);
};
//消息确认失败 回调函数
ConfirmCallback nackCallback = (deliveryTag , multiple) -> {
System.out.println("未确认的消息的标记:" + deliveryTag);
};
/**
* 添加异步确认的监听器:监听哪些消息成功了 哪些消息失败了
*/
channel.addConfirmListener(ackCallback,nackCallback);//异步通知
然后发布消息
channel.basicPublish("",queueName,null,message.getBytes());
总代码:
//3、异步批量确认发布
public static void messageConfirmAsync() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列名称
String queueName = UUID.randomUUID().toString();
//声明队列
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//消息确认成功 回调函数
ConfirmCallback ackCallback = (deliveryTag , multiple) -> {
System.out.println("确认的消息的标记:" + deliveryTag);
};
//消息确认失败 回调函数
ConfirmCallback nackCallback = (deliveryTag , multiple) -> {
System.out.println("未确认的消息的标记:" + deliveryTag);
};
/**
* 添加异步确认的监听器:监听哪些消息成功了 哪些消息失败了
*/
channel.addConfirmListener(ackCallback,nackCallback);//异步通知
long begin = System.currentTimeMillis();
//批量发消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
channel.basicPublish("",queueName,null,message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个异步确认发布消息,耗时" + (end - begin) + "ms");
}
思路:
过程:
创建一个Map,用于存储消息(key:消息序列号,value:消息)
ConcurrentSkipListMap<Object, Object> outstandingConfirms = new ConcurrentSkipListMap<>();
批量发消息的时候,先将消息存储到这个Map中去(现在这些消息时未确认的),然后再发送
//channel.getNextPublishSeqNo() 获取下一个消息的序列号
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
channel.basicPublish("",queueName,null,message.getBytes());
这时候异步确认的监听器起作用了
消息确认成功回调函数
如果是批量确认,就将这些消息批量删除
如果时单个确认,就删除当前序列号的消息
if(multiple){
//返回的是小于等于当前序列号的未确认消息 是一个 map
ConcurrentNavigableMap<Object, Object> confirmed = outstandingConfirms.headMap(deliveryTag, true);
//消除该部分未确认的消息
confirmed.clear();
}else{
//只消除当前序列号的消息
outstandingConfirms.remove(deliveryTag);
}
消息确认失败回调函数
就可以根据序列号查询未确认的消息,然后输出
String message = (String) outstandingConfirms.get(deliveryTag);
System.out.println("发布的消息"+message+"未被确认,序号"+deliveryTag);
总代码:
//3、异步批量确认发布
public static void messageConfirmAsync() throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列名称
String queueName = UUID.randomUUID().toString();
//声明队列
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
/**
* 线程安全有序的一个哈希表,适用于高并发的情况
* 1.轻松的将序号与消息进行关联
* 2.轻松批量删除条目 只要给到序列号
* 3.支持并发访问
*/
ConcurrentSkipListMap<Object, Object> outstandingConfirms = new ConcurrentSkipListMap<>();
//消息确认成功 回调函数
/**
* 确认收到消息的一个回调
* 1.deliveryTag:消息序列号
* 2.multiple:是否为批量确认
* true 批量确认,可以确认小于等于当前序列号的消息
* false 单个确认 确认当前序列号消息
*/
ConfirmCallback ackCallback = (deliveryTag , multiple) -> {
System.out.println("确认的消息的标记:" + deliveryTag);
if(multiple){
//返回的是小于等于当前序列号的未确认消息 是一个 map
ConcurrentNavigableMap<Object, Object> confirmed = outstandingConfirms.headMap(deliveryTag, true);
//消除该部分未确认的消息
confirmed.clear();
}else{
//只消除当前序列号的消息
outstandingConfirms.remove(deliveryTag);
}
};
//消息确认失败 回调函数
ConfirmCallback nackCallback = (deliveryTag , multiple) -> {
String message = (String) outstandingConfirms.get(deliveryTag);
System.out.println("发布的消息"+message+"未被确认,序列号"+deliveryTag);
};
/**
* 添加一个异步确认的监听器 监听哪些消息成功了 哪些消息失败了
* 1.确认收到消息的回调
* 2.未收到消息的回调
*
*/
channel.addConfirmListener(ackCallback,nackCallback);//异步通知
long begin = System.currentTimeMillis();
//批量发消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
/**
* channel.getNextPublishSeqNo() 获取下一个消息的序列号
* 通过序列号与消息体进行一个关联
* 全部都是未确认的消息体
* 放入map中去
*/
outstandingConfirms.put(channel.getNextPublishSeqNo(),message);
channel.basicPublish("",queueName,null,message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个异步确认发布消息,耗时" + (end - begin) + "ms");
}
单独发布消息
同步等待确认,简单,但吞吐量非常有限。
批量发布消息
批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
异步处理:
最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些
RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产
者甚至都不知道这些消息传递传递到了哪些队列中。相反,生产者只能将消息发送到交换机(exchange),
交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确
切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢
弃它们。这就的由交换机的类型来决定。
总共有以下类型:
直接(direct),
主题(topic)
,标题(headers) , 扇出(fanout)
在本教程的前面部分我们对 exchange 一无所知,但仍然能够将消息发送到队列。之前能实现的
原因是因为我们使用的是默认交换机,我们通过空字符串(“”)
进行标识。
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
第一个参数是交换机的名称。**空字符串表示默认或无名称交换机:**消息能路由发送到队列中其实是由routingKey(bindingkey)
绑定 key 指定的,如果它存在的话。
之前的章节我们使用的是具有特定名称的队列(还记得 hello 和 ack_queue 吗?)。队列的名称我们来说至关重要,我们需要指定我们的消费者去消费哪个队列的消息。每当我们连接到 RabbitMQ 时,我们都需要一个全新的空队列,为此我们可以创建一个具有随机名称的队列,或者能让服务器为我们选择一个随机队列名称那就更好了。其次一旦我们断开了消费者的连接,队列将被自动删除。
创建临时队列的方式如下:
String queueName = channel.queueDeclare().getQueue();
创建出来之后长成这样:
什么是 bingding 呢,binding 其实是 exchange 和 queue 之间的桥梁,它告诉我们 exchange 和那个队
列进行了绑定关系。
Fanout,也称为广播。
流程图:
在广播模式下,消息发送流程是这样的:
1) 可以有多个消费者
2) 每个消费者有自己的queue(队列)
3) 每个队列都要绑定到Exchange(交换机)
4) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定。
5) 交换机把消息发送给绑定过的所有队列
6) 队列的消费者都能拿到消息。实现一条消息被多个消费者消费
package com.lisa.rabitmq.five;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* 发布订阅模式——fanout
* 生产者
*/
public class Producer2 {
public static final String EXCHANGE_NAME = "exchange_fanout";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明exchange,指定为fanout
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
System.out.println("生产者发送消息完成:"+message);
}
}
}
package com.lisa.rabitmq.five;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 发布订阅模式——fanout
* 消费者1
*
*/
public class Consumer01 {
public static final String EXCHANGE_NAME = "exchange_fanout";
public static final String QUEUE_NAME = "queue01_fanout";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
System.out.println("等待接收消息,把接收到的消息打印在屏幕........... ");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody()).toString();
System.out.println("消费者1 控制台打印接收到的消息:"+ m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
package com.lisa.rabitmq.five;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 发布订阅模式——fanout
* 消费者1
*
*/
public class Consumer02 {
public static final String EXCHANGE_NAME = "exchange_fanout";
public static final String QUEUE_NAME = "queue02_fanout";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
System.out.println("等待接收消息,把接收到的消息打印在屏幕........... ");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody()).toString();
System.out.println("消费者2 控制台打印接收到的消息:"+ m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
先启动生产者,发送消息
消费者1收到消息:
消费者2收到消息:
BuiltinExchangeType:交换机的类型
package com.rabbitmq.client;
/**
* Enum for built-in exchange types.
*/
public enum BuiltinExchangeType {
DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
private final String type;
BuiltinExchangeType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
有选择性的接收消息
在订阅模式中,生产者发布消息,所有消费者都可以获取所有消息。
在路由模式中,我们将添加一个功能 - 我们将只能订阅一部分消息。 例如,我们只能将重要的错误消息
引导到日志文件(以节省磁盘空间),同时仍然能够在控制台上打印所有日志消息。
但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
消息的发送方在向Exchange发送消息时,也必须指定消息的routing key。
package com.lisa.rabitmq.direct;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
/**
* 发布订阅模式——direct
* 生产者
*/
public class Producer3 {
public static final String EXCHANGE_NAME = "exchange_direct";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明exchange,指定为direct
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
//创建多个bindingkey
Map<String , String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("info","普通info消息");
bindingKeyMap.put("warning","警告 warning 信息");
bindingKeyMap.put("error","错误 error 信息");
//debug 没有消费这接收这个消息 所有就丢失了
bindingKeyMap.put("debug","调试 debug 信息");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
//获取绑定键
String bindingKey = bindingKeyEntry.getKey();//info warning error debug
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,bindingKey,null,message.getBytes("UTF-8"));
System.out.println("生产者发出的消息:" + message);
}
}
}
routingkey:error
package com.lisa.rabitmq.direct;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 发布订阅模式——direct
* 消费者1
*
*/
public class Consumer03 {
public static final String EXCHANGE_NAME = "exchange_direct";
public static final String QUEUE_NAME = "queue01_direct";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机 绑定到error
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"error");
System.out.println("消费者1等待接收消息,把接收到的消息打印在屏幕........... ");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody(),"UTF-8").toString();
m = "接收绑定键:" + message.getEnvelope().getRoutingKey()+"消息:"+m.toString();
System.out.println("消费者1 控制台打印接收到的消息:"+ m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
routingkey:info warning
package com.lisa.rabitmq.direct;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 发布订阅模式——direct
* 消费者1
*
*/
public class Consumer04 {
public static final String EXCHANGE_NAME = "exchange_direct";
public static final String QUEUE_NAME = "queue02_direct";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机 绑定到info warning
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"info");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"warning");
System.out.println("消费者2等待接收消息,把接收到的消息打印在屏幕........... ");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody(),"UTF-8").toString();
m = "接收绑定键:" + message.getEnvelope().getRoutingKey()+"消息:"+m.toString();
System.out.println("消费者2 控制台打印接收到的消息:"+ m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
生产者
消费者1
消费者2
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。
只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如:
item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
audit.#
:能够匹配audit.irs.corporate
或者 audit.irs
audit.*
:只能匹配audit.irs
我们创建了三个绑定:
Q1绑定了绑定键*.orange.*
Q1匹配所有的橙色动物。
Q2绑定了*.*.rabbit
和lazy.#
。 Q2匹配关于兔子以及懒惰动物的消息。
练习:
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
package com.lisa.rabitmq.topic;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
/**
* 发布订阅模式——topic
* 生产者
*/
public class Producer4 {
public static final String EXCHANGE_NAME = "Topic2";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明exchange,指定为topic
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
//创建多个bindingkey
Map<String , String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit","1)被队列 Q1 Q2 接收到");
bindingKeyMap.put("quick.orange.fox","2) 被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox","3) 被队列 Q2 接收到");
for (Map.Entry<String, String> bindingKeyEntry : bindingKeyMap.entrySet()) {
//获取绑定键
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,bindingKey,null,message.getBytes("UTF-8"));
System.out.println("生产者发出的消息:" + message);
}
}
}
package com.lisa.rabitmq.topic;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 发布订阅模式——topic
* 消费者1
*
*/
public class Consumer05 {
public static final String EXCHANGE_NAME = "Topic2";
public static final String QUEUE_NAME = "Q1";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机 绑定到error
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.orange.*");
System.out.println("消费者1等待接收消息,把接收到的消息打印在屏幕........... ");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody(),"UTF-8").toString();
m = "接收的队列是:"+QUEUE_NAME + "接收绑定键:" + message.getEnvelope().getRoutingKey()+" 消息: "+m.toString();
System.out.println("消费者1接收到的消息:" );
System.out.println(m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
package com.lisa.rabitmq.topic;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import com.rabbitmq.client.Delivery;
/**
* 发布订阅模式——direct
* 消费者1
*
*/
public class Consumer06 {
public static final String EXCHANGE_NAME = "Topic2";
public static final String QUEUE_NAME = "Q2";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//绑定队列到交换机 绑定到error
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"lazy.#");
System.out.println("消费者2等待接收消息........... ");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody(),"UTF-8").toString();
m = "接收的队列是:"+QUEUE_NAME + "接收绑定键:" + message.getEnvelope().getRoutingKey()+" 消息: "+m.toString();
System.out.println("消费者2 控制台打印接收到的消息:");
System.out.println(m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。
应用场景:为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息消费发生
异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时间未支付时
自动失效
package com.lisa.rabitmq.deadQueue;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.util.HashMap;
import java.util.Map;
/**
* 消费者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);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定死信交换机与死信队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
//声明普通队列
Map<String, Object> arguments = new HashMap<>();
//声明过期时间 也可以在生产方指定过期时间
//arguments.put("x-message=ttl",100000);
//正常队列设置死信交换机
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信队列routing-key
arguments.put("x-dead-letter-routing-key","lisi");
//arguments:用于正常队列与死信队列之间的绑定
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//绑定普通交换机与普通队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
System.out.println("C1等待接收消息:");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody(),"UTF-8").toString();
System.out.println("消费者1接收到的消息:"+m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(NORMAL_QUEUE,true,deliverCallback,cancelCallback);
}
}
消费者01启动:
后台显示:
队列显示:
交换机显示:
消费者01启动之后,停止
package com.lisa.rabitmq.deadQueue;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
/**
* 死信队列实战
* 生产者
*/
public class Producer01 {
//普通交换机
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());
System.out.println("生产者发送消息:"+message);
}
}
}
生产者发送10条消息:显示正常队列中有10条消息。
10秒之后,消息到达了死信队列中:
package com.lisa.rabitmq.deadQueue;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
/**
* 死信队列实战
* 消费者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("消费者02等待接收死信队列消息。。。。。");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody(),"UTF-8").toString();
System.out.println("消费者2接收到死信队列的消息:"+m);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,cancelCallback);
}
}
启动消费者02后,死信队列中的消息被消费:
修改生产代码:取消TTL过期时间:
package com.lisa.rabitmq.deadQueue;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
/**
* 死信队列实战
* 生产者
*/
public class Producer01 {
//普通交换机
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();
//channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
for (int i = 0; i < 10; i++) {
String message = "info" + i;
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,message.getBytes());
System.out.println("生产者发送消息:"+message);
}
}
}
在消费者01中添加正常队列的最大长度限制:
//设置正常队列长度的限制
arguments.put("x-max-length",6);
消费者02代码不变:
测试:
首先删除原先的正常队列,因为参数变了
然后启动消费者01后,停止
启动生产者:
可以看出正常队列最多消费6条消息,其余的消息放到了死信队列
启动消费者02后,看到死信队列中的消息被消费了
消息生产者代码同上生产者一致
C1 消费者代码
关闭队列最大长度限制
如果消息是info5就被拒绝
package com.lisa.rabitmq.deadQueue;
import com.lisa.rabitmq.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.util.HashMap;
import java.util.Map;
/**
* 消费者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);
//声明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定死信交换机与死信队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
//声明普通队列
Map<String, Object> arguments = new HashMap<>();
//声明过期时间 也可以在生产方指定过期时间
//arguments.put("x-message=ttl",100000);
//正常队列设置死信队列
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信队列routing-key
arguments.put("x-dead-letter-routing-key","lisi");
//设置正常队列长度的限制
//arguments.put("x-max-length",6);
channel.queueDeclare(NORMAL_QUEUE,false,false,false,arguments);
//绑定普通交换机与普通队列
channel.queueBind(NORMAL_QUEUE,NORMAL_EXCHANGE,"zhangsan");
System.out.println("C1等待接收消息:");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
String m = new String(message.getBody(),"UTF-8").toString();
if(m.equals("info5")){
System.out.println("消费者1接收到的消息:"+m + "此消息是被C1拒绝的");
//拒绝消息:(消息序号,是否放回原队列中)
//requeue 设置为 false 代表拒绝重新入队 该队列如果配置了死信交换机将发送到死信队列中
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
}else{
System.out.println("消费者1接收到的消息:"+m);
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
//开启手动应答
channel.basicConsume(NORMAL_QUEUE,false,deliverCallback,cancelCallback);
}
}
C2 消费者代码不变
启动消费者 1 然后再启动消费者 2
进入死信队列中去,看到就是消息info5
启动消费者2:消费掉死信队列中拒绝的消息
延迟队列就是死信队列的一种,是消息TTL过期
延时队列,队列内部是有序的,最重要的特性就体现在它的延时属性上,延时队列中的元素是希望
在指定时间到了以后或之前取出和处理,简单来说,延时队列就是用来存放需要在指定时间被处理的
元素的队列。
这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务,如:
发生订单生成事件,在十分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;看起来似乎
使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?如果
数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求,
如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支
付的账单,确实也是一个可行的方案。但对于数据量比较大,并且时效性较强的场景,如:“订单十
分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万
级别,对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单
的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。
TTL 是什么呢?TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有
消息的最大存活时间,
单位是毫秒。换句话说,如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这条消息如果在TTL 设置的时间内没有被消费,则会成为"死信"。如果同时配置了队列的TTL 和消息的TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。
另一种方式便是针对每条消息设置TTL
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.2.RELEASEversion>
<relativePath/>
parent>
<groupId>org.examplegroupId>
<artifactId>springboot-rabbitmqartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.9.2version>
dependency>
<dependency>
<groupId>org.springframework.amqpgroupId>
<artifactId>spring-rabbit-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
application.yml
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: admin
password: admin
package com.example.springbootRabbitmq.config;
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(webApi())
.select()
.build();
}
public ApiInfo webApi(){
return new ApiInfoBuilder()
.title("rabbitmq 接口文档")
.description("本文档描述了 rabbitmq 微服务接口定义")
.version("1.0")
.contact(new Contact("lisa",
"http://baidu.com",
"[email protected]"))
.build();
}
}
package com.example.springbootRabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootRabbitmq {
public static void main(String[] args) {
SpringApplication.run(SpringBootRabbitmq.class,args);
}
}
创建两个队列 QA 和 QB,两者队列 TTL 分别设置为 10S 和 40S,然后再创建一个交换机 X 和死信交
换机 Y,它们的类型都是direct,创建一个死信队列 QD,它们的绑定关系如下:
P生产者发送消息C,C只要在QD 队列中收消息即可
不管是10s的消息还是40s的消息,都会发送到QD(死信队列)
package com.example.springbootRabbitmq.config;
import com.rabbitmq.client.AMQP;
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;
/**
* TTL队列 配置文件
*/
@Configuration
public class TTLQueueConfig {
//普通交换机
public static final String X_EXCHANGE = "X";
//死信交换机
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
//普通队列
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
//死信队列
public static final String DEAD_LETTER_QUEUE = "QD";
//声明普通X交换机
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
//声明死信Y交换机
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明普通队列A并绑定死信队列 TTL 10s
@Bean("queueA")
public Queue queueA(){
Map<String, Object> arguments = new HashMap<>();
//声明当前队列绑定的死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
arguments.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL 10s
arguments.put("x-message-ttl", 10000);
return QueueBuilder.durable(QUEUE_A).withArguments(arguments).build();
}
//声明普通队列B并绑定死信队列 TTL 40s
@Bean("queueB")
public Queue queueB(){
Map<String, Object> arguments = new HashMap<>();
//声明当前队列绑定的死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
arguments.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL 10s
arguments.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(arguments).build();
}
//声明死信队列
@Bean("queueD")
public Queue queueD(){
return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
}
//队列QA绑定交换机X
@Bean
public Binding queueABindingX(@Qualifier("queueA") Queue queueA,@Qualifier("xExchange") DirectExchange xExchange){
// 队列QA X交换机 routing key
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//队列QB绑定交换机X
@Bean
public Binding queueBBindingX(@Qualifier("queueB") Queue queueB,@Qualifier("xExchange") DirectExchange xExchange){
// 队列QA X交换机 routing key
return BindingBuilder.bind(queueB).to(xExchange).with("XB");
}
//队列QD绑定交换机Y
@Bean
public Binding queueDBindingY(@Qualifier("queueD") Queue queueD,@Qualifier("yExchange") DirectExchange yExchange){
// 队列QA X交换机 routing key
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
生产者发送消息,主要是在Controller中发现两条消给两条普通队列
package com.example.springbootRabbitmq.controller;
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;
/**
* 生产者
* 发送延迟消息
* http://localhost:8080/ttl/sendMsg/嘻嘻嘻
*/
@Slf4j
@RestController
@RequestMapping("/ttl")
public class SendMessage {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMsg/{message}")
public String sendMsg(@PathVariable String message){
log.info("当前时间:{},发送一条信息给两个 TTL 队列,消息为:{}", new Date(), message);
rabbitTemplate.convertAndSend("X","XA","消息来自ttl为10s的队列:"+message);
rabbitTemplate.convertAndSend("X","XB","消息来自ttl为40s的队列:"+message);
return "发送消息OK";
}
}
所有的消息都会转到死信队列中,消费者只要在死信队列中接收消息即可
package com.example.springbootRabbitmq.consumer;
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.io.UnsupportedEncodingException;
import java.util.Date;
/**
* 死信队列ttl
* 消费者
*/
@Component
@Slf4j
public class DeadLetterConsumer {
//监听死信队列QD
@RabbitListener(queues = "QD")
public void receiveD(Message message, Channel channel) throws UnsupportedEncodingException {
String msg = new String(message.getBody(),"UTF-8").toString();
log.info("当前时间:{},收到死信队列信息为{}",new Date().toString(),msg);
}
}
观察时间,一个相差10s的消息,一个相差40s的消息
没有DirectExchange这个类
org.springframework.amqp.core.DirectExchange;即这个包没有
解决办法:提升spring-boot-starter-parent的版本
之前用的是2.2.2 改为2.3.11后可行
第一条消息在 10S 后变成了死信消息,然后被消费者消费掉,第二条消息在 40S 之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。不过,如果这样使用的话,岂不是每增加一个新的时间需求,就要新增一个队列,这里只有 10S 和 40S两个时间选项,如果需要一个小时后处理,那么就需要增加TTL 为一个小时的队列,如果是预定会议室然后提前通知这样的场景,岂不是要增加无数个队列才能满足需求?
在这里新增了一个队列 QC,绑定关系如下,该队列不设置TTL 时间,主要是通过生产者在发消息时来指定所需的时间
@Configuration
public class TTLQueueConfig {
//延迟队列优化
public static final String QUEUE_C = "QC";
//延迟队列优化:声明队列C
@Bean("queueC")
public Queue queueC(){
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
arguments.put("x-dead-letter-routing-key", "YD");
return QueueBuilder.durable(QUEUE_C).withArguments(arguments).build();
}
//队列QC绑定交换机X
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,@Qualifier("xExchange") DirectExchange xExchange){
// 队列QA X交换机 routing key
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
}
//延迟队列优化
@GetMapping("sendMsg/{message}/{ttlTime}")
public String sendMsg2(@PathVariable String message,@PathVariable String ttlTime){
log.info("当前时间:{},发送一条时长{}毫秒TTL信息给队列C,消息为:{}", new Date(), ttlTime,message);
rabbitTemplate.convertAndSend("X","XC",message,correlationData->{
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
return "延迟队列优化发送消息Ok";
}
源码:
// 交换机 routingkey 消息 messagePostProcessor
@Override
public void convertAndSend(String exchange, String routingKey, final Object message,
final MessagePostProcessor messagePostProcessor) throws AmqpException {
convertAndSend(exchange, routingKey, message, messagePostProcessor, null);
}
MessagePostProcessor
@FunctionalInterface
public interface MessagePostProcessor {
Message postProcessMessage(Message message) throws AmqpException;
。。。
}
http://localhost:8080/ttl/sendMsg/你好1/20000
http://localhost:8080/ttl/sendMsg/你好2/2000
问题:
看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消
息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,
如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。
上文中提到的问题,确实是一个问题,如果不能实现在消息粒度上的 TTL,并使其在设置的TTL 时间
及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题。
在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载
rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。
方法一:
进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启 RabbitMQ
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
方法二:使用Docker安装的RabbitMQ使用这个方法:
# 容器外面复制插件到容器内部的/plugins目录下:b539e7f05ea2:是容器ID
docker cp /home/下载/rabbitmq_delayed_message_exchange-3.9.0.ez b539e7f05ea2:/plugins
# 进入容器内部 查看插件是否放进去
$ docker exec -it myrabbitmq /bin/bash
root@b539e7f05ea2:/plugins# ls
启用插件
命令:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
然后重启容器
# 进入容器内部的/plugins目录下
$ docker exec -it myrabbitmq /bin/bash
root@b539e7f05ea2:/# cd /plugins
root@b539e7f05ea2:/plugins# rabbitmq-plugins enable rabbitmq_delayed_message_exchange # 启用插件
Enabling plugins on node rabbit@b539e7f05ea2:
rabbitmq_delayed_message_exchange
The following plugins have been configured:
rabbitmq_delayed_message_exchange
rabbitmq_management
rabbitmq_management_agent
rabbitmq_prometheus
rabbitmq_web_dispatch
Applying plugin configuration to rabbit@b539e7f05ea2...
The following plugins have been enabled:
rabbitmq_delayed_message_exchange
started 1 plugins.
root@b539e7f05ea2:/plugins# exit # 退出容器
$ docker restart myrabbitmq #重启容器
myrabbitmq
可以看到延迟插件生效:
package com.example.springbootRabbitmq.config;
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;
/**
* Rabbitmq 插件实现延迟队列
*/
@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_NAME = "delayed.routingkey";
//声明交换机 自定义交换机 我们在这里定义的是一个延迟交换机
@Bean
public CustomExchange delayedExchange(){
Map<String, Object> arguments = new HashMap<>();
//自定义交换机的类型
arguments.put("x-delayed-type","direct");
//CustomExchange(String name, String type, boolean durable, boolean autoDelete, Map arguments)
return new CustomExchange(DELAYED_EXCHANGE_NAME,"x-delayed-message",true,false,arguments);
}
//声明队列
@Bean
public Queue delayedQueue(){
return QueueBuilder.durable(DELAYED_QUEUE_NAME).build();
//return new Queue(DELAYED_QUEUE_NAME);
}
//队列与交换机的绑定
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,@Qualifier("delayedExchange") CustomExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with(DELAYED_ROUTING_KEY_NAME).noargs();
}
}
//延迟队列优化:基于插件
@GetMapping("sendDelayMsg/{message}/{delayedTime}")
public String sendDelayMsg(@PathVariable String message,@PathVariable Integer delayedTime){
log.info("当前时间:{},发送一条延迟{}毫秒信息给队列delayed.queue,消息为:{}", new Date(), delayedTime,message);
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME,DelayedQueueConfig.DELAYED_ROUTING_KEY_NAME,message, correlationData->{
correlationData.getMessageProperties().setDelay(delayedTime);
return correlationData;
});
return "延迟队列优化:基于插件发送消息Ok";
}
package com.example.springbootRabbitmq.consumer;
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;
/**
* 延迟队列:基于插件
* 消费者
*/
@Component
@Slf4j
public class DelayedConsumer {
//队列
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){
String msg = new String(message.getBody());
log.info("当前时间:{},收到延时队列信息为{}",new Date().toString(),msg);
}
}
访问:
http://localhost:8080/ttl/sendDelayMsg/你好1/20000
http://localhost:8080/ttl/sendDelayMsg/你好2/2000
第二个消息被先消费掉了,符合预期
延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。当然,延时队列还有很多其它选择,比如利用 Java DelayQueue,利用 Redis 的 zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景
在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢? 特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢:
主要是声明交换机、队列以及绑定队列与交换机
package com.example.springbootRabbitmq.config;
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_ROUTING_KEY = "key1";
//声明交换机
@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 bindingQueueExchange(@Qualifier("confirmQueue") Queue confirmQueue,@Qualifier("confirmExchange") DirectExchange confirmExchange){
return BindingBuilder.bind(confirmQueue).to(confirmExchange).with(CONFIRM_ROUTING_KEY);
}
}
以下是正常消息发送
package com.example.springbootRabbitmq.controller;
import com.example.springbootRabbitmq.config.ConfirmConfig;
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;
/**
* 发布确认高级——消息生产者
*/
@Slf4j
@RestController()
@RequestMapping("/confirm")
public class ConfirmController {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("/sendConfirmMsg/{message}")
public String sendConfirmMsg(@PathVariable String message){
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message);
log.info("生产者发送消息的内容:{}",message);
return "发送消息成功!";
}
}
主要是监听队列中的消息,接收消息
package com.example.springbootRabbitmq.consumer;
import com.example.springbootRabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 发送确认高级——消费者
*/
@Slf4j
@Component
public class ConfirmConsumer {
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME)
public void receive(Message message){
String msg = new String(message.getBody());
log.info("消费者收到的消息为:{}",msg);
}
}
访问:http://localhost:8080/confirm/sendConfirmMsg/AAA1
发送消息、接收消息正常!
当交换机接收不到消息,就需要一个回调接口将当前的消息保存下来,以便日后发送消息
package com.example.springbootRabbitmq.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
/**
* 回调接口
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
/**
* 交换机不管是否接收到消息都会触发这个回调接口
* ack=true:交换机接收到消息
* ack=false:交换机未接收到消息
*
* @param correlationData 保存回调消息的ID及相关信息
* @param ack 是否接收到消息
* @param cause 失败的原因,若ack=true,cause=null
*/
@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);
}
}
}
要想让这个回调接口生效,就需要在配置文件中进行配置
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,
其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法
等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是
waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker
application.yml
spring:
rabbitmq:
host: 172.17.0.2
port: 5672
username: admin
password: admin
publisher-confirm-type: correlated #发布确认模式
修改生产者,设置回调消息的ID
@GetMapping("/sendConfirmMsg/{message}")
public String sendConfirmMsg(@PathVariable String message){
//设置回调消息的ID,然后放入发送消息的方法中
CorrelationData correlationData = new CorrelationData("1"); rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
log.info("生产者发送消息的内容:{}",message);
return "发送消息成功!";
}
启动测试:可以看到交换机已经接收到消息了
模拟交换机出错,接收不到消息:修改交换机
//写错交换机的名字
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+1,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData);
再次测试:
生产者发送消息的内容:AAA1
2021-12-01 11:06:53.127 ERROR 1113286 --- [172.17.0.2:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange1' in vhost '/', class-id=60, method-id=40)
2021-12-01 11:06:53.128 INFO 1113286 --- [nectionFactory2] c.e.s.config.MyCallBack : 交换机没有接收到 id 为:1的消息,原因是:channel error; protocol method: #method(reply-code=404, reply-text=NOT_FOUND - no exchange 'confirm.exchange1' in vhost '/', class-id=60, method-id=40)
模拟队列接收不到消息:修改routingkey
@GetMapping("/sendConfirmMsg/{message}")
public String sendConfirmMsg(@PathVariable String message){
//设置回调消息的ID,然后放入发送消息的方法中
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,message,correlationData1);
CorrelationData correlationData2 = new CorrelationData("2");
//模拟队列接收不到消息
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY+1,message,correlationData2);
log.info("生产者发送消息的内容:{}",message);
return "发送消息成功!";
}
可以看到,发送了两条消息,第一条消息的 RoutingKey 为 “key1”,第二条消息的 RoutingKey 为"key2",两条消息都成功被交换机接收,也收到了交换机的确认回调,但消费者只收到了一条消息,因为第二条消息的 RoutingKey 与队列的 BindingKey 不一致,也没有其它队列能接收这个消息,所有第二条消息被直接丢弃了。
在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息,如
果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的。那么如何
让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。
方法有两种:
方法一:
在application.yml
中设置spring.rabbitmq.publisher-returns: true
方法二:
通过设置 mandatory 参数
可以在当消息传递过程中不可达目的地时将消息返回给生产者。
spring:
rabbitmq:
host: 172.17.0.2
port: 5672
username: admin
password: admin
publisher-confirm-type: correlated #发布确认模式
publisher-returns: true #消息路由失败回退消息给生产者
如果在application.yml中设置了spring.rabbitmq.publisher-returns: true就不必设置Mandatory为true,二者效果一致
import javax.annotation.PostConstruct;
/**
* 回调接口
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
//[注入]把当前写的这个类注入到RabbitTemplate中去
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
/**
[二选一]:如果在application.yml中设置了spring.rabbitmq.publisher-returns: true就不必设置Mandatory为true
* true:交换机无法将消息进行路由时,会将该消息返回给生产者
* false:如果发现消息无法进行路由,则直接丢弃
*/
rabbitTemplate.setMandatory(true);
//设置回退消息交给谁处理
rabbitTemplate.setReturnCallback(this);
}
/**
* 可以在当消息传递过程中不可达目的地时将消息返回给生产者。
* 只有在不可达时才进行回调
* @param message 消息
* @param replyCode 响应码
* @param replyText 响应内容
* @param exchange 交换机
* @param routingKey 路由
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息:{},被交换机 :{}退出,退回原因 {},路由key:{}",new String(message.getBody()),exchange,replyText,routingKey);
}
}
生产者代码保持上面8.1.9中测试2的队列接收不到消息的场景:
生产者发送消息CCC,两个交换机已经收到消息,消费者只有收到一条消息,另一条消息被退回
有了 mandatory 参数和回退消息,我们获得了对无法投递消息的感知能力,有机会在生产者的消息
无法被投递时发现并处理。但有时候,我们并不知道该如何处理这些无法路由的消息,最多打个日志,然
后触发报警,再来手动处理。而通过日志来处理这些无法路由的消息是很不优雅的做法,特别是当生产者
所在的服务有多台机器的时候,手动复制日志会更加麻烦而且容易出错。而且设置 mandatory 参数会增
加生产者的复杂性,需要添加处理这些被退回的消息的逻辑。如果既不想丢失消息,又不想增加生产者的
复杂性,该怎么做呢?前面在设置死信队列的文章中,我们提到,可以为队列设置死信交换机来存储那些
处理失败的消息,可是这些不可路由消息根本没有机会进入到队列,因此无法使用死信队列来保存消息。
在 RabbitMQ 中,有一种备份交换机的机制存在,可以很好的应对这个问题。什么是备份交换机呢?备份
交换机可以理解为 RabbitMQ 中交换机的“备胎”,**当我们为某一个交换机声明一个对应的备份交换机时,就是为它创建一个备胎,当交换机接收到一条不可路由消息时,将会把这条消息转发到备份交换机中,由备份交换机来进行转发和处理,通常备份交换机的类型为 Fanout ,**这样就能把所有消息都投递到与其绑定的队列中,然后我们在备份交换机下绑定一个队列,这样所有那些原交换机无法被路由的消息,就会都进入这个队列了。当然,我们还可以建立一个报警队列,用独立的消费者来进行监测和报警。
生产者发送两条消息,一条可以被[确认消费者]收到,另一条由于不可路由,该消费者不能收到。所以创建出一个备份交换机,来接收交换机不可路由的消息,然后将消息发送给备份队列和警告队列,然后用独立的消费者进行监测和警告
/**
* 发布确认高级
*/
@Configuration
public class ConfirmConfig {
//1、备份交换机、备份队列、警告队列
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";
//2、声明备份交换机
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
//3、声明备份队列
@Bean("backupQueue")
public Queue backupQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
//4、绑定
//备份队列绑定备份交换机
@Bean
public Binding backupQueueBindingBackupExchange(@Qualifier("backupQueue") Queue backupQueue,@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(backupQueue).to(backupExchange);
}
//警告队列绑定备份交换机
@Bean
public Binding warningQueueBindingBackupExchange(@Qualifier("warningQueue") Queue warningQueue,@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(warningQueue).to(backupExchange);
}
//5、确认交换机与备份交换机的绑定
//声明确认交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return ExchangeBuilder.directExchange(CONFIRM_QUEUE_NAME).durable(true).withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
//return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
}
由于确认交换机已经改变,所以应该删除后再启动,否则会报错
#method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'type' for exchange 'backup.exchange' in vhost '/': received 'fanout' but current is 'direct', class-id=40, method-id=10)
package com.example.springbootRabbitmq.consumer;
import com.example.springbootRabbitmq.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 警告消费者
*/
@Component
@Slf4j
public class WarningConsumer {
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message){
String msg = new String(message.getBody());
log.info("报警!发现不可路由的消息: {}",msg);
}
}
mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,消息究竟何去何从?谁优先
级高,经过上面结果显示答案是备份交换机优先级高。
用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。
举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是在响应客户端的时候也有可能出现网络中断或者异常等等
消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给MQ 返回 ack 时网络中断,
故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但
实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。
MQ 消费者的幂等性的解决一般使用全局 ID
或者写个唯一标识
比如时间戳 或者 UUID 或者订单消费
者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。
在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,
这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。
业界主流的幂等性有两种操作:
唯一 ID+指纹码机制
,利用数据库主键去重,利用 redis 的原子性
去实现指纹码
:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基
本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中
优势就是实现简单就一个拼接,然后查询判断是否重复;
劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。
利用 redis 执行setnx 命令
,天然具有幂等性。从而实现不重复消费
在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是,tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以**订单量大了后采用 RabbitMQ 进行改造和优化,**如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。
a. 控制台页面添加
b. 队列中代码添加优先级
HashMap<String, Object> params = new HashMap<>();
params.put("x-max-priority",10);//优先级的区间时0-255,这里设置队列的优先级是10
channel.queueDeclare(QUEUE_NAME,true,false,false,params);
c. 消息中代码添加优先级
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
注意:要让队列实现优先级需要做的事情有如下事情:
- 队列需要设置为优先级队列
- 消息需要设置消息的优先级
- **消费者需要等待消息已经发送到队列中才去消费因为,**这样才有机会对消息进行排序
生产者
package com.lisa.rabitmq.one;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;
/**
* 生产者
*/
public class Producer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
Channel channel = connection.createChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
HashMap<String, Object> params = new HashMap<>();
params.put("x-max-priority",10);//优先级的区间时0-255,这里设置队列的优先级是10
channel.queueDeclare(QUEUE_NAME,true,false,false,params);
for (int i = 1; i < 11; i++) {
String message = "hello Lisa "+ i;
//设置消息的优先级:当i=5时,设置该消息的优先级为5,其他的消息优先级默认
if(i == 5){
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
}else{
//默认优先级
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
}
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
//channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕!");
}
}
消费者
package com.lisa.rabitmq.one;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* 消费者
*/
public class Customer {
private static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("admin");
factory.setPassword("admin");
Connection connection = factory.newConnection();
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
Channel channel = connection.createChannel();
System.out.println("等待接收消息!");
//推送的消息如何进行消费的接口回调
DeliverCallback deliverCallback = (String consumerTag, Delivery message) -> {
System.out.println(new String(message.getBody()).toString());
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (String consumerTag) ->{
System.out.println("消息消费被中断!");
};
/**
* 消费者消费消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
测试
先启动生产者:
再启动消费者:
可以看到第5条消息被优先消费
RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。
当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。
默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,
这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留
一份备份。**当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,**这个操作会耗费较长的
时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。
队列具备两种模式:default
和 lazy
。
默认的为 default 模式
,在 3.6.0 之前的版本无需做任何变更。
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。因为内存当中只剩下消息的索引,消息的内容存放到了磁盘中。
最开始我们介绍了如何安装及运行 RabbitMQ 服务,不过这些是单机版的,无法满足目前真实应用的
要求。如果 RabbitMQ 服务器遇到内存崩溃、机器掉电或者主板故障等情况,该怎么办?单台 RabbitMQ
服务器可以满足每秒 1000 条消息的吞吐量,那么如果应用需要 RabbitMQ 服务满足每秒 10 万条消息的吞
吐量呢?购买昂贵的服务器来增强单机 RabbitMQ 务的性能显得捉襟见肘,搭建一个 RabbitMQ 集群才是
解决实际问题的关键.
本文介绍RabbitMQ搭建普通集群模式和镜像集群模式的操作指南。
RabbitMQ有2种集群模式,分别是普通集群模式和镜像集群模式。
第一种 普通集群模式:rabbitmq集群与其他集群有些不同,rabbitmq集群同步的指是复制队列,元数据信息的同步,即同步的是数据存储信息;消息的存放只会存储在创建该消息队列的那个节点上。并非在节点上都存储一个完整的数据。在通过非数据所在节点获取数据时,通过元数据信息,路由转发到存储数据节点上,从而得到数据 。
第二种 镜像集群模式:与普通集群模式区别 主要是消息实体会主动在镜像节点间同步数据,而不是只存储数据元信息。 故普通集群模式 但凡数据节点挂了,容易造成数据丢失但镜像集群模式可以保证集群只要不全部挂掉,数据就不会丢失,当相对于性能来说,镜像集群模式会比普通集群模式多出消耗数据的传输。故取决于业务场景进行取舍。
sudo docker pull rabbitmq:management
mkdir rabbitmqcluster
cd rabbitmqcluster/
mkdir rabbitmq01 rabbitmq02 rabbitmq03
创建完成映射目录后,执行如下命令创建容器:
使用的镜像rabbitmq:management
创建容器
docker run -d --hostname rabbitmq01 --name rabbitmqCluster01 -v /home/dockertest/rabbitmqcluster/rabbitmq01:/var/lib/rabbitmq -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie' -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:management
docker run -d --hostname rabbitmq02 --name rabbitmqCluster02 -v /home/dockertest/rabbitmqcluster/rabbitmq02:/var/lib/rabbitmq -p 15673:15672 -p 5673:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie' -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --link rabbitmqCluster01:rabbitmq01 rabbitmq:management
docker run -d --hostname rabbitmq03 --name rabbitmqCluster03 -v /home/dockertest/rabbitmqcluster/rabbitmq03:/var/lib/rabbitmq -p 15674:15672 -p 5674:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie' -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin --link rabbitmqCluster01:rabbitmq01 --link rabbitmqCluster02:rabbitmq02 rabbitmq:management
由于上面这个镜像创建容器出现了问题,所以使用下面的镜像创建容器
使用rabbitmq:3.8.0-beta.4-management创建容器
docker run -d --hostname rabbitmq01 --name rabbitmqCluster01 -p 5672:5672 -p 15672:15672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' rabbitmq:3.8.0-beta.4-management
docker run -d --hostname rabbitmq02 --name rabbitmqCluster02 -p 5673:5672 -p 15673:15672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --link rabbitmqCluster01:rabbitmq01 rabbitmq:3.8.0-beta.4-management
docker run -d --hostname rabbitmq03 --name rabbitmqCluster03 -p 5674:5672 -p 15674:15672 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --link rabbitmqCluster01:rabbitmq01 --link rabbitmqCluster02:rabbitmq02 rabbitmq:3.8.0-beta.4-management
使用docker ps 查看:
注:--hostname
设置容器的主机名 RABBITMQ_ERLANG_COOKIE
节点认证作用,部署集成时需要同步该值。
启动容器成功后,可以访问
http://localhost:15672/#/
http://localhost:15673/#/
http://localhost:15674/#/
查看是否正常启动成功。账号/密码:guest / guest。 读者登陆后,查看overview Tab页,可看到节点信息。
如果登录不进去,可以设置新用户并授予权限:
# 进去rabbitmqCluster01
docker exec -it rabbitmqCluster01 bash
# 添加用户admin
root@rabbitmq01:/# rabbitmqctl add_user admin admin
# 给admin授予权限
root@rabbitmq01:/# rabbitmqctl set_user_tags admin administrator
# 退出
root@rabbitmq01:/# exit
# 重启rabbitmqCluster01
/home/dockertest/rabbitmqcluster$ docker restart rabbitmqCluster01
rabbitmqCluster02 和rabbitmqCluster03执行相同的操作
再次访问
http://localhost:15672/#/
http://localhost:15673/#/
http://localhost:15674/#/
正常访问!
首先在centos窗口中,执行如下命令,进入第一个rabbitmq节点容器:
docker exec -it rabbitmqCluster01 bash
进入容器后,操作rabbitmq,执行如下命令:
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
exit
操作日志:
/home/dockertest/rabbitmqcluster$ docker exec -it rabbitmqCluster01 bash
root@rabbitmq01:/# rabbitctl stop_app
root@rabbitmq01:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbitmq01 ...
root@rabbitmq01:/# rabbitmq reset
root@rabbitmq01:/# rabbitmqctl reset
Resetting node rabbit@rabbitmq01 ...
root@rabbitmq01:/# rabbitmqctl start_app
Starting node rabbit@rabbitmq01 ...
root@rabbitmq01:/# exit
exit
接下来,设置节点2,加入到集群,执行如下命令:
docker exec -it myrabbit2 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbit1
rabbitmqctl start_app
exit
操作日志:
/home/dockertest/rabbitmqcluster$ docker exec -it rabbitmqCluster02 bash
root@rabbitmq02:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbitmq02 ...
root@rabbitmq02:/# rabbitmqctl reset
Resetting node rabbit@rabbitmq02 ...
root@rabbitmq02:/# rabbitmqctl join_cluster --ram rabbit@rabbitmq01
Clustering node rabbit@rabbitmq02 with rabbit@rabbitmq01
root@rabbitmq02:/#
root@rabbitmq02:/# rabbitmqctl start_app
Starting node rabbit@rabbitmq02 ...
completed with 3 plugins.
root@rabbitmq02:/#
root@rabbitmq02:/# exit
exit
设置节点3,加入到集群:
docker exec -it rabbit3 bash
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbitmq01
rabbitmqctl start_app
exit
guest guest
节点设置完成之后,在浏览器访问
http://localhost:15672/#/
http://localhost:15673/#/
http://localhost:15674/#/
中任意一个,都会看到RabbitMQ集群已经创建成功。
查看集群的状态:
$ docker exec -it rabbitmqCluster01 bash
root@rabbitmq01:/# rabbitmqctl cluster_status #查看集群的状态
Cluster status of node rabbit@rabbitmq01 ...
Basics
Cluster name: rabbit@rabbitmq01
Disk Nodes
rabbit@rabbitmq01
RAM Nodes
rabbit@rabbitmq02
rabbit@rabbitmq03
Running Nodes
rabbit@rabbitmq01
rabbit@rabbitmq02
rabbit@rabbitmq03
Versions
rabbit@rabbitmq01: RabbitMQ 3.8.0-beta.4 on Erlang 22.0.5
rabbit@rabbitmq02: RabbitMQ on Erlang
rabbit@rabbitmq03: RabbitMQ on Erlang
Alarms
(none)
Network Partitions
(none)
Listeners
Node: rabbit@rabbitmq01, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit@rabbitmq01, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit@rabbitmq01, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Feature flags
Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: quorum_queue, state: enabled
root@rabbitmq01:/#
如果 RabbitMQ 集群中只有一个 Broker 节点,那么该节点的失效将导致整体服务的临时性不可用,并
且也可能会导致消息的丢失。可以将所有消息都设置为持久化,并且对应队列的durable 属性也设置为true, 但是这样仍然无法避免由于缓存导致的问题:因为消息在发送之后和被写入磁盘井执行刷盘动作之间存在一个短暂却会产生问题的时间窗。通过 publisherconfirm 机制能够确保客户端知道哪些消息己经存入磁盘,尽管如此,一般不希望遇到因单点故障导致的服务不可用。
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中
的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。
package com.lisa.rabitmq.one;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.concurrent.TimeoutException;
/**
* 生产者
*/
public class Producer {
private static final String QUEUE_NAME = "mirror_hello";
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
//channel 实现了自动 close 接口 自动关闭 不需要显示关闭
Channel channel = connection.createChannel();
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 默认消息存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
HashMap<String, Object> params = new HashMap<>();
params.put("x-max-priority",10);//优先级的区间时0-255,这里设置队列的优先级是10
channel.queueDeclare(QUEUE_NAME,true,false,false,params);
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息
* 4.发送消息的消息体
*/
String message = "hello Lisa ";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕!");
}
}
可以看到node+1,点进去
root@rabbitmq01:/# rabbitmqctl stop_app #停止节点1
Stopping rabbit application on node rabbit@rabbitmq01 ...
root@rabbitmq01:/#
可以看到第一个节点停止
查看队列,节点2升为主节点,节点3为备份节点
5.就算整个集群只剩下一台机器了 依然能消费队列里面的消息 说明队列里面的消息被镜像队列传递到相应机器里面了