最近的业务需要用到rabbitMQ,这里记录一下配置。本地使用Docker自己启动的rabbitMQ测试,线上使用的阿里的消息队列 for AMQP,完全兼容 RabbitMQ 开源生态,不用修改代码。

1.导入依赖包

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2. springboot配置

spring.rabbitmq.host = localhost
spring.rabbitmq.port = 5672
spring.rabbitmq.username = test
spring.rabbitmq.password = test
spring.rabbitmq.virtual-host= /
spring.rabbitmq.connection-timeout = 60s

这里直接使用了springboot的默认配置,没有自己写 ConnectionFactory。生产者和消费者都使用此配置。

 

3. 配置文件

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.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * rabbitmq配置
 * @author qiqi.zhao
 * @date 2019/9/11
 */
@Configuration
public class RabbitConfig {

    //durable = true开启持久化将消息持久化到磁盘
    @Bean
    public Queue testQueue() {
        return new Queue("test-queue",true);
    }

    @Bean
    TopicExchange testExchange() {
        return new TopicExchange("test-exchange", false, false);
    }

    @Bean
    Binding bindingStepsExchangeMessage(@Qualifier("testQueue") Queue testQueue, @Qualifier("testExchange") TopicExchange testExchange) {
        // 将队列burnQueue绑定到名为routingKey的routingKey
        return BindingBuilder.bind(testQueue).to(testExchange).with("*.test.*");
    }

    /**
     * 配置json转换,使rabbitMQ可以接收和发送json格式数据
     * @param connectionFactory
     * @return
     */
    @Bean("rabbitListenerContainerFactory")
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        return factory;
    }

}

这里配置了队列和话题模式的交换机,并把它们绑定,并配置了RabbitListenerContainerFactory用于发送和接收json格式数据。生产者需要配置以上所有的信息,消费者只需要配置RabbitListenerContainerFactory就可以了。

 

4. 生产者

@Slf4j
@Component
public class RabbitProducer<T> {
    private AmqpTemplate amqpTemplate;
    private Gson gson = new Gson();
    private MessageProperties properties = new MessageProperties();

    @Autowired
    public RabbitProducer(AmqpTemplate amqpTemplate) {
        this.amqpTemplate = amqpTemplate;
        properties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
    }

    public void sendData(T data, String routingKey) {
        log.info("Rabbit send:" + data.toString());
        //Topic 方式;
        String json = gson.toJson(data);
        Message msg = new Message(json.getBytes(), properties);
        amqpTemplate.convertAndSend("test-exchange", routingKey, msg);
    }
}

5.  消费者

@Slf4j
@Component
public class RabbitConsume {
    
    @RabbitHandler
    @RabbitListener(queues = "test-queue", containerFactory="rabbitListenerContainerFactory")
    public void process(@Payload Data data) {
        log.info("-----------------------------------");
        System.out.println(data.toString());
    }
    
}

6. 关于消息不丢失

这里摘录一下博主“一条路上的咸鱼”:关于MQ的几件小事(四)如何保证消息不丢失

6.1 丢失数据场景

丢数据一般分为两种,一种是mq把消息丢了,一种就是消费时将消息丢了。下面从rabbitmq和kafka分别说一下,丢失数据的场景,
A:生产者弄丢了数据 生产者将数据发送到rabbitmq的时候,可能在传输过程中因为网络等问题而将数据弄丢了。
B:rabbitmq自己丢了数据 如果没有开启rabbitmq的持久化,那么rabbitmq一旦重启,那么数据就丢了。所依必须开启持久化将消息持久化到磁盘,这样就算rabbitmq挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢失。除非极其罕见的情况,rabbitmq还没来得及持久化自己就挂了,这样可能导致一部分数据丢失。
C:消费端弄丢了数据 主要是因为消费者消费时,刚消费到,还没有处理,结果消费者就挂了,这样你重启之后,rabbitmq就认为你已经消费过了,然后就丢了数据。

6.2 如何防止消息丢失

A:生产者丢失消息
①:可以选择使用rabbitmq提供是事物功能,就是生产者在发送数据之前开启事物,然后发送消息,如果消息没有成功被rabbitmq接收到,那么生产者会受到异常报错,这时就可以回滚事物,然后尝试重新发送;如果收到了消息,那么就可以提交事物。

channel.txSelect();//开启事物
  try{
      //发送消息
  }catch(Exection e){
      channel.txRollback();//回滚事物
      //重新提交
  }
复制代码

缺点: rabbitmq事物已开启,就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,太耗性能会造成吞吐量的下降。

②:可以开启confirm模式。在生产者哪里设置开启了confirm模式之后,每次写的消息都会分配一个唯一的id,然后如何写入了rabbitmq之中,rabbitmq会给你回传一个ack消息,告诉你这个消息发送OK了;如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息失败了,你可以进行重试。而且你可以结合这个机制知道自己在内存里维护每个消息的id,如果超过一定时间还没接收到这个消息的回调,那么你可以进行重发。

//开启confirm
    channel.confirm();
    //发送成功回调
    public void ack(String messageId){
      
    }

    // 发送失败回调
    public void nack(String messageId){
        //重发该消息
    }
复制代码

二者不同 事务机制是同步的,你提交了一个事物之后会阻塞住,但是confirm机制是异步的,发送消息之后可以接着发送下一个消息,然后rabbitmq会回调告知成功与否。 一般在生产者这块避免丢失,都是用confirm机制。
B:rabbitmq自己弄丢了数据 设置消息持久化到磁盘。设置持久化有两个步骤:
①创建queue的时候将其设置为持久化的,这样就可以保证rabbitmq持久化queue的元数据,但是不会持久化queue里面的数据。
②发送消息的时候讲消息的deliveryMode设置为2,这样消息就会被设为持久化方式,此时rabbitmq就会将消息持久化到磁盘上。 必须要同时开启这两个才可以。

而且持久化可以跟生产的confirm机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前rabbitmq挂了,数据丢了,生产者收不到ack回调也会进行消息重发。
C:消费者弄丢了数据 使用rabbitmq提供的ack机制,首先关闭rabbitmq的自动ack,然后每次在确保处理完这个消息之后,在代码里手动调用ack。这样就可以避免消息还没有处理完就ack。

7. 如何设置消息过期时间

请看
摘录:

1.设置队列中所有消息过期时间

@Bean
public Queue msgQueue() {
    // 设置队列消息过期时间
    Map<String, Object> arguments = new HashMap<>();
    arguments.put("x-message-ttl", 5000);
    return new Queue(msgQueue, true, false, false, arguments);
}

2.在发送时为每条消息设置

String content = "test ttl message";
this.rabbitTemplate.convertAndSend(exchange, routingKey, content, message -> {
    MessageProperties messageProperties = message.getMessageProperties();
    // 设置这条消息的过期时间
    messageProperties.setExpiration("5000");
    return message;
});