上篇文章主要介绍Producer的mandatory参数,备份队列和TTL的内容,这篇文章讲继续介绍Producer端的开发,主要包括发布方确认和事务机制。
消息持久化机制可以保证应服务器出现异常导致消息丢失的问题,但是Producer将消息发送出去,并不知道消息是否正确到达服务端并持久化。如果在到达服务端前,或者到达服务端未持久化到磁盘,消息就丢失,那么问题仍然存在。如下图,第一步或者第二步出现问题,消息依然会丢失。
RabbitMQ为解决这个问题,提供了发布方确认(Publisher Confirm)机制。
生产者将Channel设置成confirm模式,所有在改channel上发布的消息都会被指派一个唯一的ID(序号从1开始),消息被路由到队列之后,RabbitMQ就会发送一个确认(ack,包含此消息的ID)给生产者,这样生产者就知道消息已经正确发送到RabbitMQ了,如果消息是持久化,RabbitMQ会等到消息落盘再回复。RabbitMQ回复的消息包含了deliveryTag,表示确认消息的序号,还包含multiple,表示到这个消息序号之前所有的消息都已得到处理了。
下面通过代码演示发布方确认的使用。
channel.confirmSelect();
String message = "test ack";
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
// 如果没有调用comfirmSelect方法开启,直接调用waitFormConfirms
// 会报java.lang.IllegalStateException
channel.waitForConfirms();
此方式也是串行同步等待的方式,生产者发送消息之后,会被阻塞,直到RabbitMQ接收到消息返回(对于持久化消息,等待消息落盘后才返回),生产者接收到ack才会下一条消息的处理,这显然会有性能问题。
解决方案有两种:
批量发送
批量发送方法,客户端程序需要定量或者定时调用waitFormConfirms方法等待RabbitMQ的确认返回,相对于上面的方式,性能有极大的提升。
下面是批量发送的代码实现。
channel.confirmSelect();
int MsgCount = 0;
while (true) {
channel.basicPublish("exchange", "routingKey", null, "batch confirm test".getBytes());
//将发送出去的消息存入缓存中,缓存可以是一个ArrayList 或者BlockingQueue 之类的
if (++MsgCount >= BATCH_COUNT) {
MsgCount = 0;
try {
if (channel.waitForConfirms()) {
//将缓存中的消息清空
}
//将缓存中的消息重新发送
} catch (InterruptedException e) {
e.printStackTrace();
//将缓存中的消息重新发送
}
}
}
但也有问题,当同一批次中出现消息被nack或者超时,需要客户端程序处理并重试,这有可能导致消息重复。
异步发送
异步方案是推荐使用的方式,它的优点初了性能优良之外,还有只需要在回调方法nack中处理没有被RabbitMQ成功处理的消息,SpringAMQP中也是这种方案。
实现的时候,只需要添加ConfirmListener接口的实现,它主要有两个方法:handleAck和handleNack。下面是代码实现,是SpringAMQP的精简版(关于SpringAMQP的细节,小伙伴们可以关注RabbitMQ系列文章后续的更新)。
// 维护消息序号和消息,在回调函数中做相应处理
// SpringAMQP也是这种方案,只不过这里简化了
ConcurrentSkipListMap unconfirmMap = new ConcurrentSkipListMap<>();
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long deliveryTag , boolean multiple) throws IOException {
System.out.println("Nack, SeqNo : " + deliveryTag + ", multiple : " + multiple);
if (multiple) {
confirmMap.headMap(deliveryTag - 1).clear();
} else {
confirmMap.remove(deliveryTag);
}
}
// 处理发送失败的场景,尝试重发
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("handleNack : " + deliveryTag + " " + multiple);
// 注意:为防止消息一直失败,导致死循环,可以在消息上加属性x-retries,每次重发前,先判断已经发送的次数,达到阈值,不再发送
if(multiple){
ConcurrentNavigableMap headMap = unconfirmMap.headMap(deliveryTag + 1);
Set> entrySet = headMap.entrySet();
Iterator> iterator = entrySet.iterator();
while(iterator.hasNext()){
String removed = iterator.next().getValue();
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, removed.getBytes());
}
} else {
String removed = unconfirmMap.remove(deliveryTag);
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, removed.getBytes());
}
}
});
//模拟一直发送消息
while (true) {
long nextSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish(exchangeName, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes ());
confirmSet.add(nextSeqNo);
}
事务机制是解决发送端无法感知消息是否正确达到服务端的另外一种方案。事务的使用非常简单,先直接上代码感受下。
String message = "tx message";
try{
channel.txSelect();
channel.basicPublish(exchange, routingKey, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
channel.txCommit();
} catch(Exception e){
e.printStackTrace();
channel.txRollback();
}
使用txSelect方法开启事务,只有消息成功被Rabbit接收,事务才会提交,如果发生任何异常,消息都会被回滚。
使用事务的缺点就是性能问题,因为发送一条消息之后,会阻塞发送端,直到Rabbit把消息持久化到磁盘,才会返回响应给发送端,之后发送端才能继续发送下一条。所以推荐使用Publisher Confirm方案。
好了,以上就是基于AMQP 0-9-1 协议,关于Producer的常用API使用第二部分的分享。
RabbitMQ系列文章会陆续更新,欢迎各位小伙伴关注后面的技术分享。