脈絡:延續訂單處理的例子,假設公司管理層要向大客戶發布價格變動與促銷訊息。價格一變就送訊息通知客戶;跑促銷活動(例如十一月所有 widget 打九折)時也一樣。

但有些客戶只對特定商品的價格更新或促銷有興趣——我主要買 gadget,就未必想知道 widget 有沒有特價。

幾條走不通的路#

只訂閱相關通道#

最基本的方式是只訂閱承載相關訊息的通道,利用 Publish-Subscribe Channel 天生的路由能力。例如開一個 widget 更新通道、一個 gadget 更新通道,客戶自由訂閱其一或兩者。優點是新訂閱者能加入而不必改動系統。

例如若要讓消費者能接收「widget 或 gadget 降價超過 5%、10% 或 15%」的所有訊息,就已經需要 6 個通道(2 種商品 × 3 個門檻)。這既難管理,又因為配置了大量通道而消耗可觀資源。

讓路由器送給多個目的地#

我們可以改造 Content-Based Router 讓它把訊息路由給多個目的地(也就是 Recipient List 的概念)。這種預測式路由只送相關訊息給每個接收者,接收者不必額外做事。

但這等於把「維護每一位訂閱者的偏好」的負擔壓在訊息發起者身上。若接收者清單或他們的偏好變動頻繁,這會是維護惡夢。

廣播給所有元件,讓元件自己濾掉#

這假設我們能控制那些元件。在許多整合情境中並非如此——我們面對的是套裝應用、遺留應用,或不在本組織控制下的應用程式。

而且把過濾邏輯放進元件內部,會讓元件相依於特定型別的訊息。例如客戶若使用一個通用的價格監看元件,他可能想同時用它看 widget 與 gadget,但套用不同的條件。

解法#

Message Filter 只有一個輸出通道

  • 訊息內容符合準則 → 路由到輸出通道
  • 不符合 → 丟棄

在我們的例子中,我們會定義單一個 Publish-Subscribe Channel 供每位客戶自由監聽,客戶再用 Message Filter 依自己選定的準則(商品類型、價格變動幅度)剔除訊息。

Message Filter 可以被描繪成 Content-Based Router 的特例:它把訊息路由到輸出通道,或路由到**「空通道」**——一個會丟棄所有發布到它的訊息的通道,類似許多作業系統中的 /dev/null,或 Null Object 模式。

圖 7-2:Message Filter 解法示意

無狀態與有狀態的過濾器#

widget/gadget 的例子是無狀態的 Message Filter:它只檢視單一訊息,僅根據該訊息所含資訊決定是否放行,因此不必跨訊息維護狀態。

無狀態元件的好處是可以並行跑多個實例來加速處理

但 Message Filter 不一定得無狀態。

常見的有狀態例子是用 Message Filter 剔除重複訊息:假設每則訊息有唯一識別碼,過濾器就保存過去訊息的識別碼,把每則新訊息的識別碼與清單比對來辨識重複。

訊息系統內建的過濾功能#

階層式通道與萬用字元#

有些發布訂閱系統允許為通道定義階層結構(多數 JMS 實作都可以)。例如把促銷發布到 wgco.update.promotion.widget

  • 訂閱 wgco.update.*.widget → 收到所有與 widget 相關的更新(促銷與價格變動)
  • 訂閱 wgco.update.promotion.* → 收到 widget 與 gadget 的所有促銷,但沒有價格變動

通道階層讓我們能用附加限定參數的方式細化通道語意,客戶因而能靠通道名稱指定額外條件來過濾訊息。

但相較於 Message Filter,階層式命名的彈性仍然有限——例如「只放行價格變動超過 11.5% 的訊息」,就很難用通道名稱表達

Selective Consumer 與訊息選擇器#

其他訊息系統在接收端應用程式內提供 Selective Consumer 的 API 支援:訊息選擇器是在應用程式看到訊息之前,先對進站訊息的表頭或屬性元素求值的運算式;條件不成立就忽略該訊息、不交給應用邏輯。

訊息選擇器等於一個內建於應用程式中的 Message Filter。使用它仍需要修改應用程式(在 EAI 中往往辦不到),但選擇規則的執行是內建在訊息基礎設施中的。

  • Selective Consumer 不消費那些不符合準則的訊息
  • Message Filter所有訊息從輸入通道移除,只把符合準則的發布到輸出通道

把過濾條件註冊給基礎設施的好處#

把過濾運算式註冊給訊息基礎設施的一個好處是:基礎設施能據此做出聰明的內部路由決策。

假設接收端與發送端位於不同網段(甚至跨越網際網路),把訊息一路送到 Message Filter 才發現要丟棄它,相當浪費。但我們又想用 Message Filter 機制,好讓控制權在接收者而非中央路由器手上。

若 Message Filter 是訊息基礎設施提供給訂閱者的 API 的一部分,基礎設施就能把過濾運算式往來源端推近——既維持「控制權在訂閱者」的原意,又讓基礎設施避免不必要的網路流量。這種行為很像動態的 Recipient List

用過濾器實作路由功能#

我們可以用「一個廣播通道 + 一組 Message Filter」達成與 Content-Based Router 等價的功能。假設有兩個接收者:Gadget 只對 gadget 訊息有興趣,Widget 只對 widget 訊息有興趣。

  • 選項一:Content-Based Router——評估每則訊息的內容,預測式地路由到適當的接收者。
  • 選項二:廣播通道 + 一組 Message Filter——把訊息廣播到 Publish-Subscribe Channel,每個接收者各配一個過濾器剔除不想要的訊息。

兩者的差異#

Content-Based RouterPub-Sub Channel + Message Filter
恰好一個消費者收到每則訊息可以有多個消費者消費同一則訊息
中央控制與維護——預測式路由分散控制與維護——反應式過濾
路由器必須知道參與者;參與者增減時路由器可能得更新不需要知道參與者;增減參與者很容易
常用於業務交易(例如訂單)常用於事件通知/資訊性訊息
搭配佇列式通道通常更有效率搭配發布訂閱通道通常更有效率

圖 7-3:選項一:使用 Content-Based Router

圖 7-4:選項二:使用廣播通道搭配一組 Message Filter

怎麼選#

有時由所需功能決定——若需要讓多個接收者處理同一則訊息,就必須用 Pub-Sub Channel + Message Filter。但多數情況下,決定因素是「路由決策該由哪一方控制與維護」:我們要保留中央控制,還是把它下放給接收者?

例如我們提供給頂級客戶的特別折扣,就不該送給非頂級客戶再期待他們忽略這些優惠。

網路流量的考量也會影響決定

  • 若有高效的廣播方式(例如內網用 IP multicast),用過濾器會非常有效率,還能避免單一路由器成為潛在瓶頸
  • 但若資訊要跨網際網路傳送,我們就只能用點對點連線;此時路由器有效率得多——它避免了把個別訊息送給所有參與者。

若想把控制權交給接收者、卻又因為網路效率必須使用路由器,可以採用動態的 Recipient List:它讓接收者表達自己的偏好並存進資料庫或規則庫,訊息抵達時再轉發給所有條件符合的接收者。