RabbitMQ概念
RabbitMQ是一套开源(MPL)的消息队列服务软件,是由 LShift 提供的一个 Advanced Message Queuing Protocol (AMQP) 的开源实现,由以高性能、健壮以及可伸缩性出名的 Erlang 写成。
介绍
RabbitMQ 是一个消息代理:它接受和转发消息。您可以将其视为邮局:当您将要投递的邮件放入邮箱时,您可以确定 Mailperson 先生或女士最终会将邮件投递给您的收件人。在这个比喻中,RabbitMQ 是一个邮箱、一个邮局和一个邮递员。
RabbitMQ 和邮局之间的主要区别在于它不处理纸张,而是接受、存储和转发二进制数据块 -消息。
RabbitMQ 和一般的消息传递使用了一些行话。
-
生产无非就是发送。发送消息的程序是生产者:
-
队列是位于 RabbitMQ 中的邮箱的名称。尽管消息流经 RabbitMQ 和您的应用程序,但它们只能存储在队列中。甲队列仅由主机的存储器&磁盘限制约束,它本质上是一个大的消息缓冲器。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这是我们表示队列的方式:
-
消费与接收具有相似的含义。一个消费者是一个程序,主要是等待接收信息:
请注意,生产者、消费者和代理不必驻留在同一主机上;实际上,在大多数应用程序中它们没有。应用程序也可以既是生产者又是消费者。
入门案例:
依赖
<dependencies>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
<!-- rabbitmq依赖客户端-->
<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
</dependencies>
生产者
public class Producer {
//队列名称
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws Exception {
//创建工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂ip连接RabbitMQ的队列
factory.setHost("127.0.0.1");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
/**
* 生成一个队列
* 1,队列名称
* 2,是否需要保存消息(队列里的消息是否需要持久化,默认下是存储在内存中)
* 3,该队列是否提供一个消费者进行消费 是否进行消息的共享, true可是是多个消费者消费 默认false
* 4,是否自动删除 对吼一个消费者断开连接以后 ,该队列是否自动删除 true自动删除false不自动删除
* 5,其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String message = "hello world";
/**
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪一个,本次是队列的名称
* 3,其他参数信息
* 4,发送消息的消息
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
/*
消息发送完毕
*/
消费者
public class Consumer {
//队列名称
public static final String QUEUE_NAME = "hello1";
//接受消息
public static void main(String[] args) throws Exception{
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("127.0.0.1");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//申明 消息的接受
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println(message);
};
//申明 消息的回调
CancelCallback cancelCallback = (String consumerTag)->{
System.out.println("消息消费被中断!");
};
/**
* 消费者消费消息
* 1,消费哪一个队列,
* 2,消费成功之后是否要自动应答, true :自动应答 false:手动应答
* 3,消费者没有成功消费的回调
* 4,消费者取消消费得到回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
/*
com.rabbitmq.client.Delivery@49e82db0
*/
Work Queues
工作队列(又称任务队列)的主要思想是避免立即执行资源密集型任务,而不得不等待它完成。相反我们安排任务在之后执行。我们把任务封装为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当有多个线程时,这些工作线程将一起处理这些任务。
轮训分发消息
这个案例中,启动两个工作线程,一个发送线程,看看两个工作线程时如何工作的。
抽取工具类
/**
* TODO 连接工厂创建信道的工具类
*
* @author qijian.
* @date 2021/8/8 16:26
*/
public class RabbitMQUtils {
public static Channel getChannel()throws Exception{
//创建工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂ip连接RabbitMQ的队列
factory.setHost("127.0.0.1");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
return channel;
}
}
public class Task01 {
//队列名称
public static final String QUEUE_NAME = "hello1";
public static void main(String[] args) throws Exception {
//创建工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂ip连接RabbitMQ的队列
factory.setHost("127.0.0.1");
factory.setUsername("guest");
factory.setPassword("guest");
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
/**
* 生成一个队列
* 1,队列名称
* 2,是否需要保存消息(队列里的消息是否需要持久化,默认下是存储在内存中)
* 3,该队列是否提供一个消费者进行消费 是否进行消息的共享, true可是是多个消费者消费 默认false
* 4,是否自动删除 对吼一个消费者断开连接以后 ,该队列是否自动删除 true自动删除false不自动删除
* 5,其他参数
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
/**
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪一个,本次是队列的名称
* 3,其他参数信息
* 4,发送消息的消息
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送消息完成"+message);
}
}
}
public class Worker01 {
//队列名称
public static final String QUEUE_NAME = "hello1";
// 接受消息
public static void main(String[] args)throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明 消息的接受
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("接收到的消息:" + new String(message.getBody()) );
};
//申明 消息的回调
CancelCallback cancelCallback = (String consumerTag)->{
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
};
System.out.println("C1等待接收消息......");
/**
* 消费者消费消息
* 1,消费哪一个队列,
* 2,消费成功之后是否要自动应答, true :自动应答 false:手动应答
* 3,消费者没有成功消费的回调
* 4,消费者取消消费得到回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
public class Worker02 {
//队列名称
public static final String QUEUE_NAME = "hello1";
// 接受消息
public static void main(String[] args)throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明 消息的接受
DeliverCallback deliverCallback = (consumerTag, message)->{
System.out.println("接收到的消息:" + new String(message.getBody()) );
};
//申明 消息的回调
CancelCallback cancelCallback = (String consumerTag)->{
System.out.println(consumerTag + "消费者取消消费接口回调逻辑");
};
System.out.println("C2等待接收消息......");
/**
* 消费者消费消息
* 1,消费哪一个队列,
* 2,消费成功之后是否要自动应答, true :自动应答 false:手动应答
* 3,消费者没有成功消费的回调
* 4,消费者取消消费得到回调
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
输入与输出:
Task01:
123
发送消息完成123
1234
发送消息完成1234
aa
发送消息完成aa
bb
发送消息完成bb
Worker01:
C1等待接收消息......
接收到的消息:1234
接收到的消息:bb
Worker02:
C2等待接收消息......
接收到的消息:123
接收到的消息:aa
消息应答
为了确保消息不会丢失,RabbitMQ支持消息应答。消费者发送一个消息应答,告诉RabbitMQ这个消息已经接收并且处理完毕了。RabbitMQ就可以删除它了。
如果一个消费者挂掉却没有发送应答,RabbitMQ会理解为这个消息没有处理完全,然后交给另一个消费者去重新处理。这样,你就可以确认即使消费者偶尔挂掉也不会丢失任何消息了。 没有任何消息超时限制;只有当消费者挂掉时,RabbitMQ才会重新投递。即使处理一条消息会花费很长的时间。
消息应答是默认打开的。我们通过显示的设置autoAsk=true关闭这种机制。现即自动应答开,一旦我们完成任务,消费者会自动发送应答。通知RabbitMQ消息已被处理,可以从内存删除。如果消费者因宕机或链接失败等原因没有发送ACK(不同于ActiveMQ,在RabbitMQ里,消息没有过期的概念),则RabbitMQ会将消息重新发送给其他监听在队列的下一个消费者
案例:
public class Task02 {
/**
* 队列名称
*/
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
/**
* 申明队列
* 1,队列
* 2,队列是否持久化
*/
channel.queueDeclare(TASK_QUEUE_NAME,false,false,false,null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
/**
* 3,消息是否持久化
*/
channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息:"+message);
}
}
}
/**
* TODO 类描述
* 消息手动应答时是不丢失的,放回队列中宠幸消费
* @author qijian.
* @date 2021/8/8 17:48
*/
public class Worker03 {
/**
* 队列名称
*/
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
System.out.println("C1等待接收消息处理时间较短");
DeliverCallback deliverCallback = ( consumerTag , message) ->{
try {
Thread.sleep(1000*1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收到的消息"+new String(message.getBody(),"UTF-8"));
//手动应答
/**
* 1,消息的标记 tag
*2,是否批量应答 false:不去量应答信道中的消息 true:批量
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
};
CancelCallback cancelCallback = (consumerTag)-> {
System.out.println(consumerTag+"消费者取消消费接口回调");
};
//采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
public class Worker04 {
/**
* 队列名称
*/
public static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception{
Channel channel = RabbitMQUtils.getChannel();
System.out.println("C2等待接收消息处理时间较长");
DeliverCallback deliverCallback = (consumerTag , message) ->{
try {
Thread.sleep(1000*30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收到的消息"+new String(message.getBody(),"UTF-8"));
//手动应答
/**
* 1,消息的标记 tag
*2,是否批量应答 false:不去量应答信道中的消息 true:批量
*/
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
};
CancelCallback cancelCallback = (consumerTag)-> {
System.out.println(consumerTag+"消费者取消消费接口回调");
};
//采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME,autoAck,deliverCallback,cancelCallback);
}
}
运行后的结果说明:Worker03,Worker04启动后,一直在等待消息。接下来启动Tack02, 发送消息。发送两条消息 ,首先收到消息的是03,过了更长的时间后04,也收到了消息。此时如果队列中还有消息,并且04终止了,那么消息就会由03处理。
RabbitMQ持久化
队列持久化
当有多个消费者同时收取消息,且每个消费者在接收消息的同时,还要做其它的事情,且会消耗很长的时间,在此过程中可能会出现一些意外,比如消息接收到一半的时候,一个消费者宕掉了,这时候就要使用消息接收确认机制,可以让其它的消费者再次执行刚才宕掉的消费者没有完成的事情。另外,在默认情况下,我们创建的消息队列以及存放在队列里面的消息,都是非持久化的,也就是说当RabbitMQ宕掉了或者是重启了,创建的消息队列以及消息都不会保存,为了解决这种情况,保证消息传输的可靠性,我们可以使用RabbitMQ提供的消息队列的持久化机制。
持久化的代码体现:
channel.queueDeclare(TASK_QUEUE_NAME,true,false,false,null);
但是注意,如果直接把之前的代码修改,那么会报如下的错误
channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'ack_queue' in vhost '/': received 'true' but current is 'false', class-id=50, method-id=10)
原因原先的队列是不持久化的,如果需要把它变为持久化的,需要把原先的队列删除,或者重新创建一个持久化的队列。
没有持久化时:
持久化后:
消息持久化
消息的可靠性是RabbitMQ的一大特色,那么RabbitMQ是如何保证消息可靠性的呢?答案就是消息持久化。持久化可以防止在异常情况下丢失数据。RabbitMQ的持久化分为三个部分:交换器持久化、队列持久化和消息的持久化。
交换器持久化可以通过在声明队列时将durable参数设置为true。如果交换器不设置持久化,那么在RabbitMQ服务重启之后,相关的交换器元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器了。
队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。队列的持久化可以通过在声明队列时将durable参数设置为true。
设置了队列和消息的持久化,当RabbitMQ服务重启之后,消息依然存在。如果只设置队列持久化或者消息持久化,重启之后消息都会消失。
当然,也可以将所有的消息都设置为持久化,但是这样做会影响RabbitMQ的性能,因为磁盘的写入速度比内存的写入要慢得多。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。鱼和熊掌不可兼得,关键在于选择和取舍。在实际中,需要根据实际情况在可靠性和吞吐量之间做一个权衡。
要想实现消息的持久化需要在消息生产者修改代码,添加MessageProperties.PERSISTENT_TEXT_PLAIN
原:
channel.basicPublish("",TASK_QUEUE_NAME,null,message.getBytes("UTF-8"));
修改后:
channel.basicPublish("",TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
消息持久化注意事项
将消息标记为持久并不能完全保证消息不会丢失。虽然它告诉 RabbitMQ 将消息保存到磁盘,但是当 RabbitMQ 已经接受一条消息并且还没有保存它时,仍然有很短的时间间隔。此外,RabbitMQ 不会对每条消息都执行fsync(2) —— 它可能只是保存到缓存中,而不是真正写入磁盘。持久性保证不强,但对于我们简单的任务队列来说已经足够了。如果您需要更强的保证,那么您可以使用 发布者确认。
不公平分发
在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然很公平的进行分发。
在消费者的一方设置:
//设置不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个
任务,然后 rabbitmq
就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完
成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加
新的 worker 或者改变其他存储任务的策略。
预期值
本身消息的发送就是异步发送的,所以在任何时候,
channel
上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos 方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认
上面的不公平分发中我们看到
//设置不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
当prefetchCount
为0时:公平分发
当prefetchCount
为1时:不公平分发
当prefetchCount
>1时:欲取值
在消费者的一方设置后,表明该消费者语句欲处理几条消息。需要注意的是:假设一个消费者的欲取值是5,它的channel
最大会积压5条消息.
针对于:上面的Worker03进行设置
//设置不公平分发
int prefetchCount = 2;
channel.basicQos(prefetchCount);
针对于:上面的Worker03进行设置
//设置不公平分发
int prefetchCount = 5;
channel.basicQos(prefetchCount);
展现:
注意有时会看到超出了预处理值:这是因为该消费者马上就要消费一条,所以队列认为它已经应答了,只要一应答条数就应该减1,但是还没有来的级又向里面添加了一条。所以会出现比预处理值多一条的情况。
发布与确认
生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式, 所有在该信道上面发布的
消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker
就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队
列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传
给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置
basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。
确认发布默认是不开启的,需要开启的话调用方法 confirmSelect。
Channel channel = RabbitMQUtils.getChannel();
//开启发布确认
channel.confirmSelect();
单个确认
这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)
这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是: 发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。
//单个确认
public static void publishMessageInvididually()throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列申明
String queueName = UUID.randomUUID().toString();
/**
* 生成一个队列
* 1,队列名称
* 2,是否需要保存消息(队列里的消息是否需要持久化,默认下是存储在内存中)
* 3,该队列是否提供一个消费者进行消费 是否进行消息的共享, true可是是多个消费者消费 默认false
* 4,是否自动删除 对吼一个消费者断开连接以后 ,该队列是否自动删除 true自动删除false不自动删除
* 5,其他参数
*/
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(StandardCharsets.UTF_8));
//单个消息马上进行确认
boolean flag = channel.waitForConfirms();
if (flag){
System.out.println("消息发送成功");
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"个单独确认,耗时:"+(end-begin)+"ms");
}
}
批量确认
上面的个人确认方式非常慢,批量确认可以极高的提高吞吐量,当然这种方式的缺点就是:当发生故障的导致发布问题的出现时,不能够确认到底是哪一个消息出现了问题。
//批量确认发布
public static void publishMessageBath()throws Exception{
Channel channel = RabbitMQUtils.getChannel();
//队列申明
String queueName = UUID.randomUUID().toString();
/**
* 生成一个队列
* 1,队列名称
* 2,是否需要保存消息(队列里的消息是否需要持久化,默认下是存储在内存中)
* 3,该队列是否提供一个消费者进行消费 是否进行消息的共享, true可是是多个消费者消费 默认false
* 4,是否自动删除 对吼一个消费者断开连接以后 ,该队列是否自动删除 true自动删除false不自动删除
* 5,其他参数
*/
channel.queueDeclare(queueName,true,false,false,null);
//开启发布确认
channel.confirmSelect();
//批量确认消息的大小
int bachSize = 100;
//开始时间
long begin = System.currentTimeMillis();
//批量发现消息
for (int i=0;i<MESSAGE_COUNT;i++){
String message = i+"";
channel.basicPublish("",queueName,null,message.getBytes(StandardCharsets.UTF_8));
if (i%bachSize==0){
channel.waitForConfirms();
}
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布"+MESSAGE_COUNT+"批量确认确认,耗时:"+(end-begin)+"ms");
}
异步确认发布
异步发布的可靠性和效率都很好,它的利用回调函数来达到消息的可靠性传递。
//异步确认发布
public static void publishMessageAsync() throws Exception {
try (Channel channel = RabbitMQUtils.getChannel()) {
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
// 开启发布确认
channel.confirmSelect();
/**
* 线程安全有序的一个哈希表,适用于高并发的情况
* 1. 轻松的将序号与消息进行关联
* 2. 轻松批量删除条目 只要给到序列号
* 3. 支持并发访问
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms = new
ConcurrentSkipListMap<>();
/**
* 确认收到消息的一个回调
* 1. 消息序列号
* 2.true 可以确认小于等于当前序列号的消息
* false 确认当前序列号消息
*/
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
// 返回的是小于等于当前序列号的未确认消息 是一个 map
ConcurrentNavigableMap<Long, String> confirmed =
outstandingConfirms.headMap(sequenceNumber, true);
// 清除该部分未确认消息
confirmed.clear();
}else{
// 只清除当前序列号的消息
outstandingConfirms.remove(sequenceNumber);
}
};
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
String message = outstandingConfirms.get(sequenceNumber);
System.out.println(" 发布的消息"+message+" 未被确认,序列号"+sequenceNumber);
};
/**
* 添加一个异步确认的监听器
* 1. 确认收到消息的回调
* 2. 未收到消息的回调
*/
channel.addConfirmListener(ackCallback, null);
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = " 消息" + i;
/**
* channel.getNextPublishSeqNo() 获取下一个消息的序列号
* 通过序列号与消息体进行一个关联
* 全部都是未确认的消息体
*/
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
channel.basicPublish("", queueName, null, message.getBytes());
}
long end = System.currentTimeMillis();
System.out.println(" 发布" + MESSAGE_COUNT + " 个异步确认消息, 耗时" + (end - begin) +
"ms");
}
}
如何处理异步确认消息
最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,
比如说用 ConcurrentLinkedQueue
这个队列在 confirm callbacks
与发布线程之间进行消息的传
递。
交换机
RabbitMQ
消息传递模型的核心思想是生产者从不直接向队列发送任何消息。实际上,生产者经常甚至根本不知道消息是否会被传送到任何队列。
相反,生产者只能将消息发送到交换。交换是一件非常简单的事情。一方面它接收来自生产者的消息,另一方面将它们推送到队列中。交易所必须确切地知道如何处理它收到的消息。它应该附加到特定队列吗?它应该附加到许多队列中吗?或者它应该被丢弃。其规则由交换类型定义 。
交换类型
direct
、topic
、headers
和fanout
。
我们将关注最后一个——扇出。让我们创建一个这种类型的交换,并将其命名为logs:
注意上面的案例代码中:
basicPublish
方法的死一个参数是""
,代表的是使用默认的交换机。
临时队列
每当我们连接到 Rabbit 时,我们都需要一个新的空队列。为此,我们可以创建一个具有随机名称的队列,或者甚至更好 - 让服务器为我们选择一个随机队列名称。我们可以通过向queue_declare提供空队列参数来做到这一点.
自动删除队列和普通队列在使用上没有什么区别,唯一的区别是,当消费者断开连接时,队列将会被删除。自动删除队列允许的消费者没有限制,也就是说当这个队列上最后一个消费者断开连接才会执行删除。
自动删除队列只需要在声明队列时,设置属性auto-delete标识为true即可。系统声明的随机队列,缺省就是自动删除的。
绑定
我们已经创建了一个扇出交换和一个队列。现在我们需要告诉交换器向我们的队列发送消息。交换和队列之间的这种关系称为绑定。
Fanout
将接收到的所有消息广播到他知道的所有队列中。
发出日志消息的生产者程序与之前的教程看起来没有太大区别。最重要的变化是我们现在想要将消息发布到我们的日志交换机而不是无名的交换机。我们需要在发送时提供一个routingKey,但它的值在扇出交换机时被忽略。
/**
* TODO 发送
*
* @author qijian.
* @date
*/
public class EmitLog {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String message = argv.length < 1 ? "info: Hello World!" :
String.join(" ", argv);
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
public class ReceiveLogs {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//申明一个队列 临时队列
/**
* 申明一个临时队列队列的名称是随机的
* 当消费者断开与队列的连接后,队列就自动删除了
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 保定交换机与队列
*/
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
/**
* 接收消息
*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
public class ReceiveLogs02 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//申明一个队列 临时队列
/**
* 申明一个临时队列队列的名称是随机的
* 当消费者断开与队列的连接后,队列就自动删除了
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 保定交换机与队列
*/
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
/**
* 接收消息
*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" [x] Received '" + message + "'");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
结果图:
生产者:
消费者2:
消费者1:
Direct Exchange
日志系统是将所有消息广播给所有消费者。我们希望扩展它以允许根据其严重性过滤消息。例如,我们可能希望将日志消息写入磁盘的程序只接收严重错误,而不是在警告或信息日志消息上浪费磁盘空间。
我们使用的是fanout
交换机,它没有给我们很大的灵活性——它只能进行无意识的广播。
我们将改用direct
交换机。direct
交换机背后的路由算法很简单 - 消息进入其binding key
与消息的routing key
完全匹配的队列 。
为了说明这一点,请考虑以下设置:
我看到,X绑定了两个队列,绑定类型是direct
。队列Q1绑定键为orange
,队列Q2绑定键有两个;一个绑定键为black
买另一个绑定键为green
.
在这种绑定下,生产者发布消息到exchange
上,绑定键为orange
的消息会被发布到队列Q1。绑定键为blackgreen
和的消息会被发布到队列Q2,其他消息类型的信息将被丢弃。
多重绑定
使用相同的绑定键绑定多个队列是完全合法的。在我们的示例中,我们可以使用绑定键black在X和Q1之间添加一个绑定。在这种情况下,直接交换的行为类似于扇出,并将消息广播到所有匹配的队列。路由键为black 的消息将同时发送到 Q1和Q2。
实战
代码实现:
import com.qijian.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class DirectLogs {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME,"warning",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("生产者发出消息:"+message);
}
}
}
import com.qijian.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogsDirect01 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//申明一个队列
channel.queueDeclare("console",false,false,false,null);
// 将队列绑定到交换机
/**
* param1:destination,目的地,队列的名字
* param2:source,资源,交换机的名字
* param3:routingKey,路由键(目前没有用到routingKey,填 "" 即可)
*/
channel.queueBind("console",EXCHANGE_NAME,"info");
channel.queueBind("console",EXCHANGE_NAME,"warning");
/**
* 接收消息
*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" ReceiveLogsDirect01控制台打印接收到的信息:'" + message + "'");
};
channel.basicConsume("console", true, deliverCallback, consumerTag -> { });
}
}
import com.qijian.utils.RabbitMQUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class ReceiveLogsDirect02 {
public static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明一个交换机
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//申明一个队列
channel.queueDeclare("disk",false,false,false,null);
// 将队列绑定到交换机
/**
* param1:destination,目的地,队列的名字
* param2:source,资源,交换机的名字
* param3:routingKey,路由键(目前没有用到routingKey,填 "" 即可)
*/
channel.queueBind("disk",EXCHANGE_NAME,"error");
/**
* 接收消息
*/
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" ReceiveLogsDirect02控制台打印接收到的信息:'" + message + "'");
};
/**
* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* deliverCallback: 当一个消息发送过来后的回调接口
* cancelCallback:当一个消费者取消订阅时的回调接口;取消消费者订阅队列时除了使用{@link Channel#basicCancel}之外的所有方式都会调用该回调方法
* @return 服务端生成的消费者标识
*/
channel.basicConsume("disk", true, deliverCallback, consumerTag -> { });
}
}
消息生产者:
消息的消费者1:
消息的消费者2:
Topic Exchange
我们改进了日志系统。我们没有使用只能进行虚拟广播的
Fanout
交换机,而是使用直接交换机(Direct Exchange
),并获得了选择性接收日志的可能性。尽管使用直接交换改进了我们的系统,但它仍然有局限性——它不能根据多个标准进行路由。
在我们的日志系统中,我们可能不仅希望根据严重性订阅日志,还希望根据发出日志的源订阅日志。您可能从syslog unix 工具中了解到这个概念,该工具根据严重性 (
info/warn/crit...
) 和设施 (auth/cron/kern...
) 路由日志。 这会给我们带来很大的灵活性——我们可能只想听来自“
cron
”的关键错误以及来自“kern
”的所有日志。为了在我们的日志系统中实现这一点,我们需要了解一个更复杂的Topic Exchange
。
发送到Topic Exchange
的消息不能具随意的写routing_key
- 它必须是一个单词列表,由点分隔。这些词可以是任何东西,但通常它们会指定一些与消息相关的特征。一些有效的路由键示例:“ stock.usd.nyse
”、“ nyse.vmw
”、“ quick.orange.rabbit
”。路由键中可以有任意多的字,最多 255 个字节。
注意:
- *(星号)可以代替一个单词
- (井号)可以替代零个或多个单词
用图解释如下:
在这个例子中,我们将要发送所有描述动物的消息。这个消息将使用由三个字(两个点)组成的routing key
发送。在routing key
中,第一个单词用来描述速度,第二个单词用来描述颜色,第三个单词用来描述物种:<speed>.<colour>.<species>
.
我们创建三个绑定:Q1与*.orange.*
, Q2与*.*.rabbit
和lazy.#
绑定。
三个的绑定关系能够描述成如下:
- Q1对所有的橙色动物感兴趣。
- Q2想要听到所有关于兔子和慢吞吞的动物的事情
routing key
设置为quick.orange.rabbit
的消息将被分发到两个队列之中。消息lazy.orange.elephant
将被发送到两个队列中。消息quick.orange.fox
只会进入第一个队列,而lazy.brown,fox
只会进入第二个队列之中。lazy.pink.rabbit
只会传送到第二个队列之中一次,即使绑定两次。quick.brown.fox
不匹配任何绑定,因此会被丢弃。
如果我们违反约定并发送一到四个字的消息,例如“ orange ”或“ quick.orange.male.rabbit ”,会发生什么?
好吧,这些消息不会匹配任何绑定并且会丢失。
“ lazy.orange.male.rabbit
”,即使它有四个字,也会匹配最后一个绑定,并被传递到第二个队列。
话题交流
主题交换功能强大,可以像其他交换一样运行。
当队列与“ # ”(散列)绑定键绑定时——它将接收所有消息,而不管路由键——就像在扇出机(
fanout
)交换中一样。当绑定中不使用特殊字符“ * ”(星号)和“ # ”(散列)时,主题交换的行为就像直接交换机(
Direct Exchange
)一样。
代码实现:
the code of EmitLogTopic.java
public class EmitLogTopic {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) 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> bindKeyEntry: bindingKeyMap.entrySet() ) {
String routingKey = bindKeyEntry.getKey();
String message = bindKeyEntry.getValue();
/**
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪一个,本次是队列的名称
* 3,其他参数信息
* 4,发送消息
*/
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
the code of ReceiveLog01.java
public class ReceiveLog01 {
//交换机名称
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
//申明队列
String queueName = "Q1";
channel.queueDeclare(queueName,false,false,false,null);
channel.queueBind(queueName,EXCHANGE_NAME,"*.orange.*");
System.out.println("等待接收消息......");
DeliverCallback deliverCallback = (consumerTag,message)->{
String msg = new String(message.getBody(), "UTF-8");
System.out.println("接收队列"+queueName+" 绑定键:" + message.getEnvelope().getRoutingKey());
};
/**
* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* deliverCallback: 当一个消息发送过来后的回调接口
* cancelCallback:当一个消费者取消订阅时的回调接口;取消消费者订阅队列时除了使用{@link Channel#basicCancel}之外的所有方式都会调用该回调方法
* @return 服务端生成的消费者标识
*/
// String basicConsume(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback)
channel.basicConsume(queueName,true,deliverCallback,consumerTag -> {});
}
}
the code of Recevice02.java
public class ReceiveLog02 {
//交换机名称
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//申明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
//申明队列
String queueName = "Q2";
channel.queueDeclare(queueName,false,false,false,null);
channel.queueBind(queueName,EXCHANGE_NAME,"*.*.rabbit");
channel.queueBind(queueName,EXCHANGE_NAME,"lazy.#");
System.out.println("等待接收消息......");
DeliverCallback deliverCallback = (consumerTag, message)->{
String msg = new String(message.getBody(), "UTF-8");
System.out.println("接收队列"+queueName+" 绑定键:" + message.getEnvelope().getRoutingKey());
};
/**
* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* deliverCallback: 当一个消息发送过来后的回调接口
* cancelCallback:当一个消费者取消订阅时的回调接口;取消消费者订阅队列时除了使用{@link Channel#basicCancel}之外的所有方式都会调用该回调方法
* @return 服务端生成的消费者标识
*/
// String basicConsume(String queue, boolean autoAck, DeliverCallback deliverCallback, CancelCallback cancelCallback)
channel.basicConsume(queueName,true,deliverCallback,consumerTag -> {});
}
}
截图:
死性队列
字面意思可以这样理解,一般来说,producer 将消息投递到 broker 或者直接到 queue 里了,consumer 从 queue 取出消息。
进行消费,但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有
后续的处理,就变成了死信,有死信自然就有了死信队列。
**应用场景:**为了保证订单业务的消息数据不丢失,需要使用到 RabbitMQ 的死信队列机制,当消息
消费发生异常时,将消息投入死信队列中.还有比如说: 用户在商城下单成功并点击去支付后在指定时
间未支付时自动失效。
死性的来源
消息 TTL 过期
队列达到最大长度(队列满了,无法再添加数据到 mq 中)
消息被拒绝(basic.reject 或 ba sic.nack)并且requeue=false
死性的实战
消息TTL过期代码:
the code of Consumer01.java
public class Consumer01 {
//普通交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
public static final String NORAML_QUEUE = "normal_queue";
//死信队列
public static final String DEAD_QUEUE = "dead_queue";
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<>();
//过期时间
// arguments.put("x-message-ttl",100000); //这里不设置有消息的生产者设置
//正常队列设置死信交换机 参数key是固定的值
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信RoutingKey 参数固定的
arguments.put("x-dead-letter-routing-key","lisi");
//申明普通队列
/**
* 生成一个队列
* 1,队列名称
* 2,是否需要保存消息(队列里的消息是否需要持久化,默认下是存储在内存中)
* 3,该队列是否提供一个消费者进行消费 是否进行消息的共享, true可是是多个消费者消费 默认false
* 4,是否自动删除 对吼一个消费者断开连接以后 ,该队列是否自动删除 true自动删除false不自动删除
* 5,其他参数
*/
channel.queueDeclare(NORAML_QUEUE,false,false,false,arguments);
//绑定普通的交换机与普通队列
channel.queueBind(NORAML_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//申明死信队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定死信的交换机与死信队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
System.out.println("等待接收消息......");
DeliverCallback deliverCallback = (consumerTag,message) ->{
System.out.println("Consumer01:"+new String(message.getBody(), "UTF-8"));
};
/**
* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* deliverCallback: 当一个消息发送过来后的回调接口
* cancelCallback:当一个消费者取消订阅时的回调接口;取消消费者订阅队列时除了使用{@link Channel#basicCancel}之外的所有方式都会调用该回调方法
* @return 服务端生成的消费者标识
*/
channel.basicConsume(NORAML_QUEUE,true,deliverCallback,consumerTag -> {});
}
}
the code of Producer.java
public class Producer {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
//死信消息 设置TTL时间 单位ms
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
for (int i=1;i<11;i++){
String message = "info"+i;
/**
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪一个,本次是队列的名称
* 3,其他参数信息
* 4,发送消息
*/
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",properties,message.getBytes());
}
}
}
the code of Consumer02.java
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("等待接收消息......");
DeliverCallback deliverCallback = (consumerTag, message) ->{
System.out.println("Consumer01:"+new String(message.getBody(), "UTF-8"));
};
/**
* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* deliverCallback: 当一个消息发送过来后的回调接口
* cancelCallback:当一个消费者取消订阅时的回调接口;取消消费者订阅队列时除了使用{@link Channel#basicCancel}之外的所有方式都会调用该回调方法
* @return 服务端生成的消费者标识
*/
channel.basicConsume(DEAD_QUEUE,true,deliverCallback,consumerTag -> {});
}
}
首先启动消费者Consumer01
,启动之后关闭该消费者 模拟其接收不到消息。之后在启动生产者
截图
启动消费者Consumer01
(此时生产者还没有发消息)。启动后关闭(模拟接收不到消息)
启动生产者。(生产者发送十条消息)
十秒钟后消息没有消费,消息进入死性队列
这时开启消费者Consumer02
consumer02的控制台截图
队列达到最大长度
the code of Consumer01.java
public class Consumer01 {
//普通交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
public static final String NORAML_QUEUE = "normal_queue";
//死信队列
public static final String DEAD_QUEUE = "dead_queue";
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<>();
//过期时间
// arguments.put("x-message-ttl",100000); //这里不设置有消息的生产者设置
//正常队列设置死信交换机 参数key是固定的值
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信RoutingKey 参数固定的
arguments.put("x-dead-letter-routing-key","lisi");
//设置正常队列的长度限制
arguments.put("x-max-length",6);
//申明普通队列
/**
* 生成一个队列
* 1,队列名称
* 2,是否需要保存消息(队列里的消息是否需要持久化,默认下是存储在内存中)
* 3,该队列是否提供一个消费者进行消费 是否进行消息的共享, true可是是多个消费者消费 默认false
* 4,是否自动删除 对吼一个消费者断开连接以后 ,该队列是否自动删除 true自动删除false不自动删除
* 5,其他参数
*/
channel.queueDeclare(NORAML_QUEUE,false,false,false,arguments);
//绑定普通的交换机与普通队列
channel.queueBind(NORAML_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//申明死性队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定死性的交换机与死性队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
System.out.println("等待接收消息......");
DeliverCallback deliverCallback = (consumerTag,message) ->{
System.out.println("Consumer01:"+new String(message.getBody(), "UTF-8"));
};
/**
* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* deliverCallback: 当一个消息发送过来后的回调接口
* cancelCallback:当一个消费者取消订阅时的回调接口;取消消费者订阅队列时除了使用{@link Channel#basicCancel}之外的所有方式都会调用该回调方法
* @return 服务端生成的消费者标识
*/
channel.basicConsume(NORAML_QUEUE,true,deliverCallback,consumerTag -> {});
}
}
the code of Producer.java
public class Producer {
public static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtils.getChannel();
for (int i=1;i<11;i++){
String message = "info"+i;
/**
* 发送一个消费
* 1,发送到哪个交换机
* 2,路由的key值是哪一个,本次是队列的名称
* 3,其他参数信息
* 4,发送消息
*/
channel.basicPublish(NORMAL_EXCHANGE,"zhangsan",null,message.getBytes());
}
}
}
先启动消费者
关闭消费者,开启生产者:
消息被拒
the code of Consumer001.java
public class Consumer001 {
//普通交换机
public static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机
public static final String DEAD_EXCHANGE = "dead_exchange";
//普通队列名称
public static final String NORAML_QUEUE = "normal_queue";
//死信队列
public static final String DEAD_QUEUE = "dead_queue";
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<>();
//过期时间
// arguments.put("x-message-ttl",100000); //这里不设置有消息的生产者设置
//正常队列设置死信交换机 参数key是固定的值
arguments.put("x-dead-letter-exchange",DEAD_EXCHANGE);
//设置死信RoutingKey 参数固定的
arguments.put("x-dead-letter-routing-key","lisi");
//申明普通队列
/**
* 生成一个队列
* 1,队列名称
* 2,是否需要保存消息(队列里的消息是否需要持久化,默认下是存储在内存中)
* 3,该队列是否提供一个消费者进行消费 是否进行消息的共享, true可是是多个消费者消费 默认false
* 4,是否自动删除 对吼一个消费者断开连接以后 ,该队列是否自动删除 true自动删除false不自动删除
* 5,其他参数
*/
channel.queueDeclare(NORAML_QUEUE,false,false,false,arguments);
//绑定普通的交换机与普通队列
channel.queueBind(NORAML_QUEUE,NORMAL_EXCHANGE,"zhangsan");
//申明死性队列
channel.queueDeclare(DEAD_QUEUE,false,false,false,null);
//绑定死性的交换机与死性队列
channel.queueBind(DEAD_QUEUE,DEAD_EXCHANGE,"lisi");
System.out.println("等待接收消息......");
DeliverCallback deliverCallback = (consumerTag, message) ->{
String msg = new String(message.getBody(), "UTF-8");
if (msg.equals("info5")){
System.out.println("Consumer001接收的消息是:"+msg+":此消息是被拒绝的");
//第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝
channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
}else {
System.out.println("Consumer001接收的消息是:"+msg);
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
};
/**
* 启动一个消费者,并返回服务端生成的消费者标识
* queue:队列名
* autoAck:true 接收到传递过来的消息后acknowledged(应答服务器),false 接收到消息后不应答服务器
* deliverCallback: 当一个消息发送过来后的回调接口
* cancelCallback:当一个消费者取消订阅时的回调接口;取消消费者订阅队列时除了使用{@link Channel#basicCancel}之外的所有方式都会调用该回调方法
* @return 服务端生成的消费者标识
*/
//这里需要开启手动应答 第二个参数设为false
channel.basicConsume(NORAML_QUEUE,false,deliverCallback,consumerTag -> {});
}
}
生产者代码如上一个案例的生产者
启动001后,启动生产者(消费者不用关闭)
此时发现001拒收了消息info5
,并且该消息放到了死信队列之中。
启动Consumer02
消费死信队列的消息
延迟队列
延时队列就是用来存放需要在指定时间被处理的元素的队列。
使用场景
1.订单在十分钟之内未支付则自动取消
2.新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
3.用户注册成功后,如果三天内没有登陆则进行短信提醒。
4.用户发起退款,如果三天内没有得到处理则通知相关运营人员。
5.预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议
以上的场景都有一个特点:需要在某个事件发生之后或者之前的指定时间点完成某一项任务