1. 生产者消息可靠投递
在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两个选项用来控制消息的投递可靠性模式。
RabbitMQ整个消息投递的路径为:
producer->rabbitmq broker cluster->exchange->queue->consumer
message 从 producer 到 rabbitmq broker cluster 则会返回一个 confirmCallback 。
message 从 exchange->queue 投递失败则会返回一个 returnCallback 。我们将利用这两个 callback 控制消息的最终一致性和部分纠错能力。
1.1 confirmCallback 确认模式
在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启 confirmcallback 。
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setPublisherConfirms(true);//开启confirm模式
RabbitTemplate rabbitTemplate = new RabbitTemplate(factory);
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
if (!ack) {
log.error("消息发送失败!" + cause + data.toString());
} else {
log.info("消息发送成功,消息ID:" + (data != null ? data.getId() : null));
}
});
我们来看下 ConfirmCallback 接口
public interface ConfirmCallback {
/**
* Confirmation callback.
* @param correlationData correlation data for the callback.
* @param ack true for ack, false for nack
* @param cause An optional cause, for nack, when available, otherwise null.
*/
void confirm(CorrelationData correlationData, boolean ack, String cause);
}
重点是 CorrelationData 对象,每个发送的消息都需要配备一个 CorrelationData 相关数据对象,CorrelationData 对象内部只有一个 id 属性,用来表示当前消息唯一性。
发送的时候创建一个 CorrelationData 对象。
User user = new User();
user.setID(1010101L);
user.setUserName("plen");
rabbitTemplate.convertAndSend(exchange, routing, user,
message -> {
message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);
return message;
},
new CorrelationData(user.getID().toString()));
这里将 user ID 设置为当前消息 CorrelationData id 。当然这里是纯粹 demo,真实场景是需要做业务无关消息 ID 生成,同时要记录下这个 id 用来纠错和对账。
消息只要被 rabbitmq broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有 broker 接收到才会调用 confirmCallback。
被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里。所以需要用到接下来的 returnCallback 。
1.2 returnCallback 未投递到queue退回模式
confirm 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到 return 退回模式。
同样创建 ConnectionFactory 到时候需要设置 PublisherReturns(true) 选项。
CachingConnectionFactory factory = new CachingConnectionFactory();
factory.setPublisherReturns(true);//开启return模式
rabbitTemplate.setMandatory(true);//开启强制委托模式
rabbitTemplate.setReturnCallback((message, replyCode, replyText,
exchange, routingKey) ->
log.info(MessageFormat.format("消息发送ReturnCallback:{0},{1},{2},{3},{4},{5}", message, replyCode, replyText, exchange, routingKey)));
这样如果未能投递到目标 queue 里将调用 returnCallback ,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据。
1.3 shovel-plugin 跨机房可靠投递
RabbitMQ 在跨机房集成提供了一个不错的插件 shovel 。使用 shovel-plugin 插件非常方便,shovel 可以接受机房之间的网络断开、机器下线等不稳定因素。
这里有两个 broker :
10.211.55.3 rabbit_node1
10.211.55.4 rabbit_node2
我们希望将发送给 rabbit_node1 plen.queue 的消息传输到 rabbit_node2 plen.queue 中。我们先开启 rabbit_node1 的 shovel-plugin。
先看下当前 RabbitMQ 版本是否安装了 shovel-plugin,如果有的话直接开启。
rabbitmq-plugins list
rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management
然后就可以在 Admin 面板里看到这个设置选项,怎么设置这里就不介绍了。主要就是配置下 amqp 协议地址,amqp://user:password@server-name/my-vhost 。
如果配置没有问题的话,应该是这样的一个状态,说明已经顺利连接到 rabbit_node2 broker 。
RabbitMQ shovel-plugin 插件在 rabbit_node1 broker 创建了两个 tcp 连接,端口 39544 连接是用来消费 plen.queue 里的消息,端口 55706 连接是用来推送消息给 rabbit_node2 。
我们来看下 rabbit_node1 tcp 连接状态:
tcp6 0 0 10.211.55.3:5672 10.211.55.3:39544 ESTABLISHED
tcp 0 0 10.211.55.3:55706 10.211.55.4:5672 ESTABLISHED
rabbit_node2 tcp 连接状态:
tcp6 0 0 10.211.55.4:5672 10.211.55.3:55706 ESTABLISHED
2. RabbitMQ数据持久化
为了防止到达queue后的消息丢失,需要RabbitMQ开启数据持久化功能。
设置持久化有两个步骤:
- 创建queue时将其设置为持久化,这样就可以保证RabbitMQ持久化queue的元数据(但不会持久化queue的消息)。
- 生产者发送消息时,将消息的deliveryMode设置为2(这样RabbitMQ会将消息持久化到磁盘)。
必须同时设置这两个持久化配置。配置后,只有消息被持久化到磁盘,才会返回ack应答给生产者。结合生产者的confirm机制,就可以确保消息的可靠投递。
3. 消费者消息可靠消费
3.1 RabbitMQ消息状态
在Messages中我们可以看到数据有两种状态:Ready和Unacked
-
Ready:按字面意思就是准备好了,可以投递给消费者了,对于未开启持久化的消息写入内存即为Ready状态;如果开启持久化了,则要持久化到磁盘之后才会变成Ready状态。
-
Unacked(Unacknowledged——未确认的):表示已经投递到消费者但是还没有收到消费者Ack确认时的消息状态。
3.2 消费者ACK机制与重回队列
为了保证消息从队列可靠地达到消费者, RabbitMQ 提供了消息确认机制( message acknowledgement,简称Ack) 。我们上面的例子中其实已经用到了这点,通过channel.basicConsume(String queue, boolean autoAck, Consumer callback) 订阅消费队列上的消息时,第二个参数autoAck表示是否自动确认。
- 当autoAck 设为true 时, RabbitMQ 会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除;
- 当autoAck 设为false时, RabbitMQ 会等待消费者通过 basicAck(long deliveryTag, boolean multiple)方法,显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。
当autoAck 参数置为false ,对于RabbitMQ 服务端而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息,即上面介绍的Ready状态;一部分是己经投递给消费者,但是还没有收到消费者确认信号的消息,即Unacked状态。如果RabbitMQ 一直没有收到消费者的确认信号,并且消费此消息的消费者己经断开连接,则RabbitMQ 会安排该消息重新进入队列,等待投递给下一个消费者。可以通过消费者休眠,把消费者关掉然后再启动即可验证。
消费者确认:basicAck(long deliveryTag, boolean multiple),其中deliveryTag 可以看作消息的编号,它是一个64 位的长整型值;multiple一般设为false,如果设为true则表示确认当前deliveryTag 编号及之前所有未被当前消费者确认的消息。
消费者拒绝:basicNack(long deliveryTag, boolean multiple, boolean requeue),其中deliveryTag 可以看作消息的编号,它是一个64 位的长整型值。multiple一般设为false,如果设为true则表示拒绝当前deliveryTag 编号及之前所有未被当前消费者确认的消息。requeue参数表示是否重回队列,如果requeue 参数设置为true ,则RabbitMQ 会重新将这条消息存入队列尾部(注意是队列尾部),等待继续投递给订阅该队列的消费者,当然也可能是自己;如果requeue 参数设置为false ,则RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。
3.3 消费者业务幂等
消费者业务一定要做到幂等性,解决消息重复投递,被重复处理的问题。
4. 死信队列
4.1 死信队列与私信概述
DLX,Dead Letter Exchange 的缩写,又被称为死信邮箱、死信交换机.DLX就是一个普通的交换机,和一般的交换机没有任何区别。
当一个消息在一个队列中变成死信(dead message)时,通过这个交换机将死信发送到死信队列中(指定好相关参数,RabbitMQ会自动发送)。
4.2 什么样的消息会变成死信
-
消息被拒绝(basic.reject或basic.nack)并且requeue=false;
-
消息TTL过期;
-
队列达到最大长度(队列满了,无法再添加数据到mq中)
4.3 死信交换机应用场景
在定义业务队列的时候,可以考虑指定一个死信交换机,并绑定一个死信队列,当消息变成死信时,该消息就会被发送到该死信队列上,这样就方便我们查看消息失败的原因。
4.4 如何使用死信交换机
定义业务(普通)队列的时候指定参数:
- x-dead-letter-exchange: 用来设置死信后发送的交换机
- x-dead-letter-routing-key: 用来设置死信的routingKey
@Bean
public Queue helloQueue() {
//将普通队列绑定到私信交换机上
Map<String, Object> args = new HashMap<>(2);
args.put(DEAD_LETTER_EXCHANGE_KEY, deadExchangeName);
args.put(DEAD_LETTER_ROUTING_KEY, deadRoutingKey);
Queue queue = new Queue(queueName, true, false, false, args);
return queue;
}