在交换机简介一篇中,通过生产者和消费者都声明同样的交换机同样的队列,可以保证无论生产者还是消费者先运行起来,都不会由于交换机或者队列不存在而出现错误。但是上一篇所有的代码都存在如下两个问题:
1、服务器宕机,消息将丢失。
2、发送的消息如果没有正确路由到队列,例如生产者先运行消费者未运行的情况,那么消息将被丢弃,且生产者没有收到任何反馈。
下面的介绍将会解决这些问题。
以前我的消息都是临时消息,服务器宕机重启后,没有被处理的消息将会被丢弃掉,通过设置交换机持久化、队列持久化、消息持久化可以解决这一个问题。
生产者:
//声明持久化交换机,类型为direct
boolean durable=true;
channel.exchangeDeclare(EXCHANGE_NAME, "direct", durable, false, null);
String message="hello world";
//发送消息,并设置为持久化
channel.basicPublish(EXCHANGE_NAME, "blue", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
消费者:
//声明持久化交换机
boolean durable=true;
channel.exchangeDeclare(EXCHANGE_NAME, "direct", durable, false, null);
//声明持久化队列
channel.queueDeclare(queueName, durable, false, false, null);
channel.queueBind(queueName, EXCHANGE_NAME, "blue");
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
QueueingConsumer consumer = new QueueingConsumer(channel);
channel.basicConsume(queueName, true, consumer);
while (true)
{
QueueingConsumer.Delivery delivery = consumer.nextDelivery();
String message = new String(delivery.getBody());
String routingKey = delivery.getEnvelope().getRoutingKey();
System.out.println(" [x] Received routingKey = " + routingKey + ",msg = " + message + ".");
}
即使交换机和队列都是持久化的,消息是否持久化取决于发送方发送消息时指定的properties,MessageProperties类中有一些常用的Properties对象,例如持久化消息MessageProperties.PERSISTENT_TEXT_PLAIN。
消息持久化,可以保证已经发送到队列中的消息被持久化到磁盘上,服务器重启后可以从磁盘恢复这些消息。那么对于正在发送还没有成功的消息呢?两种解决方案:
RabbitMQ中与事务机制有关的方法有三个,分别是Channel里面的txSelect(),txCommit()以及txRollback(),txSelect用于将当前Channel设置成是transaction模式,txCommit用于提交事务,txRollback用于回滚事务,在通过txSelect开启事务之后,我们便可以发布消息给broker代理服务器了,如果txCommit提交成功了,则消息一定是到达broker了,如果在txCommit执行之前broker异常奔溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。
channel.txSelect();
try{
channel.basicPublish(EXCHANGE_NAME, "blue", MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
System.out.println("commit");
channel.txCommit();
}catch(Exception e){
System.out.println("rollback");
channel.txRollback();
}
Publisher Confirm机制(又称为Confirms或Publisher Acknowledgements)是作为解决事务机制性能开销大(导致吞吐量下降)而提出的另外一种保证消息不会丢失的方式。
生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理。
confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息。
在channel 被设置成 confirm 模式之后,所有被 publish 的后续消息都将被 confirm(即 ack) 或者被nack一次。但是没有对消息被 confirm 的快慢做任何保证,并且同一条消息不会既被 confirm又被nack 。
Confirm机制在性能上要比事务优越很多。但是Confirm机制,无法进行回滚,就是一旦服务器崩溃,生产者无法得到Confirm信息,生产者其实本身也不知道该消息吃否已经被持久化,只有继续重发来保证消息不丢失,但是如果原先已经持久化的消息,并不会被回滚,这样队列中就会存在两条相同的消息,系统需要支持去重。
Channel对象提供的ConfirmListener()回调方法只包含deliveryTag(当前Chanel发出的消息序号),我们需要自己为每一个Channel维护一个unconfirm的消息序号集合,每publish一条数据,集合中元素加1,每回调一次handleAck方法,unconfirm集合删掉相应的一条(multiple=false)或多条(multiple=true)记录。从程序运行效率上看,这个unconfirm集合最好采用有序集合SortedSet存储结构。
SortedSet confirmSet = Collections.synchronizedSortedSet(new TreeSet());
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("Nack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
}
});
while (true) {
long nextSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(ConfirmConfig.exchangeName, ConfirmConfig.routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, ConfirmConfig.msg_10B.getBytes());
confirmSet.add(nextSeqNo);
}
事务机制和消息确认机制都是为了保证异常状态下的消息不丢失,其实正常状态下也可能存在消息丢失问题,例如交换机按照路由规则未找到该消息对应的队列。confirm机制配合mandatory标志使用可以实现消息发送的可靠性,且性能较好。
上一篇博客中,曾经介绍过生产者发送消息时可以设置的一个标志位mandatory。
当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者, channel.addReturnListener添加一个监听器,当broker执行basic.return方法时,会回调handleReturn方法,这样我们就可以处理变为死信的消息了;当mandatory设为false时,出现上述情形broker会直接将消息扔掉;通俗的讲,mandatory标志告诉broker代理服务器至少将消息route到一个队列中,否则就将消息return给发送者。
关键代码:
//声明持久化交换机,类型为direct
boolean durable=true;
channel.exchangeDeclare(EXCHANGE_NAME, "direct", durable, false, null);
//添加回调监听,用于处理basic.return命令返回的消息
channel.addReturnListener(new ReturnListener() {
public void handleReturn(int replyCode,String replyText,String exchange,String routingKey,AMQP.BasicProperties properties,byte[] body)
throws IOException{
System.out.println("handleReturn");
System.out.println("replyCode:"+replyCode);
System.out.println("replyText:"+replyText);
System.out.println("messagebody:"+new String(body));
}
});
String message="hello world";
//设置mandatory为true
boolean mandatory=true;
channel.basicPublish(EXCHANGE_NAME, "blue", mandatory, false, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
运行结果:
[x] Sent ‘hello world’
handleReturn
replyCode:312
replyText:NO_ROUTE
messagebody:hello world