1 RabbitMQ在收到消息后,还需要有一段时间才能将消息存入磁盘之中。RabbitMQ并不是为每条消息都做fsync的处理,可能仅仅保存到cache中而不是物理磁盘上,在这段时间内RabbitMQ broker发生crash, 消息保存到cache但是还没来得及落盘,那么这些消息将会丢失。为了解决这个问题RabbitMQ引入发送端消息确认机制,主要通过事务和publisher Confirm机制。
2. 事务机制保证消息不丢失
RabbitMQ支持事务(transaction),通过调用tx.select方法开启事务模式。当开启了事务模式后,只有当一个消息被所有的镜像队列保存完毕后,RabbitMQ才会调用tx.commit-ok返回给客户端。
2.1. 代码
发送端的关键代码TransactionalSend:通过channel.txSelect()开启事务,发送消息,最后执行channel.txCommit()提交事务。如果发送失败,则使用channel.txRollback()回滚事务
// 开启事务
channel.txSelect();
// 发送消息
while(num-- > 0) {
// 发送一个持久化消息到特定的交换机
channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
System.out.println(" [TransactionalSend] Sent + [" + num + "] '" + message + "'");
}
// 不注解下面语句,可以进入channel.txRollback()逻辑
// if(true){
// throw new IOException("consumer channel.txRollback() ");
// }
// 提交事务
channel.txCommit();
}catch(IOException e){
e.printStackTrace();
// 回滚事务
channel.txRollback();
}
2.2 事务的优点和缺点
事务的实现简单,能够保证消息正确到达RabbitMQ,但是它的效率低,只有一般发送消息的效率的1/250
3. publisher confirms机制保证消息不丢失
在AMPQ-0-9-1中,有定义从消费者到RabbitMQ的处理确认机制。但是没有定义消息代理到生产者的确认机制,在RabbitMQ中对此进行扩展,叫做publisher confirms机制
在标准的AMQP 0-9-1,保证消息不会丢失的唯一方法是使用事务:在通道上开启事务,发布消息,提交事务。但是事务是非常重量级的,它使得RabbitMQ的吞吐量降低250倍。为了解决这个问题,RabbitMQ引入的Publisher Confirms机制,它是模仿AMQP协议中消费者消息确认机制
生产者端可以通过confirm.select来启用方法Publisher Confirms机制,RabbitMQ服务端根据是否设置no-wait的值,返回confirm.select-ok。一旦在通道上使用confirm.select方法,就认为它处于Publisher Confirms模式。事务通道不能进入Publisher Confirms模式,一旦通道处于Publisher Confirms模式,不能开启事务。即事务和Publisher Confirms模式只能二选一。
Publisher Confirm模式有以下几种使用方式:
同步方式的发送端的单个Publisher Confirm模式
同步方式的发送端的批量Publisher Confirm机制
异步方式的发送端的Publisher Confirm机制
4. 同步方式的发送端的Publisher Confirm模式
4.1. 测试代码
这个代码实现发送者端发送一个持久化消息到特定的交换机,然后等待服务端返回Basic.Ack后,才执行发送消息
while(num-- > 0) {
// 发送一个持久化消息到特定的交换机
channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
System.out.println(" [SimpleConfirmSend] Sent '" + message + "'");
// 等待服务端返回Basic.Ack后,才执行下一个循环
if(!channel.waitForConfirms()){
System.out.println("message haven't arrived broker");
// 在这里可以对发送失败的记录进行处理:如重发
}
}
和普通发送最大的不同是,在执行发送消息前执行Confirm.Select,RabbitMQ在消息已经收到并处理完毕(如果消息需要,则持久化消息后,才返回Basic.Ok; 如果对应消息的镜像队列,则队列完全同步后,才返回Basic.Ok。总之,必须保证消息不会因为RabbitMQ异常丢失)后返回Basic.Ok给客户端
5. 同步方式的发送端的批量Publisher Confirm机制
// 发送消息
while(num-- > 0) {
// 发送一个持久化消息到特定的交换机
channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
System.out.println(" [SimpleConfirmSend] Sent '" + message + "'");
}
// 批量等待确认: 返回true: 如果所有的消息都收到有确认应答,没有消息被拒绝
if(!channel.waitForConfirms()){
System.out.println("Not all message have arrived broker");
// 实际应用中,需要在这是添加发送消息失败的处理逻辑:如重发等等
// 在这种的模式中,如果发送N条消息,如果有一条失败,则所有的消息都需要重新推送
}
6. 异步Publisher Confirm机制
// 添加回调对象,处理返回值
channel.addConfirmListener(new ConfirmListener(){
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("[AsynConfirmSend] handleAck : deliveryTag = " + deliveryTag + " multiple = " + multiple);
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("[AsynConfirmSend] handleNack : deliveryTag = " + deliveryTag + " multiple = " + multiple);
}
});
// 开启confirm模式:
channel.confirmSelect();
// 发送消息
while(num-- > 0) {
// 发送一个持久化消息到特定的交换机
channel.basicPublish(EXCHANGE_NAME, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes("UTF-8"));
System.out.println(" [TransactionalSend] Sent '" + message + "'");
}
这里和上面的同步批量confirm的最大不同是,发送端在发送消息时,另一个线程同步进行消息的确认。使用此接口连续发送10个消息,包的信令和同步批量confirm是几乎相同的,在信令上两者没有本质区别。
7. 使用Publisher Confirms的其它注意事项
7.1. 否定确认和重新入队
当RabbitMQ无法成功的处理消息时,它会返回生产者端basic.nack,而不是basic.ack。在这种情况下, basic.nack的字段与basic.ack相对应的字段意义相同,且requeue 字段是没有意义的。是否重发消息由发送者端自己决定。
当进入通道进入Publisher Confirms模式,所有的消息只能被confirmed确认或者nack一次。另外没有机制保证消息需要多久被confirmed。
basic.nack只有Erlange进程在处理队列时发生内部错误时才会被回送。
7.2. 消息确认的时机
1)对于不可路由的消息(即RabbitMQ发现收到的消息不能被路由到队列),有两种情况:
a. 消息的mandatory=false,RabbitMQ发现消息不可路由后,马上确认消息,即发送basic.ack或baisc.nack给生产者
b. 消息的mandatory=true,RabbitMQ发现消息不可路由后,先发送basic.return,再确认消息,即发送basic.ack或baisc.nack给生产者
2)对于可路由的消息,需要同时满足如下所有的条件才可以回送确认消息
1 消息被路由到所有的队列中
2 对于路由到持久队列的持久消息,需要持久化消息到磁盘
3 如果队列是镜像队列,则需要将消息同步到所有的队列中
7.3. 持久化消息的确认延迟
持久化的消息路由到持久化队列时,RabbitMQ会将消息存储到磁盘空间。为了保证持久化效率,RabbitMQ不是来一条存一条,而是定时批量地持久化消息到磁盘,这个时间间隔通常是几百毫秒,或者队列空闲执行消息持久化。如果队列支持镜像队列,则延迟时间更大。如果生产者每发送一条消息,等待basic.ack来了再发送一条消息,则等待时间可以达到几百毫秒。
为了提高吞吐量,RabbitMQ强烈建议应用程序异步处理确认或批量发送消息后再等待未完成的确认
7.4. Publisher Confirms的顺序
在大多数情况下,RabbitMQ将以发布的顺序向发布者确认消息(这适用于发布在单个通道上的消息)。 但是,发布者确认是异步发送的,并且可以确认一条消息或一组消息。 发出确认的确切时间取决于消息的传递模式(持久性与非持久性)以及消息被路由到的队列的属性。所以RabbitMQ可能不以发布的顺序向发布者发送确认消息。生产者端尽量不要依赖消息确认的顺序做服务
7.5 对于不可以路由的消息,我们也可以添加监听return listener
但是有一个前提条件就是 只有设置了mandatory=true ,服务器才会通知客户端消息不可达,否则 服务器直接丢弃消息。
package com.mq.rabbit.returnlistener;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @Author Mqs
* @Date 2018/10/27 22:05
* @Desc
*/
public class Producer {
public static void main(String[] args) throws Exception{
// 1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("/");
factory.setPort(AMQP.PROTOCOL.PORT);
factory.setHost("192.168.200.130");
factory.setUsername("mqs");
factory.setPassword("mqs123");
// 2、创建连接
Connection connection = factory.newConnection();
// 3、获取通道
Channel channel = connection.createChannel();
String exchangeName = "return_exchange";
String routingKey = "return.key";
String routingKeyError = "return.error.key";
String msg = "send message test return mandatory ";
// 用于监听不可达的消息
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText,
String exchange, String routingKey,
AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("======= handle ======= return ========");
System.out.println("replyCode: " + replyCode);
System.out.println("replyText: " + replyText);
System.out.println("exchange: " + exchange);
System.out.println("routingKey: " + routingKey);
System.out.println("properties: " + properties);
System.out.println("body: " + new String(body));
}
});
// 可以正常的路由到routingkey
// channel.basicPublish(exchangeName, routingKey, true, null, msg.getBytes());
// 不可以正常的路由到routingkey mandatory 为ture
// TODO 这个会被上面的监听器监听到,会打印要输出的信息
channel.basicPublish(exchangeName, routingKeyError, true, null, msg.getBytes());
// 不可以正常的路由到routingkey mandatory 为false
// TODO 这个不会打印任何消息,会直接删除这个消息
// channel.basicPublish(exchangeName, routingKeyError, false, null, msg.getBytes());
}
}
package com.mq.rabbit.returnlistener;
import com.rabbitmq.client.*;
/**
* @Author Mqs
* @Date 2018/10/27 22:16
* @Desc
*/
public class Consumer {
public static void main(String[] args)throws Exception {
// 1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("/");
factory.setPort(AMQP.PROTOCOL.PORT);
factory.setHost("192.168.200.130");
factory.setUsername("mqs");
factory.setPassword("mqs123");
// 2、创建连接
Connection connection = factory.newConnection();
// 3、获取通道
Channel channel = connection.createChannel();
String exchangeName = "return_exchange";
String routingKey = "return.key";
String exchangeType = "direct";
String queueName = "return_queue";
channel.exchangeDeclare(exchangeName, exchangeType, true, false, null);
channel.queueDeclare(queueName, true, false, false, null);
channel.queueBind(queueName, exchangeName, routingKey);
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
while (true){
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
byte[] body = delivery.getBody();
String msg = new String(body);
System.out.println("消费者收到生产者生产的消息是: " + msg);
}
}
}
结果展示:
可以正常的路由到routingkey【消费者控制台输出】
消费者收到生产者生产的消息是: send message test return mandatory
不可以正常的路由到routingkey mandatory 为ture【生产者控制台输出】
======= handle ======= return ========
replyCode: 312
replyText: NO_ROUTE
exchange: return_exchange
routingKey: return.error.key
properties: #contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
body: send message test return mandatory
不可以正常的路由到routingkey mandatory 为false
生产者控制台不输出任何信息