脈絡:你正在用 Message Router 把訊息路由到多個目的地。

效率與相依的兩難#

Message Router 非常有效率,因為它能把訊息直接送到正確的目的地。

其他路由方案——尤其是反應式過濾(見 Message FilterRouting Slip)——效率較低,因為它們採試誤法:先把訊息送到第一個可能的目的地,若是正確的就接受,否則再傳給第二個,依此類推。

分散式的路由方案還有另一個風險:一則訊息可能有多個接收者,或一個都沒有。若沒有中央路由元件,這兩種狀況都可能無人察覺。

但要達到這種準確度,Message Router 就必須內含每個目的地的知識與路由規則。

解法#

除了一般的輸入與輸出通道之外,Dynamic Router 多用一個控制通道

  1. 系統啟動時,每個潛在接收者在控制通道上送一則特殊訊息給 Dynamic Router,宣告自己的存在,並列出自己能處理訊息的條件
  2. Dynamic Router 把每個參與者的「偏好」存進規則庫
  3. 訊息抵達時,Dynamic Router 評估所有規則,把訊息路由給規則被滿足的那個接收者

這讓我們得到有效率的預測式路由,卻不必背負「路由器相依於每個潛在接收者」的維護代價

圖 7-5:Dynamic Router 解法示意

組態的兩種作法#

  • 各參與者啟動時主動宣告——這要求每個參與者知道 Dynamic Router 使用的控制佇列,也要求 Dynamic Router 把規則持久化儲存;否則路由器一旦故障重啟,就無法回復路由規則。
  • 由 Dynamic Router 廣播訊息觸發各方回覆控制訊息——這種組態更強健,但需要額外的 Publish-Subscribe Channel

把控制通道擴充成允許參與者送出「訂閱」與「取消訂閱」訊息,可能很有價值——這讓接收者能在執行期把自己加入或移出路由方案。

規則衝突#

因為接收者彼此獨立,Dynamic Router 必須處理規則衝突——多個接收者宣告對同一類訊息有興趣。可用的策略有:

  • 忽略與既有規則衝突的控制訊息——確保路由規則無衝突。

但這樣一來,路由表的狀態會取決於各接收者啟動的先後順序。若所有接收者同時啟動、同時往控制佇列宣告偏好,可能導致無法預測的行為。

  • 送給第一個條件相符的接收者——允許路由表存在衝突,但在訊息進來時才化解它們
  • 送給所有條件相符的接收者——能容忍衝突,但這把 Dynamic Router 變成了 Recipient List

一般而言,Content-Based Router 的行為意味著每則輸入訊息對應一則輸出訊息,而最後這個策略違反了這條規則

主要代價#

Dynamic Router 的主要負債是方案的複雜度,以及對動態設定的系統除錯的困難

兩個對照#

Dynamic Router 是「訊息式中介軟體執行類似低階 IP 網路功能」的又一例——它的運作方式非常接近 IP 路由中用來在網路之間轉送封包的動態路由表,而接收者用來設定它的協定,則類比於 IP 的 Routing Information Protocol(RIP)

Dynamic Router 與 Dispatcher 的差別在於:它可以比單純的查表更聰明。

常見用途:SOA 的動態服務發現#

Dynamic Router 的一個常見用途是服務導向架構中的動態服務發現

  • 客戶端想存取某個服務時,送一則含有服務名稱的訊息給 Dynamic Router
  • Dynamic Router 維護一份服務目錄——所有服務的名稱與它們監聽的通道
  • 路由器把請求的服務名稱與目錄比對,並把訊息路由到正確的通道

這種配置允許同一個服務由多個提供者提供:客戶端可以持續往單一通道送命令訊息,而不必操心指定服務提供者的性質或位置

範例:用 C# 與 MSMQ 實作動態路由器

這個範例建立在 Content-Based Router 的範例之上,把它強化成 Dynamic Router。新元件監聽兩個通道:inQueuecontrolQueue。控制佇列接受 "X:QueueName" 格式的訊息,讓路由器把所有內文以字母 X 開頭的訊息路由到 QueueName

class DynamicRouter
{
    protected MessageQueue inQueue;
    protected MessageQueue controlQueue;
    protected MessageQueue dunnoQueue;

    protected IDictionary routingTable = (IDictionary)(new Hashtable());

    public DynamicRouter(MessageQueue inQueue, MessageQueue controlQueue, MessageQueue dunnoQueue)
    {
        this.inQueue = inQueue;
        this.controlQueue = controlQueue;
        this.dunnoQueue = dunnoQueue;

        inQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(OnMessage);
        inQueue.BeginReceive();

        controlQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(OnControlMessage);
        controlQueue.BeginReceive();
    }

    protected void OnMessage(Object source, ReceiveCompletedEventArgs asyncResult)
    {
        MessageQueue mq = (MessageQueue)source;
        mq.Formatter = new System.Messaging.XmlMessageFormatter(
            new String[] {"System.String,mscorlib"});
        Message message = mq.EndReceive(asyncResult.AsyncResult);

        String key = ((String)message.Body).Substring(0, 1);

        if (routingTable.Contains(key))
        {
            MessageQueue destination = (MessageQueue)routingTable[key];
            destination.Send(message);
        }
        else
            dunnoQueue.Send(message);
        mq.BeginReceive();
    }

    // control message format is X:QueueName as a single string
    protected void OnControlMessage(Object source, ReceiveCompletedEventArgs asyncResult)
    {
        MessageQueue mq = (MessageQueue)source;
        mq.Formatter = new System.Messaging.XmlMessageFormatter(
            new String[] {"System.String,mscorlib"});
        Message message = mq.EndReceive(asyncResult.AsyncResult);

        String text = ((String)message.Body);
        String [] split = (text.Split(new char[] {':'}, 2));
        if (split.Length == 2)
        {
            String key = split[0];
            String queueName = split[1];
            MessageQueue queue = FindQueue(queueName);
            routingTable.Add(key, queue);
        }
        else
        {
            dunnoQueue.Send(message);
        }
        mq.BeginReceive();
    }

    protected MessageQueue FindQueue(string queueName)
    {
        if (!MessageQueue.Exists(queueName))
        {
            return MessageQueue.Create(queueName);
        }
        else
            return new MessageQueue(queueName);
    }
}

這個範例的衝突化解機制非常簡單——後到者獲勝。若兩個接收者都表示想收 X 開頭的訊息,只有第二個會收到,因為 hashmap 對每個鍵只存一個佇列。

另外注意 dunnoQueue 現在會收到兩類訊息:找不到相符路由規則的進站訊息,以及格式不符要求的控制訊息