RabbitMQ入门

136 阅读8分钟

RabbitMQ

1. 概念背景

官网: www.rabbitmq.com/getstarted.…

一种消息队列,用于 异步处理事务

2. 常用消息队列对比

image.png

总结一下就是

rabbitmq的性能居中 小项目常用

kafka的能力更强 适用的场景更加复杂

3. RabbitMQ的核心概念

架构图

image.png Broker: 接收和分发消息的应用,RabbitMQ Server就是 Message Broker

Virtual host: 出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网 络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等

Connection:publisher/consumer 和 broker 之间的 TCP 连接

Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线 程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销

Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)

Queue:消息最终被送到这里等待 consumer 取走

Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存 到 exchange 中的查询表中,用于 message 的分发依据

4.RabbitMQ 快速入门

安装及部署

RabbitMQ类似于Redis 有服务器与客户,需要应用提供服务

服务器应用安装

(81条消息) Docker部署RabbitMQ_普通网友的博客-CSDN博客

入门案例

入门需求: 利用生产者发送消息到MQ ,消费者消费消息

操作步骤:

  • 创建生产者工程和消费者工程
  • 添加依赖
<dependencies>
        <dependency>
           <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.6.0</version>
        </dependency>
</dependencies>

编写生产者


public class Send {
	// 消息所在的队列
    private final static String QUEUE_NAME = "hello";
    public static void main(String[] argv) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
	// 设置rabbitmq的ip地址
        factory.setHost("192.168.xxx.xxx");
        try (Connection connection = factory.newConnection();
             Channel channel = connection.createChannel()) {
           /**
             * 声明队列
             * 第一个参数queue:队列名称
             * 第二个参数durable:是否持久化
             * 第三个参数Exclusive:
	     * 排他队列,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。
             * 这里需要注意三点:
             *   1. 排他队列是基于连接可见的,同一连接的不同通道是可以同时访问同一个连接创建的排他队列的。
             *   2. "首次",如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同。
             *   3. 即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除的。
             *   这种队列适用于只限于一个客户端发送读取消息的应用场景。
             * 第四个参数Auto-delete:自动删除,如果该队列没有任何订阅的消费者的话,该队列会被自动删除。
             *         这种队列适用于临时队列。
             */   
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

编写消费者

public class Recv {
	// 消息所在的队列
    private final static String QUEUE_NAME = "hello";
    public static void main(String[] argv) throws Exception {
	// 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.xxx.xxx");
        // 创建连接
	Connection connection = factory.newConnection();
        // 创建channel
	Channel channel = connection.createChannel();
	// 声明接收的队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
	// 如果接收成功 触发的回调方法
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
	// 1.队列名称
	// 2.是否自动确认
	// 3.回调函数 用于处理接收到的消息
	// 4.一个消费者标签的回调函数,可以在需要的情况下保存该标签或执行其他操作。
        channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
    }
}

五种基本的工作模式

简单模式

image.png 一对一 一个发一个接收 代码就是入门案例中的

Work工作模式

image.png

生产者

 // 循环发送很多条消息
for(int i = 0 ; i <= 20 ; i++){
            channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,(message+i).getBytes("UTF-8"));
            System.out.println(message+i);
}

消费者1

public class Recver1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
 
        String QUEUE_NAME = "work_queue";
        channel.queueDeclare(QUEUE_NAME,false,false,false, null);
 
        //接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                try {
                    String msg = new String(body);
                    System.out.println("Recver1:" + msg);
                    channel.basicAck(envelope.getDeliveryTag(),false);
 
                    Thread.sleep(2000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        channel.basicConsume(QUEUE_NAME,false, consumer);
    }
}

消费者2

public class Recver1 {
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
 
        String QUEUE_NAME = "work_queue";
        channel.queueDeclare(QUEUE_NAME,false,false,false, null);
 
        //接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                try {
                    String msg = new String(body);
                    System.out.println("Recver1:" + msg);
                    channel.basicAck(envelope.getDeliveryTag(),false);
 
                    Thread.sleep(2000);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        };
        channel.basicConsume(QUEUE_NAME,false, consumer);
    }
}

即两个类都接收一个项目的消息队列,谁接收了谁就处理

存在的问题是: 目前两个类的接收能力可能不一样,但处理的消息数相同,无法发挥全部性能

解决 :

​channel.basicQos(1)​是用于设置消费者的预取计数值(prefetch count)。

​basicQos​方法用于控制消费者从队列中预获取(prefetch)的消息数量。它接受一个参数,即预取计数值,用于限制消费者在收到确认之前可以预取的消息数量。

在这里,channel.basicQos(1)​将预取计数值设置为1,表示每次只预取1条消息。这样设置有几个效果:

  1. 公平分发:当多个消费者同时订阅同一个队列时,预取计数值为1可以确保消息以轮询(round-robin)的方式公平分发给各个消费者,避免某个消费者长时间占用高负载的情况。
  2. 负载均衡:预取计数值为1也可以实现负载均衡。当有多个消费者时,RabbitMQ会将消息均匀地发送给消费者,避免某个消费者被大量消息堆积和处理压力过大。

需要注意的是,basicQos​方法只对手动确认模式(manual acknowledge mode)的消费者有效,即当消费者通过调用channel.basicAck​方法手动确认消息时。在自动确认模式下,预取计数值不生效,消费者会立即接收所有可用的消息。

希望这能帮助你理解channel.basicQos(1)​的作用。如果还有其他问题,请随时提问。

Pub/Sub模式

image.png

p 将消息发给交换机,只要queue订阅了此交换机,那么都会接收到消息

P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)

C:消费者,消息的接收者,会一直等待消息到来

Queue:消息队列,接收消息、缓存消息

Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、

递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。

Exchange有常见以下3种类型:

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

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!

生产者

  channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
  String message = "info: Hello World!";
  channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
  System.out.println(" [x] Sent '" + message + "'");

​basicPublish​ 方法用于将消息发布到指定的队列中。它接受四个参数:

  • ​exchange​:交换机名称。在这里我们传递一个空字符串,表示使用默认的交换机。
  • ​routingKey​:路由键。它指定了消息在交换机和队列之间的路由规则。在这里我们传递的是队列的名称,表示将消息发送到该队列。
  • ​basicProperties​:消息的基本属性。在这里我们传递了一个空值 null​,表示没有附加的属性。
  • ​body​:消息的内容,即消息的字节数组。在这里我们将字符串 message​ 转换为 UTF-8 编码的字节数组作为消息的内容。

消费者

	// 声明所订阅的交换机
	channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        String queueName = channel.queueDeclare().getQueue();
        //绑定队列
	channel.queueBind(queueName, EXCHANGE_NAME, "");
        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        DeliverCallback deliverCallback = (consumerTag, delivery) -> {
            String message = new String(delivery.getBody(), "UTF-8");
            System.out.println(" [x] Received '" + message + "'");
        };
        channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });

​channel.queueBind(queueName, EXCHANGE_NAME, "");​ 是用于绑定队列和交换机的方法调用。

  • ​queueName​:要绑定的队列的名称。
  • ​exchangeName​:要绑定到的交换机的名称。
  • ​routingKey​:路由键。它指定了消息在交换机和队列之间的路由规则。在这里我们传递了一个空字符串 ""​,表示使用默认的路由键。

Routing路由模式

image.png

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

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

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

Topic模式

Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型

Exchange 可以让队列在绑定 Routing key 的时候使用通配符!

Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert

通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert

image.png

工作模式总结

1、简单模式 HelloWorld

一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)。

2、工作队列模式 Work Queue

一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。

3、发布订阅模式 Publish/subscribe

需要设置类型为 fanout 的交换机 ,并且交换机和队列进行绑定 ,当发送消息到交换机后,交换机会将消

息发送到绑定的队列。

4、路由模式 Routing

需要设置类型为 direct 的交换机 ,交换机和队列进行绑定 , 并且指定 routing key,当发送消息到交换机 后 ,交换机会根据 routing key 将消息发送到对应的队列。

5、通配符模式 Topic

需要设置类型为 topic 的交换机 ,交换机和队列进行绑定 ,并且指定通配符方式的 routing key ,当发送消息到交换机后 ,交换机会根据 routing key 将消息发送到对应的队列。

RabbitMQ应用场景

  1. 服务间异步通信

image.png

  1. 顺序消费

    用单一队列去监听,顺序执行每一条消息

  2. 定时任务

    利用私信队列 来进行定时任务

  3. 请求削峰

image.png

#bug#

rabbitmq启动后无法打开页面-->安装的是官方镜像没有启动rabbitmq_management插件

(78条消息) RabbitMQ启动后无法打开界面_rabbitmq已启动无法访问主页_tag心动的博客-CSDN博客

Linux部署RabbitMQ

拉取镜像

// 拉取镜像 默认是最新版的
docker pull rabbitmq

启动rabbitmq


// -d 后台启动
// 本条命令包括安装Web页面管理的 rabbitmq:management组件,账号和密码都为 admin ;
// -p 后面参数表示公网IP地址的端口号对应容器内部的端口号。

docker run -d --name rabbit -e RABBITMQ_DEFAULT_USER=admin 
-e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 -p 25672:25672 -p 61613:61613 -p 1883:1883 rabbitmq

关闭docker容器

docker stop [容器名或ID]