RabbitMQ初步介绍(一)

一、什么是RabbitMQ

RabbitMQ,遵循AMQP协议,由内在高并发的erlanng语言开发,用在实时的对可靠性要求比较高的消息传递上。

  学过websocket的来理解rabbitMQ应该是非常简单的了,websocket是基于服务器和页面之间的通信协议,一次握手,多次通信。 而rabbitMQ就像是服务器之间的socket,一个服务器连上MQ监听,而另一个服务器只要通过MQ发送消息就能被监听服务器所接收。

  但是MQ和socket还是有区别的,socket相当于是页面直接监听服务器。而MQ就是服务器之间的中转站,例如邮箱,一个人投递信件给邮箱,另一个人去邮箱取,他们中间没有直接的关系,所以耦合度相比socket小了很多。

 

上图是最简单的MQ关系,生产者-MQ队列-消费者

 

二、docker部署RabbitMQ

2.1、获取镜像

docker pull rabbitmq:management

2.2、创建并运行容器:

docker run -d --hostname my-rabbit --name rabbit -p 8080:15672 rabbitmq:management
--hostname:指定容器主机名称
--name:指定容器名称
-p:将mq端口号映射到本地

或在运行时设置用户和密码 docker run
-d --hostname my-rabbit --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:management  15672:控制台端口号  5672:应用访问端口号

2.3、查看rabbit运行状况:

docker logs rabbit

2.4、访问平台

使用 http://ip:15672访问rabbit控制台

 三、专业术语介绍

1. 生产者: 在现实生活中就好比制造商品的工厂,他们是商品的生产者。生产者只意味着发送。发送消息的程序称之为一个生产者。

2. 队列:rabbitMQ就像一个仓库,一个仓库里面可以 有很多队列,每个队列才是服务器之间消息通信的载体。

3.消费者:消费者就好比是从商店购买或从仓库取走商品的人,消费的意思就是接收。消费者是一个程序,主要是等待接收消息。

4.交换器:在生产者和消息队列之间的交换器,功能类似于网络宽带的交换机,可以根据不同的关键字,将信息发送到不同的队列。

 

上图的E就是交换器,通过关键字绑定,如果生产者给的消息中指定类型是ERROR,就给队列1,如果是INFO或者WARN就给队列2。当然也可以一个关键字绑定两个队列。(INFO等字段自己可以定义,也可以用*,#来匹配。*(星号)表示一个单词#(井号)表示零个或者多个单词。 比如ok.yes可以被ok.*匹配到)

5.临时队列:根据需求临时创建的一条队列,在断开连接后自动删除。

四、案例演示

RabbitMQ 支持多种语言访问,以 Java 为例看下一般使用 RabbitMQ 的步骤。 
1、maven工程的pom文件中添加依赖 
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.5.1</version>
        </dependency>
2、消息生产者 
// 可靠生产
// https://www.rabbitmq.com/confirms.html
public class Producer {
    public static void main(String[] args) {
        // 1、创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2、设置连接属性
        factory.setHost("46.654.44.123");
        factory.setUsername("dfg");
        factory.setPassword("efgy");

        Connection connection = null;
        Channel channel = null;

        try {
            // 3、从连接工厂获取连接
            connection = factory.newConnection("生产者");
            // 4、从链接中创建通道
            channel = connection.createChannel();

            // 进入confirm模式, 每次发送消息,rabbtiqm处理之后会返回一个对应的回执消息
            AMQP.Confirm.SelectOk selectOk = channel.confirmSelect();
            // 增加监听器
            ArrayList<String> queues = new ArrayList<>();
            channel.addConfirmListener(new ConfirmListener () {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    // deliveryTag 同一个channel中此条消息的编号 。
                    // 业务..
                    System.out.println("受理成功 " + queues.get((int) deliveryTag) + " " + multiple);
                }

                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    // 失败重发
                    // queues.get((int) deliveryTag)
                    System.out.println("受理失败 " + deliveryTag);
                }
            });
            // 定义fanout类型的交换器
            channel.exchangeDeclare("ps_test", "fanout");

            for (int i = 0; i < 10; i++) {
                // 消息内容
                String message = "Hello " + i;
                queues.add(message);
                // 发送消息到ps_test交换器上
                AMQP.BasicProperties basicProperties = new AMQP.BasicProperties();
                channel.basicPublish("ps_test", "", basicProperties, message.getBytes());
                System.out.println("消息 " + message + " 已发送!");
            }

            // 等待20秒
            Thread.sleep(20 * 1000L);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }

            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
3、消息消费者
/**
 * 消息确认机制
 */
public class Consumer {

    private static Runnable receive = new Runnable() {
        public void run() {
            // 1、创建连接工厂
            ConnectionFactory factory = new ConnectionFactory();
            // 2、设置连接属性
            factory.setHost("46.654.44.123");
            factory.setUsername("dfg");
            factory.setPassword("efgy");
            Connection connection = null;
            Channel channel = null;
            final String clientName = Thread.currentThread().getName();

            try {
                // 3、从连接工厂获取连接
                connection = factory.newConnection("消费者");
                // ###死信队列相关:专门用来存储 出错 出异常的数据
                channel = connection.createChannel();
                // 1、 创建一个exchange
                channel.exchangeDeclare("dlq_exchange", "fanout");
                // 2、 创建一个queue,和exchange绑定起来
                channel.queueDeclare("dlq_queue1", false, false, false, null);
                channel.queueBind("dlq_queue1", "dlq_exchange", "");
                // ######死信队列结束


                // 4、从链接中创建通道
                channel = connection.createChannel();
                // 代码定义交换器
                channel.exchangeDeclare("ps_test", "fanout");
                //  还可以定义一个临时队列,连接关闭后会自动删除,此队列是一个排他队列
                String queueName = "queue1";
                // 队列中有死信产生时,消息会转发到交换器 dlq_exchange。
                Map<String, Object> args = new HashMap<String, Object> ();
                args.put("x-dead-letter-exchange", "dlq_exchange");
                channel.queueDeclare(queueName, false, false, false, args);
                // 将队列和交换器绑定
                channel.queueBind(queueName, "ps_test", "");

                // 监听队列
                Channel finalChannel = channel;
                channel.basicConsume(queueName, false, "消费者-手动回执",
                        new DefaultConsumer (finalChannel) {
                            @Override
                            public void handleDelivery(String consumerTag,
                                                       Envelope envelope,
                                                       AMQP.BasicProperties properties,
                                                       byte[] body)
                                    throws IOException {
                                try {
                                    System.out.println("收到消息: " + new String(body));
                                    // TODO 业务处理
                                    long deliveryTag = envelope.getDeliveryTag();
                                    // 模拟业务处理耗时
                                    Thread.sleep(1000L);
                                    // 正常消费
                                    // finalChannel.basicAck(deliveryTag, false);
                                    // 异常消费
                                    finalChannel.basicNack(envelope.getDeliveryTag(), false, false);
                                } catch (InterruptedException e) {
                                    // 异常消费, requeue参数 true重发,false不重发(丢弃或者移到DLQ死信队列)
                                    // finalChannel.basicNack(envelope.getDeliveryTag(), false, false);
                                    e.printStackTrace();
                                }
                            }
                        });


                System.out.println(clientName + " 开始接收消息");
                System.in.read();

            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            } finally {
                // 8、关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        e.printStackTrace();
                    }
                }

                // 9、关闭连接
                if (connection != null && connection.isOpen()) {
                    try {
                        connection.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    public static void main(String[] args) {
        new Thread(receive, "c1").start();
    }

}

五、AMQP 的基本概念

AMQP(RabbitMQ)必须由三部分:交换器、队列和绑定

1、Message(消息)
消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(传输模式,指出该消息可能需要持久化存储)等。

2、Publisher
消息生产者,也是一个向交换器发布消息的客户端应用程序,就是投递消息的程序。

3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。消息交换机,它指定消息按什么规则,路由到哪个队列。

4、Routing Key
路由关键字,exchange根据这个关键字进行消息投递。

5、Binding(绑定)
用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。它的作用就是把exchange和queue按照路由规则绑定起来。绑定其实就是关联了exchange和queue,或者这么说:queue对exchange的内容感兴趣,exchange要把它的Message deliver到queue。

6、Queue(消息队列)
消息的载体,每个消息都会被投到一个或多个队列,等待消费者连接到这个队列将其取走。它是消息的容器,也是消息的终点。

7、Connection
网络连接,例如一个TCP连接。

8、Channel(信道,通道)
消息通道,在客户端的每个连接里,可建立多个channel。多路复用连接中的一条独立双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念以达到复用一条TCP连接的目的。

9、Consumer
消息消费者,表示一个从消息队列中取得消息的客户端应用程序,就是接受消息的程序。

10、Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。一个broker里可以有多个vhost,用作不同用户的权限分离。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的rabbitmq服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,rabbitmq默认的vhost是 / 。

11、Broker
表示消息队列服务器实体。它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输。

六、AMQP中的消息路由

生产者把消息发布到Exchange上,消息最终到达队列并被消费者接收,而Binding决定交换器的消息应该发送到那个队列。如下图所示:

七、Exchange类型

 Exchange分发消息时根据类型的不同分发策略有区别,目前共有四种类型:direct、fanout、topic、headers。headers匹配AMQP消息的header而不是路由键,此外headers交换器和direct交换器完全一致,但性能差很多,目前几乎用不到。且看direct、fanout、topic这三种类型。

1、direct类型
消息中的路由键routing key如果和Binding中的binding key一致,交换器就将消息发到对应的队列中去。路由键与队列名完全匹配,如果一个队列绑定到交换器要求路由键为“dog”,则只转发routing key标记为“dog”的消息,不会转发“dog.puppy”等等。它是完全匹配、单传播的模式。

Driect exchange的路由算法非常简单:通过bindingkey的完全匹配,可以用下图来说明:

 

 

 

Exchange和两个队列绑定在一起,Q1的bindingkey是orange,Q2的binding key是black和green。当Producer publish key是orange时,exchange会把它放到Q1上,如果是black或green就会到Q2上,其余的Message被丢弃。
2、fanout类型
每个发到fanout类型交换器的消息都会分到所有绑定的队列上去。fanout交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。类似于子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的。 如下图所示:

 

 

 3、topic类型
topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:#*#匹配0个或多个单词,*只能匹配一个单词。
对于Message的routing_key是有限制的,不能是任意的。格式是以点号“.”分割的字符表。比如:”stock.usd.nyse”,“nyse.vmw”, “quick.orange.rabbit”。你可以放任意的key在routing_key中,当然最长不能超过255 bytes。对于routing_key,有两个特殊字符#*#匹配0个或多个单词,*只能匹配一个单词。如下图所示:

 

 

 

 Producer发送消息时需要设置routing_key,routing_key包含三个单词和两个点号,第一个key描述了celerity(灵巧),第二个是color(色彩),第三个是物种。
在这里我们创建了两个绑定: Q1 的binding key 是”.orange.“; Q2 是 “..rabbit” 和 “lazy.#”:Q1感兴趣所有orange颜色的动物;Q2感兴趣所有rabbits和所有的lazy的。
例如:rounting_key 为 “quick.orange.rabbit”将会发送到Q1和Q2中。rounting_key 为”lazy.orange.rabbit.hujj.ddd”会被投递到Q2中,#匹配0个或多个单词。

八、ConnectionFactory、Connection、Channel

ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。

1、Connection
Connection是Rabbitmq的socket连接,它封装了socket协议相关部分逻辑。

2、ConnectionFactory
ConnectionFactory是connection的制造工厂。

3、Channel
Channel是我们与rabbitmq打交道的最重要的一个接口,大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。

九、任务分发机制

1、Round-robin dispathching 循环分发
RabbbitMQ的分发机制非常适合扩展,而且它是专门为并发程序设计的,如果现在load加重,那么只需要创建更多的Consumer来进行任务处理。

2、Message acknowledgment 消息确认
为了保证数据不被丢失,RabbitMQ支持消息确认机制,为了保证数据能被正确处理而不仅仅是被Consumer收到,这就需要在处理完数据之后发送一个确认ack。
在处理完数据之后发送ack,就是告诉RabbitMQ数据已经被接收并且处理完成,RabbitMQ可以将消息从队列中移除了。如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer,这样就保证在Consumer异常退出情况下数据也不会丢失。
RabbitMQ没有用到超时机制,它仅仅通过Consumer的连接中断来确认该Message并没有被正确处理,一个消费者处理消息的时间再长也不会导致该消息被发送给其他消费者,即RabbitMQ给了Consumer足够长的时间来做数据处理。如果忘记ack,那么当Consumer退出时,Mesage会被重新分发,从而导致队列中的累积的消息越来越多,然后RabbitMQ会占用越来越多的内存。

3、Message durability 消息持久化
如果我们希望即使在rabbitmq服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置成可持久化的(durable),这样就可以保证绝大部分情况下我们的rabbitmq消息不会丢失。但依然解决不了小概率丢失事件的发生(例如rabbitmq服务器已经接收到了生产者的消息,但还没来得及持久化该消息时rabbitmq服务器就断电了)。如果也要将这种小概率事件管理起来就需要使用到事务了。要持久化队列需要在声明时指定durable=True;这里要注意,队列的名字一定要是Broker中不存在的,不然不能改变此队列的任何属性。队列和交换机有一个创建时候指定的标志durable,durable的唯一含义就是让具有这个标志的队列和交换机会在重启之后重新建立。

消息持久化包括3部分
(1)exchange持久化,在声明时指定durable => true

channel.ExchangeDeclare(ExchangeName, "direct", durable: true, autoDelete: false, arguments: null);//声明消息队列,且为可持久的

(2)queue持久化,在声明时指定durable => true

channel.QueueDeclare(QueueName, durable: true, exclusive: false, autoDelete: false, arguments: null);//声明消息队列,且为可持久的

(3)消息持久化,在投递时指定delivery_mode => 2(1是非持久化)。

channel.basicPublish("", queueName, MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes()); 
如果exchange和queue都是持久化的,那么它们之间的binding也是持久化的;如果exchange和queue两者之间有一个持久化,一个非持久化,则不允许建立绑定。注意:一旦创建了队列和交换机,就不能修改其标志了。例如创建了一个non-durable的队列,然后想把它改变成durable的,唯一的办法就是删除这个队列然后重新创建。
4、Fair dispath 公平分发
你可能也注意到了,分发机制不是那么优雅,默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。n是取余后的,它不管Consumer是否还有unacked Message,只是按照这个默认的机制进行分发。那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却毫无休息的机会,那么Rabbit是如何处理这种问题呢?
 

 

 通过basic.qos方法设置prefetch_count=1,如下设置

channel.basic_qos(prefetch_count=1) 

这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message,换句话说,在接收到该Consumer的ack前,它不会将新的Message分发给它。但是这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计。

5、分发到多个Consumer
Direct Exchange:直接匹配,通过Exchange名称+RountingKey来发送与接收消息。

Fanout Exchange:广播订阅,向所有的消费者发布消息,但是只有消费者将队列绑定到该路由器才能收到消息,忽略Routing Key。

Topic Exchange:主题匹配订阅,这里的主题指的是RoutingKey,RoutingKey可以采用通配符,如:*#,RoutingKey命名采用英文句点来分隔多个词,只有消息将队列绑定到该路由器且指定RoutingKey符合匹配规则时才能收到消息。

Headers Exchange:消息头订阅,消息发布前为消息定义一个或多个键值对的消息头,然后消费者接收消息,同时需要定义类似的键值对请求头(如

x-mactch=all或者x_match=any),只有请求头与消息头匹配,才能接收消息,忽略RoutingKey。

默认的exchange:如果用空字符串去声明一个exchange,那么系统就会使用”amq.direct”这个exchange。我们创建一个queue时,默认的都会有一个和新建queue同名的routingKey绑定到这个默认的exchange上去。如下:

channel.BasicPublish("", "TaskQueue", properties, bytes); 

因为在第一个参数选择了默认的exchange,而我们声明的队列叫TaskQueue,所以默认的,它要新建一个也叫TaskQueue的routingKey,并绑定在默认的exchange上,导致了我们可以在第二个参数routingKey中写TaskQueue,这样它就会找到定义的同名的queue并把消息放进去。
如果有两个接收程序都是用了同一个的queue和相同的routingKey去绑定direct exchange的话,分发的行为是负载均衡的,也就是说第一个是程序1收到,第二个是程序2收到,以此类推。
如果有两个接收程序用了各自的queue,但使用相同的routingKey去绑定direct exchange的话,分发的行为是复制的,即每个程序都会收到这个消息的副本。行为相当于fanout类型的exchange。
多个queue绑定同一个key也是可以的,对于下图的例子,Q1和Q2都绑定了black,对于routing key是black的Message,会被deliver到Q1和Q2,其余的Message都会被丢弃。

 

 

 十、web控制台手动演示

1.建立两个队列

 

 

 

 2.新建一个交换机

 

 点进刚刚新建的交换机里面看,会发现Bindings提醒的是no bindings,他没有绑定任何东西

 

 那么接下来要做的事就是将之前创建的两个队列和这个交换机绑定起来;因为fanout是广播类型,所以key值就没有绑定

 

 

 

 3.代码发送消息

public class Producer {

    public static void main(String[] args) {
        // 1、创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2、设置连接属性
        factory.setHost("123.456.654.54");
        factory.setPort(5672);
        factory.setUsername("admin");
        factory.setPassword("admin");

        Connection connection = null;
        Channel channel = null;

        try {
            // 3、从连接工厂获取连接
            connection = factory.newConnection("生产者");

            // 4、从链接中创建通道
            channel = connection.createChannel();

            /**
             * 5、声明(创建)队列
             * 如果队列不存在,才会创建
             * RabbitMQ 不允许声明两个队列名相同,属性不同的队列,否则会报错
             *
             * queueDeclare参数说明:
             * @param queue 队列名称
             * @param durable 队列是否持久化
             * @param exclusive 是否排他,即是否为私有的,如果为true,会对当前队列加锁,其它通道不能访问,并且在连接关闭时会自动删除,不受持久化和自动删除的属性控制
             * @param autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除
             * @param arguments 队列参数,设置队列的有效期、消息最大长度、队列中所有消息的生命周期等等
             */
           // channel.queueDeclare("queue1", false, false, false, null);

            // 消息内容
            String message = "Hello World!";
            // 6、发送消息*
            channel.basicPublish("faout-e", "", null, message.getBytes());
            System.out.println("消息已发送!");

        } catch (IOException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }

            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 4.web页面发送消息

 

 

5.进入页面查看测试的消息

 

 上面演示的是fanout类型的交换机类型,下来接着来演示一个direct类型的;

1.进入之前创建好的交换进里,绑定队列,这里面我取了KEY值

 

 2.发送测试数据

 

 

3.查验数据,会发现只有队列1收到消息,队列3没有消息

 

 

 

 

 

我们常用的有三个类型,接下来演示最后一个类型topic类型;

1.和前面一样,进入对应交换机绑定队列

 

 

2.发送测试数据

 

 

 

 3.验证,发现1和4通过通配符都接收到消息了

 

 最后想了想还是把headers演示下,虽然现在已经弃用了

1.和前面样进入交换机绑定队列

 

 2.发送测试数据

 

 3.验证查看

 

 

 git源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/rabbit.git

posted @ 2021-10-13 11:43  童话述说我的结局  阅读(409)  评论(0编辑  收藏  举报