既然消息在发布时可以执行一些保证可靠性的机制,那么消费者在消费消息时是否也同样的有类似的机制来通知队列消息的消费成功以否。答案是肯定的。
在实际业务场景中,一般消息的业务处理都集中在消费端.所以消费时消息的可靠性尤为重要。
我们先了解一下消费者拉取队列的两种方式
1. get主动拉取
2. consumer接受推送
1. Get方式
// 省略获取连接,声明队列,并绑定交换机代码
while (true) {
//主动去 "lb-queue" 这个队列拉取消息
GetResponse getResponse = channel.basicGet("lb-queue", false);
if (getResponse != null) {
//打印获取到的消息
System.out.println(new String(getResponse.getBody()));
} else {
System.out.println("等待消息中.....");
Thread.sleep(1000);
}
}
我们先启动消费者。
image.png
生产者投递一条消息到该队列,消费者就可以从队列中主动的获取到消息。
image.png
总的来说,这种方式不推荐使用,性能比较差,消费者永远也不知道队列里什么时候会有消息,消费者只能不停的主动的去轮循队列,非常的耗费性能且容易造成大量的空操作。
2. Consumer方式
属于一种推送模型。注册一个消费者后,
RabbitMQ 会在消息可用时,自动将消息进行推送给消费者。
这种模式我们已经使用过很多次了,这里就不演示了
消息的应答
消费者收到的每一条消息都必须进行确认(ACK)。消息确认(ACK)后,RabbitMQ 才会从队列删除这条消息,RabbitMQ 不会为未确认的消息设置超时时
间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。这么设计的原因是 RabbitMQ 允许消费者消费一条消
息的时间可以很久很久
查看Amqp-client源码中Channel.baseConsumer方法
/**
* Start a non-nolocal, non-exclusive consumer, with
* a server-generated consumerTag.
* @param queue the name of the queue
* @param autoAck true if the server should consider messages
* acknowledged once delivered; false if the server should expect
* explicit acknowledgements
* @param callback an interface to the consumer object
* @return the consumerTag generated by the server
* @throws java.io.IOException if an error is encountered
* @see com.rabbitmq.client.AMQP.Basic.Consume
* @see com.rabbitmq.client.AMQP.Basic.ConsumeOk
* @see #basicConsume(String, boolean, String, boolean, boolean, Map, Consumer)
*/
String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;
这个方法中的第二个参数要求我们传一个boolean来指定消费消息时,是否自动确认,我们先传false来消费消息,但不手动确认,看看会有什么效果。
// 省略获取连接,声明队列,并绑定交换机 代码 。。....
//指定第二个参数为false,表示不自动提交
channel.basicConsume(queueName, false,
new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
System.out.println("消费者接收到消息***:" + new String(body));
}
});
启动这个消费者等待消息,生产者投递消息。
image.png
发现消费者已经接受到消息了,这时我们再次重启消费者,会发现该消息仍然可以被消费,这就导致了消息的重复消费问题。
这时我们手动在代码中增加手动确认的话,当消息被消费,确认后,rabbitmq就会从队列中消息,如果消息是持久化的话也从硬盘,队列中删除消息。
// 省略获取连接,声明队列,并绑定交换机 代码 。。....
//指定第二个参数为false,表示不自动提交
channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者接收到消息***" + envelope.getDeliveryTag() + ":" + new String(body));
//针对deliveryTag进行手动确认
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
总结:
当 autoAck=true时,rabbitmq在消费者消费后会立即删除该消息。
当 autoAck=false 时,RabbitMQ 会等待消费者显式发回 ack 信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ 会在队列
中消息被消费后立即删除它
采用消息确认机制后,只要令 autoAck=false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,
因为 RabbitMQ 会一直持有消息直到消费者显式调用 basicAck 为止。
当 autoAck=false 时,对于 RabbitMQ 服务器端而言,队列中的消息分成了两部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,
但是还没有收到消费者 ack 信号的消息。如果服务器端一直没有收到消费者的 ack 信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消
息重新进入队列,等待投递给下一个消费者(也可能还是原来的那个消费者)
QoS 预取模式:
在确认消息被接收之前,消费者可以预先要求接收一定数量的消息,在处理完一定数量的消息后,批量进行确认。如果消费者应用程序在确认消息
之前崩溃,则所有未确认的消息将被重新发送给其他消费者。所以这里存在着一定程度上的可靠性风险。
这种机制一方面可以实现限速(将消息暂存到 RabbitMQ 内存中)的作用,一方面可以保证消息确认质量(比如确认了但是处理有异常的情况)。
注意:消费确认模式必须是非自动 ACK 机制(这个是使用 baseQos 的前提条件,否则会 Qos 不生效),然后设置 basicQos 的值;另外,还可以基于
consume 和 channel 的粒度进行设置(global)。
basicQos 方法参数详细解释:
prefetchSize:最多传输的内容的大小的限制,0 为不限制,但据说 prefetchSize 参数,rabbitmq 没有实现。
prefetchCount:会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,即一旦有 N 个消息还没有 ack,则该 consumer将 block 掉,直到有消息ack
global:true\false
是否将上面设置应用于 channel,简单点说,就是上面限制是 channel 级别的还是 consumer 级别。
如果同时设置 channel 和消费者,会怎么样?AMQP 规范没有解释如果使用不同的全局值多次调用 basic.qos 会发生什么。
RabbitMQ 将此解释为意味着两个预取限制应该彼此独立地强制执行;消费者只有在未达到未确认消息限制时才会收到新消息。
channel.basicQos(10, false); // Per consumer limit
channel.basicQos(15, true); // Per channel limit
channel.basicConsume("my-queue1", false, consumer1);
channel.basicConsume("my-queue2", false, consumer2);
也就是说,整个通道加起来最多允许 15 条未确认的消息,每个消费者则最多有 10 条消息
消费者中的事务:
使用方法和生产者一致
假设消费者模式中使用了事务,并且在消息确认之后进行了事务回滚,会是什么样的结果?
结果分为两种情况:
1. autoAck=false
手动应对的时候是支持事务的,也就是说即使你已经手动确认了消息已经收到了,但 RabbitMQ 对消息的确认会等事务的返回结果,
再做最终决定是确认消息还是重新放回队列,如果你手动确认之后,又回滚了事务,那么以事务回滚为准,此条消息会重新放回队列;
2. autoAck=true
如果自动确认为 true 的情况是不支持事务的,也就是说你即使在收到消息之后在回滚事务也是于事无补的,
队列已经把消息移除了。