RabbitMQ5种基础模型

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/about4years/article/details/82669222

通常来说,像MQ这种中间件服务,肯定是安装在Linux上,不过对于初学者,我们可以在windows上搭建Rabbit的环境,因为RabbitMQ为我们提供了一个Web控制台,默认开在15672端口,十分方便,对于初学者比较友好,下面开始学习官方提供的tutorial 。

1.最简单的模型,也就是最传统的 producer ——>queue <——consumer,生产者往队列发消息,消费者从队列取消息。使用Java实现:

注意导入相关包:

                <dependency>
			<groupId>com.rabbitmq</groupId>
			<artifactId>amqp-client</artifactId>
			<version>4.0.2</version>
		</dependency>

生产者获取连接,通道,声明队列,发送消息。消费者同理,都是简单的api调用。如下代码所示:

                //获取一个连接
		Connection connection = ConnectionUtils.getConnection();
		//从连接中获取一个通道
		Channel channel = connection.createChannel();
		//创建队列声明 
		channel.queueDeclare("test_simple_queue", false, false, false, null);
		String msg="hello";
		for (int i=0; i<2; i++)
			channel.basicPublish("", "test_simple_queue", null, msg.getBytes());

获取连接封装在util中,代码如下:

public static Connection  getConnection() throws IOException, TimeoutException{
		//定义一个连接工厂
		ConnectionFactory factory =new ConnectionFactory();
		
		//设置服务地址
		factory.setHost("127.0.0.1");

		//AMQP 5672
		factory.setPort(5672);
		
		//vhost 
		factory.setVirtualHost("/");
		
		//用户名 
		factory.setUsername("guest");
		
		//密码
		factory.setPassword("guest");
		return factory.newConnection();
	}
	
}

关于virtualHost等概念参照官方文档,这里使用默认的即可。

回到生产者代码,运行该例子,可以看到生产者发送了2条消息到test_simple_queue这个队列中。登陆到控制台可以看到:

下面是消费者代码:

                // 获取连接
		Connection connection = ConnectionUtils.getConnection();
		// 创建频道
		Channel channel = connection.createChannel();
		//队列声明  
		channel.queueDeclare("test_simple_queue", false, false, false, null);
		//定义消费者
		DefaultConsumer consumer = new DefaultConsumer(channel){
			//获取到达消息
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope,
					BasicProperties properties, byte[] body) throws IOException {
			 
				String msg=new String(body,"utf-8");
				System.out.println("new api recv:"+msg);

			}
		};
		//监听队列
		channel.basicConsume("test_simple_queue", true,consumer);

类似于生产者,只是角色变成了对应的消费者,,运行程序,控制台会打印出收到的2条消息,web控制台上对应队列的消息也清零了,说明被正确消费了。

至此,简单的helloworld就完成了,但是还有许多细节需要探讨。

第一个问题:假如有多个消费者都绑定了这个队列呢,消息是会被分发还是每个消费者都发一份。

用代码再次声明一个消费者绑定同样的队列,同时运行2个消费者(使用2个类),接着运行生产者,我们会发现,消息被分发给2个消费者了。

第二个问题:分发策略是什么?

我们可以再来一个消费者,同时运行起来,发送100条消息,会发现消息会平摊给消费者,也就是分发是按照公平的策略。

第三个问题:是不是因为消费者的消费时间是一样的,导致了公平分发,如果让其中的某个消费者sleep若干秒呢?

更改代码,我们会发现依然是平摊给不同的消费者。而且还会发现,没有sleep的消费者是立即打印所有的消息,也就是说他并没有收到其他sleep的消费者的影响,依然是立马收到了所有的消息。其实很好理解,消息的分发是绝对不能因为消费者的业务处理而受到阻塞,不然这样会影响到整个消息分发,也就影响到了其他的消费者。

第四个问题:队列是怎么知道自己的消息被消费了的?万一某个消费者的服务崩溃了,那这个消息就消失了吗?应该被重新放回队列然后分配给其他的消费者进行处理吗?

我们看消费者对应的代码:channel.basicConsume("test_simple_queue", true,consumer);我们看一下方法定义:

 /**
     * Start a non-nolocal, non-exclusive consumer, with
     * a server-generated consumerTag.
     * @param queue the name of the queue
     * @param autoAck true if the server should consider messages
     * acknowledged once delivered; false if the server should expect
     * explicit acknowledgements
     * @param callback an interface to the consumer object
     * @return the consumerTag generated by the server
     * @throws java.io.IOException if an error is encountered
     * @see com.rabbitmq.client.AMQP.Basic.Consume
     * @see com.rabbitmq.client.AMQP.Basic.ConsumeOk
     * @see #basicConsume(String, boolean, String, boolean, boolean, Map, Consumer)
     */
    String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException;

很显然第一个参数声明消费的是哪个队列,第三个是一个回调,也很好理解,看第二个参数的解释:

autoAck true if the server should consider messages acknowledged once delivered; false if the server should expect
explicit acknowledgements。意思就是设置为true,消费者一收到消息,队列就认为这个消息传递成功。而false则是队列需要收到消费者主动发起的明确的回复,不然队列认为该消息没用被成功处理。

我们回到最开始的代码,将该参数设置为false,然后进行生产消费,看一下会发生什么。运行生产者消费者,消费者正确打印2条消息,但是因为我们设置了autoack为false,所以此时队列会认为这2个消息没有被正确处理!怎么验证呢,停止生产者消费者的程序,再次单独运行消费者,发现又打印了这2条数据,说明消费者又收到这2条消息了。关闭再运行,都是同样的结果,去控制台看也会发现消息依然存在。

我们进行进一步的测试。开启2个消费者,其中一个消费者(消费者1)autoack设置为true,另一个(消费者2)设置为false,并且设置为false的这个消费者我们在他的consumer逻辑处理中sleep10秒。如下:

public void handleDelivery(String consumerTag, Envelope envelope,
					BasicProperties properties, byte[] body) throws IOException {
					String msg=new String(body,"utf-8");
					System.out.println("recv:"+msg);
					try {
						TimeUnit.SECONDS.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

			}

同时启动消费者,接着启动生产者(发送2条消息)。我们会发现2个消费者各收到一条消息,此时我们将消费者2停止(杀死该程序),我们会发现消费者1又打印了一条,也就是说由于消费者2的autoack为false,然后程序又被停止了,所以队列回收了消息并进行重新分发。我从官方文档中copy了一段:

If a consumer dies (its channel is closed, connection is closed, or TCP connection is lost) without sending an ack, RabbitMQ will understand that a message wasn't processed fully and will re-queue it. If there are other consumers online at the same time, it will then quickly redeliver it to another consumer. That way you can be sure that no message is lost, even if the workers occasionally die.

所以我们的杀死程序就相当于:its channel is closed, connection is closed, or TCP connection is lost。

继续考虑一种情况,之前的杀死程序代表的意外情况,例如网络中断。那要是消费者服务内部出现异常,或者说服务拒绝消费,想要主动回退消息呢?这里就涉及到2个新的api方法:channel.basicAck(); channel.basicNack(); 也就是主动回复消息/拒绝消息,并且可以根据不同的传参控制该消息是否要退回队列。

官方tutorial的第一种第二种其实就是上文所说的,模型图如下:

当然可能只有1个Consumer,也有可能有不止2个,但是消息不会每个消费者都发送一份,而是平摊。注意,该模型并不是Rabbit的真正模型,事实上,生产者不会直接发送消息到队列。

2.以上就是最简单的例子,一个消息被传递给一个消费者或者是被分发给不同的消费者。但是这并不是rabbit的消息传递模型。来看一种新的场景:一个消息需要被传递给不同的消费者,也就是每个消费者会受到同样的一个消息。最简单的模型:发布订阅。

我们需要引入一个新的概念:exchange。直接引入官方文档:

The core idea in the messaging model in RabbitMQ is that the producer never sends any messages directly to a queue. Actually, quite often the producer doesn't even know if a message will be delivered to any queue at all.

Instead, the producer can only send messages to an exchange. An exchange is a very simple thing. On one side it receives messages from producers and the other side it pushes them to queues. The exchange must know exactly what to do with a message it receives. Should it be appended to a particular queue? Should it be appended to many queues? Or should it get discarded. The rules for that are defined by the exchange type.

翻译一下:RabbitMQ消息传递模型的核心思想是,生产者从不直接向队列发送任何消息。实际上,通常情况下,制作人甚至不知道消息是否会被发送到任何队列。相反,生产者只能将消息发送到交易所。交换是一件非常简单的事情。一方面,它接收来自生产者的消息,另一端则将它们推送到队列中。交换必须确切地知道如何处理它接收到的消息。它应该被附加到一个特定的队列吗?它应该被附加到许多队列吗?或者它应该被丢弃。规则是由交换类型定义的。

也就是在生产者和队列中又加了一层,在这一层进行扩展,可以十分灵活的控制消息,假如我们把控制消息的逻辑都放在队列上,显然会比较的臃肿,队列要干的事很简单,把消息传递给绑定在队列上的消费者。

上文提到了不同的exchange类型,下面看第一种:fanout。可以理解为广播:

The fanout exchange is very simple. As you can probably guess from the name, it just broadcasts all the messages it receives to all the queues it knows.(fanout交换非常简单。你可以从这个名字中猜出来,它只会把它接收到的所有信息广播到它知道的所有队列中)。

再次回到上文,按照之前说的,生产者应该把消息传递给exchange而不是队列,我们看一下我们之前写的简单的代码:

channel.basicPublish("", "test_simple_queue", null, msg.getBytes()); 注意第一个参数"",我们传了空的字符串,是的,这就是代表着exchange,这是rabbitmq提供的一个默认的exchange,你也可以理解为匿名交换,他会根据routingkey去找到对应名字的queue进行消息的传递。

接下来构造一个自定义的exchange,在web控制台我们可以添加一个exchange,注意Durability的设置和到时候代码中exchange的声明要对应上,我们选择Durability为transient,名字叫做test_exchange_fanout,类型选择fanout,也就是俗称的广播。cmd中输入rabbitmqctl list_exchanges可以看到我们新建的exchange。接下来思考一个问题,就是队列的选择,之前我们都是特别声明的队列的名字,因为之前的模型,我们需要依靠队列的名字去绑定,然而在发布订阅模型中,队列显得不是那么的重要,并且我们所在乎的只是当前存在于队列的消息,对旧消息并不感兴趣。所以,我们可以借助rabbit原生提供的Temporary queues,顾名思义,临时队列,该队列有一个特点,当队列与某个绑定好的消费者一旦解绑,就会自动被删除。

可以看一下消费者代码:

                //队列声明
		String queueName = channel.queueDeclare().getQueue();
		System.out.println("queue name is " + queueName);
		//绑定队列到交换机 转发器
		channel.queueBind(queueName, "test_exchange_fanout", "");
		channel.basicQos(1);//保证一次只分发一个  
		//定义一个消费者
		Consumer consumer=new DefaultConsumer(channel){
			//消息到达 触发这个方法
			@Override
			public void handleDelivery(String consumerTag, Envelope envelope, 
																 BasicProperties properties, byte[] body) throws IOException {
				String msg=new String(body,"utf-8");
				System.out.println("[2] Recv msg:"+msg);

			}
		};
		
		channel.basicConsume(queueName,true , consumer);

第一行代码就是使用rabbit提供的Temporary queue。我们可以有多个消费者,生产者代码与之前没有什么差别。譬如我们使用2个消费者,运行起来,此时可以cmd中输入rabbitmqctl list_bindings,观察结果:

可以看到,rabbit生产的queue的名字是比较有特点的,上面这个输出结果就对应于下面这个模型:

,现在运行生产者,可以看到2个消费者都收到了该条消息。注意,当Stop消费者,再进行rabbitmqctl list_bindings,你会发现之前的bindings已经消失了,因为相关队列已经删除了。

思考一个问题,假如先运行生产者再运行消费者,结果一样吗?换句话说,生产者所生产的消息会被保留下来吗?运行之后发现消费者不再受到消息,也就是消息丢失了,这很好理解:我发送消息的时候没有订阅者,那我自然把消息丢掉,而不是保留着,就好比你今天订阅了一个新的电台,你是听不到这电台前几天的节目的。

以上就是最简单的发布订阅模式。

3.第三种,是一种相较于发布订阅更加灵活的模型,称作direct,想象一种场景,你并不想收到所有的消息,你只对某种类型的消息感兴趣,那么发布订阅模型显然是做不到的。直接看模型图:

可以看到,exchange和queue有一个key绑定,可以推测出:消息也会有一个对应的key,例如所有key为orange的消息会被exchange分发到Q1队列。当然,Q1,Q2...所有队列都可以绑定同一种KEY,那么这样就成了某种程度的发布订阅,只是只有Key对应上的消息会被广播,譬如一个Key为white的消息就会被丢弃,因为exchange找不到可以分发的队列。

这里需要注意一种情况:以white为例,如果系统中从未有过white的绑定(bingding),那么此时生产者发送一个key为white的消息,然后关闭生产者服务,此时我们开启一个消费者服务,绑定了white,那么之前的消息收不到。但是假如此时,我们关闭生产者消费者服务,启动生产者,发送key为white的消息,关闭生产者,紧接着,开启消费者服务,那么是会受到一个消息的,因为系统中其实已经存在了white这个binding,所以消息没有被丢掉。

4.第四种,是最灵活强大的模型。主题模型:topic。其实之前提到的所谓的发布订阅,是一种弱化版的发布订阅,传统的发布订阅当然得支持我想要订阅什么类型的消息,所以,其实Topic模式可以理解为一种强大版本的发布订阅。话说回来,Topic模式其实可以替代之前所有的模型,因为他的Key太灵活了。这里还是直接搬出官方的例子:

可以看到,包含了占位符的Key,其中,占位符分为2种:# 和 *,#代表0,1,2...,*代表1个,当然,对应消息的Key也需要是这种例子,如上图,Key可以由三部分组成,形式为:<speed>.<colour>.<species>",所以C1的订阅了:所有黄色的动物,不论你跑得快不快,是什么种类的动物,C2订阅了:兔子,以及懒惰的动物。应该很好理解。注意,假如有一个消息违反了既定的条约,发送了一条lazy.orange.dog.male,那么C2依旧能够收到,C1不能收到,注意#和*的区别。

所以,之前说的Topic模式其实可以替代之前所有的模型也很好理解了:如果key为#,那么就代表原始的发布订阅,如果Key写死,没有使用占位符,那么就是direct模式。

到此,4种最简单的模型讲完了,但是其实是有很多很多细节待考究的,不管是ack机制,还是断开连接重连等各种情况,需要好好去尝试各种场景,正如官方例子所说:Such simplified code should not be considered production ready.

接下来的博文会尽量尝试各种情况,场景并记录结果,并且会用原生Java代码以及Java的观察者模式实现一种简单的消息队列服务,加深对MQ的理解。

猜你喜欢

转载自blog.csdn.net/about4years/article/details/82669222