RabbitMQ学习笔记

RabbitMQ学习笔记

简介

RabbitMQ是基于AMQP协议使用Erlang语言开发的一款消息队列产品

AMQP消息队列角色解析:

Publisher:消息生产者

Exchange:交换机,负责分发消息

Queue:存储消息的容器

Consumer:消息消费者

上图中矩形框起来的部分就是消息队列中间件

整体运作流程为:
Publisher生产出消息发布给Exchange,Exchange根据不同的规则将消息以Routes(路由)的形式分发给不同的Queue,Queue中保存消息,Consumer会监听Queue中保存的消息,当有自己需要的消息出现时就从Queue中取出并进行消费。

RabbitMQ基础架构

Broker为RabbitMQ的服务端,两侧的Producer和Consumer称为客户端

客户端通过Connection(TCP链接)与服务端进行通信,但如果每次通信都新建TCP链接则会造成资源浪费。所以在每个Connection中都存在多个channel(可以理解为轻量级的Connection),最终通过channel进行通信

接下来看Broker。其中存在多个Virtual Host(虚拟机),出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中,类似于网络中的namespace概念。当多个不同的用户使用同一个RabbitMQ server提供的服务时,可以划分多

个Virtual Host,每个用户在自己的Virtual Host创建exchange / queue等。

每个Virtual Host中都存在很多个Exchange和Queue,每个Exchange都可以与一个或多个Queue绑定(Binding)。

  • Exchange是message到达broker的第一站, 根据分发规则,匹配查询表中的routing key,根据匹配到的routing key分发消息到对应的Queue中去。Exchange的常用类型有: direct (point to- point), topic (publish subscribe) and fanout (multicast)
  • Binding是Exchange和Queue之间的虚拟连接, Binding中可以包含routing key。Binding信息被保存到Exchange中的查询表中,作为message的分发依据

RabbitMQ的6种工作模式:

简单模式、work queues模式、Publish/Subscribe 发布与订阅模式、Routing路由模式、Topics主题模式、RPC远程调用模式

RabbitMQ基本使用

引入依赖:

java 复制代码
dependencies {
    implementation 'com.rabbitmq:amqp-client:5.7.2'
}

简单模式(一个生产者对应一个消费者)

架构图:

注意:不存在自己创建的交换机,只有默认的交换机

消息生产端

java 复制代码
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ProductMsg {
    public static void main(String[] args) {
        // 1.创建链接(Connection)工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2.设置参数(比如虚拟机、用户名、密码、IP、端口等等)
        connectionFactory.setHost("服务器IP");
        // 端口默认为5672
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("rabbitmq登录用户名");
        connectionFactory.setPassword("rabbitmq登录密码");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3.创建链接
            connection = connectionFactory.newConnection();
            // 4.创建Channel
            channel = connection.createChannel();
            // 5.创建队列Queue
            /**
             * 参数解析
             * String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
             * queue:设置发送消息队列的名称
             * durable:设置队列是否持久化,如果为true则队列信息会持久化到硬盘,当mq重启时队列信息不会丢失
             * exclusive:两个功能:1.设置是否只能有一个消费者监听这个队列 2.当Connection链接关闭时是否删除队列
             * autoDelete:设置当没有消费者时是否自动删除该队列
             * arguments:设置删除队列时的参数
             */
            // 如果该名称队列不存在则会创建否则不会
            channel.queueDeclare("lc_test",true,false,false,null);
            // 6.发送消息
            /**
             * 参数解析
             * String exchange, String routingKey, BasicProperties props, byte[] body
             * exchange:设置交换机名称。简单模式下会使用默认的""
             * routingKey:路由名称,当队列名称与路由名称相同时才能绑定
             * props:配置信息
             * body:具体发送的消息数据
             */
            String msg = "hello_rabbitmq";
            channel.basicPublish("","lc_test",null,msg.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
            // 7. 释放资源
        	try {
                if (!Objects.isNull(connection) && !Objects.isNull(channel)) {
                    channel.close();
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

这里生产了一条信息,内容是hello_rabbitmq字符数组

消息消费端

java 复制代码
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConsumeMsg {
    public static void main(String[] args) {
        // 1.创建链接(Connection)工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2.设置参数(比如虚拟机、用户名、密码、IP、端口等等)
        connectionFactory.setHost("服务器IP");
        // 端口默认为5672
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("rabbitmq登录用户名");
        connectionFactory.setPassword("rabbitmq登录密码");
        Connection connection = null;
        try {
            // 3.创建链接
            connection = connectionFactory.newConnection();
            // 4.创建Channel
            Channel channel = connection.createChannel();
            // 5.创建队列Queue
            /**
             * 参数解析
             * String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
             * queue:设置发送消息队列的名称
             * durable:设置队列是否持久化,如果为true则队列信息会持久化到硬盘,当mq重启时队列信息不会丢失
             * exclusive:两个功能:1.设置是否只能有一个消费者监听这个队列 2.当Connection链接关闭时是否删除队列
             * autoDelete:设置当没有消费者时是否自动删除该队列
             * arguments:设置删除队列时的参数
             */
            // 如果该名称队列不存在则会创建否则不会
            channel.queueDeclare("lc_test",true,false,false,null);

            // 6.接收消息
            /**
             * 参数解析
             * String queue, boolean autoAck, Consumer callback
             * queue:设置获取消息队列的名称
             * autoAck:设置是否自动确认,确认是指消费者收到消息会告诉mq收到消息了
             * callback:回调对象
             */
            // 这里的队列名称要与发送消息所使用的队列名称一致
            Consumer Consumer = new DefaultConsumer(channel){
                // 回调方法,当收到消息后会自动执行该方法
                /**
                 * 参数解析
                 * @param consumerTag 消息标识
                 * @param envelope 获取一些信息,比如交换机、routingKey等相关信息
                 * @param properties 配置信息
                 * @param body 具体数据
                 * @throws IOException
                 */
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    System.out.println("consumerTag: "+consumerTag);
                    System.out.println("envelope: exchangeMsg: "+envelope.getExchange()+"routingKeyMsg: "+envelope.getRoutingKey());
                    System.out.println("properties: "+properties);
                    System.out.println("body: "+new String(body));
                }
            };
            channel.basicConsume("lc_test",true,Consumer);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }finally {
        }
    }
}

这里获取信息后的处理关键在Consumer回调对象的重写回调方法(handleDelivery)中,通过该回调方法可以获取到具体的消息。
注意:消息消费端不要立即关闭Connection,Channel资源,我们需要其处在一个持续监听状态。如果关闭了资源就无法监听了。
此外消费端可以不用创建队列Queue,因为已经在生产端创建了,只需监听对应队列即可

work queues模式(一个生产者对应多个消费者)

架构图:

虽然是多个消费者但它们彼此之间是竞争关系,一条消息只能被一个消费者取到

作用:在任务过重的情况下该模式能提高任务处理的速度

比如队列中有1000条消息,有两个消费者,每个消费者只能消费500条消息。如果采用简单模式那么消息是来不及消费的,如果采用work queues模式则可以加入两个消费者来消费消息,此时能达到要求(采用轮询的方式,消费者1拿完消费者2拿再1拿再2拿...)

两端代码与简单模式一致,区别在于可以创建多个消费者端来监听同一条队列

Publish/Subscribe 发布与订阅模式(通过exchange分发消息)

架构图:

图中的X即为交换机

Exchange:交换机。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:

  • Fanout:广播,将消息交给所有绑定的交换机队列
  • Direct:定向,把消息交给符合指定routing key 的队列
  • Topic:通配符,把消息交给符合routing pattern(路由模式)的队列

通过交换机可以实现一条消息被多个消费者消费
注意:Exchange只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

消息生产端

java 复制代码
public class ProductPubSub {
    public static void main(String[] args) {
        // 1.创建链接(Connection)工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2.设置参数(比如虚拟机、用户名、密码、IP、端口等等)
         connectionFactory.setHost("服务器IP");
        // 端口默认为5672
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("rabbitmq登录用户名");
        connectionFactory.setPassword("rabbitmq登录密码");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3.创建链接
            connection = connectionFactory.newConnection();
            // 4.创建Channel
            channel = connection.createChannel();
            // 5.创建交换机
            /**
             * 参数解析
             * String exchange,BuiltinExchangeType type,boolean durable,boolean autoDelete,boolean internal,Map<String, Object> arguments
             * exchange:设置交换机名称
             * type:设置交换机类型(4种,枚举)
             *      DIRECT("direct"):定向
             *      FANOUT("fanout"):广播,发送消息给每个与该交换机绑定的队列
             *      TOPIC("topic"):通配符方式
             *      HEADERS("headers"):参数匹配(不常用)
             * durable:是否持久化
             * autoDelete:自动删除
             * internal:是否mq内部使用(一般设为false)
             * arguments:参数列表
             */
            String exchangeName = "test_fanout";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,true,false,false,null);
            // 6.创建队列Queue
            /**
             * 参数解析
             * String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
             * queue:设置队列名称
             * durable:设置队列是否持久化,如果为true则队列信息会持久化到硬盘,当mq重启时队列信息不会丢失
             * exclusive:两个功能:1.设置是否只能有一个消费者监听这个队列 2.当Connection链接关闭时是否删除队列
             * autoDelete:设置当没有消费者时是否自动删除该队列
             * arguments:设置删除队列时的参数
             */
            // 如果该名称队列不存在则会创建否则不会
            String queue1Name = "lc_test1";
            String queue2Name = "lc_test2";
            channel.queueDeclare(queue1Name,true,false,false,null);
            channel.queueDeclare(queue2Name,true,false,false,null);
            // 7.绑定队列和交换机
            /**
             * 参数解析
             * String queue, String exchange, String routingKey
             * queue:被绑定队列名称
             * exchange:被绑定交换机名称
             * routingKey:路由键,即绑定规则
             */
            // 当交换机类型为fanout时,需要将routingKey设置为""以实现广播效果
            channel.queueBind(queue1Name,exchangeName,"");
            channel.queueBind(queue2Name,exchangeName,"");
            // 8.发送消息
            /**
             * 参数解析
             * String exchange, String routingKey, BasicProperties props, byte[] body
             * exchange:设置交换机名称。简单模式下会使用默认的""
             * routingKey:路由名称,当队列名称与路由名称相同时才能绑定
             * props:配置信息
             * body:具体发送的消息数据
             */
            String msg = "hello_rabbitmq";
            channel.basicPublish(exchangeName,"",null,msg.getBytes());
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        } finally {
            // 9. 释放资源
            try {
                if (!Objects.isNull(connection) && !Objects.isNull(channel)) {
                    channel.close();
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

这里创建了一个交换机(test_fanout)和两个队列(lc_test1和lc_test2),并且将这两个队列与交换机绑定。交换机类型设置为fanout

广播类型,这样同一条消息会由交换机分发给这两个队列进行消费。注意设置交换机的routingKey为""(空字符串)

消息消费端

与之前无异,注意修改监听的队列名称即可。可以创建多个消费者来监听不同的队列

Routing路由模式(Exchange根据routingKey分发消息)

架构图:

队列与交换机的绑定,不再是任意绑定了,而是要指定一个RoutingKey(路由key)

消息的发送方在向Exchange发送消息时,也必须指定消息的RoutingKey

Exchange不再把消息交给每一个绑定的队列,而是根据消息的RoutingKey进行判断,只有队列的Routingkey与消息的Routing key完全---致,才会接收到消息

消息生产端

java 复制代码
package com.lc.example;

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

import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.TimeoutException;

/**
 * @Title ProductMsg
 * @Author LC
 * @Description //TODO $
 * @Date $ $
 **/
public class ProductRouting {
    public static void main(String[] args) {
        // 1.创建链接(Connection)工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
       	// 2.设置参数(比如虚拟机、用户名、密码、IP、端口等等)
        connectionFactory.setHost("服务器IP");
        // 端口默认为5672
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setUsername("rabbitmq登录用户名");
        connectionFactory.setPassword("rabbitmq登录密码");
        Connection connection = null;
        Channel channel = null;
        try {
            // 3.创建链接
            connection = connectionFactory.newConnection();
            // 4.创建Channel
            channel = connection.createChannel();
            // 5.创建交换机
            /**
             * 参数解析
             * String exchange,BuiltinExchangeType type,boolean durable,boolean autoDelete,boolean internal,Map<String, Object> arguments
             * exchange:设置交换机名称
             * type:设置交换机类型(4种,枚举)
             *      DIRECT("direct"):定向
             *      FANOUT("fanout"):广播,发送消息给每个与该交换机绑定的队列
             *      TOPIC("topic"):通配符方式
             *      HEADERS("headers"):参数匹配(不常用)
             * durable:是否持久化
             * autoDelete:自动删除
             * internal:是否mq内部使用(一般设为false)
             * arguments:参数列表
             */
            String exchangeName = "test_direct";
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT,true,false,false,null);
            // 6.创建队列Queue
            /**
             * 参数解析
             * String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
             * queue:设置队列名称
             * durable:设置队列是否持久化,如果为true则队列信息会持久化到硬盘,当mq重启时队列信息不会丢失
             * exclusive:两个功能:1.设置是否只能有一个消费者监听这个队列 2.当Connection链接关闭时是否删除队列
             * autoDelete:设置当没有消费者时是否自动删除该队列
             * arguments:设置删除队列时的参数
             */
            // 如果该名称队列不存在则会创建否则不会
            String queue1Name = "lc_test1_direct";
            String queue2Name = "lc_test2_direct";
            channel.queueDeclare(queue1Name,true,false,false,null);
            channel.queueDeclare(queue2Name,true,false,false,null);
            // 7.绑定队列和交换机
            /**
             * 参数解析
             * String queue, String exchange, String routingKey
             * queue:被绑定队列名称
             * exchange:被绑定交换机名称
             * routingKey:路由键,即绑定规则
             */
            // 当交换机类型为direct时,需要将routingKey设置为不同的string以实现定向分发效果
            // queue1的routingKey为error,所有routingKey也为error的消息都会分发给queue1
            channel.queueBind(queue1Name,exchangeName,"error");
            // queue2的routingKey为info和warning,所有routingKey也为info或warning的消息都会分发给queue2
            channel.queueBind(queue2Name,exchangeName,"info");
            channel.queueBind(queue2Name,exchangeName,"warning");
            // 8.发送消息
            /**
             * 参数解析
             * String exchange, String routingKey, BasicProperties props, byte[] body
             * exchange:设置交换机名称。简单模式下会使用默认的""
             * routingKey:路由名称,当队列名称与路由名称相同时才能绑定
             * props:配置信息
             * body:具体发送的消息数据
             */
            for (int i=0 ; i<10 ; i++){
                String msg = "hello_rabbitmq"+i;
                if (0 == i%2) {
                    channel.basicPublish(exchangeName,"error",null,(msg+"error").getBytes());
                }else {
                    channel.basicPublish(exchangeName,"info",null,(msg+"info").getBytes());
                    channel.basicPublish(exchangeName,"warning",null,(msg+"warning").getBytes());
                }
            }
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        } finally {
            // 9. 释放资源
            try {
                if (!Objects.isNull(connection) && !Objects.isNull(channel)) {
                    channel.close();
                    connection.close();
                }
            } catch (IOException | TimeoutException e) {
                e.printStackTrace();
            }
        }
    }
}

这里首先将Exchange的类型设置为DIRECT,以实现定向分发消息的功能,定向的依据是routingKey

然后创建了两个不同的queue,关键在queue与Exchange绑定时,需要分别指定不同的routingKey。如果一个queue需要接收多种类型的消息,那么就进行多次绑定每次传入不同的routingKey即可

最后在消息发送时也需要指定消息携带的routingKey,携带不同routingKey的消息会进入对应的queue

消息消费端

与之前无异,注意修改监听的队列名称即可。可以创建多个消费者来监听不同的队列

Topics主题模式(Exchange根据routingKey模糊定向分发消息)

架构图:

消息生产端

整体写法与Routing路由模式大同小异,区别在Exchange的类别和RoutingKey的值上:
Exchange的类别应当设置为BuiltinExchangeType.TOPIC
RoutingKey的值不再写为固定值而是写为通配符表达式,如下例子:
*.order:匹配以.order结尾的RoutingKey,*代表刚好一个"单词",如果是a.b.order则无法匹配
#.order:匹配以.order结尾的RoutingKey,#代表0个或多个"单词",可以匹配a.b.order

这里的"单词"指的是以.分割的字符串,比如上述order就算一个"单词"

消息消费端

与之前无异,注意修改监听的队列名称即可。可以创建多个消费者来监听不同的队列

RPC远程调用模式

  1. 当客户端启动时,创建一个匿名的回调队列。
  2. 客户端为RPC请求设置2个属性:replyTo,设置回调队列名字;correlationId,标记request。
  3. 请求被发送到rpc_queue队列中。
  4. RPC服务器端监听rpc_queue队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
  5. 客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了。

SpringBoot整合RabbitMQ

引入依赖:

java 复制代码
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-amqp:2.5.6'
}

在application.properties配置文件中进行RabbitMQ的基本配置:

shell 复制代码
# 配置RabbitMQ的基本信息,包括:ip、端口、userName、passWord等
spring.rabbitmq.host=服务器ip
spring.rabbitmq.port=5672(默认)
spring.rabbitmq.username=RabbitMQ账户名
spring.rabbitmq.password=RabbitMQ密码
spring.rabbitmq.virtual-host=/(默认)

创建配置类

java 复制代码
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";

    // 1.交换机配置
    @Bean("exchange1")
    public Exchange bootExchange(){
        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }
    // 2.队列配置
    @Bean("queue1")
    public Queue bootQueue(){
        return QueueBuilder.durable(QUEUE_NAME).build();
    }
    // 3.队列和交换机绑定配置
    @Bean
    public Binding bindQueueAndExchange(@Qualifier("queue1") Queue queue, @Qualifier("exchange1") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }
}

这里创建了一个名为boot_topic_exchange的TOPIC类型的交换机,以及一个名为boot_queue的队列

在进行绑定时,为了区分不同的交换机和队列实例(可能会有多个),在@Bean注解中为它们设置了名称并且在需要注入的地方使用@Qualifier注解指定被注入实例的名称

消息生产端

上述配置完毕后,在消息生产端只需要注入RabbitTemplate类实例,即可进行消息的发送

java 复制代码
// 在消息生产端注入
@Autowired
private RabbitTemplate rabbitTemplate;

@Test
public void producterTest1(){
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.haha", "我是消息1");
}

调用convertAndSend方法来发送消息,其有多个重载方法。这里调用的参数分别为:交换机名称、RoutingKey以及具体的消息

因为交换机采用的是TOPIC类型,所以这里的RoutingKey值为通配符表达式

消息消费端

同样需要引入依赖和在application.properties配置文件中进行RabbitMQ的基本配置

但是如果纯粹作为消息的消费端则不需要创建配置类进行相应配置;如果既要作为消费端同时也要作为发送端则需要需要创建配置类进行相应配置,总结来说配置类是为发送端服务的

只通过@RabbitListener实现

创建一个类并在类中创建相应方法进行监听
注意:一定要添加@Component注解将该类实例交给Spring容器,因为在该项目启动时,所有监听方法都会进入监听状态

java 复制代码
@Component
public class MsgConsumer {
    @RabbitListener(queues = "boot_queue")
    public void listenerQueue(Message message){
        System.out.println(message);
    }
}

通过@RabbitListener注解的queues参数来设置,当前方法监听的队列。获取到的数据会通过MessageConvert转化为Message类型

可以通过message.getBody()方法获取到具体消息,但注意此时的类型为byte[],需要根据实际业务进行处理

通过@RabbitListener和@RabbitHandler实现

@RabbitListener注解不仅可以注解在方法上还可以注解在类上。用于设置该类中的所有方法监听同一个队列

java 复制代码
@Component
@RabbitListener(queues = "boot_queue")
public class MsgConsumer {
    @RabbitHandler
    public void listenerQueue(String message){
        System.out.println(message);
    }
	
	@RabbitHandler(isDefault = true)
    public void listenerQueueObj(Object message){
        // 具体处理...
    }
}

在类中可以创建方法并添加@RabbitHandler注解来对监听到的消息进行获取

消息的 content_type 属性表示消息 body 数据以什么数据格式存储,接收消息除了使用 Message 对象接收消息(包含消息属性等信息)之外,还可直接使用对应类型接收消息 body 内容,但若方法参数类型不正确会抛异常:

text 复制代码
application/octet-stream:二进制字节数组存储,使用 byte[]
application/x-java-serialized-object:java 对象序列化格式存储,使用 Object、相应类型(反序列化时类型应该同包同名,否者会抛出找不到类异常)
text/plain:文本数据类型存储,使用 String
application/json:JSON 格式,使用 Object、相应类型

可以在类中创建多个方法来处理不同类型的消息
建议将某个方法的@RabbitHandler注解的isDefault属性设置为true表明其为默认处理方法。当某条消息不能被其它所有监听方法处理时会走默认方法,所以默认方法的参数类型一般为Object

消息的 content_type 属性可以在RabbitMQ客户端中查看:

Message 内容对象序列化与反序列化

注意:被序列化对象应提供一个无参的构造函数,否则会抛出异常

生产端序列化

使用默认SimpleMessageConverter

如果发送的消息对象不手动转成Json格式则消息在MQ中的类型为application/x-java-serialized-object,如下例子:

在MQ客户端中查看消息类型

如果手动将消息对象转成Json字符串则消息在MQ中的类型为text/plain,如下例子:

在MQ客户端中查看消息类型

使用Jackson2JsonMessageConverter

首先需要在RabbitMQ配置类中加入以下配置:

java 复制代码
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
    RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
    rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
    return rabbitTemplate;
}

此时就无需手动将消息对象转为Json格式了,会自动转换:

在MQ客户端中查看消息类型

统一为application/json格式

一个小坑

如果此时依旧将消息对象转为Json字符串并发送则消息内容会发生错误:

在MQ客户端中查看消息类型和具体内容

可以看出消息的内容并不是我们所期望的Json字符串,原因是这里进行了两次转换(一次手动的一次RabbitTemplate中的转换)

此时如果想传Json字符串可以使用 send() 方法

需要构造一个Message实例,在其构造方法的参数中传入Json字符串转为Byte[]后的结果:

在MQ客户端中查看消息类型

可以看出消息类型为application/octet-stream即byte[]

消费端反序列化

建议使用默认SimpleMessageConverter而非Jackson2JsonMessageConverter

原因:

消费端如下代码:

java 复制代码
@RabbitListener(queues = "test")
public void handler(Rabbit rabbit) {
    System.out.println("收到消息:" + rabbit);
}
   
@RabbitListener(queues = "test")
public void handler(Message message) {
    System.out.println("收到消息:" + new String(message.getBody()));
}
  • 使用第一种,首先这样子拿不到完整消息内容(缺少Header信息),不利于排查问题。 如果别人发的是字符串,消息头没有content-type或者不是application/json,就会报错;也可能别人发的消息头的content-type是application/json,而消息内容并不是Rabbit类,虽然不报错,但字段可能会对应不上,出现些奇怪的问题。
  • 使用第二种,如果消息头没有content-type或者不是application/json,会打印一个告警信息Could not convert incoming message with content-type [xxx], 'json' keyword missing。还有个更坑的是,如果别人发的消息头中的__type_id__ 字段是一个自己项目中不存在的类,那会报类找不到的错误。

@Payload 与 @Headers注解

使用 @Payload 和 @Headers 注解可以获取消息中的 body 与 Header信息

java 复制代码
@RabbitListener(queues = "debug")
public void processMessage1(@Payload String body, @Headers Map<String,Object> headers) {
    System.out.println("body:"+body);
    System.out.println("Headers:"+headers);
}

headers以key-value的形式传输

body即是消息的具体内容

@Headers也可以获取单个 Header 属性

java 复制代码
@RabbitListener(queues = "debug")
public void processMessage1(@Header String token) {
    System.out.println("token:"+token);
}

token是Header中的某个单一属性

@Payload处理Json类型数据

例子:

在生产端生产了一组Json格式的消息并发送到MQ里(这里已经进行了配置,使用Jackson2JsonMessageConverter进行序列化):

java 复制代码
public void producterTest1() throws JsonProcessingException {
	for(int i=0 ; i<10 ; i++){
	    Person person = new Person();
	    person.setName("我是"+i+"号person");
	    person.setAge(i);
	    person.setMoney(new BigDecimal(i));
	    ObjectMapper objectMapper = new ObjectMapper();
	    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "boot.haha", person);
	}
}

在MQ客户端中查看消息的类型和具体内容:

是Json类型并且内容是一个Json字符串
注意到左侧显示消息的Payload类型为byte[],编码类型为String

在消费端需要通过以下方式来进行处理:

java 复制代码
@RabbitHandler
public void listenerQueueObj(Object message,@Payload String personJson){
	Person person = JSON.parseObject(personJson, Person.class);
	System.out.println(person.toString());
}

首先Json类型的消息会被反序列化为Object类型的实例,所以需要提供一个参数为Object类型的监听方法来获取消息

此时获取到的消息为Object类型的对象而非Json字符串,当使用工具直接进行转换时会出问题

所以需要使用@Payload注解并根据在MQ客户端中查到的消息的Payload类型来直接获取消息的内容

此时的内容就是我们想要的Json字符串了

如果处理方法中存在参数类型为byte[]的方法则会优先走该方法! 如下:

java 复制代码
@RabbitHandler
public void listenerQueueByte(byte[] message){
    System.out.println(new String(message));
}

消息的可靠投递(保证生产端发送数据给MQ的可靠性)

两种实现方式:

  • confirm确认模式
  • return退回模式

confirm确认模式

在该模式下,会在消息生产端设置一个监听回调方法(confirmCallback),无论将来消息是否到达Exchange该方法都会被执行

如果Exchange成功收到消息,那么回调方法会返回一个flag为true,否则返回flag为false

例子:

首先在配置文件中开启RabbitMQ确认模式

shell 复制代码
spring.rabbitmq.publisher-confirm-type=correlated

然后编写监听回调方法

java 复制代码
public void testConfirm() {
	// 定义监听回调方法
	rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
	    /**
	     * 参数解析
	     * @param correlationData 相关配置信息实例,配置信息可以在消息发送时设置(比如下面的convertAndSend方法中)
	     * @param ack Exchange是否成功收到消息的flag,true表示成功,false表示失败
	     * @param cause 失败原因,当Exchange未正常收到消息时会有值
	     */
	    @Override
	    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
	        System.out.println("confirm回调被执行");
	        if (ack) {
	            System.out.println("success: "+cause);
	        }else {
	            System.out.println("failed: "+cause);
	        }
	    }
	});
	rabbitTemplate.convertAndSend("RabbitMQConfig.EXCHANGE_NAME_DIRECT", "confirm", "message of confirm");
}

通过判断回调方法中的ack是否为true来对消息从生产端发送到Exchange是否成功进行处理

return退回模式

在该模式下,当消息从Exchang路由到Queue失败时会执行一个回调方法(returnCallback),只有失败才会执行

例子:

首先在配置文件中开启RabbitMQ回退模式

shell 复制代码
spring.rabbitmq.publisher-returns=true

然后编写监听回调方法

java 复制代码
public void testReturn() {
    // 设置Exchange处理失败消息的方式
    rabbitTemplate.setMandatory(true);
    // 定义监听回调函数
    rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
        /**
         * 参数解析
         * @param returned 封装了这些属性:Message message、int replyCode、String replyText、String exchange、String routingKey
         *      message:消息对象
         *      replyCode:错误码
         *      replyText:错误信息
         *      exchange:交换机名称
         *      routingKey:路由键
         */
        @Override
        public void returnedMessage(ReturnedMessage returned) {
            System.out.println("return回调被执行");
			// 相应处理
        }
    });
    rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME_DIRECT, "confirm111", "message of confirm");
}

Exchange处理消息存在两种方式:
默认 当消息未成功路由到Queue时丢弃消息,既然消息已经丢弃了那么就不会走这里设置的回调方法了

所以需要调用rabbitTemplate.setMandatory(true)方法改变Exchange处理消息的方式为当消息未成功路由到Queue时返回消息给回调方法

Consumer ACK(保证MQ发送数据给消费端的可靠性)

三种确认方式:

  • 自动确认:acknowledge="none"
  • 手动确认:acknowledge="manual"
  • 根据异常情况确认:acknowledge="auto"

一般使用手动确认的方式,在实际业务处理过程中,会出现消费端接收到消息但业务处理出现异常的情况,这时如果采用自动确认的方式那么该消息就会丢失。所以会采用手动确认的方式,在业务成功处理后调用channel.basicAck()方法 手动签收,如果产生异常则调用channel.basicReject()或channel.basicNack()方法让MQ重新发送消息

例子:

在消费者端的配置文件中开启手动签收(全局设置)

java 复制代码
spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual

或者通过@RabbitListener注解的ackMode属性设置(局部设置)

java 复制代码
@RabbitListener(queues = "boot_queue_confirm", ackMode = "MANUAL")
public void listenQueueAckMsg(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
   // 具体操作...
}
java 复制代码
@Component
public class AckMsgConsumer {

    @RabbitListener(queues = "boot_queue_confirm")
    public void listenQueueAckMsg(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        try {
            Thread.sleep(2000);
            System.out.println("TAG: "+tag);
            System.out.println("MSG: "+new String(message.getBody()));
            int i = 1/0;
            channel.basicAck(tag,true);
        } catch (Exception e) {
            //e.printStackTrace();
            System.out.println("产生异常重发消息:"+tag);
            channel.basicNack(tag,true,true);
        }
    }
}

在try/catch块中如果未出现异常则会执行channel.basicAck()方法正常的签收消息

一旦出现异常进入了catch块,则会调用channel.basicNack()方法,进行消息的重发(这里会产生死循环,具体业务中一般不进行重发而是放入死信队列)

channel.basicReject()与channel.basicNack()的区别

这两个方法都在消费端使用,可以拒绝MQ发送的消息并让MQ重新发送相同消息或者将消息放入死信队列

channel.basicReject()

有两个参数:

  • long deliveryTag:可以看作消息的编号,以此来区分不同的消息
  • boolean requeue:设置MQ处理消息的方式,如果requeue 参数设置为true ,则RabbitMQ 会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者, 如果requeue 参数设置为false ,则RabbitMQ立即会把消息从队列中移除(加入死信队列),而不会把它发送给新的消费者

该方法一次只能拒绝一条消息

channel.basicNack()

有三个参数:

  • long deliveryTag:与channel.basicReject()一致
  • boolean multiple:设置拒绝消息的范围,multiple 参数设置为false 则表示拒绝编号为deliveryTag的这一条消息,这时候basicNack 和basicReject 方法一样;multiple 参数设置为true 则表示拒绝deliveryTag 编号之前所有未被当前消费者确认的消息
  • boolean requeue:与channel.basicReject()一致

该方法一次可以拒绝多条消息(multiple 参数设置为true)

将channel.basicReject()或者channel.basicNack()中的requeue设置为false ,可以启用死信队列的功能。死信队列可以通过检测被拒绝或者未送达的消息来追踪问题

消费端限流

首先要确保消费端为手动确认

然后需要在消费者端的配置文件中设置每次从MQ中获取消息的数量(全局设置)

java 复制代码
spring.rabbitmq.listener.direct.prefetch=1
spring.rabbitmq.listener.simple.prefetch=1

这里设置为1表示Consumer每次从MQ中拉取一条数据,只有当这一条数据消费完成(手动签收)后才会拉取下一条

TTL(生产端设置)

TTL即(Time To Live)。被设置TTL的消息如果在设置的时间内未被消费则会被丢弃。
既可以对消息设置TTL,也可以对整个Queue设置TTL

设置队列TTL

当时间达到设置的TTL后对列中的消息会被全部丢弃!

例子:

java 复制代码
@Bean("queue3")
public Queue bootQueue3(){
	return QueueBuilder.durable(QUEUE_NAME3).ttl(100000).build();
}

只需要配置类创建Queue的时候调用ttl()方法设置过期时间即可,单位为ms

设置消息TTL

例子:

java 复制代码
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME_FANOUT, "", "message of confirm", new MessagePostProcessor() {
    // 在该方法中可以设置消息的一些参数
    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        // 1.设置消息参数
        // 这里设置消息的过期时间
        message.getMessageProperties().setExpiration("5000");
        // 2.返回消息对象
        return message;
    }
});

在消息生产时提供一个MessagePostProcessor接口对象,并实现其中postProcessMessage方法。在该方法中可以通过参数获取当前message对象。调用getMessageProperties()方法来获取message对象的配置信息,然后调用对应的set方法即可完成设置。这里调用setExpiration()方法来设置TTL时间,注意为字符串

注意点(关注)

如果同时设置了队列和消息的TTL时间则以时间较短者为准
消息过期后只有在Queue顶端的消息才会被丢弃

例子:

java 复制代码
for (int i=0 ; i<10 ; i++) {
    if (5 == i) {
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME_FANOUT, "", "message of confirm", new MessagePostProcessor() {
            // 在该方法中可以设置消息的一些参数
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 1.设置消息参数
                // 这里设置消息的过期时间
                message.getMessageProperties().setExpiration("5000");
                // 2.返回消息对象
                return message;
            }
        });
    }else {
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME_FANOUT, "", "message of confirm");
    }
}

这里在Queue的第5个下标处生产了一条有TTL时间的消息但其它消息均无TTL时间,此时即使那条消息达到了TTL时间也不会被丢弃。当它移动到Queue顶端时才会被丢弃

当一个队列设置了TTL并且已经被创建,则在修改TTL时间前需要将队列删除

死信队列

在RabbitMQ中准确称为死信交换机,当消息成为Dead message后,可以被重新发送到另一个Exchange,这个Exchange就是死信交换机

架构图:

消息成为死信的三种条件:

  1. 队列长度达到限制
  2. 消费端拒绝接受消息(basicNack/basicReject),并且不将消息放入原队列(requeue=false)
  3. 原队列存在TTL时间,或者消息本身存在TTL时间且达到时间时未被消费

例子:

这里测试消息过期进入死信队列的情况

java 复制代码
@Configuration
public class RabbitMQConfig {
	// 定义死信交换机和业务交换机名称
	public static final String EXCHANGE_NAME_BUSINESS_DIRECT = "boot_business_direct_exchange";
	public static final String EXCHANGE_NAME_DEAD_DIRECT = "boot_dead_direct_exchange";
	// 定义死信队列和业务队列名称
	public static final String QUEUE_NAME4 = "boot_queue_business_direct";
    public static final String QUEUE_NAME5 = "boot_queue_dead_direct";
	
	// 创建业务队列交换机
    @Bean("business_exchange")
    public Exchange bootExchange4(){
        return ExchangeBuilder.directExchange(EXCHANGE_NAME_BUSINESS_DIRECT).durable(true).build();
    }
    // 创建死信队列交换机
    @Bean("dead_exchange")
    public Exchange bootExchange5(){
        return ExchangeBuilder.directExchange(EXCHANGE_NAME_DEAD_DIRECT).durable(true).build();
    }

	//创建业务队列并设置要绑定的死信交换机信息
    @Bean("business_queue")
    public Queue bootQueue4() {
        Map<String, Object> args = new HashMap<>();
        // x-dead-letter-exchange:这里声明当前业务队列绑定的死信交换机
        args.put("x-dead-letter-exchange", EXCHANGE_NAME_DEAD_DIRECT);
        // x-dead-letter-routing-key:这里声明当前业务队列的死信路由 key
        args.put("x-dead-letter-routing-key", "dead");
        // x-message-ttl,设置队列TTL时间为5s
        args.put("x-message-ttl", 5000L);
        return new Queue(QUEUE_NAME4, true, false, false, args);
    }
    //创建死信队列
    @Bean("dead_queue")
    public Queue bootQueue5() {
        return new Queue(QUEUE_NAME5);
    }

	// 绑定死信队列和死信交换机并指定Routing_Key
    @Bean
    public Binding bindQueueAndExchange4(@Qualifier("dead_queue") Queue queue, @Qualifier("dead_exchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("dead").noargs();
    }

    // 业务队列与交换机绑定,并指定Routing_Key
    @Bean
    public Binding bindQueueAndExchange5(@Qualifier("business_queue") Queue queue, @Qualifier("business_exchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("businessA").noargs();
    }
}

这里的核心在于创建业务队列时要指定与之绑定的死信交换机的信息,包括死信交换机的名称和与之通信的Routingkey

通过创建业务Queue时的最后一个参数来设置,需要传入一个Map集合以键值对的方式来设置

其包含多个key,意义如下表:

注意:创建业务Queue设置被绑定交换机的Routingkey时,其内容需要与死信队列和死信交换机通信的Routingkey一致或满足通配符表达式。
比如上面例子中死信队列和死信交换机通信的Routingkey设置为"dead",那么业务Queue设置被绑定交换机的Routingkey也必须为"dead"(因为这里是DIRECT类型的死信交换机)
如果是TOPIC类型的死信交换机并且Routingkey设置为"dead.#",那么业务Queue设置被绑定交换机的Routingkey可以为"dead.666",只要满足通配符表达式即可

这样配置完成后当有消息发送到业务队列并且5s后未被消费的话就会进入死信队列

延迟队列

RabbitMQ并未直接提供延迟队列的实现,而是采用TTL+死信队列 的方式来实现

架构图:

例子:

java 复制代码
public class RabbitMQConfig {
	// 定义死信交换机和业务交换机名称
	public static final String EXCHANGE_NAME_BUSINESS_DIRECT = "boot_business_direct_exchange";
	public static final String EXCHANGE_NAME_DEAD_DIRECT = "boot_dead_direct_exchange";
	// 定义死信队列和业务队列名称
	public static final String QUEUE_NAME4 = "boot_queue_business_direct";
    public static final String QUEUE_NAME5 = "boot_queue_dead_direct";
	
	// 创建业务队列交换机
    @Bean("business_exchange")
    public Exchange bootExchange4(){
        return ExchangeBuilder.directExchange(EXCHANGE_NAME_BUSINESS_DIRECT).durable(true).build();
    }
    // 创建死信队列交换机
    @Bean("dead_exchange")
    public Exchange bootExchange5(){
        return ExchangeBuilder.directExchange(EXCHANGE_NAME_DEAD_DIRECT).durable(true).build();
    }

	//创建业务队列并设置要绑定的死信交换机信息
    @Bean("business_queue")
    public Queue bootQueue4() {
        Map<String, Object> args = new HashMap<>();
        // x-dead-letter-exchange:这里声明当前业务队列绑定的死信交换机
        args.put("x-dead-letter-exchange", EXCHANGE_NAME_DEAD_DIRECT);
        // x-dead-letter-routing-key:这里声明当前业务队列的死信路由 key
        args.put("x-dead-letter-routing-key", "dead");
        // x-message-ttl,设置队列TTL时间为5s
        args.put("x-message-ttl", 5000L);
        return new Queue(QUEUE_NAME4, true, false, false, args);
    }
    //创建死信队列
    @Bean("dead_queue")
    public Queue bootQueue5() {
        return new Queue(QUEUE_NAME5);
    }

	// 绑定死信队列和死信交换机并指定Routing_Key
    @Bean
    public Binding bindQueueAndExchange4(@Qualifier("dead_queue") Queue queue, @Qualifier("dead_exchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("dead").noargs();
    }

    // 业务队列与交换机绑定,并指定Routing_Key
    @Bean
    public Binding bindQueueAndExchange5(@Qualifier("business_queue") Queue queue, @Qualifier("business_exchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("businessA").noargs();
    }
}

在MQ的代码上与死信队列中的例子并无差别(关键是设置TTL时间以达到延迟的目的)

这里提供另外一种消息进入死信队列的处理方式------消费端拒绝接受消息,并且不将消息放入原队列:

java 复制代码
@Component
public class DeadMsgConsumer {

    @RabbitListener(queues = "boot_queue_business_direct")
    public void listenBusinessQueue(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        try {
            System.out.println("接收业务队列消息");
            int a = 1/0;
            channel.basicAck(tag,true);
        } catch (Exception e) {
            e.printStackTrace();
            channel.basicReject(tag, false);
        }
    }

    @RabbitListener(queues = "boot_queue_dead_direct")
    public void listenDeadLetterQueue(Message message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        try {
            Thread.sleep(5000);
            System.out.println("接收死信队列消息");
            System.out.println("消息内容:"+ new String(message.getBody()));
            channel.basicAck(tag,true);
        } catch (Exception e) {
            e.printStackTrace();
            channel.basicReject(tag, false);
        }
    }
}

这里消费端监听两个队列,一个是业务队列(boot_queue_business_direct)并在监听方法中模拟消息消费出错的场景,出错后设置消息不返回原队列而是丢弃,此时消息会进入死信队列

另一个是死信队列(boot_queue_dead_direct),在监听方法中进行对被丢弃消息的处理

相关推荐
南宫生4 分钟前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
sanguine__20 分钟前
Web APIs学习 (操作DOM BOM)
学习
冷眼看人间恩怨32 分钟前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
来一杯龙舌兰2 小时前
【RabbitMQ】RabbitMQ保证消息不丢失的N种策略的思想总结
分布式·rabbitmq·ruby·持久化·ack·消息确认
数据的世界012 小时前
.NET开发人员学习书籍推荐
学习·.net
四口鲸鱼爱吃盐3 小时前
CVPR2024 | 通过集成渐近正态分布学习实现强可迁移对抗攻击
学习
OopspoO5 小时前
qcow2镜像大小压缩
学习·性能优化
A懿轩A5 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
居居飒5 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
kkflash36 小时前
提升专业素养的实用指南
学习·职场和发展