最近项目中用到RabbitMQ,用到消息中间件,消息丢失,消息重复消息是必须需要面对和解决的。

因为项目需要动态创建交换机,队列。在条件未知的情况下,无法使用SpringCloudStream。

通过参考文档,博客,采用了RabbitTemplate,RabbitAdmin 提供的方法进行配置。

 

首先我们要明确,如果才能确保消息的可靠:

1.交换机,队列和消息都要持久化

2.消息失败重试

3.消息消费确认

 

一、持久化

持久化Exchange(交换机),RabbitAdmin实现是对Channel的操作

1.使用RabbitAdmin进行持久化

public void createTopicExchange(String exchangeName){

   //队列名称,durable(服务器重启后继续存在),自动删除(服务器不使用时自动删除交换器)

   TopicExchange topicExchange = new TopicExchange(exchangeName);//默认创建持久存在,不自动删除的交换器

   try{

      rabbitAdmin.declareExchange(topicExchange);

   }catch (Exception e){

      logger.error("createTopicExchange error",e);

   }

}

2.使用channel创建

//示例

channel.exchangeDeclare(exchange, "topic", true, false, null);


//源码接口

/**

* Declare an exchange.

* @see com.rabbitmq.client.AMQP.Exchange.Declare

* @see com.rabbitmq.client.AMQP.Exchange.DeclareOk

* @param exchange the name of the exchange  交换机名称

* @param type the exchange type 交换机类型

* @param durable 是否持久化(如果为true,服务器重启依旧存在)

* @param autoDelete 是否自动删除(如果为true自动删除)

* @param arguments 其他属性

* @return a declaration-confirm method to indicate the exchange was successfully declared

* @throws java.io.IOException if an error is encountered

*/

Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete,

                                   Map<String, Object> arguments) throws IOException;

 

 

持久化Queue

1.使用RabbitAdmin

public void createQueue(String queueName){

   //队列是持久的,非独占的和非自动删除

   //exclusive - 如果我们声明一个独占队列,则为true(该队列仅由声明者的连接使用)

   Queue queue = new Queue(queueName);

   rabbitAdmin.declareQueue(queue);

}

2.直接使用channel

//队列名称,是否持久化,是否独占,是否自动删除,队列其他属性

channel.queueDeclare(queue, true, false, false, null);

 

持久化消息

RabbitTemplate默认消息为持久化消息(默认2,1非持久化消息)

spring.jms.template.delivery-mode=2

 

交换机队列绑定

1.rabbitAdmin方式

/**

* 队列交换机绑定

* @param queueName 队列名称

* @param topicExchangeName 交换机名称

* @param routingKey 路由 topic.#(多级通配) topic.*(一级通配) topic.message(固定路由)

*/

public void bind(String queueName,String topicExchangeName,String routingKey){

   Binding binding = BindingBuilder

         .bind(new Queue(queueName))

         .to(new TopicExchange(topicExchangeName))

         .with(routingKey);

   rabbitAdmin.declareBinding(binding);

}

2.channel方式

channel.queueBind(queue, exchange, routingKey);

 

 

二、消息失败重试

当消息无法路由到队列时,确认消息路由失败。消息成功路由时,当需要发送的队列都发送成功后,进行确认消息,对于持久化队列意味着写入磁盘,对于镜像队列意味着所有镜像接收成功。

1.配置publisherConfirms 和publisherReturns 为true

#启用消息发送成功确认,就是确认消息是否到达交换器中

spring.rabbitmq.publisher-confirms=true

#启用消息失败返回,比如路由不到队列时触发回调

spring.rabbitmq.publisher-returns=true

使用rabbitTemplate回调(可以通过correlationData 关联对象,将对象加入队列,轮询队列,发送成功则,从队列中移除)

//correlationData 绑定消息发送的对象,可以通过这个对象进行  消息重试

//ack 是否发送成功

//cause 失败原因

rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {



});



//message(消息体) replyCode(回复编码)  replyText(回复文本)  exchange(交换器名称) routingKey(路由key)

rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {



});

 

三、消息消费确认

采用手动Ack

1.配置手动ack,可以在配置文件或RabbitListenerContainerFactory中配置

spring.rabbitmq.listener.simple.acknowledge-mode=manual

 

消息确认方法

/**

* 确认消息

* deliveryTag 消息标签

* multiple 是否确认多个

*/

void basicAck(long deliveryTag, boolean multiple) throws IOException;





/**

* 拒绝多个消息

* deliveryTag 消息标签

* multiple 是否拒绝多个 

* requeue 是否重新入队列

*/

void basicNack(long deliveryTag, boolean multiple, boolean requeue)

        throws IOException;





/**

* 拒绝单个消息

* deliveryTag 消息标签

* requeue 是否重新入队列

*/

void basicReject(long deliveryTag, boolean requeue) throws IOException;

 

如有理解不当的地方,欢迎大家指正。

参考: 消息监听配置 、消息确认、 大神博客Spring-Rabbit相关操作API