1、前言
因朋友提到参考源码的需求,已经更新至github 教程测试源码:https://github.com/lhmyy521125/rabbitmq-test
在前面的章节中我们已经了解到生产者确认机制、Return机制,我们回顾一下我们之前在生产者确认机制中抛出的第三个问题:忘记的小伙伴可以回去看一下 RabbitMQ可靠性投递生产者确认机制
问题如下:
在接收者Receive Message(消息消费者) 在接收到消息后,如何通知RabbitMQ我已经接收到该消息?是否消费者也需要一个确认告知RabbitMQ已经接收到消息?答案是肯定的,就是今天我们要介绍的消费端ACK
2、消费端ACK
不知道大家还是否对以下这个方法有印象?设置消费确认
//参数:队列名称、是否自动ACK、Consumer
channel.basicConsume(queueName, true, consumer);
第二个参数:
- 当autoAck等于true时,RabbitMQ会自动把发送出去的消息设置为确认,然后从队列中删除,而不管消费者是否真正地接收到了这些消息;
- 当autoAck等于false时,RabbitMQ会设置当消费者收到消息采用手工形式进行确认,证明消费端已经接收到了该消息了,RabbitMQ可以从队列中删除该消息了
样例
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println(message);
//消费端消息确认,并删除
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
//参数:队列名称、是否自动ACK、Consumer
channel.basicConsume(queueName, false, consumer);
通过调用以上basicAck方法来告诉消息服务器来删除消息,参数getDeliveryTag() 就是我们的消息标识ID
3、消费端拒绝
在消费者接收到消息后,如果想明确拒绝当前的消息而不是确认,可以调用 channel.basicReject 告诉RabbitMQ拒绝该消息,方法参数介绍如下
//deliveryTag 消息ID
//requeue true = (重回队列 / false = 删除该消息)
void basicReject(long deliveryTag, boolean requeue) throws IOException;
而channel.basicReject 一次只能拒绝一条消息,如果需要批量拒绝那么就需要用到 channel.basicNack,参数介绍如下
//deliveryTag 消息ID
//multiple (true = 批量 / false = 不批量)
//requeue (true = 重回队列 / false = 删除该消息)
void basicNack(long deliveryTag, boolean multiple , boolean requeue) throws IOException
接下来我们用样例来说明:
生产者:
public class AckMsgProducer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.1.28");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("toher");
connectionFactory.setPassword("toher888");
//2 创建Connection
Connection connection = connectionFactory.newConnection();
//3 创建Channel
Channel channel = connection.createChannel();
//4 指定我们的消息投递模式: 消息的确认模式
channel.confirmSelect();
//5 声明交换机 以及 路由KEY
String exchangeName = "test_ack_exchange";
//这里故意用一个错误的routingKey 以便测试交换机路由不到队列
String routingKey = "ack.send";
//6 发送一条消息
String msg = "Test ACK Message";
for(int i =1; i<5; i ++){
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("num", i);
//为了测试消息拒绝 我们传递一个自定义参数 消费端进行测试
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2)
.contentEncoding("UTF-8")
.headers(headers)
.build();
msg = msg + " 第"+ i +"条";
channel.basicPublish(exchangeName, routingKey, true, properties, msg.getBytes());
}
}
}
消费者:
public class AckMsgConsumer {
public static void main(String[] args) throws Exception {
//1 创建ConnectionFactory
ConnectionFactory connectionFactory = new ConnectionFactory() ;
connectionFactory.setHost("192.168.1.28");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("toher");
connectionFactory.setPassword("toher888");
//2 创建Connection
Connection connection = connectionFactory.newConnection();
//3 创建Channel
Channel channel = connection.createChannel();
//4 声明
String exchangeName = "test_ack_exchange";
//指定类型为topic
String exchangeType = "topic";
String queueName = "test_ack_queue";
//因为*号代表匹配一个单词
String routingKey = "ack.*";
//表示声明了一个交换机
channel.exchangeDeclare(exchangeName, exchangeType, true, false, false, null);
//表示声明了一个队列
channel.queueDeclare(queueName, true, false, false, null);
//建立一个绑定关系:
channel.queueBind(queueName, exchangeName, routingKey);
//5 创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
throws IOException {
String msg = new String(body, "UTF-8");
//让第一条数据 采用 Nack 拒绝 并重回队列
if((Integer)properties.getHeaders().get("num") == 1) {
System.out.println("未消费:" + msg);
channel.basicNack(envelope.getDeliveryTag(), false, true);
} else {
System.out.println("已消费:" + msg);
channel.basicAck(envelope.getDeliveryTag(), false);
//测试channel.basicReject 开启
//channel.basicReject(envelope.getDeliveryTag(), false);
}
}
};
//参数:队列名称、是否自动ACK、Consumer
channel.basicConsume(queueName, false, consumer);
}
}
运行效果:
以上代码传递了自定义参数,以参数num =1 设定拒绝让其重回队列,可以看到后几条都已经消费成功,唯独第一条数据一直重复消费,重复回归队列;
到RabbitMQ控制台会发现一直有一条Ready中的消息
4、结语
本文介绍了消费端ACK和消费端拒绝,在实际开发过程中大家可以根据系统的业务需求来确定消费端的确认和拒绝,上述的样例中大家有没有发现一个问题,第一条数据不断重复的进行消费,然后重回队列,在实际开发过程中,我们一般会采用将channel.basicReject 或者channel.basicNack 中的requeue 设直为false ,以启用"死信队列"的功能。死信队列可以通过检测被拒绝或者未送达的消息来追踪问题,下一篇我们在来介绍死信队列的使用;
5、预告
下一篇:消息的TTL和RabbitMQ的各种队列介绍