RabbitMQ 之修炼手册上卷

版权声明:作者:星云 交流即分享,分享才能进步! https://blog.csdn.net/hadues/article/details/83058427

1. 什么是RabbitMQ?

RabbitMQ 是一个非常优秀的消息中间件,它使用专门处理高并发的Erlang 语言编写而成的消息中间件产品。

RabbitMQ官网:http://www.rabbitmq.com/

2. 什么是消息中间件?

在这里插入图片描述
正如我们所看到的,消息中间件其实简言之可以理解成一个应用和应用之间交换信息的中间站。

3. 为什么要有消息中间件?

我们做个假设,应用A需要给应用D发送一个消息,如果应用D正在忙碌,没空理应用A,那么应用A就不得不继续等待,这样会造成应用A时间上的浪费,表现在Web应用中就是长时间段的阻塞。
因此为了改善这种架构模式,我们设置一个消息中间件,让应用A把消息放到消息中间件中,然后应用A就不用管了,就可以做继续做其他想做的事情了。当应用D有时间后去消息中间件里面取消息即可。

4. 消息 中间件有哪些?

  • ActiveMQ: Apache 出品
  • RabbitMQ: Erlang 语言实现的 AMQP 协议的消息中间件。
  • Kafka : LinkedIn 公司采用 Scala 语言开发的分布式消息系统
  • RocketMQ:阿里开源的消息中间件
  • ZeroMQ:史上最快的消息队列,基于 C 语言开发。

5. 选RabbitMQ 还是Kafka?

  • RabbitMQ客户端库已经成熟并且文档齐全
  • Kafka 高度可扩展,可自定义组合一些其他组件。

5. 如何集成RabbitMQ?

RabbitMQ支持多种客户端集成:
在这里插入图片描述
当然,由于时间关系,我只讲述以下两种方式集成:

  • Java 集成:Java Client
  • Spring 集成:Spring AMQP

6. RabbitMQ 服务器下载和安装

在使用之前,我们需要安装RabbitMQ服务器,而且由于RabbitMQ 是Erlang语言编写的,因此还需要安装Erlang 运行环境。

官方安装说明:各平台下RabbitMQ安装指南
Windows下RabbitMQ 服务器安装:RabbitMQ 服务器之下载安装

7.RabbitMQ 开发集成

7.1 RabbitMQ Java 集成

RabbitMQ 进行Java集成需要下载三个jar 包。

Jar名称 当前版本
RabbtiMQ Java Client 5.3.0
SLF4J API 1.7.21
SLF4J Simple 1.7.22

7. 1.1 RabbitMQ Java 集成之基础模型

在下图中,“P”是我们的生产者,“C”是我们的消费者。 中间的盒子是一个队列 - RabbitMQ代表消费者保存的消息缓冲区。

RabbitMQ基础消息传递模型图示如下:
RabbitMQ基础消息传递模型
试想下有这样一个场景,领导(P生产者) 发送了一个消息给 秘书(消息队列),员工 (C消费者) 收到这个消息并告诉秘书已经知道了这个消息。

7.1.1.1 编码实现 P 生产者

领导(P生产者) 发送了一个消息给 秘书(消息队列),编码实现需要这么做:

  1. 创建一个到RabbitMQ Server 的连接
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost"); 
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
  1. 必须声明一个队列
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
  1. 发送消息给队列
     String message = "Hello World!";
     channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
  1. 关闭连接对象
   channel.close();
   connection.close();

生产者P 完整代码 Send.java

import java.util.concurrent.TimeoutException;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/***
 * @author 星云
 * 生产者
 * **/
public class Send {
	  //设置消息队列的名称
    private final static String QUEUE_NAME="hello";
    public static void main(String[] args) throws java.io.IOException, TimeoutException{
         //创建一个到RabbitMQ Server 的连接
        ConnectionFactory factory = new ConnectionFactory();
        //在这里,我们连接到本地机器上的代理 - 因此是本地主机。
        //如果我们想连接到另一台机器上的代理,我们只需在此指定其名称或IP地址。
        factory.setHost("localhost");
        //连接抽象出套接字连接,并为我们处理协议版本协商和身份验证等。 
        Connection connection = factory.newConnection();
        //接下来我们创建一个Channel 对象,这是大部分用于完成任务的API驻留的地方。
        Channel channel = connection.createChannel();
        //要想发送出去,我们必须声明一个队列来执行发送,那么我们可以将消息发布到队列中:
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        //声明一个队列是幂等的 - 只有当它不存在时才会被创建。 
        //消息内容是一个字节数组,所以你可以编码任何你喜欢的地方。
        String message = "Hello World!";
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");
        //最后我们关闭这些连接对象
        channel.close();
        connection.close();
    }
}

7.1.1.1 编码实现 C 消费者

员工 (C消费者) 收到这个消息并告诉秘书已经知道了这个消息,编码步骤:

  1. 创建一个到RabbitMQ Server 的连接
    ConnectionFactory factory = new ConnectionFactory();
    factory.setHost("localhost"); 
    Connection connection = factory.newConnection();
    Channel channel = connection.createChannel();
  1. 必须声明一个队列
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
  1. 阅读一个消息并告诉队列阅读完成
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, "UTF-8");
            //打印阅读的消息
           System.out.println(" [x] Received '" + message + "'");
         }
 };
 //告诉队列消息阅读完成
 channel.basicConsume(QUEUE_NAME, true, consumer);

消费者C 完整代码Recv.java:

import java.io.IOException;
import java.util.concurrent.TimeoutException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

/***
 * 消费者
 * ****/
public class Recv {
 private final static  String QUEUE_NAME="hello";
     
     public static void main(String[] args) throws IOException, TimeoutException {
         
          /**
           * 设置与发布者相同; 
           * 我们打开一个连接和一个通道,并声明我们将要使用的队列。 
           * 请注意,这与发送发布到的队列相匹配
           * */
           ConnectionFactory factory = new ConnectionFactory();
            factory.setHost("localhost");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();

            /**
             * 请注意,我们也在这里声明队列。 
             * 因为我们可能会在发布者之前启动消费者,所以我们希望在我们尝试使用消息之前确保队列已存在。   
             * */
            channel.queueDeclare(QUEUE_NAME, false, false, false, null);
            System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
            
            /**
             *  我们即将告诉服务器将队列中的消息传递给我们。
             *   由于它会异步推送消息,因此我们以对象的形式提供回调,该消息将缓冲消息,直到我们准备好使用它们。 
             *  这是一个DefaultConsumer子类的作用。
             * */
            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, "UTF-8");
                    System.out.println(" [x] Received '" + message + "'");
                  }
                };
            channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

7.1.2 RabbitMQ Java 之工作队列

刚才,我们编写了一个程序来发送和接收来自命名队列的消息。
但是假设,我们有一个比较 耗时的任务要分配给多个人合作完成呢?
RabbitMQ工作队列的消息传递模型图示如下:
RabbitMQ工作队列的消息传递模型
在开始之前,我们先来分析下刚才案例 中存在的问题。

7.1.2.1 消息队列持久化

队列持久化

在刚才的代码中,如果你细心观察就会发现,生产者发送完消息,这时候一旦RabbitMQ服务器重启,声明的队列“hello” 以及发送的消息就会消失不见。
这是怎么回事呢?
原来在RabbitMQ 中的队列有两种类型:

  • Transient: 临时的队列
  • Durable: 持久的队列

刚才的案例中我们声明一个队列是这么声明的:

channel.queueDeclare(QUEUE_NAME, false, false, false, null);

这行代码第二个参数就是指定队列的类型,
如果是false 即 Transient :临时队列类型。
如果是true 即 Durable: 持久队列类型。

知道这个就好办了,我们只需要将上面代码中第二个参数改成true, 这样的话即使我们重启RabbitMQ 服务器,声明的队列也不会丢失。

channel.queueDeclare(QUEUE_NAME, true, false, false, null);

值得注意的是:
RabbitMQ 队列的这个类型是不可以被随意更改的,由于刚才我们声明的队列hello 是用的false 即 Transient临时队列类型,我们是无法修改它的。
因此要想解决这个问题只有这两种方法:

  1. 删除这个队列,消费者和生产者中重新声明第二个参数为true
  2. 新建一个不同名字的新队列声明时将第二个参数为true
消息持久化

刚才我们向一个队列中发送一个消息是这么发送的:

channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

想将消息持久化,重启RabbitMQ 消息不丢失,我们只需要将上面代码改成

 channel.basicPublish("",
	    		TASK_QUEUE_NAME,//指定消息队列的名称
	    		MessageProperties.PERSISTENT_TEXT_PLAIN,//指定消息持久化
	        message.getBytes("UTF-8"));//指定消息的字符编码

即:将第三个参数指定为MessageProperties.PERSISTENT_TEXT_PLAIN 即可

7.1.2.2 循环调度

假设领导(P生产者手里)如果有大量的消息任务需要处理,我们只需要多增加几个员工 (消费者C)来处理即可。
比如,我们有三个消息任务,两个员工,那么执行流程就是;
员工一 收到一个消息A
员工二收到一个消息B
员工一收到一个消息C
那么员工A 收到的消息就是A C
员工B 收到的消息就是B

默认情况下,RabbitMQ将按顺序将每条消息发送给下一个消费者。平均而言,每个消费者将获得相同数量的消息。这种分发消息的方式称为循环法

也就是说,RabbitMQ 默认情况下会按顺序循环发消息给消费者。

7.1.2.3 公平派遣

还是刚才的场景,但是已知消息A 是一个非常耗时的消息,B 是一个非常简单的消息。这时候又来了一个消息,按照RabbitMQ默认的循环调度算法,会再次分配给A, 但是A 此时很忙碌,B很快解决完了消息B之后却很空闲,这样就会出现不公平现象。
我们希望能够改善这种分配模式,那么就需要这么做:

int prefetchCount = 1 ;
channel.basicQos(prefetchCount);

这两行代码会告诉RabbitMQ在处理并确认前一个消息之前,不要向工作人员发送新消息。相反,它会将它发送给下一个仍然不忙的工人.

也就是说,RabbitMQ 会给员工一只发一个消息,给员工二也只发一个消息,在员工一和员工二未处理完这条消息之前,就算再有消息也不再发送,当然,如果B空闲了,就把消息发送给B.

在这里插入图片描述

7.1.2.4 消息确认

一个消费者执行消息任务可能只需要几秒钟,但是当一个消费者正在执行比较耗时的工作时,突然死了,那么工作还没做完,此时如果不做任何处理,任务就会丢失。我们肯定希望这时候把任务转交给另外一个消费者来继续完成这个消息任务。
手动应答

当自动应答等于true的时候,表示当消费者一收到消息就表示消费者收到了消息,消费者收到了消息就会立即从队列中删除。

手动应答和自动应答不一样,需要将autoAck设置为false

//boolean autoAck = false;
//channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);

当消费者收到消息在合适的时候来显示的进行确认,说我已经接收到了该消息了,RabbitMQ可以从队列中删除该消息了,可以通过显示调用channel.basicAck(envelope.getDeliveryTag(), false);来告诉消息服务器来删除消息

				try {
					doWork(message);
				} finally {
					System.out.println(" [x] Done");
					channel.basicAck(envelope.getDeliveryTag(), false);
				}

完整代码如下:
生产者

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

/***
 * 生产者
 * ********/
public class NewTask {
	private static final String TASK_QUEUE_NAME = "task_queue";

	  public static void main(String[] argv) throws Exception {
		  
		//创建和消息队列的连接  
	    ConnectionFactory factory = new ConnectionFactory();
	    factory.setHost("localhost");
	    Connection connection = factory.newConnection();
	    Channel channel = connection.createChannel();

	    //第二个参数为true 确保关闭RabbitMQ服务器时执行持久化
	    channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);

	    //从命令行发送任意消息
	    String message = getMessage(argv);

	    //将消息标记为持久性 - 通过将MessageProperties(实现BasicProperties)设置为值PERSISTENT_TEXT_PLAIN。
	    channel.basicPublish("",
	    		TASK_QUEUE_NAME,//指定消息队列的名称
	    		MessageProperties.PERSISTENT_TEXT_PLAIN,//指定消息持久化
	        message.getBytes("UTF-8"));//指定消息的字符编码
	    //打印生产者发送成功的消息
	    System.out.println(" [x] Sent '" + message + "'");

	    //关闭资源
	    channel.close();
	    connection.close();
	  }

	  
	  /***
	   * 一些帮助从命令行参数获取消息
	   * @param strings 从命令行发送任意消息字符串
	   * */
	  private static String getMessage(String[] strings) {
	    if (strings.length < 1)
	      return "Hello World!";
	    return joinStrings(strings," ");
	  }

	  /**
	   * 字符串数组
	   * @param delimiter 分隔符
	   * */
	  private static String joinStrings(String[] strings, String delimiter) {
	    int length = strings.length;
	    if (length == 0) return "";
	    StringBuilder words = new StringBuilder(strings[0]);
	    for (int i = 1; i < length; i++) {
	      words.append(delimiter).append(strings[i]);
	    }
	    return words.toString();
	  }
}

消费者

import java.io.IOException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class Worker {

	private static final String TASK_QUEUE_NAME = "task_queue";

	public static void main(String[] argv) throws Exception {
		
		//和消息队列创建连接
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		final Connection connection = factory.newConnection();
		final Channel channel = connection.createChannel();

		//指定消息队列的名称
		channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
		System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

		//在处理并确认前一个消息之前,不要向工作人员发送新消息。相反,它会将它发送给下一个仍然不忙的工人
		int prefetchCount = 1 ; 
		channel.basicQos(prefetchCount);
		//channel.basicQos(1);

		final 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, "UTF-8");

				System.out.println(" [x] Received '" + message + "'");
				try {
					doWork(message);
				} finally {
					System.out.println(" [x] Done");
					channel.basicAck(envelope.getDeliveryTag(), false);
				}
			}
		};
		//boolean autoAck = false;
		//channel.basicConsume(TASK_QUEUE_NAME, autoAck, consumer);
		channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
	}

	/*****
	 * 我们的假任务是模拟执行时间
	 * */
	private static void doWork(String task) {
		for (char ch : task.toCharArray()) {
			if (ch == '.') {
				try {
					Thread.sleep(1000);
				} catch (InterruptedException _ignored) {
					Thread.currentThread().interrupt();
				}
			}
		}
	}
}

7.1.3 RabbitMQ Java 之发布订阅(fanout 交换)

接下来我们将向多个队列传递信息。此模式称为“发布/订阅”。
发布和订阅最简单的消息传递模型图示如下:
发布和订阅最简单的消息传递模型图示如下

  • RabbitMQ中消息传递模型的核心思想是生产者永远不会将任何消息直接发送到队列 实际上,生产者通常甚至不知道消息是否会被传递到任何队列。
  • 相反,生产者只能向交换器发送消息。交换是一件非常简单的事情。一方面,它接收来自生产者的消息,另一方面将它们推送到队列。

Tips:
可以看出,这节课我们多了一个Exchanges ,生产者产生的消息将不再直接发送给队列,而是由Exchange来处理这件事情。

Exchange必须知道将发送什么样的消息给哪个队列,其规则由交换类型定义

Exchange四种交换类型

  • direct
    在这里插入图片描述
  • topic
    在这里插入图片描述
  • headers

Headers
类型的Exchanges是不处理路由键的,而是根据发送的消息内容中的headers属性进行匹配。在绑定Queue与Exchange时指定一组键值对;当消息发送到RabbitMQ时会取到该消息的headers与Exchange绑定时指定的键值对进行匹配;如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers属性是一个键值对,可以是Hashtable,键值对的值可以是任何类型。而fanout,direct,topic
的路由键都需要要字符串形式的。
参考博文:https://blog.csdn.net/qq1052441272/article/details/53940754

  • fanout
    在这里插入图片描述

7.3.1.1 交换类型 fanout

fanout (扇出交换)这种交换类型规则很简单,将收到的所有消息广播到它知道的所有队列中。

fanout 交换类型的消息传递模型图示如下:
RabbitMQ完整的消息传递模型

7.3.1.1 .1 如何查看服务器上所存在的交换类型 ?

如果想要列出服务器上的交换,您可以运行有用的rabbitmqctl:

  • Linux 执行下列命令:
sudo rabbitmqctl list_exchanges
  • Windows 执行下列命令:
rabbitmqctl list_exchanges

执行后回显如下:
在这里插入图片描述

注意:可以看出RabbitMQ Spring AMQP 默认使用的交换类型是direct 直接交换,在这个列表中有一些 amq.* exchanges(交换) 和一些默认的 (没有命名的) exchange(交换)们是默认创建的,但是你可能不需要使用他们现在

7.3.1.1 .2 如何声明一个fanout交换类型 ?

还记得之前我们如何发送一个消息到队列么?

       String message = "Hello World!";
       channel.basicPublish("", QUEUE_NAME, null, message.getBytes());

在之前的学习中,我们虽然对交换一无所知,但仍能够向队列发送消息。 这是可能的,因为我们使用的是默认的无名交换,我们通过空字符串("")来识别。

channel.basicPublish方法第一个参数是交换的名称。
空字符串表示默认或无名交换:消息被路由到具有routingKey指定名称的队列(如果存在)

好了现在让我们声明一个有名字的fanout交换
private static final String EXCHANGE_NAME = “logs”;

//P-----> Exchange------> Queue
		channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);		
		String message = getMessage(argv);
		channel.basicPublish(
				EXCHANGE_NAME,//交换器名称
				"",//指定消息队列的名称
				null,//
				message.getBytes("UTF-8"));
7.3.1.1 .2 如何使用Temporary queues 临时队列?

之前我们使用时都指定了一个队列名称

	    String message = getMessage(argv);
	    channel.basicPublish("",
	    		TASK_QUEUE_NAME,//指定消息队列的名称
	    		MessageProperties.PERSISTENT_TEXT_PLAIN,//指定消息持久化
	        message.getBytes("UTF-8"));//指定消息的字符编码

能够命名队列对我们来说至关重要 - 我们需要将工作人员指向同一个队列
当您想要在生产者和消费者之间共享队列时,为队列命名很重要。
但是如果我们的需求是一个日志系统的话,那么对于生产者来说,需要的是所有的队列都收到这个消息,并不需要特别指定哪个队列。

如果生产者在声明队列时不指定名字,那么RabbitMQ会随机为我们选择生成一个名字,它可能看起来像amq.gen-JzTY20BRgKO-HjmUJj0wLg。
在这里插入图片描述

private static final String EXCHANGE_NAME = "logs";
//P-----> Exchange------> Queue
		channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);		
		String message = getMessage(argv);
		channel.basicPublish(
				EXCHANGE_NAME,//交换器名称
				"",//不指定消息队列的名称,RabbitMQ会随机生成一个
				null,//
				message.getBytes("UTF-8"));

注意: 刚才随机生成的这个队列将是一个非持久的,独占的自动删除队列。 一旦我们断开消费者,就会自动删除改队列

消费者如果想要获取这随机生成的队列名称,可以通过这个方法:

String queueName = channel.queueDeclare().getQueue();
7.3.1.1 .3 如何将Exchange 和随机生成队列绑定?

在这里插入图片描述
然后我们将Exchange和上面生成的随机队列进行绑定

channel.queueBind(queueName, "logs", "");
7.3.1.1 .4 发布订阅------整体来看

发布订阅模式消息模型:
在这里插入图片描述
发布和订阅模式与前一个队列中的教程没有太大的不同。
之前的时候是生产者和队列直接交换信息,Exchange 没有指定名称,使用的是默认的无名交换

channel.basicPublish("",//Exchange name 没有指定名称
	    		TASK_QUEUE_NAME,//指定消息队列的名称
	    		MessageProperties.PERSISTENT_TEXT_PLAIN,//指定消息持久化
	        message.getBytes("UTF-8"));//指定消息的字符编码

而现在我们给Exchange 指定了名字,是有名交换。

channel.basicPublish(
				EXCHANGE_NAME,// Exchange名称
				"",//指定消息队列的名称
				null,//
				message.getBytes("UTF-8"));

这里简单谈下我的理解:
假设P是我们平时工作的领导,X是秘书(某任务自动分配系统),C1 是员工张三,C2 是员工李四,领导制定(发布)好任务列表后,交给秘书(X, 任务分配系统(Exchange)),秘书(X, 任务分配 系统Exchange)将任务发送到这两个邮箱(消息队列)中即可。张三,李四都绑定(订阅)了不同的邮箱(不同的队列名称),那么张三和李四取消息便从自己绑定的邮箱(队列)中取即可。
上篇博文中的工作队列所谓的无名交换可以理解为没有秘书(exchange)这个角色,而且共用同一个消息队列,如此而已。

生产者EmitLog.java

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

public class EmitLog {
	private static final String EXCHANGE_NAME = "logs";

	public static void main(String[] argv) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		//P-----> Exchange------> Queue
		channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
		
		String message = getMessage(argv);
		channel.basicPublish(
				EXCHANGE_NAME,//交换器名称
				"",//指定消息队列的名称
				null,//
				message.getBytes("UTF-8"));
		System.out.println(" [x] Sent '" + message + "'");
		channel.close();
		connection.close();
	}

	//从命令行获取消息
	private static String getMessage(String[] strings) {
		if (strings.length < 1)
			return "info: Hello World!";
		return joinStrings(strings, " ");
	}

	private static String joinStrings(String[] strings, String delimiter) {
		int length = strings.length;
		if (length == 0)
			return "";
		StringBuilder words = new StringBuilder(strings[0]);
		for (int i = 1; i < length; i++) {
			words.append(delimiter).append(strings[i]);
		}
		return words.toString();
	}
}

消费者 ReceiveLogs.java

import java.io.IOException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class ReceiveLogs {

	private static final String EXCHANGE_NAME = "logs";

	public static void main(String[] argv) throws Exception {
		//RabbitMQ建立连接
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		//启用交换
		channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
		
		//使用生成的名称创建一个非持久的,独占的自动删除队列
		String queueName = channel.queueDeclare().getQueue();
		
		//绑定
		channel.queueBind(queueName,EXCHANGE_NAME, "");
		
		System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
		
		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, "UTF-8");
				System.out.println(" [x] Received '" + message + "'");
			}
		};
		channel.basicConsume(queueName, true, consumer);
	}
}

7.1.4 RabbitMQ Java 之路由 (Direct exchange)

7.1.4.1 如何给 Exchange 绑定秘钥?

在前面的例子中,我们已经在创建绑定。您可能会记得以下代码:

channel.queueBind(queueName, EXCHANGE_NAME, "");

绑定是交换和队列之间的关系, 可理解为队列对来自此交换的消息感兴趣。
绑定可以采用额外的routingKey参数。为了避免与basic_publish参数混淆,我们将其称为 绑定密钥。这就是我们如何使用键创建绑定:
在这里插入图片描述
比如一个日志系统,我们的队列只需要过滤接受error级别的

channel.queueBind(queueName,EXCHANGE_NAME,"error");

7.1.4.2 对比 fanout 和Direct

  • fanout( 扇出)交换:没太大的灵活性 ,它只能进行无意识的广播。
    在这里插入图片描述
  • Driect(直接)交换:Direct直接交换背后的路由算法很简单 - 消息进入队列,其 绑定密钥与消息的路由密钥完全匹配。
    在这里插入图片描述
    Direct 交换秘钥是可以相同的,比如队列Q1 和Q2 都可以接受来自black 关键字的消息
    在这里插入图片描述

7.1.4.3 如何实现Direct 路由交换?

  1. 生产者声明Exchange名称,以及Exchange类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
  1. 生产者中声明时设置消息级别
channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
  1. 消费者声明Exchange名称和交换类型
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
  1. 订阅我们想要的消息
String queueName = channel.queueDeclare().getQueue();
for (String severity : argv) {
			channel.queueBind(queueName, EXCHANGE_NAME, severity);
}

7.1.4.4 放到 一起来看

在这里插入图片描述
生产者代码:

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

public class EmitLogDirect {
	private static final String EXCHANGE_NAME = "direct_logs";

	public static void main(String[] argv) throws Exception {

		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

		String severity = getSeverity(argv);
		String message = getMessage(argv);

		channel.basicPublish(EXCHANGE_NAME, severity, null, message.getBytes("UTF-8"));
		System.out.println(" [x] Sent '" + severity + "':'" + message + "'");

		channel.close();
		connection.close();
	}

	private static String getSeverity(String[] strings) {
		if (strings.length < 1)
			return "info";
		return strings[0];
	}

	private static String getMessage(String[] strings) {
		if (strings.length < 2)
			return "Hello World!";
		return joinStrings(strings, " ", 1);
	}

	private static String joinStrings(String[] strings, String delimiter, int startIndex) {
		int length = strings.length;
		if (length == 0)
			return "";
		if (length < startIndex)
			return "";
		StringBuilder words = new StringBuilder(strings[startIndex]);
		for (int i = startIndex + 1; i < length; i++) {
			words.append(delimiter).append(strings[i]);
		}
		return words.toString();
	}
}

消费者代码:

import java.io.IOException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class ReceiveLogsDirect {
	private static final String EXCHANGE_NAME = "direct_logs";

	public static void main(String[] argv) throws Exception {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();

		channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
		String queueName = channel.queueDeclare().getQueue();

		if (argv.length < 1) {
			System.err.println("Usage: ReceiveLogsDirect [info] [warning] [error]");
			System.exit(1);
		}

		for (String severity : argv) {
			channel.queueBind(queueName, EXCHANGE_NAME, severity);
		}
		System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

		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, "UTF-8");
				System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'");
			}
		};
		channel.basicConsume(queueName, true, consumer);
	}
}

7.1.5 RabbitMQ Java 之主题(Topic Exchange)

在上一节中,我们消息订阅是根据Exchange 的路由Key 来决定的,
这种模式存在一些限制,送到主题交换的消息不能具有任意 routing_key - 它必须是由点分隔的单词列表。
但是如果我们使用topic 交换类型,就可以模糊匹配关键词作为消息路由

  • *(星号)可以替代一个单词。
  • #(hash)可以替换零个或多个单词
    在这里插入图片描述

在这个例子中,我们将发送所有描述动物的消息。消息将与包含三个单词(两个点)的路由键一起发送。

7.1.5.1 Topic Exchnage匹配规则详解

路由键中的第一个单词将描述速度,第二个是颜色,第三个是物种:“ 。。 ”。

我们创建了三个绑定:Q1绑定了绑定键“ * .orange。* ”,Q2 绑定了“ 。rabbit ”和“ lazy。# ”。

这些绑定可以概括为:

  • Q1对所有橙色动物感兴趣。
  • Q2希望听到关于兔子的一切,以及关于懒惰动物的一切。

路由密钥设置为“ quick.orange.rabbit ”的消息将传递到两个队列。
消息“ lazy.orange.elephant ”也将同时发送给他们。
另一方面,“ quick.orange.fox ”只会进入第一个队列,而“ lazy.brown.fox”只会进入第二个队列。 “ lazy.pink.rabbit ”将仅传递到第二个队列一次,即使它匹配两个绑定。“
quick.brown.fox ”与任何绑定都不匹配,因此它将被丢弃。

如果我们违反合同并发送带有一个或四个单词的消息,例如“ orange ”或“ quick.orange.male.rabbit”,会发生什么?好吧,这些消息将不匹配任何绑定,将丢失。

另一方面,“ lazy.orange.male.rabbit ”,即使它有四个单词,也会匹配最后一个绑定,并将被传递到第二个队列。

主题交换功能强大,可以像其他Exchange一样。

当队列与“ # ”(哈希)绑定密钥绑定时 - 它将接收所有消息,而不管路由密钥 - 如扇出交换。 当特殊字符“ * ”(星号)和“ #
”(哈希)未在绑定中使用时,主题交换的行为就像直接交换一样

我们将在日志记录系统中使用主题交换。我们将首先假设日志的路由键有两个词:“ 。 ”。

7.1.5.2 放到一起来看

生产者
EmitLogTopic.java

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

public class EmitLogTopic {
	private static final String EXCHANGE_NAME = "topic_logs";

	public static void main(String[] argv) {
		Connection connection = null;
		Channel channel = null;
		try {
			ConnectionFactory factory = new ConnectionFactory();
			factory.setHost("localhost");

			connection = factory.newConnection();
			channel = connection.createChannel();

			channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);

			String routingKey = getRouting(argv);
			String message = getMessage(argv);

			channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
			System.out.println(" [x] Sent '" + routingKey + "':'" + message + "'");

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if (connection != null) {
				try {
					connection.close();
				} catch (Exception ignore) {
				}
			}
		}
	}

	private static String getRouting(String[] strings) {
		if (strings.length < 1)
			return "anonymous.info";
		return strings[0];
	}

	private static String getMessage(String[] strings) {
		if (strings.length < 2)
			return "Hello World!";
		return joinStrings(strings, " ", 1);
	}

	private static String joinStrings(String[] strings, String delimiter, int startIndex) {
		int length = strings.length;
		if (length == 0)
			return "";
		if (length < startIndex)
			return "";
		StringBuilder words = new StringBuilder(strings[startIndex]);
		for (int i = startIndex + 1; i < length; i++) {
			words.append(delimiter).append(strings[i]);
		}
		return words.toString();
	}
}

消费者 ReceiveLogsTopic.java

import java.io.IOException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class ReceiveLogsTopic {
	private static final String EXCHANGE_NAME = "topic_logs";

	  public static void main(String[] argv) throws Exception {
	    ConnectionFactory factory = new ConnectionFactory();
	    factory.setHost("localhost");
	    Connection connection = factory.newConnection();
	    Channel channel = connection.createChannel();

	    channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
	    String queueName = channel.queueDeclare().getQueue();

	    if (argv.length < 1) {
	      System.err.println("Usage: ReceiveLogsTopic [binding_key]...");
	      System.exit(1);
	    }

	    for (String bindingKey : argv) {
	      channel.queueBind(queueName, EXCHANGE_NAME, bindingKey);
	    }

	    System.out.println(" [*] Waiting for messages. To exit press CTRL+C");

	    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, "UTF-8");
	        System.out.println(" [x] Received '" + envelope.getRoutingKey() + "':'" + message + "'");
	      }
	    };
	    channel.basicConsume(queueName, true, consumer);
	  }
}

7. 1.6 RabbitMQ Java 集成之RPC 远程过程调用

7.1.6.1 什么是RPC ?

RPC : Remote procedure call (RPC,即远程过程调用)
我们都知道,微服务和分布式天下的今天,很多应用和应用之间,有时候可能并不都在同一个计算机上,一旦应用A 需要和远程的另外一台计算机上的应用B 进行交互,那么就会出现远程过程调用以及如何通信问题。

7.1.6.2 RabbitMQ RPC 消息 模型

在这里插入图片描述

在本教程中,我们将使用RabbitMQ构建RPC系统:客户端和可伸缩的RPC服务器。由于我们没有任何值得分发的耗时任务,我们将创建一个返回Fibonacci数字的虚拟RPC服务。

客户端界面

为了说明如何使用RPC服务,我们将创建一个简单的客户端类。它将公开一个名为call的方法,该方法 发送一个RPC请求并阻塞,直到收到答案为止:

FibonacciRpcClient fibonacciRpc = new FibonacciRpcClient();
String result = fibonacciRpc.call("4");
System.out.println( "fib(4) is " + result);
回调队列

一般来说,通过RabbitMQ进行RPC很容易。客户端发送请求消息,服务器回复响应消息。为了接收响应,我们需要发送带有请求的“回调”队列地址。我们可以使用默认队列(在Java客户端中是独占的)。我们来试试吧:

callbackQueueName = channel.queueDeclare().getQueue();

BasicProperties props = new BasicProperties
                            .Builder()
                            .replyTo(callbackQueueName)
                            .build();

channel.basicPublish("", "rpc_queue", props, message.getBytes());

// ... then code to read a response message from the callback_queue ...

注意导入的包类是:import com.rabbitmq.client.AMQP.BasicProperties;

AMQP 0-9-1协议预定义了一组带有消息的14个属性。大多数属性很少使用,但以下情况除外:

  • deliveryMode:将消息标记为持久性(值为2)或瞬态(任何其他值)。您可能会记住第二个教程中的这个属性。
  • contentType:用于描述编码的mime类型。例如,对于经常使用的JSON编码,将此属性设置为:application / json是一种很好的做法。
  • replyTo:通常用于命名回调队列。
  • correlationId:用于将RPC响应与请求相关联。
执行流程

对于RPC请求,客户端发送带有两个属性的消息: replyTo,设置为仅为请求创建的匿名独占队列;以及correlationId,设置为每个请求的唯一值。
请求被发送到rpc_queue队列。

RPC worker(aka:server)正在等待该队列上的请求。当出现请求时,它会执行该作业,并使用来自replyTo字段的队列将带有结果的消息发送回客户端。

客户端等待回复队列上的数据。出现消息时,它会检查correlationId属性。如果它与请求中的值匹配,则返回对应用程序的响应。

整体来看

斐波那契数列算法:

private static int fib(int n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    return fib(n-1) + fib(n-2);
}

我们声明我们的斐波那契函数。它假定只有有效的正整数输入。(不要指望这个适用于大数字,它可能是最慢的递归实现)。

服务器代码非常简单:

像往常一样,我们首先建立连接,通道和声明队列。
我们可能希望运行多个服务器进程。为了在多个服务器上平均分配负载,我们需要在channel.basicQos中设置 prefetchCount设置。
我们使用basicConsume来访问队列,我们​​以对象(DefaultConsumer)的形式提供回调,它将完成工作并发回响应。
服务端源码:

import java.io.IOException;
import java.util.concurrent.TimeoutException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class RPCServer {
	private static final String RPC_QUEUE_NAME = "rpc_queue";

	  private static int fib(int n) {
	    if (n ==0) return 0;
	    if (n == 1) return 1;
	    return fib(n-1) + fib(n-2);
	  }

	  public static void main(String[] argv) {
	    ConnectionFactory factory = new ConnectionFactory();
	    factory.setHost("localhost");

	    Connection connection = null;
	    try {
	      connection      = factory.newConnection();
	      final Channel channel = connection.createChannel();

	      channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
	      channel.queuePurge(RPC_QUEUE_NAME);

	      channel.basicQos(1);

	      System.out.println(" [x] Awaiting RPC requests");

	      Consumer consumer = new DefaultConsumer(channel) {
	        @Override
	        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
	          AMQP.BasicProperties replyProps = new AMQP.BasicProperties
	                  .Builder()
	                  .correlationId(properties.getCorrelationId())
	                  .build();

	          String response = "";

	          try {
	            String message = new String(body,"UTF-8");
	            int n = Integer.parseInt(message);

	            System.out.println(" [.] fib(" + message + ")");
	            response += fib(n);
	          }
	          catch (RuntimeException e){
	            System.out.println(" [.] " + e.toString());
	          }
	          finally {
	            channel.basicPublish( "", properties.getReplyTo(), replyProps, response.getBytes("UTF-8"));
	            channel.basicAck(envelope.getDeliveryTag(), false);
	            // RabbitMq consumer worker thread notifies the RPC server owner thread 
	            synchronized(this) {
	            	this.notify();
	            }
	          }
	        }
	      };

	      channel.basicConsume(RPC_QUEUE_NAME, false, consumer);
	      // Wait and be prepared to consume the message from RPC client.
	      while (true) {
	      	synchronized(consumer) {
	      		try {
	      			consumer.wait();
	      	    } catch (InterruptedException e) {
	      	    	e.printStackTrace();	    	
	      	    }
	      	}
	      }
	    } catch (IOException | TimeoutException e) {
	      e.printStackTrace();
	    }
	    finally {
	      if (connection != null)
	        try {
	          connection.close();
	        } catch (IOException _ignore) {}
	    }
	  }
}

客户端代码稍微复杂一些:

我们建立了一个连接和渠道。我们的调用方法生成实际的RPC请求。在这里,我们首先生成一个唯一的correlationId 数并保存它 - 我们 在RpcConsumer中的handleDelivery实现将使用该值来捕获适当的响应。

然后,我们为回复创建一个专用的独占队列并订阅它。

接下来,我们发布请求消息,其中包含两个属性: replyTo和correlationId。
在这一点上,我们可以坐下来等待正确的响应到来。

由于我们的消费者交付处理是在一个单独的线程中进行的,因此我们需要在响应到来之前暂停主线程。使用BlockingQueue是一种可能的解决方案。

这里我们创建了ArrayBlockingQueue ,容量设置为1,因为我们只需要等待一个响应。该handleDelivery方法是做一个很简单的工作,对每一位消费响应消息它会检查的correlationID 是我们要找的人。

如果是这样,它会将响应置于BlockingQueue。同时主线程正在等待响应从BlockingQueue获取它。

最后,我们将响应返回给用户。
客户端源码:

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;

public class RPCClient {
	private Connection connection;
	  private Channel channel;
	  private String requestQueueName = "rpc_queue";

	  public RPCClient() throws IOException, TimeoutException {
	    ConnectionFactory factory = new ConnectionFactory();
	    factory.setHost("localhost");

	    connection = factory.newConnection();
	    channel = connection.createChannel();
	  }

	  public String call(String message) throws IOException, InterruptedException {
	    final String corrId = UUID.randomUUID().toString();

	    String replyQueueName = channel.queueDeclare().getQueue();
	    AMQP.BasicProperties props = new AMQP.BasicProperties
	            .Builder()
	            .correlationId(corrId)
	            .replyTo(replyQueueName)
	            .build();

	    channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

	    final BlockingQueue<String> response = new ArrayBlockingQueue<String>(1);

	    String ctag = channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
	      @Override
	      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
	        if (properties.getCorrelationId().equals(corrId)) {
	          response.offer(new String(body, "UTF-8"));
	        }
	      }
	    });

	    String result = response.take();
	    channel.basicCancel(ctag);
	    return result;
	  }

	  public void close() throws IOException {
	    connection.close();
	  }

	  public static void main(String[] argv) {
	    RPCClient fibonacciRpc = null;
	    String response = null;
	    try {
	      fibonacciRpc = new RPCClient();

	      for (int i = 0; i < 32; i++) {
	        String i_str = Integer.toString(i);
	        System.out.println(" [x] Requesting fib(" + i_str + ")");
	        response = fibonacciRpc.call(i_str);
	        System.out.println(" [.] Got '" + response + "'");
	      }
	    }
	    catch  (IOException | TimeoutException | InterruptedException e) {
	      e.printStackTrace();
	    }
	    finally {
	      if (fibonacciRpc!= null) {
	        try {
	          fibonacciRpc.close();
	        }
	        catch (IOException _ignore) {}
	      }
	    }
	  }
}

本篇博文所用到的所有代码均可免费下载:
https://github.com/geekxingyun/RabbitMQSample

继续学习 RabbitMQ 之修炼手册中卷

猜你喜欢

转载自blog.csdn.net/hadues/article/details/83058427