RabbitMQ(一) -- 初识RabbitMQ

3,558 阅读5分钟

一:RabbitMQ简介

RabbitMQ队列基于AMQP协议使用Erlang语言开发实现,支持多客户端类型如Java、Ruby、Go、PHP等。其余比较流行的消息队列中间件,相对的还有RocketMQ、ActiveMQ、Kafka等等。后续需要测试RabbitMQ,如果没有安装服务请移步RabbitMQ服务安装

MQ总结而言最大的三个特点就是异步、削峰、解耦,如下图所示。其余复杂的概念就不照本宣科抄袭了,反正写在这里也是废话。最简单的概括就是存储数据的容器,与MySQL数据库、Redis数据库等类似,区别在于自身实现特点决定了应用场景

在这里插入图片描述

二:AMQP协议模型

可以简单理解为一套消息传递的标准协议,例如HTTP协议、HTTPS协议都有自身相应的规则。下图所示为AMQP协议的基础模型

在这里插入图片描述

序号 组件 描述
1 Publisher 生产者,应用客户端用于向服务端发送消息
2 Connection 连接,生产者与消费者客户端都需要Connection连接消息应用服务端。Connection连接创建、销毁成本太高,衍生轻量级逻辑连接Channel
3 Channel 轻量级逻辑连接,也称之为信道。Channel之间完全隔离,线程安全
4 Broker 消息应用服务主体,客户端写代码不会涉及,相当于一个逻辑上的概念
5 Virtual Host 相当于namespace,多用户时每个用户可以在自己分配的Virtual Host区域操作
6 Exchange 消息交换器,生产者不直接与队列耦合,通过交换器进行消息转发
7 Binding 绑定关系,消息交换器与队列之间绑定的关系,通过与生产者传递消息携带的RoutingKey比较得知消息路由转发到哪个绑定队列
8 Queue 队列,消息最后储存的位置
9 Consumer 消费者,直接通过与队列耦合进行消费,这与生产者具备一定区别

三:AMQP交互流程

HTTP协议连接创建、销毁过程划分为三次握手、四次挥手,那么在AMQP协议中的生产者、消费者客户端与消息应用服务端交互是怎样的一个流程设计?生产者、消费者与服务端连接创建、销毁命令保持一致,但是中间逻辑流程涉及到的命令肯定不同。所以将整个生产 -- 消费流程划分为三个部分,连接创建与销毁、生产消息、消费消息

3.1 连接创建销毁

连接的创建销毁包含四个模块的通信,首先是连接创建开启,然后信道创建开启,最后则是信道关闭、连接关闭。从流程中可以更加理解信道Channel是基于连接Connection的逻辑连接,必须依赖于连接Connection。其余的步骤则是命令 -- 确认命令的格式,确保客户端、服务端通信正常

在这里插入图片描述

3.2 消息生产

消息生产流程如下图所示,重点在于服务端对于这个操作是没有任何响应的。所以这里就是一个造成消息丢失的环节,当服务端与客户端之间网络连接出现波动等问题导致连接不可用关闭,这时候传输的消息就可能会丢失

在这里插入图片描述

3.3 消息消费

消息消费的重点在于确认机制,即消息由服务端推送至客户端后并不会立即删除,而是变更消息状态标志,等待客户端反馈确认结果后再执行相对应的逻辑。当然,反馈确认的结果可能有多种,操作也就有多种,后续文章再讲解

在这里插入图片描述

四:RabbitMQ客户端基本操作

前面讲过RabbitMQ支持多语言客户端,因为作者自身从事Jave开发,所以接下来示例将采用Java。其它计算机语言客户端可以参考RabbitMQ官网,如果直接在J2EE项目中测试RabbitMQ功能Demo可以引入下列依赖即可

		<!--RabbitMQ依赖-->
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.4.3</version>
        </dependency>
4.1 创建连接信道
    @SneakyThrows
    public static Channel createChannel(){
        
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        
        // 设置RabbitMQ服务应用信息
        // 服务默认端口5672、安装启动后会默认有个账号guest、密码guest
        factory.setHost("RabbitMQ应用服务安装IP地址");
        factory.setPort(5672);
        factory.setUsername("guest");
        factory.setPassword("guest");
        
        // 实例化连接
        Connection connection = factory.newConnection();
        
        // 获取信道实例
        Channel channel = connection.createChannel();
        return channel;
    }
4.2 推送消息

注意这里只是最基础的推送消息到交换器后根据routingKey与Binding进行消息路由,至于代码中涉及到的一些交换器或是队列、消息的特征属性后续讲解。当然交换器不止代码中出现的fanout一种,多种交换器提供不同消息路由特点

    @SneakyThrows
    public static void publishMessage(Channel channel,byte[] message){
        
        // 实例化消息服务组件
        String exchangeName = "exchangeName";
        String queueName = "queueName";
        String binding = "binding";
        String routingKey = binding;
        // 交换器持久化、自动删除
        boolean durable = true , autoDelete = false;
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT,durable,autoDelete,null);
        // 独占队列
        boolean exclusive = false;
        channel.queueDeclare(queueName,durable,exclusive,autoDelete,null);
        // 交换器、队列绑定
        channel.queueBind(queueName,exchangeName,binding);
        // 发送生产消息
        channel.basicPublish(exchangeName,routingKey,null,message);
        
    }
4.3 消费消息

消费消息这里其实就是涉及到了确认机制,前文已经提示过,后续也会深入讲解。注意一点就是消息消费使用DefaultConsumer类,该类实现接口Consumer,Consumer接口提供了系列消息处理的方法,也就是RabbitMQ中处理消息要么实现Consumer接口或者继承DefaultConsumer类重写逻辑。下面代码中重写的方法handleDelivery(),回头看一下RabbitMQ的消息消费交互流程命令就可以理解了

    @SneakyThrows
    public static void consumeMessage(Channel channel){
        String queueName = "queueName";
        String consumerTag = "consumerTag";
        // 消息消费实例
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery (String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {
                // 处理消息
            }
        };
        // 自动消息确认
        boolean autoAck = true;
        channel.basicConsume(queueName,autoAck,consumerTag,consumer);
    }
4.4 销毁连接信道

这里就需要注意关闭的顺序问题,也不是说直接关闭Connection有错,但是如果这个连接还有其它信道那么这里是不允许关闭Connection的,不然其它使用Channel的代码会抛出异常。即Connection关闭等于关闭所有Channel,Channel是依赖于Connection的逻辑连接

    @SneakyThrows
    public static void destroyConnection(Connection connection,Channel channel){
        channel.close();
        connection.close();
    }