一、Virtual Hosts
每一個 RabbitMQ 伺服器都能建立虛擬的訊息伺服器,我們稱之為虛擬主機 (virtual host) ,簡稱為vhost。每一個 vhost 本質上是一個獨立的小型 RabbitMQ 伺服器,擁有自己獨立的佇列、交換器及繫結關係等,井且它擁有自己獨立的許可權。vhost 就像是虛擬機器與物理伺服器一樣,它們在各個例項間提供邏輯上的分離,為不同程式安全保密地執行資料,它既能將同一個RabbitMQ 中的眾多客戶區分開,又可以避免佇列和交換器等命名衝突。vhost 之間是絕對隔離的,無法將 vhostl 中的交換器與 vhost2 中的佇列進行繫結,這樣既保證了安全性,又可以確保可移植性。如果在使用 RabbitMQ 達到一定規模的時候,建議使用者對業務功能、場景進行歸類區分,併為之分配獨立的 vhost。
1.1、Virtual Hosts 的功能說明
vhost可以限制最大連線數和最大佇列數,並且可以設定vhost下的使用者資源許可權和Topic許可權,具體許可權見下方說明。
- 在 Admin -> Limits 頁面可以設定vhost的最大連線數和最大佇列數,達到限制後,繼續建立,將會報錯。
- 使用者資源許可權是指RabbitMQ 使用者在客戶端執行AMQP操作命令時,擁有對資源的操作和使用許可權。許可權分為三個部分: configure、write、read ,見下方表格說明。參考:http://www.rabbitmq.com/access-control.html#permissions
AMQP 0-9-1 Operation | configure | write | read | |
---|---|---|---|---|
exchange.declare | (passive=false) | exchange | ||
exchange.declare | (passive=true) | |||
exchange.declare | (with [AE](ae.html)) | exchange | exchange (AE) | exchange |
exchange.delete | exchange | |||
queue.declare | (passive=false) | queue | ||
queue.declare | (passive=true) | |||
queue.declare | (with [DLX](dlx.html)) | queue | exchange (DLX) | queue |
queue.delete | queue | |||
exchange.bind | exchange (destination) | exchange (source) | ||
exchange.unbind | exchange (destination) | exchange (source) | ||
queue.bind | queue | exchange | ||
queue.unbind | queue | exchange | ||
basic.publish | exchange | |||
basic.get | queue | |||
basic.consume | queue | |||
queue.purge | queue |
舉例說明:
-
- 比如建立佇列時,會呼叫 queue.declare 方法,此時會使用到 configure 許可權,會校驗佇列名是否與 configure 的表示式匹配。
- 比如佇列繫結交換器時,會呼叫 queue.bind 方法,此時會用到 write 和 read 許可權,會檢驗佇列名是否與 write 的表示式匹配,交換器名是否與 read 的表示式匹配。
-
- Topic許可權是RabbitMQ 針對STOMP和MQTT等協議實現的一種許可權。由於這類協議都是基於Topic消費的,而AMQP是基於Queue消費,所以AMQP的標準資源許可權不適合用在這類協議中,而Topic許可權也不適用於AMQP協議。所以,我們一般不會去使用它,只用在使用了MQTT這類的協議時才可能會用到。
2.2、vhost使用示例
1. 使用管理員使用者登入Web管理介面。
2.頁面新增一個名為 v1 的Virtual Hosts。(此時還需要為此vhost分配使用者,新增一個新使用者)
3.在 Admin -> Users 頁面新增一個名為 order-user 的使用者,並設定為 management 角色。
4. 從 Admin 進入 order-user 的使用者設定介面,在 Permissions 中,為使用者分配vhost為/v1,併為每種許可權設定需要匹配的目標名稱的正規表示式。
欄位名
|
值
|
說明
|
Virtual Host
|
/v1
|
指定使用者的vhost,以下許可權都只限於 /v1 vhost中
|
Configure regexp
|
eq-.*
|
只能操作名稱以eq-開頭的exchange或queue;為空則不能操作任何exchange和queue
|
Write regexp
|
.*
|
能夠傳送訊息到任意名稱的exchange,並且能繫結到任意名稱的佇列和任意名稱的目標交
換器(指交換器繫結到交換器),為空表示沒有許可權
|
Read regexp
|
^test$
|
只能消費名為test佇列上的訊息,並且只能繫結到名為test的交換器
|
5.程式碼演示
public class Producer { public static void main(String[] args) { // 1、建立連線工廠 ConnectionFactory factory = new ConnectionFactory(); // 2、設定連線屬性 factory.setUsername("order-user"); factory.setPassword("order-user"); factory.setVirtualHost("v1"); Connection connection = null; Channel channel = null; // 3、設定每個節點的連結地址和埠 Address[] addresses = new Address[]{ new Address("192.168.0.1", 5672), new Address("192.168.0.2", 5672) }; try { // 開啟/關閉連線自動恢復,預設是開啟狀態 factory.setAutomaticRecoveryEnabled(true); // 設定每100毫秒嘗試恢復一次,預設是5秒:com.rabbitmq.client.ConnectionFactory.DEFAULT_NETWORK_RECOVERY_INTERVAL factory.setNetworkRecoveryInterval(100); factory.setTopologyRecoveryEnabled(false); // 4、使用連線集合裡面的地址獲取連線 connection = factory.newConnection(addresses, "生產者"); // 新增重連監聽器 ((Recoverable) connection).addRecoveryListener(new RecoveryListener() { /** * 重連成功後的回撥 * @param recoverable */ public void handleRecovery(Recoverable recoverable) { System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS").format(new Date()) + " 已重新建立連線!"); } /** * 開始重連時的回撥 * @param recoverable */ public void handleRecoveryStarted(Recoverable recoverable) { System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SS").format(new Date()) + " 開始嘗試重連!"); } }); // 5、從連結中建立通道 channel = connection.createChannel(); /** * 6、宣告(建立)佇列 * 如果佇列不存在,才會建立 * RabbitMQ 不允許宣告兩個佇列名相同,屬性不同的佇列,否則會報錯 * * queueDeclare引數說明: * @param queue 佇列名稱 * @param durable 佇列是否持久化 * @param exclusive 是否排他,即是否為私有的,如果為true,會對當前佇列加鎖,其它通道不能訪問,並且在連線關閉時會自動刪除,不受持久化和自動刪除的屬性控制 * @param autoDelete 是否自動刪除,當最後一個消費者斷開連線之後是否自動刪除 * @param arguments 佇列引數,設定佇列的有效期、訊息最大長度、佇列中所有訊息的生命週期等等 */ channel.exchangeDeclare("test-exchange", "fanout"); channel.queueDeclare("queue1", false, false, false, null); channel.queueBind("queue1", "test-exchange", "xxoo"); for (int i = 0; i < 100; i++) { // 訊息內容 String message = "Hello World " + i; try { // 7、傳送訊息 channel.basicPublish("test-exchange", "queue1", null, message.getBytes()); } catch (AlreadyClosedException e) { // 可能連線已關閉,等待重連 System.out.println("訊息 " + message + " 傳送失敗!"); i--; TimeUnit.SECONDS.sleep(2); continue; } System.out.println("訊息 " + i + " 已傳送!"); TimeUnit.SECONDS.sleep(2); } } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } finally { // 8、關閉通道 if (channel != null && channel.isOpen()) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } // 9、關閉連線 if (connection != null && connection.isOpen()) { try { connection.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
public class VirtualHosts { public static void main(String[] args) { // 1、建立連線工廠 ConnectionFactory factory = new ConnectionFactory(); // 2、設定連線屬性 factory.setUsername("order-user"); factory.setPassword("order-user"); factory.setVirtualHost("v1"); Connection connection = null; Channel prducerChannel = null; Channel consumerChannel = null; // 3、設定每個節點的連結地址和埠 Address[] addresses = new Address[]{ new Address("192.168.0.1", 5672), new Address("192.168.0.2", 5672) }; try { // 4、從連線工廠獲取連線 connection = factory.newConnection(addresses, "消費者"); // 5、從連結中建立通道 prducerChannel = connection.createChannel(); prducerChannel.exchangeDeclare("test-exchange", "fanout"); prducerChannel.queueDeclare("queue1", false, false, true, null); prducerChannel.queueBind("queue1", "test-exchange", "xxoo"); // 訊息內容 String message = "Hello A"; prducerChannel.basicPublish("test-exchange", "c1", null, message.getBytes()); consumerChannel = connection.createChannel(); // 建立一個消費者物件 Consumer consumer = new DefaultConsumer(consumerChannel) { @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("收到訊息:" + new String(body, "UTF-8")); } }; consumerChannel.basicConsume("queue1", true, consumer); System.out.println("等待接收訊息"); System.in.read(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } finally { // 9、關閉通道 if (prducerChannel != null && prducerChannel.isOpen()) { try { prducerChannel.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } // 10、關閉連線 if (connection != null && connection.isOpen()) { try { connection.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
2.3、叢集連線恢復
官方資料:https://www.rabbitmq.com/api-guide.html#connection-recovery;根據官方文件說明可知:
- 通過 factory.setAutomaticRecoveryEnabled(true); 可以設定連線自動恢復的開關,預設已開啟
- 通過 factory.setNetworkRecoveryInterval(10000); 可以設定間隔多長時間嘗試恢復一次,預設是5秒: com.rabbitmq.client.ConnectionFactory.DEFAULT_NETWORK_RECOVERY_INTERVAL
- 如果啟用了自動連線恢復,將由以下事件觸發:
-
- 連線的I/O迴圈中丟擲IOExceiption
- 讀取Socket套接字超時
- 檢測不到伺服器心跳
- 在連線的I/O迴圈中引發任何其他異常
- 如果客戶端第一次連線失敗,不會自動恢復連線。需要我們自己負責重試連線、記錄失敗的嘗試、實現重試次數的限制等等。
ConnectionFactory factory = new ConnectionFactory(); // configure various connection settings try { Connection conn = factory.newConnection(); } catch (java.net.ConnectException e) { Thread.sleep(5000); // apply retry logic }
-
- 如果程式中呼叫了 Connection.Close ,也不會自動恢復連線。
- 如果是 Channel-level 的異常,也不會自動恢復連線,因為這些異常通常是應用程式中存在語義問題(例如試圖從不存在的佇列消費)。
- 在Connection和Channel上,可以設定重新連線的監聽器,開始重連和重連成功時,會觸發監聽器。新增和移除監聽,需要將Connection或Channel強制轉換成Recoverable介面。
((Recoverable) connection).addRecoveryListener()
((Recoverable) connection).removeRecoveryListener()