消息应答机制为RabbitMQ服务器向消费者传递了一个消息后,消费者给服务器的一个回复,服务器接到答复后决定是否删除这个已经消费的消息。RabbitMQ的消息应答机制分为自动应答和手动应答两种形式。
1. 自动应答
RabbitMQ服务器一旦把消息传输给消费者后,服务器就默认为消息已经传送成功,服务器队列中便自动删除该消息。
自动应答机制虽然传输方面的吞吐量比较高,但是这种机制存在严重的弊端。一方面如果消费者程序出现bug,或者消费者端链接或者channel已关闭导致消息消费失败,此时服务端已把该消息删除,最终导致消息丢失;另一方面服务器可以向消费者无限制的传输消息,消费者端积压太多消息来不及处理,导致内存耗尽,消费者线程被操作系统杀死。
因此自动应答机制适合消费者可以高效并能正确处理消息的前提下才可使用。
案例一:下面看一下消费者成功处理后自动应答的案例
首先创建一个生产者Producer用于生产消息,其中用到的工具类RabbitmqUtil见此篇文章中。
import com.lzj.rabbitmq.RabbitmqUtil;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class producer {
private final static String QUEUE_NAME = "hello_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/*创建信道*/
Channel channel = RabbitmqUtil.getChannel();
/*
* 声明一个队列
* 1. 第一个参数表示队列的名字
* 2. 第二个参数表示队列中消息是否要持久化, false表示不持久化存储在内存中, 默认为false
* 3. 第三个参数表示该队列是否只供一个消费者消费, 不与其它消费者共享, false表示不共享
* 4. 最后一个消费者断开链接后是否自动删除队列, true表示自动删除
* 5. 其它参数
* */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
/*
* 发送消息
* 1. 发送到哪个交换机
* 2. 指定路由的key是哪个
* 3. 其它参数信息
* 4. 消息体
* */
String message = "hello world";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("消息发送完毕");
}
}
然后创建一个Consumer类用于消费消息,其中在调用消费消息方法basicConsume时设置的第二个参数为true表示自动向服务器应答。
import com.lzj.rabbitmq.RabbitmqUtil;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
private final static String QUEUE_NAME = "hello_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/*创建信道*/
Channel channel = RabbitmqUtil.getChannel();
/*消息消费时如何消费需回调的接口*/
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(consumerTag);
System.out.println(new String(message.getBody()));
System.out.println("消费成功");
};
/*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
CancelCallback cancelCallback = consumerTag -> {
System.out.println(consumerTag);
System.out.println("消费被取消");
};
/*
* 消费者消费消息
* 1. 第一个参数代表消费哪个队列
* 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
* */
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
创建好生产者和消费者后,下面进行测试,首先启动生产者程序向消息队列中发送消息,终端窗口输出下面一句话表示消息生产成功
消息发送完毕
此时查看RabbitMQ浏览器插件管理端确认消息状态如下所示,表示有一个消息已经处于Ready状态了,说明消息已经发送成功。
下面再启动消费端程序消费消息,输出下面内容表示消息已经被消费。
amq.ctag-H9XdjQz2w75wExXrALaw_w
hello world
消费成功
此时再查看RabbitMQ浏览器管理端插件确认消息的状态如下所示,之前Ready状态的消息已被消费并删除了。
案例二:下面看另外一个例子,消费者处理失败后自动应答案例
生产者程序同案例一一致,只修改消费者代码在最后面手动抛出异常,模仿失败处理
public class Consumer {
private final static String QUEUE_NAME = "hello_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/*创建信道*/
Channel channel = RabbitmqUtil.getChannel();
/*消息消费时如何消费需回调的接口*/
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(consumerTag);
System.out.println(new String(message.getBody()));
System.out.println("消费成功");
};
/*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
CancelCallback cancelCallback = consumerTag -> {
System.out.println(consumerTag);
System.out.println("消费被取消");
};
/*
* 消费者消费消息
* 1. 第一个参数代表消费哪个队列
* 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
* */
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
/*模拟抛出异常*/
throw new NullPointerException("空指针");
}
}
分别启动消费者程序和生产者程序,发现即使消费者程序最后执行失败了,消息还是被认为执行成功并在hello_queue队列中被删除了,导致消息被丢失。
2. 手动应答
手动应答就是消费端处理完消息后才通知服务器队列删除消息,虽然产生了一次应答通信,但保证了消息可靠性。
消息应答方法主要包括下面3种形式:
- Channel.basicAck 用于肯定应答
消费端通知RabbitMQ服务器消息已经消费成功,可以删除消息了。 - Channel.basicNack 用于否定应答
消费端通知RabbitMQ服务器消息未消费成功,不可删除消息。 - Channel.Reject 用于否定应答
消费端通知RabbitMQ服务器不处理该消息了,可以删除消息了。
其中basicAck和basicNack在应答时可以设置批量应答标志,批量应答是指消费端可以一次性的向RabbitMQ服务器应答成功或者失败。以basicAck为例,在消费端的channel上在处理5个消息,假如5个消息编号分别为1、2、3、4、5,当在处理完最后一个消息5号消息后,向服务器发一个确定应答,一次性将channel上未确认应答的消息全部确认应答,节省了每个消息分别进行应答的网络消耗。但批量应答也有其不足之处,批量应答是一次性向服务器确认应答或者否定应答channel上的消息,假如有些消息还未完全执行完也会被进行应答,造成消息丢失。
案例一:消费端肯定应答服务器队列中消息
首先创建生产端Producer, 代码如下所示
public class producer {
private final static String QUEUE_NAME = "hello_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/*创建信道*/
Channel channel = RabbitmqUtil.getChannel();
/*
* 声明一个队列
* 1. 第一个参数表示队列的名字
* 2. 第二个参数表示队列中消息是否要持久化, false表示不持久化存储在内存中, 默认为false
* 3. 第三个参数表示该队列是否只供一个消费者消费, 不与其它消费者共享, false表示不共享
* 4. 最后一个消费者断开链接后是否自动删除队列, true表示自动删除
* 5. 其它参数
* */
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = null;
for(int i=0; i<5; i++){
message = "hello world " + i;
/*
* 发送消息
* 1. 发送到哪个交换机
* 2. 指定路由的key是哪个
* 3. 其它参数信息
* 4. 消息体
* */
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("hello world " + i + "消息发送完毕");
}
}
}
然后创建消费端Consumer,代码如下所示,在调用消息消费方法basicConsume时第二个参数autoAct设置的false表示需要手动应答服务器。然后消息消费成功后主动调用了一次basicAck方法用于手动应答服务器可以删除消息了。
public class Consumer {
private final static String QUEUE_NAME = "hello_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/*创建信道*/
Channel channel = RabbitmqUtil.getChannel();
/*消息消费时如何消费需回调的接口*/
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println(new String(message.getBody()));
/*
* 向服务器发送肯定应答
* 第一个参数表示消息的标签, 标识消息头的唯一性, 代表要确认哪一个消息
* 第二个参数表示是否批量确认, false表示不需要批量确认, 就需要每个消息都要手动确认
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.println("消费成功");
};
/*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
CancelCallback cancelCallback = consumerTag -> {
System.out.println(consumerTag);
System.out.println("消费被取消");
};
/*
* 消费者消费消息
* 1. 第一个参数代表消费哪个队列
* 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
* */
channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
下面分别启动生产端程序和消费端程序,生产端生产向消息队列中生产了5个消息,消费端消费这5个消息,消费端输出结果如下所示
hello world 0
消费成功
hello world 1
消费成功
hello world 2
消费成功
hello world 3
消费成功
hello world 4
消费成功
通过RabbitMQ浏览器插件查看,消息均已被确认
案例二:消费端通信异常,消息不会被删除,服务器会重新发给其他消费者进行消费
假如一个生产者生产消息,有2个消费者消费消息,在消息消费过程中有一个消费者与服务器通信异常,此时该消费者未应答的消息不会被删除而是会重新被其他消费者进行消费。
首先创建一个Producer类,该类与案例一种的Producer类一致,此处就不再贴代码。
然后创建第一个消费者程序Consumer1
public class Consumer1 {
private final static String QUEUE_NAME = "hello_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/*创建信道*/
Channel channel = RabbitmqUtil.getChannel();
/*消息消费时如何消费需回调的接口*/
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("Consumer1正在处理消息:" + new String(message.getBody()));
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
* 向服务器发送肯定应答
* 第一个参数表示消息的标签, 标识消息头的唯一性, 代表要确认哪一个消息
* 第二个参数表示是否批量确认, false表示不需要批量确认, 就需要每个消息都要手动确认
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.println("Consumer1消费成功");
};
/*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
CancelCallback cancelCallback = consumerTag -> {
System.out.println(consumerTag);
System.out.println("消费被取消");
};
/*
* 消费者消费消息
* 1. 第一个参数代表消费哪个队列
* 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
* */
channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
}
然后创建Consumer2消费者,该代码基本与Consumer1保持一致,所以复制Consumer1代码为Consumer2一份,如下所示
public class Consumer2 {
private final static String QUEUE_NAME = "hello_queue";
public static void main(String[] args) throws IOException, TimeoutException {
/*创建信道*/
Channel channel = RabbitmqUtil.getChannel();
/*消息消费时如何消费需回调的接口*/
DeliverCallback deliverCallback = (consumerTag, message) -> {
System.out.println("Consumer2正在处理消息:" + new String(message.getBody()));
try {
Thread.sleep(1000*10);
} catch (InterruptedException e) {
e.printStackTrace();
}
/*
* 向服务器发送肯定应答
* 第一个参数表示消息的标签, 标识消息头的唯一性, 代表要确认哪一个消息
* 第二个参数表示是否批量确认, false表示不需要批量确认, 就需要每个消息都要手动确认
* */
channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
System.out.println("Consumer2消费成功");
};
/*取消消费时的回调接口, 比如队列被删除了, 取消消费*/
CancelCallback cancelCallback = consumerTag -> {
System.out.println(consumerTag);
System.out.println("消费被取消");
};
/*
* 消费者消费消息
* 1. 第一个参数代表消费哪个队列
* 2. 第二个参数表示消息被消费成功后是否自动向服务器发送应答。true表示自动向服务器发送应答, false表示需要手动向服务器发送应答
* */
channel.basicConsume(QUEUE_NAME, false, deliverCallback, cancelCallback);
}
}
然后分别启动Consumer1和Consumer2程序,最后启动Producer程序,模拟消费者与服务器通信异常,当Consumer2消费消息过程中与服务器断开链接(停掉该程序),Consumer2消费端输出如下所示,说明Consumer2已成功消费了hello world 1消息,在处理消息hello world 3的过程中退出了。
Consumer2正在处理消息:hello world 1
Consumer2消费成功
Consumer2正在处理消息:hello world 3
Process finished with exit code -1
下面看下Consumer1消费者输出如下所示,说明hello world 0、hello world 2、hello world 4、hello world 3 成功被Consumer1消费,而hello world 3消息就是被Consumer2消费失败后由Consumer1重新进行消费的。
Consumer1正在处理消息:hello world 0
Consumer1消费成功
Consumer1正在处理消息:hello world 2
Consumer1消费成功
Consumer1正在处理消息:hello world 4
Consumer1消费成功
Consumer1正在处理消息:hello world 3
Consumer1消费成功