RabbitMQ系列(二)深入瞭解RabbitMQ工作原理及簡單使用

王磊的部落格發表於2018-07-06

RabbitMQ簡介

在介紹RabbitMQ之前實現要介紹一下MQ,MQ是什麼?

MQ全稱是Message Queue,可以理解為訊息佇列的意思,簡單來說就是訊息以管道的方式進行傳遞。

RabbitMQ是一個實現了AMQP(Advanced Message Queuing Protocol)高階訊息佇列協議的訊息佇列服務,用Erlang語言的。

使用場景

在我們秒殺搶購商品的時候,系統會提醒我們稍等排隊中,而不是像幾年前一樣頁面卡死或報錯給使用者。

像這種排隊結算就用到了訊息佇列機制,放入通道里面一個一個結算處理,而不是某個時間斷突然湧入大批量的查詢新增把資料庫給搞當機,所以RabbitMQ本質上起到的作用就是削峰填谷,為業務保駕護航。

為什麼選擇RabbitMQ

現在的市面上有很多MQ可以選擇,比如ActiveMQ、ZeroMQ、Appche Qpid,那問題來了為什麼要選擇RabbitMQ?

  1. 除了Qpid,RabbitMQ是唯一一個實現了AMQP標準的訊息伺服器;
  2. 可靠性,RabbitMQ的持久化支援,保證了訊息的穩定性;
  3. 高併發,RabbitMQ使用了Erlang開發語言,Erlang是為電話交換機開發的語言,天生自帶高併發光環,和高可用特性;
  4. 叢集部署簡單,正是應為Erlang使得RabbitMQ叢集部署變的超級簡單;
  5. 社群活躍度高,根據網上資料來看,RabbitMQ也是首選;

工作機制

生產者、消費者和代理

在瞭解訊息通訊之前首先要了解3個概念:生產者、消費者和代理。

生產者:訊息的建立者,負責建立和推送資料到訊息伺服器;

消費者:訊息的接收方,用於處理資料和確認訊息;

代理:就是RabbitMQ本身,用於扮演“快遞”的角色,本身不生產訊息,只是扮演“快遞”的角色。

訊息傳送原理

首先你必須連線到Rabbit才能釋出和消費訊息,那怎麼連線和傳送訊息的呢?

你的應用程式和Rabbit Server之間會建立一個TCP連線,一旦TCP開啟,並通過了認證,認證就是你試圖連線Rabbit之前傳送的Rabbit伺服器連線資訊和使用者名稱和密碼,有點像程式連線資料庫,使用Java有兩種連線認證的方式,後面程式碼會詳細介紹,一旦認證通過你的應用程式和Rabbit就建立了一條AMQP通道(Channel)。

通道是建立在“真實”TCP上的虛擬連線,AMQP命令都是通過通道傳送出去的,每個通道都會有一個唯一的ID,不論是釋出訊息,訂閱佇列或者介紹訊息都是通過通道完成的。

為什麼不通過TCP直接傳送命令?

對於作業系統來說建立和銷燬TCP會話是非常昂貴的開銷,假設高峰期每秒有成千上萬條連線,每個連線都要建立一條TCP會話,這就造成了TCP連線的巨大浪費,而且作業系統每秒能建立的TCP也是有限的,因此很快就會遇到系統瓶頸。

如果我們每個請求都使用一條TCP連線,既滿足了效能的需要,又能確保每個連線的私密性,這就是引入通道概念的原因。

RabbitMQ系列(二)深入瞭解RabbitMQ工作原理及簡單使用

你必須知道的Rabbit

想要真正的瞭解Rabbit有些名詞是你必須知道的。

包括:ConnectionFactory(連線管理器)、Channel(通道)、Exchange(交換器)、Queue(佇列)、RoutingKey(路由鍵)、BindingKey(繫結鍵)。

**ConnectionFactory(連線管理器):**應用程式與Rabbit之間建立連線的管理器,程式程式碼中使用;

**Channel(通道):**訊息推送使用的通道;

**Exchange(交換器):**用於接受、分配訊息;

Queue(佇列):用於儲存生產者的訊息;

RoutingKey(路由鍵):用於把生成者的資料分配到交換器上;

BindingKey(繫結鍵):用於把交換器的訊息繫結到佇列上;

看到上面的解釋,最難理解的路由鍵和繫結鍵了,那麼他們具體怎麼發揮作用的,請看下圖:

RabbitMQ系列(二)深入瞭解RabbitMQ工作原理及簡單使用

關於更多交換器的資訊,我們在後面再講。

訊息持久化

Rabbit佇列和交換器有一個不可告人的祕密,就是預設情況下重啟伺服器會導致訊息丟失,那麼怎麼保證Rabbit在重啟的時候不丟失呢?答案就是訊息持久化。

當你把訊息傳送到Rabbit伺服器的時候,你需要選擇你是否要進行持久化,但這並不能保證Rabbit能從崩潰中恢復,想要Rabbit訊息能恢復必須滿足3個條件:

  1. 投遞訊息的時候durable設定為true,訊息持久化,程式碼:channel.queueDeclare(x, true, false, false, null),引數2設定為true持久化;
  2. 設定投遞模式deliveryMode設定為2(持久),程式碼:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),引數3設定為儲存純文字到磁碟;
  3. 訊息已經到達持久化交換器上;
  4. 訊息已經到達持久化的佇列;

持久化工作原理

Rabbit會將你的持久化訊息寫入磁碟上的持久化日誌檔案,等訊息被消費之後,Rabbit會把這條訊息標識為等待垃圾回收。

持久化的缺點

訊息持久化的優點顯而易見,但缺點也很明顯,那就是效能,因為要寫入硬碟要比寫入記憶體效能較低很多,從而降低了伺服器的吞吐量,儘管使用SSD硬碟可以使事情得到緩解,但他仍然吸乾了Rabbit的效能,當訊息成千上萬條要寫入磁碟的時候,效能是很低的。

所以使用者要根據自己的情況,選擇適合自己的方式。

虛擬主機

每個Rabbit都能建立很多vhost,我們稱之為虛擬主機,每個虛擬主機其實都是mini版的RabbitMQ,擁有自己的佇列,交換器和繫結,擁有自己的許可權機制。

vhost特性

  1. RabbitMQ預設的vhost是“/”開箱即用;

  2. 多個vhost是隔離的,多個vhost無法通訊,並且不用擔心命名衝突(佇列和交換器和繫結),實現了多層分離;

  3. 建立使用者的時候必須指定vhost;

vhost操作

可以通過rabbitmqctl工具命令建立:

rabbitmqctl add_vhost[vhost_name]

刪除vhost:

rabbitmqctl delete_vhost[vhost_name]

檢視所有的vhost:

rabbitmqctl list_vhosts

環境搭建

前文我們已經介紹了Ubuntu搭建RabbitMQ的步驟:RabbitMQ在Ubuntu上的環境搭建

如果你是在Windows10上去安裝那就更簡單了,先放下載地址:

Erlang/Rabbit Server百度網盤連結:pan.baidu.com/s/1TnKDV-Zu… 密碼:wct9

當然也可去Erlang和Rabbit官網去下,就是速度比較慢。我的百度雲Rabbit最新版本:3.7.6,Erlang版本:20.2,注意:不要下載最新的Erlang,在Windows10上開啟擴充套件外掛有問題,打不開。

  1. 安裝Erlang;

  2. 安裝Rabbit Server;

  3. 進入安裝目錄\sbin下,使用命令“rabbitmq-plugins enable rabbitmq_management”啟動網頁管理外掛;

  4. 重啟Rabbit服務;

使用:http://localhost:15672進行測試,預設的登陸賬號為:guest,密碼為:guest

重複安裝Rabbit Server的坑

如果不是第一次在Windows上安裝Rabbit Server一定要把Rabbit和Erlang解除安裝乾淨之後,找到登錄檔:HKEY_LOCAL_MACHINE\SOFTWARE\Ericsson\Erlang\ErlSrv 刪除其下的所有項。

不然會出現Rabbit安裝之後啟動不了的情況,理論上解除安裝的順序也是先Rabbit在Erlang。

程式碼實現

java版實現,使用maven專案,建立可以檢視:MyEclipse2017破解設定與maven專案搭建

專案建立成功之後,新增Rabbit Client jar包,只需要在pom.xml裡面配置,如下資訊:

 <dependency>
  <groupId>com.rabbitmq</groupId>
  <artifactId>amqp-client</artifactId>
  <version>5.2.0</version>
</dependency>
複製程式碼

java實現程式碼分為兩個類,第一個是建立Rabbit連線,第二是應用類使用最簡單的方式釋出和消費訊息。

Rabbit的連線,兩種方式:

方式一:

public static Connection GetRabbitConnection() {
	ConnectionFactory factory = new ConnectionFactory();
	factory.setUsername(Config.UserName);
	factory.setPassword(Config.Password);
	factory.setVirtualHost(Config.VHost);
	factory.setHost(Config.Host);
	factory.setPort(Config.Port);
	Connection conn = null;
	try {
		conn = factory.newConnection();
	} catch (Exception e) {
		e.printStackTrace();
	}
	return conn;
}
複製程式碼

方式二:

public static Connection GetRabbitConnection2() {
	ConnectionFactory factory = new ConnectionFactory();
	// 連線格式:amqp://userName:password@hostName:portNumber/virtualHost
	String uri = String.format("amqp://%s:%s@%s:%d%s", Config.UserName, Config.Password, Config.Host, Config.Port,
			Config.VHost);
	Connection conn = null;
	try {
		factory.setUri(uri);
		factory.setVirtualHost(Config.VHost);
		conn = factory.newConnection();
	} catch (Exception e) {
		e.printStackTrace();
	}
	return conn;
}
複製程式碼

第二部分:應用類,使用最簡單的方式釋出和消費訊息

public static void main(String[] args) {
	Publisher(); // 推送訊息

	Consumer(); // 消費訊息
}

/**
 * 推送訊息
 */
public static void Publisher() {
	// 建立一個連線
	Connection conn = ConnectionFactoryUtil.GetRabbitConnection();
	if (conn != null) {
		try {
			// 建立通道
			Channel channel = conn.createChannel();
			// 宣告佇列【引數說明:引數一:佇列名稱,引數二:是否持久化;引數三:是否獨佔模式;引數四:消費者斷開連線時是否刪除佇列;引數五:訊息其他引數】
			channel.queueDeclare(Config.QueueName, false, false, false, null);
			String content = String.format("當前時間:%s", new Date().getTime());
			// 傳送內容【引數說明:引數一:交換機名稱;引數二:佇列名稱,引數三:訊息的其他屬性-routing headers,此屬性為MessageProperties.PERSISTENT_TEXT_PLAIN用於設定純文字訊息儲存到硬碟;引數四:訊息主體】
			channel.basicPublish("", Config.QueueName, null, content.getBytes("UTF-8"));
			System.out.println("已傳送訊息:" + content);
			// 關閉連線
			channel.close();
			conn.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

/**
 * 消費訊息
 */
public static void Consumer() {
	// 建立一個連線
	Connection conn = ConnectionFactoryUtil.GetRabbitConnection();
	if (conn != null) {
		try {
			// 建立通道
			Channel channel = conn.createChannel();
			// 宣告佇列【引數說明:引數一:佇列名稱,引數二:是否持久化;引數三:是否獨佔模式;引數四:消費者斷開連線時是否刪除佇列;引數五:訊息其他引數】
			channel.queueDeclare(Config.QueueName, false, false, false, null);

			// 建立訂閱器,並接受訊息
			channel.basicConsume(Config.QueueName, false, "", new DefaultConsumer(channel) {
				@Override
				public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
						byte[] body) throws IOException {
					String routingKey = envelope.getRoutingKey(); // 佇列名稱
					String contentType = properties.getContentType(); // 內容型別
					String content = new String(body, "utf-8"); // 訊息正文
					System.out.println("訊息正文:" + content);
					channel.basicAck(envelope.getDeliveryTag(), false); // 手動確認訊息【引數說明:引數一:該訊息的index;引數二:是否批量應答,true批量確認小於index的訊息】
				}
			});

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
複製程式碼

程式碼裡面已經寫了很詳細的註釋,在這裡也不過多的介紹了。

執行效果,如圖:

RabbitMQ系列(二)深入瞭解RabbitMQ工作原理及簡單使用

RabbitMQ系列(二)深入瞭解RabbitMQ工作原理及簡單使用

相關文章