一、消息应答概念
消息消费现象:消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会导致消息丢失。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。
消息应答机制:为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
二、消息应答方式
方式一:自动应答
消息发送后立即被认为已经传送成功
弊端:如果消息在接收到之前,消费者那边出现连接或者 信道 关闭,那么消息就丢 失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终 使得内存耗尽,最终这些消费者线程被操作系统杀死
方式二:手动应答
实际开发也是手动应答而非自动应答,手动应答为了避免消费者未消费完消息,消息丢失的风险。手动应答需要在消费者消费消息的时候手动应答。
Channel.basicAck():用于肯定确认,RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
Channel.basicNack():用于否定确认
Channel.basicReject():用于否定确认 (推荐使用)
关于basicAck中的第二个参数multiple表示批量应答的意思,ture表示开启批量应答,false表示单条应答,在实际工作中都是设置为false,只开启单条应答。 批量应答存在未消费的消息也会被应答的风险。
/**
* Acknowledge one or several received
* messages. Supply the deliveryTag from the {@link com.rabbitmq.client.AMQP.Basic.GetOk}
* or {@link com.rabbitmq.client.AMQP.Basic.Deliver} method
* containing the received message being acknowledged.
* @see com.rabbitmq.client.AMQP.Basic.Ack
* @param deliveryTag the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver}
* @param multiple true to acknowledge all messages up to and
* including the supplied delivery tag; false to acknowledge just
* the supplied delivery tag.
* @throws java.io.IOException if an error is encountered
*/
void basicAck(long deliveryTag, boolean multiple) throws IOException;
三、代码示例
生产者代码
public class WorkAckProducer {
private static final String QUEUE_NAME = "ack_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitUtils.getChannel();
List<String> messgageList = Arrays.asList("AA", "BB", "CC", "DD");
/**
* 生成一个队列
* 1. 队列名称
* 2. 队列里面的消息是否持久化 默认消息存储在内存中(false表示不持久化,即保存在内存中);true表示持久化到硬盘中
* 3. 该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4. 是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5. 其他参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for (String message : messgageList) {
/**
* 发送一个消息
* 1. 发送到那个交换机
* 2. 路由的 key 是哪个
* 3. 其他的参数信息
* 4. 发送消息的消息体
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" 消息发送完毕"+ new String(message.getBytes()));
}
}
}
消费者代码
在channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);方法中的第二个参数设置为false开启手动应答。消费者在消费后调用 channel.basicAck(message.getEnvelope().getDeliveryTag(), false);方法进行手动应答。
public class WorkAck {
public static final String QUEUE_NAME = "ack_queue";
/**
* 消息手动应答代码示例
* @param args
* @throws IOException
* @throws TimeoutException
*/
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitUtils.getChannel();
System.out.println("线程1较快,在等待消息:");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println(Thread.currentThread().getName() + "接收到了消息:" + new String(message.getBody()));
// 消息手动应答,第二个参数false表示单条应答
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println(Thread.currentThread().getName()+"取消发送了");
}
};
System.out.println(Thread.currentThread().getId() + "消费者正在等待消息----");
/**
* 第一个参数是队列名称
* 第二个参数表示是否自动应答,如果是true,则无需手动应答,如果是false,则需要手动应答;
* 手动应答是消费者处理完本条消息,然后会向mq服务器进行消息确认,允许mq服务器在队列中把消息删除;一旦消费者没有向mq服务器进行确认,即使消费者断开后;
* mq服务器也会把消息重新入到mq队列,给其他的消费者消费。
* 生产上一般都开启手动应答;自动应答存在消费者还没处理完消息时,队列清空了消息,导致消息无法重新入队,有消息丢失的风险;
*/
channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
public class WorkAck2 {
public static final String QUEUE_NAME = "ack_queue";
/**
* 消息手动应答代码示例
* @param args
* @throws IOException
* @throws TimeoutException
*/
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitUtils.getChannel();
System.out.println("线程2非常慢,在等待消息:");
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
System.out.println(Thread.currentThread().getName() + "接收到了消息:" + new String(message.getBody()));
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "在处理消息:" + new String(message.getBody()));
// 消息手动应答
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
System.out.println(Thread.currentThread().getName()+"取消发送了");
}
};
System.out.println(Thread.currentThread().getId() + "消费者正在等待消息----");
/**
* 第一个参数是队列名称
* 第二个参数表示是否自动应答,如果是true,则无需手动应答,如果是false,则需要手动应答;
* 手动应答是消费者处理完本条消息,然后会向mq服务器进行消息确认,允许mq服务器在队列中把消息删除;一旦消费者没有向mq服务器进行确认,即使消费者断开后;
* mq服务器也会把消息重新入到mq队列,给其他的消费者消费。
* 生产上一般都开启手动应答;自动应答存在消费者还没处理完消息时,队列清空了消息,导致消息无法重新入队,有消息丢失的风险;
*/
channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
测试结果:
一、消费者一和消费者二正常消费:
生产者:
Connected to the target VM, address: '127.0.0.1:4888', transport: 'socket'
消息发送完毕AA
消息发送完毕BB
消息发送完毕CC
消息发送完毕DD
消费者一:
Connected to the target VM, address: '127.0.0.1:4872', transport: 'socket'
线程1较快,在等待消息:
1消费者正在等待消息----
pool-1-thread-4接收到了消息:AA
pool-1-thread-4接收到了消息:CC
消费者二:
线程2非常慢,在等待消息:
1消费者正在等待消息----
pool-1-thread-4接收到了消息:BB
pool-1-thread-4在处理消息:BB
pool-1-thread-4接收到了消息:DD
pool-1-thread-4在处理消息:DD
从上述结果看,消息是轮询被消费者一和消费者二正常消费。
现在在消费者二接收到消息BB时,但是还没应答的时立即停止消费者二服务,然后看一下消费者一和消费者二的结果:
消费者一:
Connected to the target VM, address: '127.0.0.1:5055', transport: 'socket'
线程1较快,在等待消息:
1消费者正在等待消息----
pool-1-thread-4接收到了消息:AA
pool-1-thread-4接收到了消息:CC
pool-1-thread-5接收到了消息:BB
pool-1-thread-5接收到了消息:DD
消费者二:
线程2非常慢,在等待消息:
1消费者正在等待消息----
pool-1-thread-4接收到了消息:BB
Disconnected from the target VM, address: '127.0.0.1:5065', transport: 'socket'
Process finished with exit code 130
从上述结果看,消费者二如果还没应答且服务挂了时,这个消息会被重新放回到队列中被消费者一消费,消息并没有丢失。