脈絡:應用程式正用訊息傳遞來宣告事件。
已有的理論基礎#
廣播早有成熟的模式:
- Observer 模式 [GoF] 談的是把觀察者與主體解耦,好讓主體能輕鬆地把事件通知給所有有興趣的觀察者——不論有多少個(甚至一個也沒有)。
- Publisher-Subscriber 模式 [POSA] 在 Observer 之上加入了事件通道的概念來傳達事件通知。
理論如此,但在訊息傳遞中怎麼運作?事件可以包成 Message,好讓訊息傳遞可靠地把事件傳達給觀察者(訂閱者);事件通道就是 Message Channel。但通道要怎麼把事件正確地傳達給所有訂閱者?
要求是:
- 每個訂閱者對某個事件只需被通知一次,不該重複收到同一事件
- 在所有訂閱者都被通知之前,事件都不能算被消費
- 一旦所有訂閱者都被通知,事件就算被消費,應該從通道上消失
但讓訂閱者互相協調來判斷訊息何時算被消費,違反了 Observer 模式的解耦精神。並行的消費者不該被視為在競爭,而應該能共享那則事件訊息。
解法#
它的運作方式是:一個輸入通道分岔成多個輸出通道,每個訂閱者一條。事件被發布進來時,通道把訊息的副本送到每個輸出通道;每個輸出通道只有一個訂閱者,而且只允許消費一次。如此一來,每個訂閱者都恰好收到訊息一次,被消費的副本也從各自的通道上消失。
意外的好處:偷聽#
Publish-Subscribe Channel 可以是很有用的除錯工具。即使某則訊息只打算給單一接收者,用發布訂閱通道也能讓你在不擾動既有訊息流的前提下偷聽通道。
監控通道上的所有流量在除錯訊息應用時極有幫助,也省下在每個參與的應用程式裡塞一堆 print 敘述的功夫。寫一個程式去監聽所有活躍通道並記錄到檔案,能帶來與 Message Store 相近的許多好處。
偷聽也可能是風險#
若你的訊息方案在薪資系統與會計系統之間傳輸薪資資料,你大概不會希望任何人都能寫個簡單程式來監聽這些流量。
- Point-to-Point Channel 在一定程度上緩解了這個問題——偷聽者會把訊息消費掉,狀況很快就會被發現。
- 但某些訊息佇列實作提供 peek 功能,讓消費者在不消費任何訊息的情況下查看佇列內的訊息。
因此,訂閱一個 Message Channel 應該是受安全政策管制的操作。許多(但非全部)商業訊息實作提供這類限制。此外,建立一個記錄「各通道上活躍訂閱者」的監控工具,也是很有用的系統管理手段。
邊欄:萬用字元訂閱者
許多訊息系統允許 Publish-Subscribe Channel 的訂閱者使用特殊的萬用字元。這是讓訂閱者一次訂閱多個通道的強大技巧。
例如某應用程式發布訊息到:
MyCorp/Prod/OrderProcessing/NewOrders
MyCorp/Prod/OrderProcessing/CancelledOrders另一個應用程式可以訂閱 MyCorp/Prod/OrderProcessing/* 來接收所有與訂單處理相關的訊息;再另一個可以訂閱 MyCorp/Dev/** 來接收開發環境中所有應用程式送出的訊息。
接下來#
- Event Message 通常送在 Publish-Subscribe Channel 上,因為往往有多個相依方對同一事件有興趣。
- 訂閱者可以是持久或非持久的——見 Durable Subscriber。
- 若通知需要被訂閱者確認,就用 Request-Reply:通知是請求,確認是回覆。
範例:股票交易、JMS Topic 與 MSMQ 一對多
股票交易#
在股票交易系統中,一筆交易完成時可能有許多系統需要被通知,因此把它們全都設為「發布交易完成事件」那個 Publish-Subscribe Channel 的訂閱者。
JMS Topic#
在 JMS 中,Publish-Subscribe Channel 實作 Topic 介面。寄件端用 TopicPublisher 送訊息,每個接收端各用自己的 TopicSubscriber 收訊息。
Topic topic = // obtain the topic via JNDI
TopicConnectionFactory factory = // obtain the connection factory via JNDI
TopicConnection connection = factory.createTopicConnection();
TopicSession session = connection.createTopicSession(true, Session.AUTO_ACKNOWLEDGE);
TopicPublisher publisher = session.createPublisher(topic);
Message message = session.createTextMessage("The contents of the message.");
publisher.publish(message);Topic topic = // obtain the topic via JNDI
TopicConnectionFactory factory = // obtain the connection factory via JNDI
TopicConnection connection = factory.createTopicConnection();
TopicSession session = connection.createTopicSession(true, Session.AUTO_ACKNOWLEDGE);
TopicSubscriber subscriber = session.createSubscriber(topic);
TextMessage message = (TextMessage) subscriber.receive();
String contents = message.getText();同樣地,JMS 1.1 統一了兩個領域的客戶端 API,上述程式碼可改用
Destination、ConnectionFactory、Connection、Session、MessageProducer、MessageConsumer。
MSMQ 的一對多訊息傳遞#
MSMQ 3.0 新增了一對多訊息模型,有兩種作法:
- Real-Time Messaging Multicast——最接近發布訂閱,但實作完全依賴透過 PGM 協定的 IP multicast。
- Distribution Lists 與 Multiple-Element Format Names——Distribution List 讓寄件端明確地把訊息送給一份接收者清單(但這違反 Observer 模式的精神);Multiple-Element Format Name 則是一個符號式的通道指定符,動態對應到多個真實通道,較貼近發布訂閱的精神,但仍迫使寄件端在「真實通道」與「不那麼真實的通道」之間做選擇。
.NET CLR 未直接支援一對多訊息模型,但可透過 COM 介面存取這項功能,並嵌入 .NET 程式碼中。