最近项目中用到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