RabbitMQ基础入门学习

716 阅读15分钟

1、RabbitMQ概述

  • MQ全称为Message Queue,即消息队列。它也是一个队列,遵循FIFO原则。RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue Protocol高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。

2、RabbitMQ使用

2.1、RabbitMQ使用场景

  • 解耦:应用程序解耦,MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦,使当一方系统无法访问时不丢失消息。
  • 异步:任务异步处理,将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理,提高了应用程序的响应时间。
  • 削峰:秒杀活动

2.2、RabbitMQ优缺点

  • 优点
    • 优点就是以上的那些场景应用,就是在特殊场景下有其对应的好处,解耦异步削峰
  • 缺点
    • 系统的可用性降低,系统引入的外部依赖越多,系统越容易挂掉,本来只是A系统调用BCD三个系统接口就好,ABCD四个系统不报错整个系统会正常运行。引入了MQ之后,虽然ABCD系统没出错,但MQ挂了以后,整个系统也会崩溃。
    • 系统的复杂性提高,引入了MQ之后,需要考虑的问题也变得多了,如何保证消息没有重复消费?如何保证消息不丢失?怎么保证消息传递的顺序?
    • 一致性问题,A系统发送完消息直接返回成功,但是BCD系统之中若有系统写库失败,则会产生数据不一致的问题。

3、RabbitMQ工作原理

3.1、组件介绍

  • Producer:消息生产者,即生产方客户端,将消息发送至MQ
  • Broker(RabbitMQ):消息队列服务进程(包括Exchange和Queue)
  • Exchange:消息队列交换机,按一定规则将消息路由转发到某个队列,对消息进行过滤
  • Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方
  • Consumer:消息消费者,即消费方客户端,接收MQ转发的消息

3.2、发送消息流程

  • 生产者与Broker建立TCP连接
  • 生产者与Broker建立通道
  • 生产者通过通道将消息发送给Broker,再由Exchange将消息进行转发
  • Exchange将消息转发至指定的Queue

3.3、接收消息流程

  • 消费者与Broker建立TCP连接
  • 消费者与Broker建立通道
  • 消费者监听指定Queue
  • 当有消息到达Queue时,Broker默认将消息推送给消费者
  • 消费者接收到消息

4、RabbitMQ常见模型

4.1、HelloWorld基本消息模型

4.1.1、工作模式

  • 一个生产者对应一个消费者

4.1.2、引入依赖包

<!-- https://mvnrepository.com/artifact/com.rabbitmq/amqp-client -->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <!--和springboot2.0.5对应-->
    <version>5.4.1</version>
</dependency>

4.1.3、建立连接工具类

package cn.gui.util;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class ConnectionUtil {
    /**
     * 建立与RabbitMQ的连接
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        //定义连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        //设置服务地址
        factory.setHost("127.0.0.1");
        //端口
        factory.setPort(5672);
        //设置账号信息,用户名、密码、vhost
        factory.setVirtualHost("/");
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 通过工程获取连接
        Connection connection = factory.newConnection();
        return connection;
    }
}

4.1.4、生产者

  • 建立连接
  • 建立通道
  • 创建队列
  • 发送消息
  • 关闭资源
package cn.gui._01HelloWorld;

import cn.gui.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Sender {

    // 队列名称
    public static final String QUEUE_NAME_HELLO = "queue_name_hello";

    public static void main(String[] args) throws Exception {
        // 建立连接
        Connection connection = ConnectionUtil.getConnection();
        // 建立通道
        Channel channel = connection.createChannel();
        /*
            创建队列
                String queue:队列名称
                boolean durable:声明是否为持久队列,true为持久队列
                boolean exclusive:声明该队列是否独占此连接
                boolean autoDelete:声明该队列用完后是否自动删除
                Map<String, Object> arguments:队列的其它属性
         */
        channel.queueDeclare(QUEUE_NAME_HELLO, true, false, false, null);
        /*
            发送消息
                String exchange:要发送消息的交换机,""代表默认交换机
                String routingKey:路由键,相当于队列的名称
                BasicProperties props:路由的其它属性
                byte[] body:消息体,要发送的消息
         */
        channel.basicPublish("", QUEUE_NAME_HELLO, null, "这是一条消息".getBytes());
        // 关闭资源
        channel.close();
        connection.close();
        System.out.println("消息发送成功");
    }
}

4.1.5、消费者

  • 建立连接
  • 建立通道
  • 监听队列
  • 接收消息
package cn.gui._01HelloWorld;

import cn.gui.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Recipient {

    public static void main(String[] args) throws Exception {
        // 建立连接
        Connection connection = ConnectionUtil.getConnection();
        // 建立通道
        Channel channel = connection.createChannel();
        // 接收消息
        DefaultConsumer callback = new DefaultConsumer(channel){
            /*
                重写处理接收消息的方法
                    String consumerTag:消费者标签
                    Envelope envelope:信封,包括发送消息交换机名,路由键(通道名),交货标签(类似接收的id)
                    AMQP.BasicProperties properties:消息头数据
                    byte[] body:接收的消息正文
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("开始接收消息");
                System.out.println("消息正文:"+new String(body));
                System.out.println("消费者标签:"+consumerTag);
                System.out.println("交货标签:"+envelope.getDeliveryTag());
                System.out.println("交换机:"+envelope.getExchange());
                System.out.println("路由键:"+envelope.getRoutingKey());
                System.out.println(properties);
                System.out.println("接收消息完成");
                /*
                    手动签收消息
                        long deliveryTag:交货标签
                        boolean multiple:是否批量签收消息
                 */
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        /*
            监听队列
                String queue:监听队列名
                boolean autoAck:是否自动签收消息,一般不自动签收消息
                Consumer callback:签收成功执行的方法
         */
        channel.basicConsume(Sender.QUEUE_NAME_HELLO, false, callback);
    }
}

4.2、Work queues工作队列模型

4.2.1、工作模式

  • 工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。相反我们稍后完成任务,我们将任务封装为消息并将其发送到队列。 在后台运行的工作进程将获取任务并最终执行作业。当你运行许多消费者时,任务将在他们之间共享,但是一个消息只能被一个消费者获取
  • 如何避免消息堆积?
    • 采用workqueue,多个消费者监听同一队列。
    • 接收到消息以后,通过线程池,异步消费。

4.2.2、生产者

  • 与基本消费模型相同

4.2.3、消费者

  • 与基本消费模型相同,设置同时处理消息数量
  • 设置同时处理消息数量为1,那么在消费者签收消息之前,队列不会再继续给该消费者发送消息,即实现能者多劳
// 设置接收消息的数量
channel.basicQos(1);

4.3、订阅模型

4.3.1、订阅模型概述

  • 在基本消息模型与工作队列模型中,会先创建队列,每个消息只会发送给一个消费者
  • 在订阅模型中则会将一个消息发送给多个消费者
    • 一个生产者对应多个消费者
    • 每个消费者都有一个自己的队列
    • 生产者不直接将消息发送到队列,而是发送到交换机,再由交换机按一定规则将消息路由转发到某个队列,对消息进行过滤
    • 每个队列都要绑定交换机

4.3.2、交换机分类

交换机会接收生产者发送的消息,然后再按一定规则将消息路由转发到某个队列,对消息进行过滤,交换机具体如何操作,取决于交换机的类型

  • Fanout:广播,将消息发送给所有绑定到该交换机的队列

  • Direct:定向,将消息发送给符合指定routingKey(路由键)的队列

  • Topic:通配符,将消息发送给routing pattern(路由模式)的队列

  • 注意:交换机只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与交换机绑定或没有任何符合规则的队列,那么消息将会丢失。

4.4、订阅模型-Fanout

4.4.1、工作模式

  • 可以拥有多个消费者
  • 每个消费者有自己的队列
  • 每个队列都要绑定到交换机
  • 生产者发送消息到交换机
  • 交换机发送消息给所有绑定过的队列
  • 每个队列的消费者都能拿到消息

4.4.2、生产者

  • 建立连接
  • 建立通道
  • 创建交换机
  • 发送消息
  • 关闭资源
package cn.gui._03fanout;

import cn.gui.util.ConnectionUtil;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Sender {

    // 交换机名称
    public static final String EXCHANGE_NAME_FANOUT = "exchange_name_fanout";

    public static void main(String[] args) throws Exception {
        // 建立连接
        Connection connection = ConnectionUtil.getConnection();
        // 建立通道
        Channel channel = connection.createChannel();
        //
        /*
            创建交换机
                String exchange:交换机名称
                String type:交换机类型
                boolean durable:声明是否为持久化
         */
        channel.exchangeDeclare(EXCHANGE_NAME_FANOUT, BuiltinExchangeType.FANOUT, true);
        /*
            发送消息
                String exchange:要发送消息的交换机,""代表默认交换机
                String routingKey:路由键,相当于队列的名称
                BasicProperties props:路由的其它属性
                byte[] body:消息体,要发送的消息
         */
        channel.basicPublish(EXCHANGE_NAME_FANOUT, "", null, "这是一条消息".getBytes());
        // 关闭资源
        channel.close();
        connection.close();
        System.out.println("消息发送成功");
    }
}

4.4.3、消费者

多个消费者即队列名称不同

  • 建立连接
  • 建立通道
  • 创建队列
  • 绑定交换机
  • 监听队列
  • 接收消息
package cn.gui._03fanout;

import cn.gui.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Recipient {

    // 队列名称
    public static final String QUEUE_NAME_FANOUT1 = "queue_name_fanout1";

    public static void main(String[] args) throws Exception {
        // 建立连接
        Connection connection = ConnectionUtil.getConnection();
        // 建立通道
        Channel channel = connection.createChannel();
        // 设置接收消息的数量
        channel.basicQos(1);
        /*
            创建队列
                String queue:队列名称
                boolean durable:声明是否为持久队列,true为持久队列
                boolean exclusive:声明该队列是否独占此连接
                boolean autoDelete:声明该队列用完后是否自动删除
                Map<String, Object> arguments:队列的其它属性
         */
        channel.queueDeclare(QUEUE_NAME_FANOUT1, true, false, false, null);
        // 绑定交换机,fanout模式无需路由键
        channel.queueBind(QUEUE_NAME_FANOUT1, Sender.EXCHANGE_NAME_FANOUT, "");
        // 接收消息
        DefaultConsumer callback = new DefaultConsumer(channel){
            /*
                重写处理接收消息的方法
                    String consumerTag:消费者标签
                    Envelope envelope:信封,包括发送消息交换机名,路由键(通道名),交货标签(类似接收的id)
                    AMQP.BasicProperties properties:消息头数据
                    byte[] body:接收的消息正文
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("开始接收消息");
                System.out.println("消息正文:"+new String(body));
                System.out.println("消费者标签:"+consumerTag);
                System.out.println("交货标签:"+envelope.getDeliveryTag());
                System.out.println("交换机:"+envelope.getExchange());
                System.out.println("路由键:"+envelope.getRoutingKey());
                System.out.println(properties);
                System.out.println("接收消息完成");
                /*
                    手动签收消息
                        long deliveryTag:交货标签
                        boolean multiple:是否批量签收消息
                 */
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        /*
            监听队列
                String queue:监听队列名
                boolean autoAck:是否自动签收消息,一般不自动签收消息
                Consumer callback:签收成功执行的方法
         */
        channel.basicConsume(QUEUE_NAME_FANOUT1, false, callback);
    }
}

4.5、订阅模型-Direct

4.5.1、工作模式

  • 该模式下将会有选择的接收消息,只有路由键相匹配的队列才会接收到消息
  • 队列与交换机绑定时需要指定routingKey(路由键)
  • 向交换机发送消息时也需要指定消息的routingKey(路由键)

4.5.2、生产者

与Fanout模型类似

  • 创建交换机时指定类型
channel.exchangeDeclare(EXCHANGE_NAME_DIRECT, BuiltinExchangeType.DIRECT, true);
  • 发送消息时指定路由键
channel.basicPublish(EXCHANGE_NAME_DIRECT, "user.insert", null, "这是一条消息".getBytes());

4.5.3、消费者

与Fanout模型类似

  • 绑定交换机时指定路由键
channel.queueBind(QUEUE_NAME_DIRECT1, Sender.EXCHANGE_NAME_DIRECT, "user.insert");
channel.queueBind(QUEUE_NAME_DIRECT1, Sender.EXCHANGE_NAME_DIRECT, "user.update");

4.6、订阅模型-Topic

4.6.1、工作模式

  • 与Direct类型,均需指定路由键,而Topic模型可以让路由键使用通配符
    • 通配符*:只匹配一个词
    • 通配符#:匹配一个词或多个词

4.6.2、生产者

与Fanout模型类似

  • 创建交换机时指定类型
channel.exchangeDeclare(EXCHANGE_NAME_TOPIC, BuiltinExchangeType.TOPIC, true);
  • 发送消息时指定路由键
channel.basicPublish(EXCHANGE_NAME_TOPIC, "user.insert", null, "这是一条消息".getBytes());

4.6.3、消费者

与Fanout模型类似

  • 绑定交换机时指定路由键
channel.queueBind(QUEUE_NAME_TOPIC1, Sender.EXCHANGE_NAME_TOPIC, "user.*");
channel.queueBind(QUEUE_NAME_TOPIC2, Sender.EXCHANGE_NAME_TOPIC, "user.#");

5、持久化

  • 如何避免消息丢失

    • 消费者的ACK机制,可以防止消费者丢失消息。
    • 但如果在消费者接收到消息之前,MQ就宕机了,消息就丢失了。
    • 要将消息持久化,前提是:队列、交换机都持久化。
  • 交换机持久化:声明交换机时,参数durable声明为true

    • channel.exchangeDeclare(EXCHANGE_NAME_FANOUT, BuiltinExchangeType.FANOUT, true);
      
  • 队列持久化:声明队列时,参数durable声明为true

    • channel.queueDeclare(QUEUE_NAME_FANOUT1, true, false, false, null);
      
  • 消息持久化:发送消息时,参数props声明为MessageProperties.PERSISTENT_TEXT_PLAIN

    • channel.basicPublish(EXCHANGE_NAME_FANOUT, "", MessageProperties.PERSISTENT_TEXT_PLAIN, "这是一条消息".getBytes());
      

6、SpringBoot集成RabbitMQ

6.1、引入依赖包

<!-- 导入SpringBoot父依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
    <!--springboot集成rabbitmq-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
</dependencies>

6.2、配置RabbitMQ

server:
  port: 44000
spring:
  application:
    name: test‐rabbitmq‐producer
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /
    listener:
      simple:
        acknowledge-mode: manual # 手动签收
        prefetch: 1 # 最大接收消息数量
    publisher-confirms: true # 消息发送到交换机的回调(成功或失败)
    publisher-returns: true # 消息发送到队列的回调(失败)
    template:
      mandatory: true # 必须设置成true,消息路由失败通知监听者,而不是将消息丢弃
logging:
  file: logs/rabbitmq.txt # 消费发送失败的日志

6.3、配置类

声明交换机与队列及绑定,定义RabbitTemplate,指定JSON转换器,定义监听器工厂,定义JSON转换器

package cn.gui.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.RabbitListenerContainerFactory;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    // 交换机名称
    public static final String NAME_EXCHANGE_DIRECT_SPRINGBOOT = "name_exchange_direct_springboot";
    // 队列名称
    public static final String NAME_QUEUE_EMAIL_SPRINGBOOT = "name_queue_email_springboot";
    public static final String NAME_QUEUE_MSG_SPRINGBOOT = "name_queue_msg_springboot";
    // 路由键名称
    public static final String NAME_ROUTING_KEY_EMAIL_SPRINGBOOT = "name_routing_key_email_springboot";
    public static final String NAME_ROUTING_KEY_MSG_SPRINGBOOT = "name_routing_key_msg_springboot";

    /**
     * 声明交换机
     * @return
     */
    @Bean
    public Exchange exchangeDirect(){
        return ExchangeBuilder.directExchange(NAME_EXCHANGE_DIRECT_SPRINGBOOT).durable(true).build();
    }

    /**
     * 声明队列
     * @return
     */
    @Bean
    public Queue queueEmail(){
        return new Queue(NAME_QUEUE_EMAIL_SPRINGBOOT, true);
    }

    /**
     * 声明队列
     * @return
     */
    @Bean
    public Queue queueMsg(){
        return new Queue(NAME_QUEUE_MSG_SPRINGBOOT, true);
    }

    /**
     * 绑定队列
     * @return
     */
    @Bean
    public Binding bindingEmail(){
        return BindingBuilder.bind(queueEmail()).to(exchangeDirect()).with(NAME_ROUTING_KEY_EMAIL_SPRINGBOOT).noargs();
    }

    /**
     * 绑定队列
     * @return
     */
    @Bean
    public Binding bindingMsg(){
        return BindingBuilder.bind(queueMsg()).to(exchangeDirect()).with(NAME_ROUTING_KEY_MSG_SPRINGBOOT).noargs();
    }

    /**
     * 定义RabbitTemplate,指定JSON转换器
     * @param connectionFactory
     * @return
     */
    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        //设置消息回调
        rabbitTemplate.setMandatory(true);
        return rabbitTemplate;
    }

    /**
     * 定义监听器工厂,定义JSON转换器
     * @param connectionFactory
     * @return
     */
    @Bean("rabbitListenerContainerFactory")
    public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setPrefetchCount(1);
        return factory;
    }

}

6.4、发送消息

package cn.gui.sender;

import cn.gui.RabbitMQApp;
import cn.gui.callback.RabbitMQCallback;
import cn.gui.domain.user;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static cn.gui.config.RabbitMQConfig.*;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitMQApp.class)
public class Sender {

    @Autowired
    RabbitTemplate rabbitTemplate;
    @Autowired
    RabbitMQCallback rabbitMQCallback;

    /**
     * 发送消息
     */
    @Test
    public void testSender(){

        // 设置消息发送至交换机的回调
        rabbitTemplate.setConfirmCallback(rabbitMQCallback);
        // 设置消息从交换机发送至队列失败的回调
        rabbitTemplate.setReturnCallback(rabbitMQCallback);
        // 发送消息
        rabbitTemplate.convertAndSend(NAME_EXCHANGE_DIRECT_SPRINGBOOT, NAME_ROUTING_KEY_EMAIL_SPRINGBOOT, new user(1L,"hh","cd"));

        /*try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }*/
    }
}

6.5、消费消息

package cn.gui.recipient;

import cn.gui.domain.user;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static cn.gui.config.RabbitMQConfig.NAME_QUEUE_EMAIL_SPRINGBOOT;

@Component
public class Recipient {

    /**
     * 监听队列,绑定JSON转换器
     * @param channel 通道
     * @param message 消息相关信息
     * @param user 消息详情
     */
    @RabbitListener(queues = {NAME_QUEUE_EMAIL_SPRINGBOOT},containerFactory = "rabbitListenerContainerFactory")
    public void handEmail(Channel channel, Message message, @Payload user user){
        // 获取消息内容
        System.out.println(user);
        // 获取交货标签
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        // 签收消息
        ackMessage(channel, deliveryTag);
    }

    /**
     * 手动签收消息
     * @param channel 通道
     * @param deliveryTag 消费者标签
     */
    private void ackMessage(Channel channel, long deliveryTag) {
        try {
            channel.basicAck(deliveryTag,false);
        } catch (IOException e) {
            e.printStackTrace();
            // 签收失败,回滚消息
            try {
                channel.basicNack(deliveryTag, false, true);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

}

6.6、配置消息发送的回调

消息发送至交换机成功或失败的回调

消息从交换机发送至队列失败的回调

package cn.gui.callback;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
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;

@Component
public class RabbitMQCallback implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {

    private Logger log = LoggerFactory.getLogger(RabbitMQCallback.class);

    @Autowired
    private RabbitTemplate rabbitTemplate;

    private int retryNum = 3;

    /**
     * 消息发送至交换机成功或失败的回调
     * @param correlationData 回调相关的数据
     * @param ack 是否确认消息
     * @param cause 原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("发送消息至交换机回调执行");
        System.out.println("回调数据"+correlationData);
        System.out.println("是否发送成功"+ack);
        System.out.println("原因"+cause);
        // 保存至日志
        log.info("消息是否投递到交换机:{},原因:{}",ack,cause);
    }

    /**
     * 消息从交换机发送至队列失败的回调
     * @param message 返回的消息
     * @param replyCode 状态码
     * @param replyText 错误信息
     * @param exchange 交换机名
     * @param routingKey 路由键
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("状态码:"+replyCode);
        System.out.println("错误信息:"+replyText);
        System.out.println("交换机:"+exchange);
        System.out.println("路由键:"+routingKey);
        // 保存至日志
        log.info("消息投递到队列失败,replyCode:{},replyText:{},exchange:{},routingKey:{},message:{}",
                replyCode,replyText,exchange,routingKey,message);
        if (retryNum-- > 0) {
            log.info("再次发送消息");
            // 再次发送消息
            rabbitTemplate.convertAndSend(exchange, routingKey, message);
        }
    }

}

7、RabbitMQ面试题

  1. 交换机有哪些类型,有什么区别?
    • Fanout:广播,将消息发送给所有绑定到该交换机的队列
    • Direct:定向,将消息发送给符合指定routingKey(路由键)的队列
    • Topic:通配符,将消息发送给routing pattern(路由模式)的队列,与Direct相比支持使用通配符作为路由键
  2. 什么情况下会出现消息丢失?
    • 消息自动签收时,若消费者未能成功处理消息,此时RabbitMQ已经清除掉消息,出现消息丢失
      • 改为手动签收,在消费成功后再签收
    • 投递失败,如果消息没有成功投递到交换机或队列,或者没有任何队列与交换机绑定或没有任何符合规则的队列,那么消息将会丢失。
    • 未设置消息持久化,那么在RabbitMQ重启或宕机时消息会丢失
  3. 系统中消息投递失败是如何处理的?
    • 重试,在投递到队列失败的回调函数中重新投递消息
    • 保存错误日志,根据错误日志进行抢修
    • 向运维人员发送报警邮件或短信
    • 保存发送日志到MySql,后续可以定时重发消息
  4. 怎么处理消息的重复消费的?
    • 正常情况下,消费者消费消息后会向队列返回一个确认信息,消息队列知道消息被正常消费之后,就会将该消息从消息队列中删除。但由于网络传输故障等原因,确认信息没有正确传递至消息队列中,导致消息队列不知道该消息已经被正常消费,会再次将该消息分发给其它消费者
    • 解决思路为:保证消息唯一性,就算是多次传输,不要让消息的多次消费带来影响,保证消息等幂性。
    • 即让每个消息携带一个全局的唯一ID
    • 如果消息是做数据库的insert操作,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。
    • 如果消息是做redis的set的操作,不用解决,因为无论set几次结果都是一样的,set操作本来就算幂等操作。
    • 如果以上两种情况还不行,可以准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。