系統拆分解耦利器之消息隊列---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

發佈了119 篇原創文章 · 獲贊 182 · 訪問量 59萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章