在使用消息机制时,我们通常需要考虑以下几个问题:
- 消息不能丢失
- 保证消息一定能投递到目的地
- 保证业务处理和消息发送/消费的一致性
本文以RabbitMQ为例,讨论如何解决以上问题。
消息持久化
如果希望RabbitMQ重启之后消息不丢失,那么需要对以下3种实体均配置持久化:
- exchange
- queue
- message
声明exchange时设置持久化(durable = true
)并且不自动删除(autoDelete = false):
boolean durable = true;
boolean autoDelete = false;
channel.exchangeDeclare("dlx", TOPIC, durable, autoDelete, null)
声明queue时设置持久化(durable = true
)并且不自动删除(autoDelete = false):
boolean durable = true;
boolean autoDelete = false;
channel.queueDeclare("order-summary-queue", durable, false, autoDelete, queueArguments);
发送消息时通过设置deliveryMode=2
持久化消息:
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.contentType("application/json")
.deliveryMode(2)
.priority(0)
.build();
channel.basicPublish("order", "order.created", false, properties, "sample-data".getBytes())
发送确认
有时,业务处理成功,消息也发了,但是我们并不知道消息是否成功到达了rabbitmq,如果由于网络等原因导致业务成功而消息发送失败,那么发送方将出现不一致的问题,此时可以使用rabbitmq的发送确认功能,即要求rabbitmq显式告知我们消息是否已成功发送。
首先需要在channel上设置ConfirmListener
:
channel.addConfirmListener(new ConfirmListener() {
public void handleAck(long seqNo, boolean multiple) {
if (multiple) {
logger.info(seqNo + "号及其以前的所有消息发送成功,当消息发送成功后执行相应逻辑,比如标记事件为已发送或者删除原来事件");
} else {
logger.info(seqNo + "号发送成功,当消息发送成功后执行相应逻辑,比如标记事件为已发送或者删除原来事件");
}
}
public void handleNack(long seqNo, boolean multiple) {
if (multiple) {
logger.info(seqNo + "号及其以前的所有消息发送失败,当消息发送失败后执行相应逻辑,比如重试或者标记事件发送失败");
} else {
logger.info(seqNo + "号发送失败,当消息发送失败后执行相应逻辑,比如重试或者标记事件发送失败");
}
}
});
然后在发送消息直线需要开启发送确认模式:
//开启发送者确认
channel.confirmSelect();
然后发送消息:
channel.basicPublish("order", "order.created", false, properties, "sample-data".getBytes());
当消息正常投递时,rabbitmq客户端将异步调用handleAck()
表示消息已经成功投递,此时程序可以自行处理投递成功之后的逻辑,比如在数据库中将消息设置为已发送
。当消息投递出现异常时,handleNack()
将被调用。
通常来讲,发送端只需要保证消息能够发送到exchange即可,而无需关注消息是否被正确地投递到了某个queue,这个是rabbitmq和消息的接收方需要考虑的事情。基于此,如果rabbitmq找不到任何需要投递的queue,那么rabbitmq依然会ack给发送方,此时发送方可以认为消息已经正确投递,而不好用关系消息没有queue接收的问题。但是,对于rabbitmq而言,这种消息是需要记录下来的,否则rabbitmq将直接丢弃该消息。此时可以为exchange设置alternate-exchange
,即表示rabbitmq将把无法投递到任何queue的消息发送到alternate-exchange
指定的exchange中,通常来说可以设置一个死信交换(DLX)。
事实上,对于exchange存在但是却找不到任何接收queue时,如果发送是设置了mandatory=true
,那么在消息被ack前将return给客户端,此时客户端可以创建一个ReturnListener
用于接收返回的消息:
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
logger.warn("消息无法正确投递,已返回。");
}
});
但是需要注意的是,在return之后,消息依然会被ack而不是nack,还不如不设置madatory呢,因此return有时并不见得有用。
需要注意的是,在发送消息时如果exchange不存在&