本文已参与「新人创作礼」活动,一起开启掘金创作之路。
RabbitMQ进阶-消息确认机制之Confirm机制-消费者
1.RabbitMQ可靠性
如何保证消息成功发送?
- 当消息的生产者者发送消息后,消息到底有没有正确到达broker代理服务器呢?
- 什么情况下,能够让我们知道生产者生产的消息正确到达broker了?
- RabbitMQ如何保证消息的成功投递呢?
- 消息如何能够正确的到达消费者
解决以上问题,RabbitMQ采用两种方式
- AMQP协议事务方式
- Channel配置Confirm模式
前面文章,我们详细讲一下 RabbitMQ系列(十二)RabbitMQ进阶-消息确认机制之事务机制
Channel配置生产者 Confirm模式 RabbitMQ系列(十三)RabbitMQ进阶-消息确认机制之Confirm机制-生产者
下面详解一下消费者Confirm模式
2.RabbitMQ通道 消费者Confirm模式
2.1 消费者Confirm概念
为了保证消息从队列可靠地到达消费者,RabbitMQ提供消息确认机制(message acknowledgment)
- 消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ会在队列中消息被消费后立即删除它。
- 采用消息确认机制后,只要令noAck=false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basicAck为止。
- 队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者ack信号的消息。
- 如果服务器端一直没有收到消费者的ack信号,而且消费该消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者。
- RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。因此RabbitMQ允许消费者消费一条消息的时间可以很久很久。
RabbitMQ管理平台界面上可以看到当前队列中Ready状态和Unacknowledged状态的消息数,分别对应上文中的等待投递给消费者的消息数和已经投递给消费者但是未收到ack信号的消息数
2.2 队列noAck参数详解-异常操作
autoAck表示收到消息后自动确认,如果不自动ack,需要使用channel.ack、channel.nack、channel.basicReject 进行消息下一步处理 看一下消费者代码channel.basicConsume(QUEUE_TEST, true, consumer); 我们设置True看一下效果,结果发现异常了,消费时候通道关闭导致异常 原理: autoAck(同no-ack)为true的时候,消息发送到操作系统的套接字缓冲区时即任务消息已经被消费,但如果此时套接字缓冲区崩溃,消息在未被消费者应用程序消费的情况下就被队列删除。
所以,如果想要保证消息可靠的达到消费者端,建议将autoAck字段设置为false,这样当上面套接字缓冲区崩溃的情况同样出现,仍然能保证消息被重新消费
2.3 noAck 为False时 消息处理
生产者,生产3条消息
package comsumerconfirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import conn.MqConnectUtil;
import subscrib3.ExchangeTypeEnum;
import static comsumerconfirm.ConsumerConfirmProducer.QUEUE_TEST;
import static comsumerconfirm.ConsumerConfirmProducer.RK_QUEUE_TEST;
public class ConsumerConfirmComsumer {
public static void main(String[] argv) throws Exception {
Connection connection = null;
Channel channel = null;
try {
connection = MqConnectUtil.getConnectionDefault();
channel = connection.createChannel();
/*声明交换机 String exchange
* 参数明细
* 1、交换机名称
* 2、交换机类型,fanout、topic、direct、headers
*/
channel.exchangeDeclare(ExchangeTypeEnum.DIRECT.getName(), ExchangeTypeEnum.DIRECT.getType());
/*声明队列
* 参数明细:
* 1、队列名称
* 2、是否持久化
* 3、是否独占此队列
* 4、队列不用是否自动删除
* 5、参数
*/
channel.queueDeclare(QUEUE_TEST, true, false, false, null);
//交换机和队列绑定String queue, String exchange, String routingKey
/**
* 参数明细
* 1、队列名称
* 2、交换机名称
* 3、路由key
*/
channel.queueBind(QUEUE_TEST, ExchangeTypeEnum.DIRECT.getName(), RK_QUEUE_TEST);
System.out.println(" **** Consumer->1 Waiting for messages. To exit press CTRL+C");
QueueingConsumer consumer = new QueueingConsumer(channel);
/* 消息确认机制
* autoAck true:表示自动确认,只要消息从队列中获取,无论消费者获取到消息后是否成功消费,都会认为消息已经成功消费
* autoAck false:表示手动确认,消费者获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态
* 并且服务器会认为该消费者已经挂掉,不会再给其发送消息,直到该消费者反馈
* !!!!!! 注意这里是 false,手动确认
*/
channel.basicConsume(QUEUE_TEST, false, consumer);
int count = 0;
while (count < 10) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" count:" + count + " **** Consumer->2 Received '" + message + "'");
doSomeThing(message);
//返回确认状态
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
count++;
}
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 模拟处理复杂逻辑:休眠100ms
*
* @param message
* @throws Exception
*/
public static void doSomeThing(String message) throws Exception {
//遍历Count ,sleep , 接收一条消息后休眠 100 毫秒,模仿复杂逻辑
Thread.sleep(100);
}
}
RabbitMQ的队列中有3条消息
消费者,开启Debug模式,控制一下消费流程
package comsumerconfirm;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;
import conn.MqConnectUtil;
import subscrib3.ExchangeTypeEnum;
import static comsumerconfirm.ConsumerConfirmProducer.QUEUE_TEST;
import static comsumerconfirm.ConsumerConfirmProducer.RK_QUEUE_TEST;
public class ConsumerConfirmComsumer {
public static void main(String[] argv) throws Exception {
Connection connection = null;
Channel channel = null;
try {
connection = MqConnectUtil.getConnectionDefault();
channel = connection.createChannel();
/*声明交换机 String exchange
* 参数明细
* 1、交换机名称
* 2、交换机类型,fanout、topic、direct、headers
*/
channel.exchangeDeclare(ExchangeTypeEnum.DIRECT.getName(), ExchangeTypeEnum.DIRECT.getType());
/*声明队列
* 参数明细:
* 1、队列名称
* 2、是否持久化
* 3、是否独占此队列
* 4、队列不用是否自动删除
* 5、参数
*/
channel.queueDeclare(QUEUE_TEST, true, false, false, null);
//交换机和队列绑定String queue, String exchange, String routingKey
/**
* 参数明细
* 1、队列名称
* 2、交换机名称
* 3、路由key
*/
channel.queueBind(QUEUE_TEST, ExchangeTypeEnum.DIRECT.getName(), RK_QUEUE_TEST);
System.out.println(" **** Consumer->1 Waiting for messages. To exit press CTRL+C");
QueueingConsumer consumer = new QueueingConsumer(channel);
/* 消息确认机制
* autoAck true:表示自动确认,只要消息从队列中获取,无论消费者获取到消息后是否成功消费,都会认为消息已经成功消费
* autoAck false:表示手动确认,消费者获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态
* 并且服务器会认为该消费者已经挂掉,不会再给其发送消息,直到该消费者反馈
* !!!!!! 注意这里是 false,手动确认
*/
channel.basicConsume(QUEUE_TEST, false, consumer);
int count = 0;
while (count < 10) {
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
System.out.println(" count:" + count + " **** Consumer->2 Received '" + message + "'");
doSomeThing(message);
//返回确认状态
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
count++;
}
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 模拟处理复杂逻辑:休眠100ms
*
* @param message
* @throws Exception
*/
public static void doSomeThing(String message) throws Exception {
//遍历Count ,sleep , 接收一条消息后休眠 100 毫秒,模仿复杂逻辑
Thread.sleep(100);
}
}
打断点
2.3.1 投递后,ACK信号处理
生产者3条消息,Unacked=0,没有消费者,所有消息全都在队列中
我们Debug开启消费者,第一次断点到达时,消费者开启、Ready消息清零、Unacked消息数为3
断点继续往下走,执行 channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); 可以看到,消费者确认了1条消息,Unacked数目变为2
断点继续往下执行,再次执行到count++,消费者又消费了1条,Unacked消息数变为1,未确认的消息数为1
这个时候,消费者还存在,而且Unacked数目为1,还剩1条消息没有确认
!!!! 注意,此时我们杀死消费者的进程,看下如果没有消费者,消息会如何处理
我们Kill掉消费者进程
看下消息如何变化,此刻,没有消费者,消息又自动回到队列Ready状态,Unacked数目为0,Ready变为1,方便下次再有其他消费者链接时,投递给其他消费者,重新进行消费
上面我们分析了 消费者Confirm 消息消费的流程
!!! 再说另一种情况,如果你有消费者,但是断点卡住了,一直没让他消费,超过一定时间后,消息还是会从新从Unacked状态变为Ready状态,方便投送给其他消费者,因为时间太久你都没有消费,它会认为你是不是消费者故障了,所以就重新选择其他消费者投递
2.3.2 消费者操作
我们投递消息后,消费者可以根据业务进行相应的处理
- basicAck:当然就是Confirm确认消息了,参数 Tag和multiple表示 multiple=false: 单个处理 Tag标签的消息 multiple=true:批量处理小于等于Tag标签的消息
- basicRecover:路由不成功的消息可以使用recovery重新发送到队列中,重新路由
- basicReject:消费者告诉服务器这个消息我拒绝接收、不处理,而且只能一次拒绝一个消息,参数 Tag及 requeue=true可以设置是否放回到队列,requeue=false表示要被丢到死信队列,此处我们在上一篇文章死信队列那里讲过详细参数
- basicNack:批量拒绝,一次拒绝N条消息,客户端可以设置basicNack方法的multiple参数为true,服务器会拒绝指定了delivery_tag的所有未确认的消息
basicRecover:是路由消息使之recovery重新发送到队列中。默认True,true则重新入队列,并且尽可能的将之前recover的消息投递给其他消费者消费。false则消息会重新被投递给自己
basicReject:是接收端告诉服务器这个消息我拒绝接收,不处理,可以设置是否放回到队列中还是丢掉,而且只能一次拒绝一个消息,官网中有明确说明不能批量拒绝消息,为解决批量拒绝消息才有了basicNack。
basicNack:可以一次拒绝N条消息,和Reject的参数类似,参数multiple=true表示服务器会拒绝小于tag的所有未确认的消息,参数multiple=true表示服务器会拒绝单条tag的未确认的消息,参数requeue就是重新路由还是丢到死信队列,和上面的一样。
下一章 ,我们介绍下 SpringBoot 集成 RabbitMQ实战