溫故之訊息佇列ActiveMQ

JameLee發表於2018-07-26

訊息佇列中介軟體是分散式系統中的重要元件,主要解決應用耦合、非同步訊息、流量削鋒等問題。可幫助實現高效能,高可用,可伸縮和最終一致性的架構

在訊息佇列方面,除了 ActiveMQ、RabbitMQ、RocketMQ、ZeroMQ,Kafka等,還有很多其他的競爭者。這篇文章我們不會去講解它們之間的區別,僅只詳細的介紹一下 ActiveMQ,以及它在 .NET 中的使用

訊息佇列應用場景

非同步任務

比如有以下場景:現在很多網站或App註冊時都採用了驗證碼的機制,因此,當伺服器收到客戶端發起獲取驗證碼的請求,有以下處理方式

  1. 在當前執行緒中立即傳送簡訊(會阻塞當前執行緒一小會兒)
  2. 新建立一個執行緒傳送簡訊(在 .NET 中建立一個 Task 就行)
  3. 交由其他的服務來處理這個任務(轉發給訊息佇列,讓訊息佇列處理)

那麼,以上幾種方式哪種更好呢?

  • 第一種:實時性肯定更好,收到請求立即處理,但它阻塞了當前執行緒,會造成其他客戶端的請求被阻塞(請求少的時候我們可能根本感覺不到);
  • 第二種:在當前程式中建立一個執行緒來處理,實時性不如第一種,但它不會阻塞其他客戶端的請求。不過一個程式中能建立的執行緒數量有限,因此也有瓶頸
  • 第三種:使用其他特定場景的服務,這種實時性最差(但如果伺服器配置好,我們也不一定能感覺到差異),但其是使用的最多的,並且其上線後效果是最好的(穩定性、可伸縮性)

因此,如果是正式上線的版本(比如專案初期用於驗證市場的版本,往往會為了速度而不考慮架構,這時可能會選擇第一種或第二種方案),且峰值較高的服務,選用第三種方案無疑是最好的。因為對於上線的服務,穩定性是非常重要的

對於傳送簡訊這樣的任務(對實時性要求不是那麼高),使用訊息佇列是非常合適的。將任務交由訊息佇列之後,傳送簡訊具體要做的事情主服務就不需要干涉了。如果需要,主服務訂閱任務的處理結果即可(傳送成功或者失敗)。這樣,主服務就可以繼續處理其他客戶端的請求,並且,有訊息佇列的參與,主服務的壓力就沒有那麼重了

當然,實際專案中,這樣的場景還有很多,比如記錄日誌,我們都知道,寫檔案(磁碟I/O)很耗時。因此現在很多大型的服務,都有專門的日誌伺服器來處理其他伺服器傳送過來的日誌,這時候我們可以使用 Kafka 來做這樣的事情(因為它就是為了處理日誌而生的)

訊息服務

比如現如今的微服務、分散式叢集等,各個節點之間的通訊,就可以使用訊息佇列來處理。具體使用什麼方式,可更具場景從以下兩種選擇

  • P2P(Point to Point)點對點模式
  • Publish/Subscribe(Pub/Sub) 釋出訂閱模式

後面在給出案例時會具體講解這兩種模式

ActiveMQ

ActiveMQ 是Apache出品,最流行的,能力強勁的開源訊息匯流排。ActiveMQ 是一個完全支援JMS1.1和J2EE 1.4規範的 JMS Provider實現,儘管JMS規範出臺已經是很久的事情了,但是JMS在當今的J2EE應用中間仍然扮演著特殊的地位。另外,在很多大型的網站或服務中,也都會使用到它

它具有以下特性

  • 多種語言和協議編寫客戶端
    語言:Java、C、C++、C#、Ruby、Perl、Python、PHP;
    應用協議:OpenWire、Stomp REST、WS Notification、XMPP、AMQP
  • 完全支援JMS1.1和J2EE 1.4規範 (持久化,XA訊息,事務)
  • 對Spring的支援,ActiveMQ可以很容易內嵌到使用Spring的系統裡面去,而且也支援Spring 的最新特性
  • 通過了常見J2EE伺服器(如 Geronimo、JBoss 4、GlassFish、WebLogic)的測試,其中通過JCA 1.5 resource adaptors的配置,可以讓ActiveMQ可以自動的部署到任何相容J2EE 1.4 商業伺服器上
  • 支援多種傳輸協議:in-VM、TCP、SSL、NIO、UDP、JGroups、JXTA
  • 支援通過JDBC和journal提供高速的訊息持久化
  • 從設計上保證了高效能的叢集,客戶端-伺服器以及點對點的通訊
  • 支援Ajax
  • 支援與Axis的整合
  • 可以很容易呼叫內嵌 JMS provider 進行測試

它的優勢

  • 穩定性:失敗重連機制,持久化服務, 容錯機制, 多種恢復機制
  • 高效性:支援多種傳送協議如TCP, SSL, NIO, UDP等,叢集訊息在多個代理之間轉發防止訊息丟失,支援超快的JDBC訊息持久化和高效的日誌系統
  • 可擴充套件:ActiveMQ 的高階特性都可以配置的形式來表現,很好的實現例如遊標,容錯機制,訊息group及監控服務,同時擴充套件了很多成熟的框架
  • 高階特性:訊息群組(Message Groups)、虛擬端點(Virtual Destinations)、萬用字元(Wildcards)、複合端點(Composite Destinations)

ActiveMQ在Windows上的安裝配置

這方面的教程在網上有很多,我們在這就不提供了,只提供一些移動端友好的連結以幫助朋友安裝配置

ActiveMQ在C#中的使用

首先,需要在 Apache官網 上下載 .NET 的驅動,也可以通過以下連結下載

mirrors.hust.edu.cn/apache/acti…

要在專案中使用 ActiveMQ,需要引入上面下載的包中的兩個 dll 檔案:Apache.NMS.ActiveMQ.dllApache.NMS.dll

P2P模式案例

P2P模式包含三個角色:訊息佇列(Queue),傳送者(Sender),接收者(Receiver)。
每條訊息都被髮送到一個特定的佇列,接收者從佇列中獲取訊息。佇列保留著訊息,直到它們被消費或超時

P2P的特點:

  • 每條訊息只有一個消費者(即一旦被消費,訊息就會被移除訊息佇列):在執行了多個消費者之後,一條訊息只會有一個消費者收到,其他的消費者是不可以收到的
  • 接收者在成功接收訊息之後需向佇列應答成功:我們可以通過指定應答模式來更改,預設是自動應答模式

因此,如果希望傳送的每個訊息都會被成功處理的話,則應該P2P模式

示例程式碼的基類如下

public abstract class ActiveMQBase {
    protected IConnectionFactory factory;
    protected IConnection connection;
    protected ISession session;

    public virtual void Init() {
        try {
            //初始化工廠, 埠預設為61616,指定其他會拋異常
            factory = new ConnectionFactory("tcp://localhost:61616");
            connection = factory.CreateConnection();
            connection.Start();
            session = connection.CreateSession();
        } catch (Exception e) {
            Console.WriteLine($"Error: {e.Message}");
        }
    }
    
    public abstract void Run();

    // 釋放相關資源
    public virtual void Release() {
        try {
            if (session != null) session.Close();
            if (connection != null) connection.Close();
        } finally {
            session = null;
            connection = null;
            factory = null;
        }
    }
}
複製程式碼

生產者(Producer)如下

public class ActiveMQP2PDemoProducer : ActiveMQBase {
    private IMessageProducer messageProducer;
    private ActiveMQQueue demoQueue;

    public override void Init() {
        base.Init();
        try {
            // 指定佇列,以實現點對點的通訊 
            demoQueue = new ActiveMQQueue("DEMO_QUEUE");
            // 建立生產者物件
            messageProducer = session.CreateProducer(demoQueue);
        } catch (Exception e) {
            Console.WriteLine($"Error: {e.Message}");
        }
    }
    
    public override void Run() {
        while (true) {
            Console.WriteLine("請輸入訊息,exit 退出");
            string line = Console.ReadLine();
            if (line.Equals("exit", StringComparison.InvariantCultureIgnoreCase)) {
                break;
            }
            // 建立一條文字訊息,在 MessageProvider 中存在多個建立訊息的方法
            // 在實際專案中靈活選擇即可
            ITextMessage message = messageProducer.CreateTextMessage(line);
            // 傳送訊息,可呼叫其他的過載,以設定是否持久化、優先順序等特性
            messageProducer.Send(message);
        }
    }

    public override void Release() {
        base.Release();
        try {
            if (demoQueue != null) demoQueue.Dispose();
            if (messageProducer != null) messageProducer.Close();
        } finally {
            demoQueue = null;
            messageProducer = null;
        }
    }
}
複製程式碼

消費者(Consumer)如下

public class ActiveMQP2PDemoComsumer : ActiveMQBase {
    private IMessageConsumer messageConsumer;
    private ActiveMQQueue demoQueue;

    public override void Init() {
        base.Init();
        try {
            demoQueue = new ActiveMQQueue("DEMO_QUEUE");
            // 建立訊息的消費者
            messageConsumer = session.CreateConsumer(demoQueue);
            // 新增監聽,當訊息來臨時,會觸發此事件
            messageConsumer.Listener += this.MessageConsumer_Listener;
        } catch (Exception e) {
            Console.WriteLine($"Error: {e.Message}");
        }
    }


    private void MessageConsumer_Listener(IMessage message) {
        // 解析接收到的訊息
        if (message is ITextMessage msg) {
            Console.WriteLine($"Received Message: {msg.Text}");
        }
    }

    public override void Run() {
        // 此處用於阻止控制檯結束,以保證訊息可被正確處理
        Console.WriteLine("請輸入訊息,exit 退出");
        string line = Console.ReadLine();
    }

    public override void Release() {
        base.Release();
        try {
            if (demoQueue != null) demoQueue.Dispose();
            if (messageConsumer != null){
               messageConsumer.Listener -= this.MessageConsumer_Listener;
               messageConsumer.Close();
            }
        } finally {
            demoQueue = null;
            messageConsumer = null;
        }
    }
}
複製程式碼

使用方式如下

// 生產者初始化
ActiveMQP2PBase demo = new ActiveMQP2PDemoProducer();
// 消費者初始化程式碼則為: ActiveMQP2PBase demo = new ActiveMQP2PDemoComsumer();
demo.Init();
demo.Run();
demo.Release();
複製程式碼

在 ActiveMQ 管理介面可以看到如下,表示生產者傳送的訊息,都已經被消費者消費了

P2P模式

Pub/Sub模式

Pub/Sub模式:包含三個角色主題(Topic),釋出者(Publisher),訂閱者(Subscriber)。多個釋出者將訊息傳送到Topic, 系統將這些訊息傳遞給多個訂閱者,可以認為生產者與消費者之間是多對多的關係

Pub/Sub的特點

  • 每條訊息可以有多個消費者
  • 為了消費訊息,訂閱者必須保持執行的狀態
  • 為了緩和這樣嚴格的時間相關性,JMS 允許訂閱者建立一個可持久化的訂閱。這樣即使訂閱者沒有執行,在執行之後它也能接收到釋出者的訊息

因此,如果允許傳送的訊息可以被一個或多個消費者消費、或者可以不被消費,那麼可以採用 Pub/Sub 模型

在 C# 中,它與 P2P 的使用區別不大,只需要將上述程式碼生產者和消費者初始化程式碼中

demoQueue = new ActiveMQQueue("DEMO_QUEUE");
複製程式碼

這部分換成

demoTopic = new ActiveMQTopic("DEMO_TOPIC");
複製程式碼

在管理員介面可以看到如下資料

Pub/Sub

通過示例可以看出,P2P 是基於 Queue 的,而 Pub/Sub 模式則是基於 Topic 的。

在 Pub/Sub 模式下,可以實現多對多的通訊,即可以有多個生產者,也可以有多個消費者,一旦有訊息到來,它們會都會收到訊息。

而P2P模式下,它可以允許有多個生產者,也可以有多個消費者。與 Pub/Sub 不同的是,如果有多個消費者,如果有訊息到來,這些消費者會輪流著去消費該訊息,而不是每個消費者都收到訊息。即一條訊息只會有一個消費者

由於在 C# 中,這兩種模式的使用方式差別很小,而執行之後產生的行為卻差別較大。因此,在實際專案中,我們需要注意這兩者之間的區別,以免帶來不必要的困惑

實際專案中的一些問題

ActiveMQ伺服器當機怎麼辦 如果我們想要在伺服器當機之後恢復資料,則需要對訊息進行持久化

在通常的情況下,非持久化訊息是儲存在記憶體中的,持久化訊息是儲存在檔案中的。它們的最大限制在配置檔案的<systemUsage>節點中配置

但是,在非持久化訊息堆積到一定程度,記憶體告急的時候,ActiveMQ 會將記憶體中的非持久化訊息寫入臨時檔案中,以騰出記憶體。雖然都儲存到了檔案裡,但它和持久化訊息的區別是,重啟後持久化訊息會從檔案中恢復,非持久化的臨時檔案會直接刪除(即重啟之後不會從臨時檔案中恢復訊息)

因此,為了保證資料的可靠性

  • 儘量使用持久化訊息(訊息不重要也可以不用持久化)
  • 可以將持久化與非持久化檔案的限制調大一點,以保證服務最大可用

丟訊息

這同樣是持久化訊息的問題。對於這種情況,我們可以

  1. 儘量將訊息持久化
  2. 如果不想持久化,那麼我們應該儘可能的及時處理非持久化的訊息
  3. 使用事務,它可以保證訊息不會因為連線關閉而丟失

持久化訊息比較慢

預設的情況下,非持久化訊息是非同步傳送的;而持久化訊息是同步傳送的。遇到慢一點的硬碟,傳送訊息的速度也會很慢

但如果開啟事務的情況下,訊息都會非同步傳送,效率會有非常大的提升。所以在傳送持久化訊息時,我們應該務必開啟事務。並且我們也建議傳送非持久化訊息時也開啟事務

自定義 ActiveMQ 的重發策略(Redelivery Policy)

可通過 ConnectionFactory.RedeliveryPolicy 屬性設定

  • CollisionAvoidancePercent:預設值 0.15, 設定防止衝突範圍的正負百分比,只有啟用 UseCollisionAvoidance 引數時才生效
  • MaximumRedeliveries:預設值 6, 最大重傳次數,達到最大重連次數後丟擲異常。為-1時不限制次數,為0時表示不進行重傳
  • InitialRedeliveryDelay:預設值 1000, 初始重發延遲時間
  • UseCollisionAvoidance:預設值 false, 啟用防止衝突功能
  • UseExponentialBackOff:預設值 false, 啟用指數倍數遞增的方式增加延遲時間
  • BackOffMultiplier:預設值 5, 重連時間間隔遞增倍數,只有值大於1和啟用 UseExponentialBackOff 引數時才生效。

多消費者併發處理

在有多個消費者,ActiveMQ 中累積了大量的資料的情況下,有可能會出現只有一個消費者消費、其他消費者不“工作”的情況

這種情況下,我們只需要將 ActiveMQ 的 prefetch 值設定得小一點即可。在 Queue模式時,其預設值為 1000;Topic 下為 32766。可通過 ConnectionFactory.PrefetchPolicy 設定

這篇文章就先講到這裡,後面我們會講解 ActiveMQ 的一些其他場景,如分散式叢集。歡迎持續關注公眾號【嘿嘿的學習日記】,Thank you~

公眾號二維碼

相關文章