在前几篇文章中已经搭建RabbitMQ服务端,下面记录客户端编写时应注意的几个方面。
持久化消息
持久化消息必须达到以下3个要求:
- 交换器持久化
- 目标队列持久化
- 消息的投递模式持久化
满足这些条件后broker(消息服务器)保存交换器元数据,队列元数据,绑定元数据,vhost元数据和消息到磁盘,这些数据将broker异常时恢复所有配置信息及消息。下面是客户端发送消息的代码段:
public void sendMsg(){
ConnectionFactory factory=new ConnectionFactory();
factory.setUsername("remoteuser1");
factory.setPassword("remoteuser1");
factory.setVirtualHost("/");
factory.setHost("192.168.10.1");
factory.setPort(5672);
Connection conn=factory.newConnection();
Channel channel=conn.createChannel();
channel.exchangeDeclare("exchangeName1","direct",true);//声明可持久化交换器
channel.queueDeclare("queue1", true, false, false, null);//声明可持久化队列
channel.queueBind("queue1", "exchangeName1", "routingkey1");//绑定队列到交换器并指定路由键
channel.queueDeclare("queue2", false, false, false, null);//声明非持久化队列
channel.queueBind("queue2", "exchangeName1", "routingkey1");
/*
此处放置发送消息的代码,暂时不写
*/
channel.close();
conn.close();
}
执行代码,并使用如下命令在RabbitMQ节点查看队列当前的状态:
# rabbitmqctl list_queues name durable state messages_ready messages_ready_ram messages_ready_persistent
从上图可以看到队列queue1设置为持久化的,queue2设置为非持久化。现在添加如下发送消息的代码段:
public void sendMsg(){
/*
建立rabbitmq server连接
*/
byte[] messageBody="hello world".getBytes();
channel.basicPublish("exchangeName1", "routingkey1", null, messageBody);//使用默认的发送模式
/*
关闭连接
*/
}
再次执行上面查看队列状态的命令,结果如下图:
可以看到两个队列在内存中都有1条消息,但是持久化消息没有。使用如下命令手动重启RabbitMQ服务器,再次查看消息队列信息,会得到下图:
# systemctl restart rabbitmq-server.service
# rabbitmqctl list_queues name durable state messages_ready messages_ready_ram messages_ready_persistent
- 消息丢失是因为
channel.basicPublish
方法没有设置发送模式,默认使用非持久化模式。 - queue2消息是因为
channel.queueDeclare
方法把队列设置为非持久化模式。
现在修改发送消息的模式,实现消息持久化:
public void sendMsg(){
/*
建立rabbitmq server连接
*/
byte[] messageBody="hello world".getBytes();
channel.basicPublish("exchangeName1", "routingkey1",
new AMQP.BasicProperties.Builder()
.deliveryMode(2).build(),
messageBody);//设置发送模式为持久化
/*
关闭连接
*/
}
再次执行下面的命令查看队列消息当前状态,如图所示:
# rabbitmqctl list_queues name durable state messages_ready messages_ready_ram messages_ready_persistent
再次执行重启RabbitMQ服务并查看队列消息状态,如下图所示:
queue1
的消息可以正常恢复。
Persistence Layer
RabbitMQ内部持久化层由两部分组成:message store和queue index,如下图所示。
- message-store,用于存储消息体,消息头,相关属性。所有队列共享。
- queue-index,每个队列对应一个,用于存储队列中消息在message-store中的位置,是否已发送,是否已消费确认。默认消息小于4kb时直接存储到queue_index,否则存储到message store。
详情请参考http://www.rabbitmq.com/persistence-conf.html
Publisher Confirm
publisher confirm机制用于解决生产者与rabbitmq服务器之间消息可靠传输。它在消息服务器持久化消息后通知消息生产者发送成功。添加如下代码片段:
channel.confirmSelect();
channel.basicPublish(...);
boolean isSuccess=channel.waitForConfirmsOrDie();
在此channel上发送的消息都将执行confirm机制。waitForConfirmsOrDie()
将会阻塞进程,降低生产者的吞吐量。confirm机制的异步实现通过订阅ConfirmListener
来实现,消息服务器不保证发送消息的顺序与confirm的顺序相同。添加如下代码片段处理confirm回调,判断消息是否发送成功:
channel.addConfirmListener(new ConfirmListener(){
public void handleAck(long deliveryTag,boolean multiple){
//发送成功
System.out.println(String.format("message ack %s",deliveryTag));
}
public void handleNack(long deliveryTag,boolean multiple){
//发送失败
System.out.println(String.format("message nack %s",deliveryTag));
}
});
生产者也可以订阅returnListener用于判断消息状态,它会在confirmListener之前执行,代码片段如下:
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(replyText);
System.out.println(routingKey);
}
});
对于不同的消息类型,confirm机制有所不同:
- 非持久化消息,消息服务器接收到消息,立即返回confirm。
- 持久化消息,消息服务器接收到消息,并且持久化到磁盘后返回confirm。
通过publisher confirm机制保证生产者明确知道消息是否发送成功,但是并不能保证消息是否已路由到队列。rabbitmq没有消息追溯的功能,因此仍需要其它解决方案来追溯消息。
Consumer acknowledge
consumer acknowledge机制用于保证rabbitmq服务器与消费者之间消息可靠传输。代码片段如下:
//第2个参数表示取消自动确认
channel.basicConsume("queue1", false, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
String routingkey=envelope.getRoutingKey();
long deliveryTag=envelope.getDeliveryTag();
String value=new String(body);
System.out.println(value);
//通知broker消息被确认
channel.basicAck(deliveryTag, false);
}
});
channel.basicAck
通知broker删除deliveryTag
对应的消息。假如,消息队列中已经存在若干条消息,如下图:
可以看到queue1
中已有两条消息,已经持久化到磁盘,并且没有消费者。现在启动消费者,但是消费消息后没有调用basicAck
方法。运行后出现下图:
消费者已经消费消息,但是messages_unacknowledged
显示2条消息没有确认,queue1
中仍有两条消息。现在重启broker用于模拟服务器出现异常又恢复的场景,会出现如下图所示:
没有ack
的消息又被重新发送,而且ack
的消息一直存在内存中。下面修改消费者应用,消费消息后调用ack
,消息会被删除。如下图:
如果有多个队列订阅同一个消息,队列之间不会相互影响。如下图:
queue1
和queue2
订阅了同一个消息,queue1
正常消费消息并且ack,queue2
消费消息但是没有ack。
参考:[ http://www.rabbitmq.com/confirms.html#when]
Consumer prefetch
consumer prefetch机制使用channel
上未被消费确认的消息个数控制broker发送给消费者消息的速率,先看下图:
具体客户端实现是通过channel.basicQos
方法设置。如果不设置此值,broker会尽最大可能将队列中的消息全部发送到consumer,消息将会堆积到consumer内存中,broker中的队列将为空。如果此时再添加一个consumer,它将不会获取到任何消息。假如设置如下:
channel.basicQos(1);
它声明当前channel
上只能存在一个未确认的消息,从而导致大量消息堆积到队列中,consumer不能得到充分运行。在实际场景中,通过估算consumer处理消息的时间,broker与consumer之间通信时间,来确定qos
的值。
具体如何配置可以参考下面两篇文章:
consumer prefetch
Some queuing theory