第一章 RabbitMQ入门

目录

前言

概念介绍

自己的理解

代码介绍

总结


前言

由于工作中没有用到RabbitMQ,所以只能本地谢谢HelloWorld这样的代码,自己主动去了解一下,学习RabbitMQ先从这一章开始了

概念介绍

VirtualHost  
RabbitMq的VirtualHost(虚拟消息服务器),每个VirtualHost相当于一个相对独立的RabbitMQ服务器;每个VirtualHost之间是相互隔离的,exchange、queue、message不能互通。 拿数据库(用MySQL)来类比:RabbitMq相当于MySQL,RabbitMq中的VirtualHost就相当于MySQL中的一个库。

Connection
是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。生产者和消费者都需要和RabbitMQ相连接,所以生产者和消费者不是直连的。

ConnectionFactory
ConnectionFactory为Connection的制造工厂。

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

Queue
Queue(队列)是RabbitMQ的内部对象,用于存储消息。多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。

Message几个关键字段
Message acknowledgment
消息回执确认,当消费者收到消息后可以发送一个确认消息给生产者,这个是为了防止消息丢失。在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。 这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑。

channel.basicAck(envelope.getDeliveryTag(),false);

 

durable
如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。

1、exchange持久化,在声明时指定durable => true
2、queue持久化,在声明时指定durable => true
3、消息持久化,在投递时指定delivery_mode=> 2(1是非持久化)
如果exchange和queue都是持久化的,那么它们之间的binding也是持久化的。如果exchange和queue两者之间有一个持久化,一个非持久化,就不允许建立绑定。

prefetchCount
prefetchCount这个值是在消费者端收集的。如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。这样就会当快速处理完消息的消费者能很快再次取得消息。

// 同一时刻服务器只会发一条消息给消费者
channel.basicQos(1)

Exchange
Exchange是一个交换机,负责连接生产者和队列,并能根据不同规则来选择传给不同的队列。

Binding
RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了。

routing key
在生产者将消息传递给Exchange的时候,也会传递一个routingkey,这个routingkey来指定路由选择。当然它只是消息路由选择的一部分,还不完整。routing key需要与Exchange Type及binding key联合使用才能最终生效。 在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。 RabbitMQ为routing key设定的长度限制为255 bytes。

binding key
在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。

Exchange Type
Exchange Type是指定routingkey和bindingkey以何种规则进行匹配。
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种
fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。 相当于忽略了routingkey和bindingkey的匹配,可以看作是不设防。
direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。 

topic是一种模糊匹配,里面有两个关键符号:#和*,#代表匹配零个字符或多个字符,*代表匹配一个字符。
headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。 在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。

RPC
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。 但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。

  RabbitMQ  中实现RPC 的机制是:

  • 客户端发送请求(消息)时,在消息的属性(MessageProperties ,在AMQP 协议中定义了14中properties ,这些属性会随着消息一起发送)中设置两个值replyTo (一个Queue 名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue 中)和correlationId (此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)

  • 服务器端收到消息并处理

  • 服务器端处理完消息后,将生成一条应答消息到replyTo 指定的Queue ,同时带上correlationId 属性

  • 客户端之前已订阅replyTo 指定的Queue ,从中收到服务器的应答消息后,根据其中的correlationId 属性分析哪条请求被执行了,根据执行结果进行后续业务处理

自己的理解

生产者,消费者,RabbitMQ服务器中的VirtualHost分为这三部分。
生产者和服务器,可以直接连接,也可以由exchange相连,但是exchange需要和queue绑定。
消费者和服务器之间是服务器将消息从queue里面取出来后传给了消费者
routing key        binding key    ExchangeType三者之间的关系
routing key是生产者发送消息的时候带上的,相当于通行证,
binding key 是exhcange和queue绑定的时候设置的,这相当于一个门,
ExhcangeType相当于是门检查通行证的一种规则,常见的由松到严分为三种

fanout,直接忽略routing key和binding key,消息会分发给所有和exchange相连的duilie
direct,完全匹配,要routing key 和binding key完全一样,才给分发
topic,模糊匹配,关键字#和*,#匹配零到多个,*是匹配一个字符。

 

代码介绍

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.findjob</groupId>
    <artifactId>RabbitMQTest</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.2.0</version>
        </dependency>
    </dependencies>


</project>

ConnectionUtil

package com.wangbiao.rabbitmq.helloworld;

import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 *连接工具
 */
public class ConnectionUtil {
    private static final String HOST="127.0.0.1";
    private static final int PORT=5672;
    private static final String USER_NAME="test";
    private static final String PASSWORD="test";
    private static final String VIRHOST="/test";
    private static ConnectionFactory connectionFactory = new ConnectionFactory();
    public static Connection getConnection(){
        connectionFactory.setHost(HOST);
        connectionFactory.setPort(PORT);
        connectionFactory.setUsername(USER_NAME);
        connectionFactory.setPassword(PASSWORD);
        connectionFactory.setVirtualHost(VIRHOST);
        try {
            Connection connection = connectionFactory.newConnection();
            return connection;
        }catch (Exception exception){
            exception.printStackTrace();
        }
        return null;
    }

}

生产者
 

package com.wangbiao.rabbitmq.exchangetest;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.wangbiao.rabbitmq.helloworld.ConnectionUtil;

/**
 * @Author wangbiao
 * @Date 2019-01-19 17:24
 * @Decripition TODO
 **/
public class ProductorTwo {
    private static Connection connection = ConnectionUtil.getConnection();
   
    private static final String EXCHANGE_NAME = "exchangeTest";

    private static final String ROUTING_KEY = "fighting";

    public static void main(String[] args) throws Exception{
        Channel channel = connection.createChannel();
        /**
         * exchange: 交换器名称
         * type : 交换器类型 DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
         * durable: 是否持久化,durable设置为true表示持久化,反之是非持久化,持久化的可以将交换器存盘,在服务器重启的时候不会丢失信息.
         * autoDelete是否自动删除,设置为TRUE则表是自动删除,自删除的前提是至少有一个队列或者交换器与这交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑,一般都设置为fase
         * 交换机在不被使用时是否删除
         * arguments:其它一些结构化参数比如:alternate-exchange
         */
        channel.exchangeDeclare(EXCHANGE_NAME,"topic",false,false,null);
        for(int i=0;i<10;i++){
            String message = "Hello World"+i;
            channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,null,message.getBytes());
        }
        System.out.println("生产者发送消息");
        channel.close();
        connection.close();

    }
}

消费者二

package com.wangbiao.rabbitmq.exchangetest;

import com.rabbitmq.client.*;
import com.wangbiao.rabbitmq.helloworld.ConnectionUtil;

import java.io.IOException;

/**
 * @Author wangbiao
 * @Date 2019-01-19 18:49
 * @Decripition TODO
 **/
public class CustomerTwo {
    private static Connection connection = ConnectionUtil.getConnection();
    private static final String QUEUE_NAME = "queueTest";
    private static final String EXCHANGE_NAME = "exchangeTest";

    private static final String BINDING_KEY = "fighting";
    public static void main(String[] args) throws Exception{
        final Channel channel = connection.createChannel();
        /**
         * 队列不存在就创建队列,如果队列存在什么都不做
         * 第一个参数:队列名字
         * 第二个参数:是否持久化,队列里的消息是保存在内存中的,rabbitmq重启会丢失,如果是ture,队列里的消息会保存到erlang自带的数据库中
         * 第三个参数:是否排外,有两个作用,连接关闭是否清除队列;是否私有当前队列,如果私有当前队列,其他通道不可以访问当前队列
         * 为ture的话,一般适用于一个队列对应一个消费者的时候
         * 第四个参数:是否自动删除
         * 第五个参数:其他参数
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,BINDING_KEY);
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println("收到的消息:"+message);
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        channel.basicConsume(QUEUE_NAME,false,consumer);
        System.out.println("=============消费者二接受完消息==============");

    }
}

消费者三

package com.wangbiao.rabbitmq.exchangetest;

import com.rabbitmq.client.*;
import com.wangbiao.rabbitmq.helloworld.ConnectionUtil;

import java.io.IOException;

/**
 * @Author wangbiao
 * @Date 2019-01-19 18:49
 * @Decripition TODO
 **/
public class CustomerThree {
    private static Connection connection = ConnectionUtil.getConnection();
    private static final String QUEUE_NAME = "queueTestTwo";
    private static final String EXCHANGE_NAME = "exchangeTest";

    private static final String BINDING_KEY = "fig#";
    public static void main(String[] args) throws Exception{
        final Channel channel = connection.createChannel();
        /**
         * 队列不存在就创建队列,如果队列存在什么都不做
         * 第一个参数:队列名字
         * 第二个参数:是否持久化,队列里的消息是保存在内存中的,rabbitmq重启会丢失,如果是ture,队列里的消息会保存到erlang自带的数据库中
         * 第三个参数:是否排外,有两个作用,连接关闭是否清除队列;是否私有当前队列,如果私有当前队列,其他通道不可以访问当前队列
         * 为ture的话,一般适用于一个队列对应一个消费者的时候
         * 第四个参数:是否自动删除
         * 第五个参数:其他参数
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,BINDING_KEY);
        Consumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println("消费者三收到的消息:"+message);
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        channel.basicConsume(QUEUE_NAME,false,consumer);
    }
}

启动生产者

消费者二和消费者三输出的结果是

总结

我们可以将队列服务器Broker看成是mysql服务器,将虚拟主机vhost看成是数据库,队列Queue看成是数据库里的表,生产者发布的消息可以看成是表里的记录。

RabbitMQ工作的过程是:
1.生产者端连接上队列服务器上的一个虚拟主机vhost。
2.再创建一个Channel,也可以创建多个Channel,Channel负责定义Exchange,Queue,绑定Exchange和Queue,发布消息,绑定消费者和队列。
3.通过Channel声明Exchange,然后生产者会将消息发布给Exchange,在声明Exchange的时候可以设置一个校验规则。
4.生产者通过Channel在发布消息的时候会将消息发布给Exchange,在发布消息的时候,可以带上routing key。
5.消费者这边先连上vhost,然后声明出一个队列,再将队列和Exchange绑定,绑定的时候可以设置校验routingkey的bindingkey。
6.消费者这边还得声明一个消费对象Consumer,这个Consumer还得实现handleDevlivery方法来处理队列里面的消息。
7.channel就可以消费对象消费队列Queue里面的消息了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章