思考:
假如我们的消费者正在处理一个任务,这个任务处理的过程中,突然这个消费者宕机了,导致这个任务没有完成,然而RabbitMQ队列却把这个任务从队列中删除了,那么这个任务就丢失了。改如何解决?
消息应答
为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
注意
在前面的代码中,我们都是使用了自动应答
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
但是在企业一般不会这样用,因为这个语句执行完就直接应答结束了,但是我们这个语句后面可能还有其他的代
码,其他的代码可能会出问题导致线程出现问题,这个时候可能我们的任务并没有执行成功呢,就会出现错误。
消息的手动应答
channel.basicAck();
表示应答成功,要携带一个boolean的参数表示是否批处理应答,一般不进行批处理应答更合理。
下面这两个都表示应答失败,前者比后者少一个参数
channel.basicReject();
channel.basicNack()
消息重新入队
如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或 TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。
生产者
package com.dongmu.reresolve;
import com.dongmu.util.RabbitMQUtil;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
public class Producer {
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtil.getChannel();
//声明队列
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
//发布消息
channel.basicPublish("", TASK_QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
消费者1
package com.dongmu.reresolve;
import com.dongmu.util.RabbitMQUtil;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class Consumer1 {
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtil.getChannel();
System.out.println("C1 等待接收消息处理时间较短");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收到消息:" + message);
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
CancelCallback cancelCallback = (s) -> {
System.out.println(s + "消费者取消消费接口回调逻辑");
};
//采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
}
}
消费者2只把延时时间改成30s即可
package com.dongmu.reresolve;
import com.dongmu.util.RabbitMQUtil;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class Consumer2 {
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtil.getChannel();
System.out.println("C2 等待接收消息处理时间较长");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody());
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收到消息:" + message);
/**
* 1.消息标记 tag
* 2.是否批量应答未应答消息
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
};
CancelCallback cancelCallback = (s) -> {
System.out.println(s + "消费者取消消费接口回调逻辑");
};
//采用手动应答
boolean autoAck = false;
channel.basicConsume(TASK_QUEUE_NAME, autoAck, deliverCallback, cancelCallback);
}
}
然后我们演示生产者发送两条消息,第一条被消费者一消费了,第二条由于延时比较长,我们在中途把它关闭,这个时候就可以发小消费者1对第二个任务进行了消费这就是消息重新入队。
队列的持久化
是如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。
我们可以直接把队列中的持久化改成true
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
但是要注意这里要先把原来的队列删除了,不然由于已经存在一个队列是不进行持久化的,如果强行把它改成持久化的就会报错。
原来的队列是这样子的
队列持久化之后就是这个样子
public class Producer {
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtil.getChannel();
//声明队列
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
//发布消息
channel.basicPublish("", TASK_QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
消息的持久化
将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是
这里依然存在当消息刚准备存储在磁盘的时候 但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。
我们原来的消息发布是这样的
channel.basicPublish("", TASK_QUEUE_NAME, null, message.getBytes("UTF-8"));
现在改成这个样子即可
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
完整代码
package com.dongmu.reresolve;
import com.dongmu.util.RabbitMQUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.MessageProperties;
import java.util.Scanner;
public class Producer {
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws Exception {
Channel channel = RabbitMQUtil.getChannel();
//声明队列
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
//发布消息
channel.basicPublish("", TASK_QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}