一、概述
消费者处理一个任务是需要一段时间的,如果有一个消费者正在处理一个比较耗时的任务并且只处理了一部分,突然这个时候消费者宕机了,那么会出现什么情况呢?
要回答这个问题,我们先了解一下 RabbitMQ 的消息应答机制
为了保证消息从队列可靠地达到消费者并且被消费者消费处理,RabbitMQ 提供了消息应答机制,RabbitMQ 有两种应答机制,自动应答和手动应答
1、自动应答、RabbitMQ 只要将消息分发给消费者就被认为消息传递成功,就会将内存中的消息删除,而不管消费者有没有处理完消息
2、手动应答、RabbitMQ 将消息分发给了消费者,并且只有当消费者处理完成了整个消息之后才会被认为消息传递成功了,然后才会将内存中的消息删除
可以看出,如果是自动应答模式,消费者在处理任务的过程中宕机了,那么消息将会丢失,而手动应答则能够保证消息不会被丢失,所以在实际的应用当中绝大多数都采用手动应答
二、手动应答常用 API
// 该消息已经处理完成了,RabbitMQ 内存可以删除该消息了
void basicAck(long deliveryTag, boolean multiple)
// 不处理该消息,直接拒绝,然后将该消息丢弃
void basicReject(long deliveryTag, boolean requeue)
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
三、原理图
Producer 生产消息发送给消息队列,Consumer01 消费消息1、Consumer02 消费消息2、Consumer01 接收到了消息之后,在处理完部分逻辑的时候突然宕机了,Consumer01 未发送 ACK,此时消息1 不会丢失,而是重新进入队列,由状态正常的 Consumer02 消费掉
四、编码
1、RabbitmqUtils(工具类)
public class RabbitmqUtils {
private static final String HOST_ADDRESS = "192.168.59.130";
private static final String USER_NAME = "admin";
private static final String PASSWORD = "admin123";
public static Channel getChannel() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(HOST_ADDRESS);
factory.setUsername(USER_NAME);
factory.setPassword(PASSWORD);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
2、Producer
public class Producer {
private static final String QUEUE_NAME = "ackDemo";
public static void main(String[] args) throws Exception {
Channel channel = RabbitmqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
String message = "有意思的消息--->";
for (int i = 1; i < 11; i++) {
channel.basicPublish("", QUEUE_NAME, null, (message + i).getBytes(StandardCharsets.UTF_8));
}
System.out.println("Producer send message successfully");
}
}
3、Consumer01
public class Consumer01 {
private static final String QUEUE_NAME = "ackDemo";
public static void main(String[] args) throws Exception {
Channel channel = RabbitmqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
try {
// 休眠 10 s
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 参数一、deliveryTag:消息应答标记
// 参数二、multiple:(false、只应答接收到的那个消息 true、应答所有传递过来的消息)
// 处理完逻辑之后应答 ack
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
System.out.println(message);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag);
};
// 设置手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
}
}
4、Consumer02
public class Consumer02 {
private static final String QUEUE_NAME = "ackDemo";
public static void main(String[] args) throws Exception {
Channel channel = RabbitmqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
// 参数一、deliveryTag:消息应答标记
// 参数二、multiple:(false、只应答接收到的那个消息 true、应答所有传递过来的消息)
// 处理完逻辑之后应答 ack
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
System.out.println(message);
};
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println(consumerTag);
};
// 设置手动应答
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
}
}
五、测试过程及结果
1、先启动 Cousumer01、Consumer02
2、生产者发送 10 条消息,根据默认的轮询规则,一个消费者(假设此时为 Consumer01)消费第 1、3、5、7、9 条消息,另外一个消费者(假设此时为 Consumer02)消费第 2、4、6、8、10 条消息
3、当 Consumer01 消费第 1、3 条消息的时候手动强制关闭 Consumer01,那么原先本应该由 Consumer01 消费的第 5、7、9 条消息不会丢失,它们将重新进入队列由 Consumer02 消费掉
4、Consumer01、Consumer02 消费的消息如下: