系统拆分解耦利器之消息队列---RabbitMQ-发布/订阅

[一曲广陵不如晨钟暮鼓]

本文,我们来介绍RabbitMQ中的发布与订阅。在正式开始之前,我们假设RabbitMQ服务已经启动,运行端口为5672,如果各位看官有更改过默认配置,那么就需要修改为对应端口,保持一致即可。

准备工作:

操作系统:window 7 x64 

其他软件:eclipse mars,jdk7,maven 3

--------------------------------------------------------------------------------------------------------------------------------------------------------

发布/订阅

在前文中,我们创建了一个工作队列。在工作队列上,我们假设每一个任务都投递给一个指定的worker(消费者)来处理。在本文中,我们将会改变上面的这种设置:任务将会被投递给多个worker(消费者)。这种模式称之为“发布/订阅”。

为了说明这种模式,我们将会构建一个简单的日志系统:

  • 生产者负责产生与发送消息。
  • 接收者其由两个程序组成,第一个:输出日志消息到控制台,第二个:输出消息到文件。

在我们构建的日志系统中,每一个复制出来的运行态的接收者都会收到相同的消息。

本质上讲:发布的日志消息最终会转发给所有的接收者。

Exchanges(交换器)

在前面的教程中,我们都是向队列发送消息,再从队列中取出消息。现在,是时候来介绍RabbitMQ的完整的消息模型。

先让我们快速的回顾下之前提及的概念术语:

producer:客户端程序,用来产生并发送消息。

queue:存储消息。

consumer:客户端程序,用来接收消息。

RabbitMQ消息模型的核心思想是:producer绝不会直接发送任何消息到队列中。事实上,producer甚至不知道消息会被投递给哪些具体的队列。

取而代之的,producer只负责将消息发送给exchange。这个exchange功能非常简单,其中一边连接producer,接受来自其上的消息,另一边连接queue,将接收到的消息push到queue中。exchange非常明确的知道消息应该投递的路径及目标。如是应该被投递给一个具体的队列?一组队列?或者丢弃等等。这些具体规则会按照exchange所定义的类型不同而不同。


以下是可用的exchange类型:direct,topic,headers,fanout,共计4种。首先,我们先来介绍最后一种“fanout”。其创建语句为:

channel.exchangeDeclare("logs", "fanout");
正如其名称所示,“fanout”模式使用非常简单策略,其是把所有的消息广播到所有的队列中。在我们构建的日志系统中,非常适合使用这种策略。

-------------------------------------------------------------------------------------------------------------------------------------------------------

备注:

各位看官可以在安装目录下运行如下命令,观察服务器中已经存在的exchange名称,及其类型有哪些;(windows环境)


在上面的截图中,名称为“  amq.*  "的exchange是默认存在的,但是,对于这些exchange,,现阶段是不大可能会使用的

匿名的exchange

在上文中,我们还没有介绍,使用任何关于exchange的内容。但是,我们同样能够将消息发送到队列中。这是因为我们使用了默认的exchange,其对应的名称是一个空字符串。

回想下上文发布消息时所以用的语句,如下:

channel.basicPublish("", "hello", null, message.getBytes());
第一个参数的左右就是声明一个exchange。即:空字符串声明了一个默认或者匿名的exchange:message通过routingKey被投递到一个指定的队列当中。

----------------------------------------------------------------------------------------------------------------------------------------------

现在,我们来讲消息发布到“logs”的exchange中,如下:

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

临时队列

各位看官还记不记得前面我们使用的两个指定名称的队列(hello,task_queue)。为一个queue命名是非常重要的---我们需要为接受者也指明同样名称的队列,这样才能保证消息正常传递。因此,如果你想在生产者与接受者之间共享队列的话,为队列指定一个名称是非常重要的。

但这不是我们日志系统所重点关注的内容。因为,我们想让所有的接受者都能够接收到消息,不仅仅是接受者集合中的一部分成员。同时,我们也关心实时传输的消息,而不是历史消息。为了实现这两个目标,需要做两件事情来保证。

第一:无论何时连接到RabbitMQ,我们需要刷新一个新的空队列。解决办法:每次可以使用随机名称创建一个空队列,或者,让服务器选择一个随机的空队列名称返回给我们。其原理都是一致的---即需要一个空队列。

第二:一旦与RabbitMQ断开连接,刚刚使用过的队列就要被删除。

在java客户端中,当我们使用无参数的queueDeclare()方法时,就会创建一个带有名称的,非持久化的,唯一的,自动删除功能的队列。如下:

String queueName = channel.queueDeclare().getQueue();
其返回结果,queueName的值,可能是这样的形式:amq.gen-JzTY20BRgKO-HjmUJj0wLg.

绑定


经过上面的步骤,我们已经创建了一个fanout的exchange,和一个空队列。现在,我们需要告诉exchange将消息发送到我们的队列当中。我们将建立这种关系的过程称之为绑定。具体做法如下:

channel.queueBind(queueName, "logs", "");
至此,消息已经能够从exchange进入到队列中了。

综上所述,我们来看看一份完成的示例工程吧,结构图如下:



1.修改pom文件,具体内容请看前文,在此不再赘述。

2.创建EmitLog文件,具体内容如下:

生产者程序,负责产生日志消息,这里的内容和我们前文示例代码并没有多大区别。最重要的变化是:我们发布消息到logs的exchange中。并且我们需要在发送时使用routingKey,但是其值对于fanout模式是没有意义的。

正如下文展示的,在建立连接之后,我们声明了exchange,这一步是必须的。因为,RabbitMQ禁止向一个不存在的exchange发布任何消息。

package com.csdn.ingo.rabbitmq_1;

import java.io.IOException;
import java.util.Date;

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


public class EmitLog {
	// 队列名称
	private final static String EXCHANGE_NAME = "ex_log";

	public static void main(String[] args) throws IOException {
		// 创建连接和频道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		// 声明队列
		channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
		String message = new Date().toLocaleString()+":log something";
		channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
		System.out.println(" [x] Sent :"+message);
		// 关闭频道和资源
		channel.close();
		connection.close();

	}
}
3.创建ReceiveLogsToConsole文件,具体内容如下:

如果不进行队列与exchange的绑定的话,消息将会丢失,但是对于目前而言:如果没有consumer在舰艇,我们可以安全的删除消息。

package com.csdn.ingo.rabbitmq_1;

import java.io.IOException;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.ConsumerCancelledException;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.ShutdownSignalException;


public class ReceiveLogsToConsole {
	private final static String EXCHANGE_NAME = "ex_log";
	
	public static void main(String[] args) throws IOException, ShutdownSignalException, ConsumerCancelledException, InterruptedException {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection conn = factory.newConnection();
		Channel channel = conn.createChannel();
		
		channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
		String queueName = channel.queueDeclare().getQueue();
		channel.queueBind(queueName, EXCHANGE_NAME, "");
		System.out.println("[*] waiting for messages. To exit press CTRL+C");
		QueueingConsumer consumer = new QueueingConsumer(channel);
		channel.basicConsume(queueName,true,consumer);
		while(true){
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			System.out.println("[x] Received:"+message);
		}
	}
}
4.创建ReceiveLogsSave文件,具体内容如下:

package com.csdn.ingo.rabbitmq_1;

import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

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


public class ReceiveLogsToSave {
	// 队列名称
	private final static String EXCHANGE_NAME = "ex_log";

	public static void main(String[] argv) throws java.io.IOException, java.lang.InterruptedException {
		// 创建连接和频道
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost("localhost");
		Connection connection = factory.newConnection();
		Channel channel = connection.createChannel();
		
		channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
		String queueName = channel.queueDeclare().getQueue();
		channel.queueBind(queueName, EXCHANGE_NAME, "");
		System.out.println("[*]Waiting for message.To exit press CTRL+C");

		QueueingConsumer consumer = new QueueingConsumer(channel);
		// 指定消费队列
		channel.basicConsume(queueName, true, consumer);
		while (true) {
			QueueingConsumer.Delivery delivery = consumer.nextDelivery();
			String message = new String(delivery.getBody());
			print2File(message);
		}

	}

	private static void print2File(String message) {
		try{
			String dir = ReceiveLogsToSave.class.getClassLoader().getResource("").getPath();
			String fileName = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
			File file= new File(dir,fileName+".txt");
			FileOutputStream fos = new FileOutputStream(file,true);
			fos.write((message+"\r\n").getBytes());
			fos.flush();
			fos.close();
		}catch(Exception e){
			e.printStackTrace();
		}
	}

}
5.测试方法:先运行两个接收端,在运行发送端。观察控制台,文件目录日志输出即可。

--------------------------------------------------------------------------------------------------------------------------------

至此,系统拆分解耦利器之消息队列---RabbitMQ-发布/订阅 结束


参考资料:

官方文档:http://www.rabbitmq.com/tutorials/tutorial-three-java.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值