【RabbitMQ 笔记】— 基本概念

RabbitMQ 整体上是一个生产者消费者模型,主要负责接收、存储和转发消息。整体模型架构图如下:
image

生产者和消费者

生产者

Producer:生产者,就是投递消息的一方。生产者创建消息,然后发布到 RabbitMQ 中。

消息一般可以分为两部分:

  • 消息体(payload):带有业务逻辑结构的数据,比如 JSON 字符串。
  • 标签(label):用来表述这条消息,比如一个交换器的名称和一个路由键。生产者把消息交给 RabbitMQ,RabbitMQ 会根据这个标签把消息发送给感兴趣的消费者(Consumer)。

注:payload 中文是负载的意思,在这里也就是实际业务数据,在一些源码中定义 ResponseVo 也有类似的声明。如 private T payload

生产者示例代码如下:

public class RabbitProducer {
    private static final String EXCHANGE_NAME = "exchange_demo";
    private static final String ROUTING_KEY = "routing_key_demo";
    private static final String QUEUE_NAME = "queue_demo";
    private static final String IP_ADDRESS = "127.0.0.1";
    private static final int PORT = 5672; // RabbitMQ default port

    public static void main(String[] args) {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(IP_ADDRESS);
        factory.setPort(PORT);
        factory.setUsername("guest");
        factory.setPassword("guest");

        // 创建连接
        try (Connection connection = factory.newConnection()) {
            // 创建信道
            Channel channel = connection.createChannel();
            // 创建一个 type = "direct"、持久化的、非自动删除的交换器
            channel.queueDeclare(QUEUE_NAME, true, false, false, null);
            // 将交换器与队列通过路由键绑定
            // 其实这里需要的是BindingKey, 只不过这里的 BindingKey 和 RoutingKey 是同一个东西
            channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

            String message = "Hello World";
            // 发送消息的时候,需要的是 RoutingKey
            channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

            // 关闭资源
            channel.close();
        } catch (TimeoutException | IOException e) {
            e.printStackTrace();
        }
    }
}

消费者

Consumer:消费者,就是接受消息的一方。

消费者连接到 RabbitMQ 服务器,并订阅到队列上。当消费者消费一条消息后,只是消费消息体(payload)。因为消息在路由的过程中,消息的标签会丢弃,存入到队列的消息也只有消息体。 消费者只会得到消息体,不知道消息的生产者是谁,当然也不用知道是谁。

消费者示例代码(可结合上面生产者代码本地运行,需要先安装 Erlang 和 RabbitMQ)

public class RabbitConsumer {
    private static final String QUEUE_NAME = "queue_demo";
    private static final String IP_ADDRESS = "127.0.0.1";
    private static final int PORT = 5672;

    public static void main(String[] args) {
        Address[] addresses = new Address[]{new Address(IP_ADDRESS, PORT)};
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");

        // 创建连接
        try (Connection connection = factory.newConnection(addresses)) {
            // 创建信道
            final Channel channel = connection.createChannel();
            // 设置客户端最多接受未被ack消息的个数
            channel.basicQos(64);
            Consumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag,
                                           Envelope envelope,
                                           AMQP.BasicProperties properties,
                                           byte[] body) throws IOException {
                    System.out.println("recv msg:" + new String(body));
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            };
            channel.basicConsume(QUEUE_NAME, consumer);

            // 等待回调函数执行完毕之后,关闭资源
            TimeUnit.SECONDS.sleep(5);
            channel.close();
        } catch (TimeoutException | IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Broker

Broker:消息中间件的服务节点。

对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以看做是一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下一个 RabbitMQ Broker 看作是一台 RabbitMQ 服务器。

Broker 中文意思是代理人,掮客。之前群里一哥们说直接理解成拉皮条,觉得挺有意思。正常情况下生产者创建完消息直接给消费者不就完了,非要经过 MQ(中间商赚差价??),那 MQ 不就是拉皮条(形象啊)。

队列

Queue:队列,是 RabbitMQ 内部对象,用来存储消息(RabbitMQ 消息只能存储在队列中)。

多个消费可以订阅同一个队列,这时队列中的消息会被平摊(Round-Robin,即轮询)给多个消费者进行消费,而不是每个消费者都会受到所有消息。

交换器、路由键、绑定

交换器

Exchange:交换器,在RabbitMQ 中,生产者并不是直接将消息投递到队列中,而是发送到交换器,由交换器将消息路由到一个或多个队列中。如果路由找不到,或许返回给生产者,或许直接丢弃。

路由键

RoutingKey:路由键。

生产者将消息发送给交换器的时候,一般会指定一个 RoutingKey, 用来指定这个消息的路由规则。而这个 RoutingKey 需要与交换器和绑定建 BindingKey 联合使用才会生效。

绑定

BindingKey:绑定。

RabbitMQ 中通过绑定将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样就 RabbitMQ 就知道如何正确的路由消息到队列了。

这里路由键、绑定建有点绕,刚接触不太好理解,先看张图:
image

交换器如何知道将消息路由到哪个队列?这就需要将交换器和队列绑定起来,那如何绑定,就要通过绑定键。

再看代码

// 创建通道
Channel channel = connection.createChannel();
// 创建一个 type = "direct"、持久化的、非自动删除的交换器
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true, false, null);
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 这里将交换器和队列进行绑定,只不过 direct 类型的交互器,路由键和绑定键相同
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, ROUTING_KEY);

String message = "Hello World";
// 发消息的时候,需要指定路由键
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

关于路由键、绑定键可以这么理解:

  • 在使用绑定的时候,需要绑定键
  • 在发消息的时候,需要的是路由键
  • 概况就是:生产者将消息发送给交换器,交换器和队列绑定。当生产者发送消息时所携带的 RoutingKey 与绑定时的 BindingKey 相匹配时,消息即被存入相应的队列之中。

交换器的种类

RabbitMQ 常用的交换器类型有四种:fanout、direct、topic、headers。

  • fanout:它会把所有发送到该交换器的消息全部路由到与该交换器绑定的队列中(无视 BindingKey,即绑定键)。
  • direct:将消息路由到 RoutingKey 和 BindingKey 完全匹配的队列中。
  • topic:将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中,只不过这个匹配规则有些不同,有如下约定:
  • RoutingKey 为一个点号“.”分隔的字符串(被点号分隔的每一段独立的字符串被称为一个单词),如com.rabbitmq.client;
  • BindingKey 和 RoutingKey 一样也是点号“.”分隔的字符串
  • BindingKey 中可以存在两种特殊的字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)
  • headers:该类型交换器不依赖与路由的匹配规则来路由消息,而是根据消息内容中的 headers 属性进行匹配。该类型的交换器性能会很差,而且也不实用,基本上看不到他的存在。

四种类型的交换器测试代码可以参考我的 GitHub

运转流程

生产者发送消息流程如下:

  1. 生产者连接到 RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
  2. 生产者声明一个交换器,并设置相关属性,比如交换器类型,是否持久化等。
  3. 生产者声明一个队列并设置相关属性,如是否排他、是否持久化、是否自动删除等。
  4. 生产者通过路由键将交换器和队列绑定起来。
  5. 生产者发送消息到 RabbitMQ Broker,其中包含路由键,交换器等信息。
  6. 相应的交换器根据接收到的路由键查找匹配的队列。
  7. 如果找到,则将消息存入队列。
  8. 如果没有找到,则根据生产者配置的属性选择是否丢弃还是回退给生产者
  9. 关闭信道
  10. 关闭连接

消费者接收消息过程如下:

  1. 消费者连接到 RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
  2. 消费者向 RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备动作。
  3. 等待 RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息
  4. 消费者确认(Ack)接收到消息
  5. RabbitMQ 从队列中删除相应已被确认的消息
  6. 关闭信道
  7. 关闭连接

连接复用

不管是生产者还是消费者,都需要和 RabbitMQ Broker 建立连接,这个连接就是一条 TCP 连接,也就是 Connection。连接建立完成,客户端紧接着可以创建一个信道(Channel),每个信道会被指定一个唯一的 ID。信道是建立在 Connection 之上的虚拟连接,RabbitMQ 处理每条指令都是通过信道完成的。

为什么要使用信道呢?试想这么个场景,多个线程要从 RabbitMQ 生产消息或消费消息,必然需要建立很多 Connection,也就是 TCP 连接。然后对于操作系统来说,建立和销毁 TCP 连接是非常昂贵的开销,如果遇到高峰期,性能瓶颈也随之而来。RabbitMQ 采用类型 NIO 的做法,选择 TCP 连接复用,不仅可以减少性能开销,同事也便于管理。

当然如果信道的流量不是很大时,复用单一的 Connection 可以在有效的节省 TCP 连接资源。如果信道的流量很大,那就需要开辟多个 Connection,将信道分摊到多个 Connection 上。

参考

内容整理自 《RabbitMQ实战指南》

posted @ 2022-06-09 13:13  Tailife  阅读(27)  评论(0编辑  收藏  举报