在RabbitMQ中,如果遇到RabbitMQ服务停止或者挂掉,那么我们的消息将会出现丢失的情况,为了在RabbitMQ服务重启的情况下,不丢失消息,我们可以将Exchange(交换机)、Queue(队列)与Message(消息)都设置为可持久化的(durable)。这样的话,能够保证绝大部分的消息不会被丢失,但是还有有一些小概率会发生消息丢失的情况。下面通过一个简单的示例总结在RabbitMQ中如何进行消息持久化。
消息持久化主要是将交换机、队列以及消息设置为durable = true(可持久化的),要点主要有三个:
//public TopicExchange(String name, boolean durable, boolean autoDelete)
public TopicExchange exchange() {
return new TopicExchange(EXCHANGE_NAME,true,false);
}
//public Queue(String name, boolean durable)
@Bean
public Queue queue() {
//durable:是否将队列持久化 true表示需要持久化 false表示不需要持久化
return new Queue(QUEUE_NAME, false);
}
使用convertAndSend方式发送消息,消息默认就是持久化的.
new MessageProperties() --> DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT --> deliveryMode = 2;
这里我们声明两种队列,一个是持久化的,一个是非持久化的,我们测试在RabbitMQ服务重启的情况下,未被消费的消息是否还存在,消费者重启之后能否重新消费之前的消息。在RabbitMQ中,通过管理员方式(必须是管理员方式,否则可能会报错)运行CMD命令行执行下面的命令:
net stop RabbitMQ && net start RabbitMQ
【a】RabbitMQ配置类: RabbitMQ配置信息,绑定交换器、队列、路由键设置
package com.wsh.springboot.springbooy_rabbitmq_message_persistence.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* @Description: RabbitMQ配置信息,绑定交换器、队列、路由键设置
* @author: weishihuai
* @Date: 2019/6/30 15:38
*
* 如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,可以将交换机、队列、消息都进行持久化,这样可以保证绝大部分情况下消息不会丢失。
* 但还是会有小概率发生消息丢失的情况(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),
* 如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。(transaction/confirm机制)
*
*
* 说明:
* 1. 队列持久化:需要在声明队列的时候设置durable=true,如果只对队列进行持久化,那么mq重启之后队列里面的消息不会保存
* 如果需要队列里面的消息也保存下来,那么还需要对消息进行持久化;
*
* 2. 消息持久化:设置消息的deliveryMode = 2,消费者重启之后还能够继续消费持久化之后的消息;
* 使用convertAndSend方式发送消息,消息默认就是持久化的,下面是源码:
* new MessageProperties() --> DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT --> deliveryMode = 2;
*
* 3.重启mq: CMD命令行下执行 net stop RabbitMQ && net start RabbitMQ
*/
@Component
public class RabbitMQConfig {
private static final String DURABLE_QUEUE_NAME = "durable_queue_name";
private static final String DURABLE_EXCHANGE_NAME = "durable_exchange_name";
private static final String ROUTING_KEY = "user.#";
private static final String QUEUE_NAME = "not_durable_queue_name";
private static final String EXCHANGE_NAME = "not_durable_exchange_name";
@Bean
public Queue durableQueue() {
// public Queue(String name) {
// this(name, true, false, false);
// }
// public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete)
//不指定durable的话默认好像也是true
//public Queue(String name, boolean durable)
//durable:是否将队列持久化 true表示需要持久化 false表示不需要持久化
return new Queue(DURABLE_QUEUE_NAME, true);
}
@Bean
public TopicExchange durableExchange() {
// public AbstractExchange(String name) {
// this(name, true, false);
// }
// public AbstractExchange(String name, boolean durable, boolean autoDelete) {
// this(name, durable, autoDelete, (Map)null);
// }
//声明交换机的时候默认也是持久化的
return new TopicExchange(DURABLE_EXCHANGE_NAME);
}
@Bean
public Binding durableBinding() {
//如果exchange和queue都是持久化的,那么它们之间的binding也是持久化的。如果exchange和queue两者之间有一个持久化,一个非持久化,就不允许建立绑定
return BindingBuilder.bind(durableQueue()).to(durableExchange()).with(ROUTING_KEY);
}
@Bean
public Queue queue() {
//public Queue(String name, boolean durable)
//durable:是否将队列持久化 true表示需要持久化 false表示不需要持久化
return new Queue(QUEUE_NAME, false);
}
@Bean
public TopicExchange exchange() {
return new TopicExchange(EXCHANGE_NAME,true,false);
}
@Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(exchange()).with(ROUTING_KEY);
}
}
比较主要的点 都在代码中写了注释,这里就不用过多说明。
【b】生产者:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @Description: 生产者
* @author: weishihuai
* @Date: 2019/6/30 15:38
*/
@Component
public class Producer {
private static final Logger logger = LoggerFactory.getLogger(Producer.class);
private static final String DURABLE_EXCHANGE_NAME = "durable_exchange_name";
private static final String ROUTING_KEY = "user.add";
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendMessage() {
for (int i = 1; i <= 3; i++) {
String message = "消息" + i;
logger.info("【生产者】发送消息:" + message);
rabbitTemplate.convertAndSend(DURABLE_EXCHANGE_NAME, ROUTING_KEY, message, new CorrelationData(UUID.randomUUID().toString()));
}
}
}
【c】消费者:
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @Description: 消费者
* @author: weishihuai
* @Date: 2019/6/30 15:38
*/
@Component
public class Consumer {
private static final Logger logger = LoggerFactory.getLogger(Consumer.class);
@RabbitListener(queues = "durable_queue_name")
public void receiveMessage(String msg, Message message, Channel channel) {
try {
logger.info("【Consumer receiveMessage】接收到消息为:[{}]", msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
logger.info("【Consumer receiveMessage】接收消息后的处理发生异常", e);
}
}
}
【d】应用配置文件
server:
port: 9999
spring:
application:
name: mq-message-persistence
rabbitmq:
host: 127.0.0.1
virtual-host: /vhost
username: wsh
password: wsh
port: 5672
connection-timeout: 10000
【e】测试用例
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbooyRabbitmqMessagePersistenceApplicationTests {
@Autowired
private Producer producer;
@Test
public void contextLoads() {
producer.sendMessage();
}
}
【f】运行结果
为了测试在MQ重启之后,消费者能够消费持久化之后的消息,这里可以先将消费者监听队列暂时注释掉,让生产者发送三条消息,但是没有消费者去消费,这样MQ重启之后,队列中还是存在三条消息。
启动项目,通过管理控制台可以看到成功创建两个交换机以及两个队列。
这时候,我们重启RabbitMQ服务,检查一下重启之后队列、消息是否还存在。
通过控制台,可见只剩下一个持久化的队列durable_queue_name,并且队列里面的消息还存在,另外一个not_durable_queue_name已经丢失,说明在该队列上的消息也已经丢失。
同理,交换机也类似。
这时候放开之前注释的消费者代码块,重启项目,通过控制台可以看到消费者成功消费了之前持久化的三条消息,由此证明了在MQ重启之后,消费者可以继续消费之前的消息
【g】关于默认持久化的说明
public void convertAndSend(String exchange, String routingKey, Object object, CorrelationData correlationData) throws AmqpException {
this.send(exchange, routingKey, this.convertMessageIfNecessary(object), correlationData);
}
protected Message convertMessageIfNecessary(Object object) {
return object instanceof Message ? (Message)object : this.getRequiredMessageConverter().toMessage(object, new MessageProperties());
}
public MessageProperties() {
this.deliveryMode = DEFAULT_DELIVERY_MODE;
this.priority = DEFAULT_PRIORITY;
}
public static final MessageDeliveryMode DEFAULT_DELIVERY_MODE;
static {
DEFAULT_DELIVERY_MODE = MessageDeliveryMode.PERSISTENT;
DEFAULT_PRIORITY = 0;
}
public static int toInt(MessageDeliveryMode mode) {
switch(mode) {
case NON_PERSISTENT:
return 1;
case PERSISTENT:
return 2;
default:
return -1;
}
}
以上就是关于在RabbitMQ中如何实现消息和队列的持久化,虽然不能说百分之百保证消息不会丢失,但是能够保证绝大部分不会丢失。在实际项目中,通常需要对消息进行持久化,因为不可能保证服务器永远不会出现down机情况。以上只是笔者学习的一些总结,希望能够对大家有所帮助,欢迎大家进行补充,一起学习。