RabbitMQ的web頁面介紹(三)

童話述說我的結局 發表於 2021-10-13
RabbitMQ

一、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 configurewriteread
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分配使用者,新增一個新使用者)
RabbitMQ的web頁面介紹(三)

3.在 Admin -> Users 頁面新增一個名為 order-user 的使用者,並設定為 management 角色。

 

RabbitMQ的web頁面介紹(三)

4. 從 Admin 進入 order-user 的使用者設定介面,在 Permissions 中,為使用者分配vhost為/v1,併為每種許可權設定需要匹配的目標名稱的正規表示式。
 RabbitMQ的web頁面介紹(三)

 

 

欄位名
值 
說明 
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();
                }
            }
        }
    }
}

RabbitMQ的web頁面介紹(三)

 

 RabbitMQ的web頁面介紹(三)

 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()

  git原始碼:https://gitee.com/TongHuaShuShuoWoDeJieJu/rabbit.git