RabbitMQ

一、MQ 概念

1. MQ

MQ(Message Queue)消息队列,是在消息的传输过程中保存消息的容器。多用于系统之间的异步通信。

同步通信相当于两个人当面对话,你一言我一语。必须及时回复。

异步通信相当于通过第三方转述对话,可能有消息的延迟,但不需要二人时刻保持联系。

2. 优势

消息队列允许不同组件或系统之间通过发送消息进行通信,而不需要直接耦合在一起。这使得系统的各个部分能够独立地进行工作,提高了系统的可伸缩性和灵活性。

消息队列的基本原理是生产者(Producer)产生消息并将其发送到队列,然后消费者(Consumer)从队列中获取消息并进行处理。这种异步通信的方式有多个优点:

  1. 解耦: 生产者和消费者之间通过消息队列进行通信,它们不需要直接了解彼此的存在。这种解耦性提高了系统的灵活性和可维护性。

  2. 异步处理: 生产者将消息发送到队列后,可以立即继续执行其他操作,而不必等待消费者的响应。消费者则在就绪时异步地从队列中获取消息并进行处理。

  3. 削峰填谷: 消息队列可以平滑处理系统中的峰值流量。即使生产者和消费者之间的处理速度不一致,消息队列能够调节消息的流量,防止系统因瞬时流量过大而崩溃。

  4. 可靠性: 消息队列通常具有持久性特性,确保消息在传递过程中不会丢失。这有助于确保系统的可靠性和数据完整性。

  5. 顺序性: 消息队列可以保证消息按照一定的顺序被消费,这对于某些应用场景(如订单处理)非常重要。

3. 劣势

  1. 复杂性: 引入消息队列会增加系统的复杂性。配置、管理和监控消息队列可能需要额外的工作,特别是在大规模系统中。

  2. 一致性问题: 在某些情况下,由于网络故障、节点故障或其他原因,消息队列可能导致消息重复、丢失或无序。确保消息队列系统具有高可用性和一致性是一个挑战。

  3. 延迟: 使用消息队列引入了一定的传输和处理延迟。在某些对实时性要求非常高的应用中,这种延迟可能是不可接受的。

  4. 维护和运营成本: 部署、维护和运营消息队列系统可能需要专业知识,增加了系统的总体成本。此外,确保消息队列的高可用性和性能可能需要更多的资源和投资。

  5. 消息序列化和反序列化开销: 将消息序列化和反序列化为可传输的格式可能会引入一些开销,尤其是对于大量的消息和复杂的消息结构。

  6. 消息顺序保障: 一些消息队列系统可能在维护消息的顺序性方面面临挑战。虽然某些系统可以提供有序性保障,但这可能在性能方面有一些牺牲。

  7. 依赖性: 使用消息队列使得系统组件之间存在依赖关系。如果消息队列出现问题,可能影响整个系统的可用性。

  8. 规模性: 随着系统规模的增加,一些消息队列系统可能遇到性能瓶颈,需要额外的调优和优化。

4. MQ应用场景

抢红包、秒杀活动、抢火车票等

  这些业务场景都是短时间内需要处理大量请求,如果直接连接系统处理业务,会耗费大量资源,有可能造成系统瘫痪。

  而使用MQ后,可以先让用户将请求发送到MQ中,MQ会先保存请求消息,不会占用系统资源,且MQ会进行消息排序,先请求的秒杀成功,后请求的秒杀失败。

消息分发

  如电商网站要推送促销信息,该业务耗费时间较多,但对时效性要求不高,可以使用MQ做消息分发。

数据同步

  假如我们需要将数据保存到数据库之外,还需要一段时间将数据同步到缓存(如Redis)、搜索引擎(如Elasticsearch)中。此时可以将数据库的数据作为消息发送到MQ中,并同步到缓存、搜索引擎中。

异步处理

  在电商系统中,订单完成后,需要及时的通知子系统(进销存系统发货,用户服务积分,发送短信)进行下一步操作。为了保证订单系统的高性能,应该直接返回订单结果,之后让MQ通知子系统做其他非实时的业务操作。这样能保证核心业务的高效及时。

离线处理

  在银行系统中,如果要查询近十年的历史账单,这是非常耗时的操作。如果发送同步请求,则会花费大量时间等待响应。此时使用MQ发送异步请求,等到查询出结果后获取结果即可。

二、RabbitMQ

RabbitMQ是由Erlang语言编写的基于AMQP的MQ产品。

1. AMQP 协议

即Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,专门为消息中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受不同中间件产品,不同开发语言等条件的限制。2006年AMQP规范发布,类比HTTP。

  •  协议定义: AMQP是一种二进制协议,定义了一组规范和协议层,用于在消息代理(Message Broker)和客户端之间进行通信。它提供了一种统一的方式来描述、传递、存储和路由消息。
  • 消息模型: AMQP基于消息模型,其中消息由生产者(Producer)创建并发送到消息代理,然后由消费者(Consumer)从消息代理中获取和处理。消息可以包含结构化数据,例如JSON或XML。

  • 交换机(Exchanges): 在AMQP中,交换机负责接收生产者发送的消息,并将它们路由到一个或多个队列。AMQP定义了不同类型的交换机,例如直连交换机、主题交换机和扇出交换机,以支持不同的消息路由模式。
  • 队列: 队列是消息的存储位置,它们由消息代理维护。消费者可以订阅队列以接收其中的消息。AMQP中的队列具有一些特性,如持久性、排他性和自动删除。
  • 绑定(Bindings): 绑定定义了将交换机和队列连接起来的规则。它决定了消息从交换机流向哪个队列,以及在什么条件下发生。
  • 通道(Channels): 在AMQP中,连接由一个或多个通道组成。通道是在连接内的独立通信路径,使得多个通信流可以并行运行。通道提供了轻量级的、低开销的通信机制。
  • 安全性: AMQP支持身份验证和授权,确保只有经过身份验证的实体才能访问和发送消息。它还提供了加密通信的选项,以确保消息的机密性。

  • 消息可靠性: AMQP通过确认机制和事务来提供消息的可靠性。生产者可以请求消息代理确认消息的接收,而消费者可以确认已成功处理的消息。

详情参考阿里开发者社区:https://developer.aliyun.com/article/847370

2. RabbitMQ 工作过程

RabbitMQ 的角色主要有生产者、交换机、队列和消费者。

生产者(Publisher)将消息发布到交换机(Exchange),交换机根据规则将消息分发给交换机绑定的队列(Queue),队列再将消息投递 给订阅了此队列的消费者。

3. RabbitMQ 工作原理

  • Producer

    消息的生产者。也是一个向交换机发布消息的客户端应用程序。生成消息并将其发送到 RabbitMQ 代理。消息可以包含任何类型的信息,例如任务、事件等。

  • Connection

    连接。生产者/消费者和RabbitMQ服务器之间建立的TCP连接。

  • Channel

    信道。是TCP里面的虚拟连接。例如:Connection相当于电缆, Channel相当于独立光纤束,一条TCP连接中可以创建多条信道,增加连接效率。无论是发布消息、接收消息、订阅队列都是 通过信道完成的。

  • Broker

    消息队列服务器实体。即RabbitMQ服务器

  • Virtual host

    虚拟主机。出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换机、绑定和权限机制。当多个不同的用户使用同一个RabbitMQ服务器时,可以划分出多个虚拟主机。RabbitMQ默认的虚拟主机路径是 /

  • Exchange

    交换机。是消息的中转站。生产者发送的消息首先被发送到交换机。交换机根据预定义的规则(绑定)将消息路由到一个或多个队列中。

  • Queue

    消息队列。用来保存消息直到发送给消费者。它是消息的容器, 也是消息的终点。消息一直在队列里面,等待消费者链接到这个队列将其取走。

  • Binding

    消息队列和交换机之间的虚拟连接,绑定中包含路由规则,绑定信息保存到交换机的路由表中,作为消息的分发依据。

  • Consumer

    消息的消费者。表示一个从消息队列中取得消息的客户端应用程序。

  • 消息确认(可选)

   消费者处理消息后,可以选择向 RabbitMQ 发送确认消息。这是可选的步骤,但通常用于确保消息被成功处理,避免消息在处理期间丢失。

  • 消息持久性(可选)

      在生产者发送消息时,可以选择使消息持久化。这意味着即使 RabbitMQ 代理重启,消息也不会丢失。这对于关键性的任务和数据是很重要的。

三、RabbitMQ 工作模式

  1. 简单队列模式(Simple Queue):

    • 单一的生产者将消息发送到一个队列,而单一的消费者从队列中接收并处理消息。
    • 这是最基本的模式,适用于简单的应用场景,但对于处理大量消息或需要水平扩展的系统可能不够。
  2. 工作队列模式(Work Queues):

    • 多个生产者将消息发送到一个共享的队列,多个消费者同时监听该队列。
    • 每条消息只能被一个消费者处理,确保消息的顺序性和不重复处理。适用于处理较大的工作负载。
  3. 发布/订阅模式(Publish/Subscribe):

    • 生产者将消息发送到交换机(fanout交换机),交换机将消息广播到与之绑定的多个队列。
    • 多个消费者分别监听不同的队列,实现消息的发布和订阅。适用于广播消息给多个消费者的场景。
  4. 路由模式(Routing):

    • 生产者将消息发送到交换机,并指定一个路由键(Routing Key)。
    • 消费者通过绑定到交换机的队列指定路由键,只有指定路由键的消息会被相应队列接收。适用于有选择性地接收消息的场景。
  5. 主题模式(Topics):

    • 类似于路由模式,但路由键可以使用通配符,以实现更灵活的消息路由。
    • 消费者通过使用通配符的路由键来订阅消息,可以根据模式匹配接收感兴趣的消息。适用于更复杂的消息过滤和分发。
    • 通配符规则:

      1. 消息设置RoutingKey时,RoutingKey由多个单词构成,中间以 . 分割。

      2. 队列设置RoutingKey时, # 可以匹配任意多个单词, * 可以匹配任意一个单词。

代码实现

在SpringBoot工程环境下引入maven依赖:

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

1. 简单队列模式

 生产者:

复制代码
public class Producer {
    @SneakyThrows
    public static void main(String[] args) {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");

        //2. 创建连接
        Connection connection = connectionFactory.newConnection();

        //3. 创建信道
        Channel channel = connection.createChannel();

        //4. 创建队列
        /**
         * 参数1:队列名
         * 参数2:是否持久化,true表示MQ重启后队列还在
         * 参数3:是否私有化,false表示所有消费者都可以访问,true表示只有第一次拥有它的消费者才能访问
         * 参数4:是否自动删除,true表示不在使用队列时自动删除队列
         * 参数5:其他额外参数
         */
        channel.queueDeclare("simple_queue",false,false,false,null);
        //5. 发送消息
        /**
         * 参数1:交换机名 ""表示默认交换机
         * 参数2:路由键,简单模式就是队列名
         * 参数3:其他额外参数
         * 参数4:要传递的消息字节数组
         */
        String message = "hello simple message";
        channel.basicPublish("","simple_queue",null,message.getBytes());
        //6. 关闭信道和连接
        channel.close();
        connection.close();
        System.out.println("发送成功");
    }
}
Producer.java
复制代码

消费者:

复制代码
public class Consumer {
    @SneakyThrows
    public static void main(String[] args) {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");
        //2. 创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 创建信道
        Channel channel = connection.createChannel();
        //4. 监听队列
        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已接收,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("simple_queue",true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("接收的消息为:"+msg);
            }
        });
        
    }
}
Cousumer.java
复制代码

2. 工作队列模式

生产者:

复制代码
public class Producer {
    @SneakyThrows
    public static void main(String[] args) {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");

        //2. 创建连接
        Connection connection = connectionFactory.newConnection();

        //3. 创建信道
        Channel channel = connection.createChannel();

        //4. 创建队列
        /**
         * 参数1:队列名
         * 参数2:是否持久化,true表示MQ重启后队列还在
         * 参数3:是否私有化,false表示所有消费者都可以访问,true表示只有第一次拥有它的消费者才能访问
         * 参数4:是否自动删除,true表示不在使用队列时自动删除队列
         * 参数5:其他额外参数
         */
        channel.queueDeclare("work_queue",true,false,false,null);
        //5. 发送消息
        /**
         * 参数1:交换机名 ""表示默认交换机
         * 参数2:路由键,简单模式就是队列名
         * 参数3:其他额外参数 ,这里表示消息为持久化消息,既除了保存内存还会保存到磁盘中
         * 参数4:要传递的消息字节数组
         */
        for (int i = 0; i < 10; i++) {
            channel.basicPublish("","work_queue", MessageProperties.PERSISTENT_TEXT_PLAIN,("第"+i+"条消息").getBytes());
        }
        channel.close();
        connection.close();
        System.out.println("发送成功");
    }
}
Producer.java
复制代码

消费者1:

复制代码
public class Consumer1 {
    @SneakyThrows
    public static void main(String[] args) {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");
        //2. 创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 创建信道
        Channel channel = connection.createChannel();
        //4. 监听队列
        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已接收,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("work_queue",true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费者1接收到消息:" + new String(body, StandardCharsets.UTF_8));
            }
        });
    }
}
Consumer1.java
复制代码

消费者2:

复制代码
public class Consumer2 {
    @SneakyThrows
    public static void main(String[] args) {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");
        //2. 创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 创建信道
        Channel channel = connection.createChannel();
        //4. 监听队列
        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已接收,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("work_queue",true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费者2接收到消息:" + new String(body, StandardCharsets.UTF_8));
            }
        });
    }
}
Consumer2.java
复制代码

消费者3:

复制代码
public class Consumer3 {
    @SneakyThrows
    public static void main(String[] args) {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");
        //2. 创建连接
        Connection connection = connectionFactory.newConnection();
        //3. 创建信道
        Channel channel = connection.createChannel();
        //4. 监听队列
        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已接收,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("work_queue",true,new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费者3接收到消息:" + new String(body, StandardCharsets.UTF_8));
            }
        });
    }
}
Cousumer3.java
复制代码

3. 发布订阅模式

生产者:

复制代码
public class Producer {
    @SneakyThrows
    public static void main(String[] args) {
        //1. 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");

        //2. 创建连接
        Connection connection = connectionFactory.newConnection();

        //3. 创建信道
        Channel channel = connection.createChannel();

        //4. 创建交换机
        /**
         * 参数1:交换机名
         * 参数2:交换机类型
         * 参数3:交换机持久化
         */
        channel.exchangeDeclare("exchange_fanout",  BuiltinExchangeType.FANOUT,true);
        //5. 创建队列,如果队列存在,则直接使用
        /**
         * 参数1:队列名
         * 参数2:是否持久化,true表示MQ重启后队列还在
         * 参数3:是否私有化,false表示所有消费者都可以访问,true表示只有第一次拥有它的消费者才能访问
         * 参数4:是否自动删除,true表示不在使用队列时自动删除队列
         * 参数5:其他额外参数
         */
        channel.queueDeclare("send_email",true,false,false,null);
        channel.queueDeclare("send_message",true,false,false,null);
        channel.queueDeclare("send_station",true,false,false,null);
        //6. 交换机绑定队列
        /**
         * 参数1:队列名
         * 参数2:交换机名
         * 参数3:路由关键字 ,发布订阅模式写""即可
         */
        channel.queueBind("send_email","exchange_fanout","");
        channel.queueBind("send_message","exchange_fanout","");
        channel.queueBind("send_station","exchange_fanout","");
        //7.发送消息
        /**
         * 参数1:交换机名 ""表示默认交换机
         * 参数2:路由键,简单模式就是队列名
         * 参数3:其他额外参数 ,这里表示消息为持久化消息,既除了保存内存还会保存到磁盘中
         * 参数4:要传递的消息字节数组
         */
        for (int i = 1; i <=10 ; i++) {
            channel.basicPublish("exchange_fanout",
                    "",
                    null,
                    ("你好,尊敬的用户,秒杀商品开抢了"+i).getBytes());
        }
        //8.关闭信道和连接
        channel.close();
        connection.close();
        System.out.println("发送成功");

    }
}
Producer.java
复制代码

邮件消费者:

复制代码
public class SendMail {
    @SneakyThrows
    public static void main(String[] args) {
        //1.创建链接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");

        //2.创建链接
        Connection connection = connectionFactory.newConnection();
        //3.创建信道
        Channel channel = connection.createChannel();
        //4.监听队列
        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已接收,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("send_email", true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body,"utf-8");
                System.out.println("接收邮件,消息为:"+message);
            }
        });
    }
}
SendMail.java
复制代码

短信消费者:

复制代码
public class SendMessage {
    @SneakyThrows
    public static void main(String[] args) {
        //1.创建链接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");
        //2.创建链接
        Connection connection = connectionFactory.newConnection();
        //3.创建信道
        Channel channel = connection.createChannel();
        //4.监听队列
        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已接收,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("send_message", true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body,"utf-8");
                System.out.println("接收短信,消息为:"+message);
            }
        });
    }
}
SendMessage.java
复制代码

公共消费者1、2:

复制代码
public class Consumer1 {
    @SneakyThrows
    public static void main(String[] args) {
        //1.创建链接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.77.129");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("young");
        connectionFactory.setPassword("young");
        connectionFactory.setVirtualHost("/young");
        //2.创建链接
        Connection connection = connectionFactory.newConnection();
        //3.创建信道
        Channel channel = connection.createChannel();
        //4.监听队列
        /**
         * 参数1:监听的队列名
         * 参数2:是否自动签收,如果设置为false,则需要手动确认消息已接收,否则MQ会一直发送消息
         * 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
         */
        channel.basicConsume("send_station", true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body,"utf-8");
                System.out.println("公共接收,消息为:"+message);
            }
        });
    }
}
Consumer1.java
复制代码

 

 

 

 

 

posted @   ygdgg  阅读(104)  评论(4编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示