RabbitMQ消息持久化总结

一、简介

在RabbitMQ中,如果遇到RabbitMQ服务停止或者挂掉,那么我们的消息将会出现丢失的情况,为了在RabbitMQ服务重启的情况下,不丢失消息,我们可以将Exchange(交换机)、Queue(队列)与Message(消息)都设置为可持久化的(durable)。这样的话,能够保证绝大部分的消息不会被丢失,但是还有有一些小概率会发生消息丢失的情况。下面通过一个简单的示例总结在RabbitMQ中如何进行消息持久化。

 

二、要点

消息持久化主要是将交换机、队列以及消息设置为durable = true(可持久化的),要点主要有三个:

  • a. 声明交换机Exchange的时候设置 durable=true;
//public TopicExchange(String name, boolean durable, boolean autoDelete)
public TopicExchange exchange() {
        return new TopicExchange(EXCHANGE_NAME,true,false);
    }
  • b. 声明队列Queue的时候设置 durable=true;
//public Queue(String name, boolean durable)
@Bean
    public Queue queue() {
        //durable:是否将队列持久化 true表示需要持久化 false表示不需要持久化
        return new Queue(QUEUE_NAME, false);
    }
  • c. 发送消息的时候设置消息的 deliveryMode = 2;
使用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服务,检查一下重启之后队列、消息是否还存在。

RabbitMQ消息持久化总结_第1张图片

通过控制台,可见只剩下一个持久化的队列durable_queue_name,并且队列里面的消息还存在,另外一个not_durable_queue_name已经丢失,说明在该队列上的消息也已经丢失。

同理,交换机也类似。

这时候放开之前注释的消费者代码块,重启项目,通过控制台可以看到消费者成功消费了之前持久化的三条消息,由此证明了在MQ重启之后,消费者可以继续消费之前的消息

RabbitMQ消息持久化总结_第2张图片

【g】关于默认持久化的说明

  • 我们使用new Queue()创建队列的时候,默认就是持久化的,即durable=true,下面是部分源码:

RabbitMQ消息持久化总结_第3张图片

  • 创建交换机的时候,也是默认持久化交换机的,下面是构造器源码:

RabbitMQ消息持久化总结_第4张图片

  • 使用convertAndSend方式发送消息,消息默认就是持久化的,下面是源码:
  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机情况。以上只是笔者学习的一些总结,希望能够对大家有所帮助,欢迎大家进行补充,一起学习。

你可能感兴趣的:(RabbitMQ)