jms詳解

本文轉載自http://shift-alt-ctrl.iteye.com/blog/1902820

 

一.JMS使用場景:
    JMS是爲"消息"而生,從使用的角度來說,任何可以與業務解耦的數據均可以作爲"消息"存儲;任何結構化(格式嚴格,適合解析)但未索引化(不能被隨意檢索)的數據均可以交付給JMS存儲,但儘管JMS不是爲存儲而生.


    1) 異構系統(應用)中,如果數據的交互無法通過DB共享/調用接口等方式實現時,可以考慮使用JMS作爲數據的中轉中心,並使用JMS API來交付消息和消費消息.主要作用爲"業務解耦"或"系統解耦".


    2) 數據交互過程,允許異步操作時,可以考慮使用JMS;比如數據的生產者交付數據後,即可立即返回而無需等待數據的執行結果,對於數據消費者可以在合適的時機(網絡允許/選擇條件允許時)接收數據並執行.這一點可能是我們使用JMS的主要原因,它較好的解決了RPC的同步調用所帶來的弊端,很多時候具有"事件驅動"特徵的調用過程,可以嘗試使用JMS來解決.主要作用爲"數據交互過程解耦".


    3) 如果數據交互的雙端執行效率不對等時,可以考慮使用JMS;主要場景爲,數據生產的速度非常高(比如隊列),但是數據消費的速度相對較低,如果沒有良好的架構設計,必將對數據生產者造成阻塞或者數據積壓,從而對整體的性能帶來挑戰或者數據安全帶來風險;JMS就是一個很好的"數據緩衝"中心,它將密集的數據提交操作以"均勻"的速度交付給數據消費者,從而實現系統整體的穩定性.這種策略,在日誌統計/小數據分析系統中,經常使用.主要作用爲"緩解系統瓶頸,提高生產效率".

    歸納爲: "異構系統通訊"/"異步調用"/"應用解耦",同時,JMS也是"異步RPC調用"的解決方案之一,同時也是"actor"設計模型的特列.開發者可以選擇合適的JMS實現,作爲消息中間件來解決系統架構中的上述問題.
    
二.JMS消息傳送模型
    JMS具備兩種"消息傳送模型":P2P和Pub/sub.

 

    1) P2P: 點對點消息傳送模型,允許JMS客戶端通過隊列(queue)這個虛擬通道來同步或異步發送消息;消息的生產者爲Sender,消費者爲receiver;消息傳送機制上,基於拉取(pull)或者輪詢(polling)方式,即receiver主動到隊列中請求消息,而不是JMS提供者將消息推送到客戶端;主要原因是一個隊列通道可能有多個receiver,每個receiver可能對消息的處理速率不同(因處理消息而造成的阻塞時間不同),對於JMS提供者而言,它無法意識到哪個receiver處於"空閒"狀態,如果JMS提供者主動推送會造成通道的阻塞或者消息在客戶端積壓等問題;所以基於客戶端pull的方式,當receiver空閒時向JMS提供者請求消息,很好的解決了這個問題,而且還能進行良好的"負載均衡".

    Queue中的消息如果被某個recervier成功接收(或者確認成功)後,消息就會被移除.

    P2P消息傳送模式即支持異步"即發即失",也支持同步的"請求/應答";這兩種實現手段,稍後會通過實例來展示.

 

    2) Pub/sub: 發佈/訂模型中,消息會發布到一個名爲主題(Topic)的虛擬通道中,消息的生產者爲Publisher,消費者爲subscriber,發佈到Topic中的消息,可以被多個客戶端同時接收(區別於queue);pub/sub消息傳送模型是基於推送(push),JMS提供者將消息主動推送給及客戶端,類似於廣播;之所以採取此方式,其實很好理解,既然每個客戶端都應該收到消息,那麼對於JMS提供者而言,只需要遍歷所有的"活躍"的鏈接,依次將消息發送出去即可,而無需客戶端"徒勞"的去輪詢.

    在Pub/sub模型內部,有多種不同類型的訂閱者;非持久訂閱者是臨時訂閱者,它們只是在主動偵聽主題式才能收到消息;持久訂閱者將接收到發佈的每一條消息,即使它的鏈接處於"離線".此外還有"動態持久訂閱者"和"受託管的持久訂閱者",稍後會通過實例展示如何使用它們.

 

    JMS提供者都支持"消息"的持久化,任何發送給JMS提供者的消息,都會首先被持久存儲(對於非持久類型的數據,是基於cache),然後適時將消息交付給消費者;這一種有效的擔保策略("保存並轉發"),有效的確保了消息的安全性.

 

三.JMS API簡析

頂級接口

P2P

Pub/sub

備註

ConnectionFactory

QueueConnectionFactory

TopicConnectionFactory

基於工廠模式,創建和JMS提供者之間的鏈接,需要制定鏈接的URL或者協議;任何JMS客戶端和JMS提供者之間的交互,都必須基於制定的連接.

Destination

Queue

Topic

“目的地”,用於描述消息的通道類型,是JMS提供者用於標記消息歸屬類型的”標記”.

Connection

QueueConnection

TopicConnection

“鏈接”,用於描述一個實際的網絡通訊鏈接,比如TCP/UDP等,任何交互數據,都必須通過”鏈接”進行傳輸,JMS實現者負責定義數據格式(協議);在物理層用於區分JMS客戶端,一般而言,一個應用只會有一個”鏈接”.

Session

QueueSession

TopicSession

“會話”,在邏輯上用於區分JMS客戶端,因爲”鏈接”可被公用以提高網絡利用率;每個session可以支持相互獨立的”事務”和相關屬性.每個session都有ID.

Message

--

--

“消息”,JMS API中提供了多種類型Message,它們有各自的”序列化/反序列化”機制;消息中可以包含多種JMS屬性以及客戶端自定義的消息屬性和內容.

MessageProducer

QueueSender

TopicPublisher

“生產者”,一種可以向JMS提供者提交消息的客戶端類型.

MessageConsumer

QueueReceiver

TopicSubscriber

“消費者”,一種可以向JMS提供獲取消息的客戶端類型.

 

四. JMS-API詳解部分

 

1. ConnectionFactory接口:

    鏈接工廠,用於創建"鏈接",此處所指的連接爲底層實際物理連接,即TCP連接(Socket通道).此接口有多個子接口:QueueConnectionFactory(用來創建Queue消息類型的鏈接),TopicConnectFactory,XAQueueConnectionFactory(基於XA分佈式事務的Queue鏈接),XATopicConnectionFactory.

    ConnectionFactory接口中並沒有約束建立鏈接所使用的協議、URL、安全策略等;這一切就交給JMS Provider去實現。通常情況下ConnectionFactory實例爲單例,而且推薦以JNDI的方式獲取.

  • Connection createConnection()
  • Connection createConnection(String username,String password):使用簡單的密碼校驗方式來創建鏈接.如果JMS Server端對受託管的queue/topic配置了需要授權才能訪問,那麼在建立相應的鏈接時需要交付密碼.

2. Connection接口:

    代表底層一個物理(或者邏輯上)的一個Socket鏈接通道,此鏈接將會保持活躍直到JMS Client關閉或者JMS提供者(即JMS server端)在socket上阻塞超時.通常情況下,Connection爲一個TCP鏈接(長連接),一個JMS Client建議維持一個Connection即可,事實上Queue/Topic不同類型的Client,那麼也將創建不同的Connection.對於Server而言,Socket的資源開支是昂貴的,儘量避免一個Client創建多個Connection的情況.

    消息將通過"數據格式協議"在socket通道中傳輸,同一個connection中消息的發送/接受是有順序的,這受限於Socket本身對流數據操作的特性.

    JMS Provider會爲每個Connection生成ID,用來監控鏈接、消息分組等;JMS server端將會維護當前所有存活的Connection列表。

  • void start(): "開啓"消息接收,此後即可接收消息;不過對於Producer而言,無論鏈接處於何種狀態,均可以發送消息;此方法主要對消費者有效.此操作對Connection上所有的session都有效.
  • void stop():"終止"消息接收,此後將不能接收到消息;當鏈接重新被start之後,將仍然可以繼續接收.
  • void close():關閉鏈接,底層爲直接關閉socket;與Connection有關的臨時(temporary)目的地都將被刪除(包括TemporaryQueue,TemporaryTopic),以及此Connection有關的Sessions/productor/consumer都將被關閉.JMS規範要求,如果close方法返回意味着connection中所有的send操作已經結束,消息接收的receive方法已經返回(或中斷);close方法會導致事務中的session無法繼續,可能會rollback.
  • Session createSession(boolean transacted,int acknowledgeMode):創建會話,並指定此Session的事務性和消息確認模式.
  • String getClientID():獲得當前JMS Client的ID;每個Connection,對於JMS提供者而言,就被認爲是一個Client,clientId用來表示全局中唯一的一個鏈接,有server端生成;不同的JMS提供者對此ID的生成策略有所不同.
  • void setClientID():設置ClientID,此操作需要在建立Connection之後,未使用Connection進行任何實際操作之前(包括創建session)進行;否則將會拋出異常.因爲clientID是JMS server用來唯一標記鏈接的,因此在全局中不能重複,如果嘗試設定一個已有的ClientId,將會拋出InvalidClientIDException.不過此方法可能在某些JMS Provider上不被支持.
  • void setExceptionListener(ExceptionListener listener):設定Connection失效異常監聽器,當JMS Client檢測到鏈接異常,比如鏈接異常斷開,將會通知此listener,可以在listener中做一些日誌記錄/補救措施,比如重新建立鏈接/會話恢復等.
  • ConnectionMetaData getMetaData():獲取JMS Provider中有關的元數據信息,比如版本號等.

3. DeliveryMode接口:

    "消息傳輸模式",此屬性可以在session中指定,也可以在發送消息時指定;用來標記此消息是否需要被持久化,對於JMS而言,支持2種(作爲DeliveryMode的靜態屬性):

    1) PERSISTENT: 表示消息需要被持久化,對於JMS Provider而言,這種類型的消息將會被存儲在磁盤上;以確保在server故障恢復後,消息仍然保留.

    2) NON_PERSISTENT:表示消息不需要持久化,消息有可能被優先存儲在內存中,或者存儲在磁盤上某個臨時文件上;當server故障失效後,消息將不能被恢復.

 

4.Destination接口:

    用來表示一個虛擬通道,或者說"目的地",消息的發送或者接收,都需要指定的destination.常見的2個子接口爲:Queue和Topic;根據Destination的約束條件不同,可以分爲"受託管Destination"、"動態Destination"、“臨時Destination”。

    其中"受託管Destination"爲JMS Provider中通過配置的方式聲明的,無法通過外部API直接修改,此destination的使用需要受到JMS server管理員的授權等.

    "動態Destination"爲通過JMS API方式創建的,比如session.createQueue(String queueName);這種類型的destination可以給JMS Client使用者提供了更多的自由空間,更加常用.

    "臨時Destination"相對於"durable"(耐久的),這種類型的destination只能被當前Client感知到,它的生命週期和使用範圍侷限於Connection;即只有創建它的session所屬的Connection中的其他消費者或者生產者才能使用它.這種destination在某些場景下很有用.

 

5. ExceptionListener接口:

    主要用來處理connection級別異常,比如鏈接異常斷開;你可以在此listener中增加比如"鏈接重建"/"會話恢復"等措施;但是對於業務異常,此listener將不負責管理.

  • void onMessage(JMSException exception)

6. Session接口:

    最常用接口,考慮到Connection的資源開支較大,那麼JMS提供了API級別的邏輯上的"鏈接",即Session;它可以更小粒度的控制消息屬性(比如,確認模式,事務支持)以及消息的發送和接收.

    session通常在單線程中使用,無論是MessageProducer還是Consumer;而且通常情況下,一個Session只維護一個Producer實例或者Consumer實例;此外session的創建是非常便捷的.此外sesison接口本身實現了runnable接口,它通常被運行在一個"SessionThread"中.

    通過Session,可以創建多種類型的Producer和Consumer,而且事務的支持,也被控制在session級別.因此在支持事務的情況下,多個consumer或者Producer公用一個Session實例是不明智的.當然這也不是錯誤,不過前提需要注意:session中數據的操作並非在多線程下,能夠得到預期的效果.

    如果你期望一個Producer持續的send消息,而另一個Consumer能夠通過listener的方式接收消息,那麼你應該將它們放在兩個session中.此外如果你的消費者是基於listener異步接收消息,那麼你應該爲每個listener使用不同的session.

    在支持事務的Session中,多個消息的發送或者接收作爲一個原子性單元,當事務提交後,消息的"確認"也是

作爲原子性單元(此處消息的確認包括事務中多個發送的消息,或者消費者中連續消費的多個消息);如果事務回滾,將導致此事務中發送的消息被銷燬(JMS server端做刪除操作),對於消費者而言,事務中接收的消息將會被恢復(即認爲消息未被收到,將會被重發).由此可見,在事務類型的session中,消息的確認時機將和事務提交的時機保持一致.

    在事務類型的Session中,如果事務沒有提交,那麼生產者send的消息對其他消費者不可見;對於消費者而言,如果事務沒有提交,那麼消息將不會從queue中刪除,對於topic類型,消息將不會從自己的"消息副本"中刪除.

    session並沒有start()方法,默認每次commit之後就會開啓一個新的事務.

  • static int AUTO_ACKNOWLEDGE: 消息自動確認,即消費者從receive()方法成功返回時或者當messageListener.onMessage(..)方法成功返回時,進行消息確認.如果receive()方法內部或者onMessage方法內部拋出異常(未捕獲),將會導致此消息不能被"確認";那麼對於JMS Provider而言,則認爲此消息消費失敗,將會重發.(對於某些Provider而言,將會在重發多次後仍然失敗,將會考慮將消息轉發給其他Connection).
  • static int CLIENT_ACKNOWLEDGE: 客戶端確認,即消息的確認需要client端選擇時機手動去觸發。這個方式給消息的確認提供了更加自由的方式;此方式針對消息消費者,消費者可以在接收到消息後,在任意時間調用此message.acknowledge()方法來確認消息。
  • static int DUPS_OK_ACKNOWLEDGE: "可重複消息確認",此模式可以允許JMS提供者將一條消息向同一個目的地發送兩次以上。
  • void close(): 關閉session;直接導致與此session有關的資源被釋放,如果消費者的recevie()正在接收消息(而不是wait阻塞)或者messageListener.onMessage()方法正在執行,那麼session關閉將會被阻塞。session關閉將會導致尚未提交的事務被回滾。session關閉後,此session創建的Producer或者Consumer將無法繼續工作。
  • BytesMessage createByteMessage():
  • MapMessage createMapMessage():創建一條消息
  • void commit():提交事務。
  • void rollback():事務回滾。
  • void recover():此方法只會被JMS Provider調用,JMS提供者將會暫停消息的發送,將此前已經發送但沒有“確認”消息標記爲“redelivered”,然後按照消息的原始順序,將消息發送給Client端(包括redelivered和新消息)。
  • MessageProducer createProducer(Destination des): 創建一個消息生產者。
  • MessageConsumer createConsumer(Destination des): 創建一個消息消費者。
  • MessageConsumer createConsumer(Destination des,String selector): 指定消息選擇器。
  • MessageConsumer createConsumer(Destination des,String selector,boolean noLocal): 指明當前消費者是否可以接受"本地"消息.noLocal參數只對Topic類型的"目的地"有效,如果消息消費者和生產者有一個Connection創建(即它們具有同一個ClientID,或者說底層是一個TCP鏈接),即使它們是在不同的session中,它們均被認爲是“本地”。
  • Queue createQueue(String queueName):創建一個“隊列”類型的目的地(動態隊列)。
  • Topic createTopic(String tpoicName):創建一個“主題”類型的目的地(動態主題)。
  • TopicSubscriber createDurableSubscriber(Topic topic,String name): 創建一個“耐久訂閱者”,並指定訂閱者的名稱(name,需要全局唯一);如果一個Client需要接收Topic的全部信息,即使當Client的鏈接失效時也需要JMS 提供者保存它“錯過”的消息。考慮到JMS 提供者內部的機制(消息副本),需要爲此“耐久訂閱者”指定一個名稱,且名稱不能改動,否則此後消息不能接收到。創建“耐久訂閱者”有個必要的先決條件:ClientID必須一致,你在創建“durableSubscriber”時,需要顯示的指定“connection.setClientID(xxx)”,且ClientID在每次啓動時必須一樣,否則將無法使用“耐久訂閱者”。(You cannot create a durable subscriber without specifying a unique clientID on a Connection)。
  • QueueBrowser createBrowser(Queue queue,String selector): 類似於創建一個Consumer,不過此時創建的是一個“Browser”,只能查看消息隊列,但不能消費(也不能干擾消費)。
  • TemporaryQueue createTemporaryQueue(): 創建一個“臨時隊列”,其生命週期爲當前Connection;如果當前session或者Connection關閉,那麼queue也將不可用;同時此Queue只能被當前Connection下的其他session使用,對於其他Connection,此Queue是不可見的。(不能跨Connection,同時此方法並沒有指名參數;Don't understand null destinations)
  • TemporaryTopic createTemporaryTopic(): 同上
  • void unsubscribe(String name): 取消“耐久訂閱者”,此後JMS Provider將不會對此訂閱者保留消息副本。 

7. MessageConsumer接口:

    消息消費者頂級接口,其子接口有QueueReceiver和TopicSubscriber;可以在Session接口中通過各種方式創建MessageConsumer.消息接收可以是同步的,也可以是異步的.這取決於編碼方式.

  • String getMessageSelector(): 獲取當前消息的"消息選擇器";Session接口中,已經提供了基於選擇器創建Consumer的方法,注意:消息選擇器必須在創建Consumer時指定,且整個session期間將無法再次改動,當然MessageConsumer接口中也沒有提供設置選擇器的方法;這個和JMS Provider對消息選擇器的使用機制有關(後端過濾機制),稍後有專題專門介紹"消息選擇器"的原理.
  • Message receive(): 以同步的方式接收消息,同步意味着"阻塞",當在connection活躍期間,當前"目的地"中沒有消息push過來(對於Topic)或者沒有偵聽到消息(對於Queue,普遍採用polling策略),那麼此方法將會阻塞,直到接收到消息,或者consumer被關閉(close方法),或者connection/session被關閉,此時將會返回null.前文已經提到,在事務類型的session中,此方法返回時表示此消息已經被確認.
  • Message receive(long timeout): 已同步的方式接收消息,不過指定了方法阻塞的時間;如果在超時時仍未收到消息,將會返回null.
  • Message receiveNoWait(): 以非阻塞的方式接收消息,不過此處的非阻塞,只是嘗試去獲取,如果此時有消息亟待接收,將會返回message.否則返回null..此方法在一定程度上要求consumer使用"輪詢"的方式獲取消息,比如在while循環中.
  • void close(): 關閉消費者,此方法會阻塞,直到正在接收消息的receive方法返回,或者基於messageListener.onMessage()方法執行結束.如果receive方法尚未收到消息,則返回null.如果session或者connection被關閉,也將連帶此consumer被關閉.

8. MessageProducer接口:

    消息生產者的頂級接口,負責向指定的"目的地"發送消息.兩個子接口:QueneSender和TopicPublisher

  • void setDisableMessageID(boolean value): 是否關閉"messageID"選項,不過首先聲明並不是所有的JMS Provider都會此選項感興趣,有些JMS Provider會忽略此選項,會對所有的消息都生成messageID.有些JMS Provider則反其道而行之,將會忽略所有的MessageID,以便減少此值帶來的額外的開支(比如網絡傳輸數據量).在很多場景下,我們需要跟蹤消息(或者過濾消息,"請求應答"模式),那麼MessageID對我們將會非常有效.JMS API規定,任何一個MessageID必須爲全局唯一的,以便Client端可以使用MessageID作爲某種檢索/存儲的主鍵.
  • setDisableMessageTimestamp(): 是否關閉"messageTimestamp"選項,它的機制和messageID一樣;messageTimestamp用來表示此消息被髮送的時間戳(send方法執行時);此選項通常可以用來跟蹤消息被創建的時間.
  • void setDeliveryMode(int mode): 設置消息的"傳輸模式";此設置將直接影響此Producer下所有發送的消息.
  • void setPriority( int priority): 設置消息的"權重",共0~9個級別,默認爲4,其中0~4表示普通優先級,5~9表示高優先級;優先級越高,將會導致此消息被優先發送給消費者.此後我們會詳解"消息權重"和系統設計.
  • void setTimeToLive(long timeToLive): 設置消息的最大存活時間,毫秒數.默認爲0表示永不過期;JMS Provider在發送消息時都會檢測消息的"存活有效性",如果消息過期,將會直接刪除,而不發送給消費者.其中mesageTimestamp + timeToLive最終表示爲消息過期的時間.在JMS Provider將消息發送給Client時,將使用過期時間和server的本地時間比較,以決定其是否過期.
  • void close(): 關閉消息生產者.如果session或者connection被關閉,也將連帶此consumer被關閉.
  • void send(Message message): 發送消息 ,不過此時將會使用MessageProducer指定的priority/deliveryMode等.
  • void send(Message message,int deliveryMode,int priority,long timeToLive); 發送消息,併爲此消息指定"傳輸模式"和"消息權重".如果某條消息需要特定聲明這些屬性,那麼你可以使用此方法.
  • void send(Destinantion des,Message message): 向指定的目的地,發送消息;這個方法並不常用,通常用在"請求-應答"模式中:即消息消費者接收到消息之後,需要臨時創建一個Producer並將"應答消息"發送到指定的"replyTo"地址.

五.JMS消息類型:

    無論是消費者還是生產者,這一切都是在圍繞:消息(Message).一條消息包括消息頭/消息屬性/消息體.

    1. 消息頭:用於標識消息的核心屬性,這些屬性通常有JMS Provider設定.API格式爲message.getJMS<header>.例如:

 

Java代碼  收藏代碼
  1. message.getJMSCorrelationID();//有消息生產者設定  
  2. message.getJMSReplyTo();//  
  3. message.getJMSDeliveryMode();  
  4. message.getJMSDestination();  
  5. message.getJMSExpiration();  
  6. message.getJMSMessageID();  
  7. message.getJMSRedelivered();  
  8. message.getJMSTimestamp()  

    JMSCorrlelationID通常有Producer在發送消息之前,由開發者指定:message.setJMSCorrelationID(..);用來表示此消息"關聯ID",此ID可以爲消息發送者指定的任意與業務有關的數據,通常用來表達當前message與某個ID具有一定的耦合關係:通常使用在"請求-應答"模式中,用來約束兩條消息的關係.

    JMSReplyTo使用在"請求-應答"場景中,用來表明此消息接收之後需要向此目的地發送"應答"消息.此屬性值需要在生產者發送消息之前指定:message.setJMSReplyTo(queue).

Java代碼  收藏代碼
  1. Message message = consumer.receive(10000);  
  2. if(message == null){  
  3.     continue;  
  4. }  
  5. Destination replyTo = message.getJMSReplyTo();  
  6. if(replyTo != null){  
  7.     Message replyMessage = session.createTextMessage("Replay message");  
  8.     String correlationID = message.getJMSMessageID();  
  9.     replyMessage.setJMSCorrelationID(correlationID);  
  10.     MessageProducer producer = session.createProducer(replyTo);  
  11.     producer.send(replyMessage);  
  12. }  

    JMSDeliveryMode:即消息的傳輸模型(持久性/非持久性),這個屬性由JMS Provider自動分配,即當消息發送給消費者時,由JMS Provider自動填充其值.此值來自消息發送者的設定.無論是何種傳輸模式,JMS Provider都不會將一條消息成功發送一次以上.

Java代碼  收藏代碼
  1. MessageProducer producer = session.createProducer(replyTo);  
  2. replyMessage.setJMSDeliveryMode(DeliveryMode.PERSISTENT);//默認爲持久性  

    JMSMessageID:消息的ID,此ID通常爲全局唯一的,用來唯一的標識一條消息.MessageID是由JMS Provider生成,因此一個沒有被JMS Provider轉發的新消息(或者通過session.createMessage()創建的新消息),其MessageID爲空.生產者無法直接對MessageID賦值,因爲JMS Provider會最終忽略它.你可以通過producer.setDisableMessageID()方法來"取消"JMSMessageID消息頭,此後嘗試獲取MessageID將得到null,但並不意味着JMS Provider不會爲Message生成ID,事實上每個消息都有ID,只是此方法可以決定是否在傳輸給消費者時它是否會被包含在消息頭上.

    JMSTimestamp:消息被髮送的時間戳,即producer.send()的時間,此時間爲Client端的本地時間.可以用來計算消息由生產者發送之後,到消息成功接收,之間的時間差.

    JMSExpiration: 消息的有效期,時間戳.常用來過濾"過期"的消息;最終Expiration = timeToLive + timestamp.在消息發送時此屬性將會被設定.

Java代碼  收藏代碼
  1. producer.setTimeToLive(30000);//默認爲0,表示永不過期.  

    JMSRedilvered: 如果消息是"重發"時,那麼此屬性將爲true.表示此消息先前未能被正確接收.由JMS Provider填充此值.

    JMSPriority: 此消息的權重,此值可以通過producer.setPriority(int)來設定.通常情況下權重有10個可選值,0~4表示普通優先級,5~9表示高優先級;JMS Provider將會把優先級較高的消息優先發送給消費者,你可以認爲JMS Server端的消息存儲爲"基於權重排序的隊列".不過事實上JMS Provider並不會對消息基於權重進行排序,而是在發送時儘可能的選擇"權重較高"的消息優先發送,這個是"盡力而爲"的,請不要依賴"權重"來實現絕對意義的"排序".

 

    上述JMS消息頭中,開發者可以分配的有:JMSReplyTo,JMSCorrelationID;由JMS Provider設定的有JMSMessageID,JMSRedelivered.其他屬性均有JMS Client自動分配(即在producer.send()時設定).

 

    2. JMS消息類型:

    1)TextMessage: 消息體爲文本字符串,最常用的消息類型.底層爲new String(byte[],"utf-8"),非常簡單.可以通過getText()/setText()方法操作文本內容.

    2)ObjectMessage: 消息體爲java對象,此對象必須是Serializable接口的實例,使用java的序列化機制.消息體在Producer發送時通過ObjectOutputStream.writeObject()序列化,消費者通過ObjectInputStream.readObject()反序列化;可以通過ObjectMessage.getObject()獲取對象實例.

    3)BytesMessage: 消息體爲字節數組,類似於byteBuffer的結構,字節數組基於utf-8編碼,可以像使用ByteBuffer一樣來獲取ByteMessage消息體中的數據.比如 readLong(),將會獲取8個字節並轉換成long.如果在對消息操作時,跑出異常,那麼仍然可以從異常中回覆:reset()方法,將指針返回到字節流的起始位置,可以繼續重新read.

    ByteMessage是最基礎/移植性最好的消息類型,它可以兼容各種語言下的Client,而且方便存儲.

    底層使用DataInputStream/DataInputStream作爲數據支撐.

    4)StreamMessage: 消息體爲byteStream,可以方便對任意流數據進行傳輸,當然文本/字節數組/圖片流等,也可以以StreamMessage的方式傳輸.

    畢竟stream並不是一個結構化的數據,因此操作起來比較複雜,這就要求Producer和Consumer需要約定流中每個元素的順序以及類型.比如:

    在Producer中,執行message.writeLong(),message.writeByte()..

    那麼在Consumter中,必須依此調用message.getLong().message.getByte()...否則將會出現異常錯誤,因此StreamMessage具有嚴格的字節順序.

    不過,在不同的JMS provider中,對StreamMessage的實現有很大差異,在ActiveMQ中,StreamMessage內部使用一個list支撐,write操作就是向list中add數據,read操作就是根據根據index獲取值(每read依此,導致position++);這個list最終將會在網絡交互是被序列化和反序列化.有點像ObjectStream的特例.

    5)MapMessage: 消息體爲map,其實消息內容可以通過類似於getString(String key)的方式獲取.也是一種常用的消息類型.和StreamMessage類似,底層使用map作爲數據支撐.

    6)Message: 默認的消息,此類型的消息不包含消息體,只能發送消息屬性(properties)和消息頭(header),通常用於表達"通知".

 

    JMS的消息本身是不可重用的,即消息發送之後,消費者得到的Message實例只能read.

    3. JMS消息屬性

    JMS header是JMS內置的特殊"消息屬性",這些header將有Producer/Consumer/Provider三方共同約定且協同維護一條消息,在消息的路由方式/操作時機上會依賴header..不過JMS只提供了有限的幾個header條目,如果開發者或者Provider期望擴展它,可以通過消息屬性來完成(Message Properties).如下爲JMS中保留的屬性:

  • JMSXAppId: application標識
  • JMSXConsumerTXID: 消息消費者的事務ID,provider自動填充
  • JMSXDeliveryCount: 消息已重發的次數.
  • JMSXGroupSeq/JMSXGroupID: 消息分組時使用.
  • JMSXProducerTXID: 消息生產者的事務ID
  • JMSXRcvTimestamp: 消息由Provider發送給consumer的時間戳.由Provider填充.
    其中JMSXGroupID和JMSXGroupSeq是JMS要求Provider必須支持的;對於其他屬性,只是可選.此外Provider也可以提供自己的"屬性",一般格式爲:JMS_<vender-name>;可以參考各自的JMS Provider來獲取相關的屬性列表.

六.消息發送與接收

    1. 消息發送:

Java代碼  收藏代碼
  1. Connection connection = connectionFactory.createConnection();  
  2. connection.setClientID("TEST+++++");//optional  
  3. Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);  
  4. Destination queue = session.createQueue(QUEUE);//better: From jndi  
  5. MessageProducer producer = session.createProducer(queue);  
  6. producer.setDeliveryMode(DeliveryMode.PERSISTENT);//持久存儲  
  7. TextMessage message = session.createTextMessage("message for test");  
  8. producer.send(message);  

    通過代碼我們能夠直觀的看到,一個connection可以創建多個session,其中每個session就是一個會話單元,事務操作依賴於session.可以通過session來創建producer或者consumer;當然session也可以創建"動態的queue".

    通常情況下,一個應用只會有一個Connection,這個Connection是物理層面的TCP連接.在一個應用中創建多個Connection是一種不良的設計,且存在性能隱患.JMS 提供了細顆粒度的控制API:Session;需要特別提醒,session非線程安全,主要是因爲其事務的問題,如果一個支持事務的session在多線程執行,極有可能在事務提交和回滾時互相干擾.通常我們建議,支持事務類型的session只爲一個producer或者consumer服務;對於非事務性的session,它們可以公用.事實上一個session可以創建多個producer或者consumer.

    producer.send()方法是阻塞方法,當消息完全傳輸給server且正確"確認"之後,纔會返回.如果session爲事務性,那麼JMS Provider將會在內存中或者臨時文件中保存此session(注意每個session會攜帶sessionId,和事務ID)中當前事務的所有未提交的消息,直到session.commit(),此時消息纔會被認爲消費者可見;如果session回滾,將導致JMS Provider刪除此事務中所有的未提交的消息.每次事務提交或回滾,都會導致新的事務開啓(新的事務ID).

    很多開發者將"消息確認"和事務混爲一談;"消息確認"即爲JMS Provider向Producer發送類似於ACK信號,以表示當前正在發送的消息已經正確被JMS Provider接收且存儲;或者Consumer向JMS Provider發送ACK信號表示當前正在接收的消息已經正確被接收且執行(比如receive方法正確返回,或者onMessage方法執行完畢且沒有拋出異常).那麼事務則表達了一個場景,就是多條消息的接收或者發送作爲一個"原子性"單元,要麼它們被全部發送成功或者要麼全部接收成功,需要顯示的執行session.commit或者rollback.

    此處還需要單獨提示一下,每個Session實例並不是一個線程,它只不過是一個普通的對象.JMS規範規定,一個Session不應該在多個線程中執行,比如Producer和異步的consumer(基於messageListener),它們應該使用不同的session;且多個異步的consumer,也應該使用不同的session;如果你違背了這個設計原則,也不意味着錯誤,當然公用session也不會導致彼此線程之間的互相阻塞(session不會帶來任何阻塞,包括同步的receive方法之間),不過session所持有的"確認模式"/"事務類型"等公用選項會在併發中導致混亂.

    消息的傳輸模型包括:持久化和非持久化;體現在API上就是DeliveryMode,其中持久化方式表示任何發送到JMS Provider的消息都將被"持久存儲",不同的JMS Provider對持久化技術的使用各不相同,有的使用外部RDBMS,有的使用內置的K-V數據庫,有的基於特定的文件存儲;無論如何,最終這些消息都將被保存在"物理存儲可靠"的地方;比如activeMQ可以基於RDBMS,也可以使用內置的kahaDB等;對於"持久化"模式,一旦生產者的消息被"確認"收到,將意味着消息已經被正確存儲在磁盤上,當然有些JMS Provider爲了提高消息分發的效率(較少額外的消息檢索),可能會額外的將最新消息的副本在內存中保存一份;持久化模式能夠確保JMS Server在故障重啓後,消息不丟失,且可以正常恢復;但是如果JMS Server的物理機器故障,且消息沒有采取額外備份的話,有可能丟失,這不是JMS所能顧及的. 非持久化模式表示JMS Provider不擔保消息的絕對安全,在JMS Server故障重啓後,非持久化的消息將會丟失;JMS Provider將會把非持久化的消息保存在內存中,有些JMS Provider可能爲了避免內存的過度消耗,而將非持久化消息存儲在DB的臨時表中,以確保內存使用量處於可控狀態,且消息的消費和生產不會受到太大的影響;但是JMS Provider在故障重啓後,將會刪除這些臨時表或者文件.

 

    2 消息的接收:

Java代碼  收藏代碼
  1. Connection connection = connectionFactory.createConnection();  
  2. connection.setClientID("test101010101");  
  3. Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE);  
  4. Destination queue = session.createQueue(QUEUE);  
  5. MessageConsumer consumer = session.createConsumer(queue);  
  6. //consumer.setMessageListener(new QueueMessageListener());  
  7. connection.start();  
  8. while(true){  
  9.     System.out.println("+++++");  
  10.     try{  
  11.         Message message = consumer.receive(10000);  
  12.         if(message == null){  
  13.             continue;  
  14.         }  
  15.         if(message instanceof TextMessage){  
  16.             String text = ((TextMessage)message).getText();  
  17.             System.out.println("...." + text);  
  18.         }  
  19.         session.commit();  
  20.     }catch(Exception e){  
  21.         session.rollback();  
  22.     }  
  23. }  

    代碼結構基本上和消息生產者一樣,不過需要注意connection.start();此方法主要用來控制此鏈接上開啓消息接收,如果不執行start,事實上connection上所有的consumer都無法正常接收到消息.消息的消費可以是同步的:recevie();也可以是異步的:使用consumer.setMessageListener(..);在異步消息接收中,最終將會在一個外部線程中調用onMessage方法.

    無論是同步,還是異步,都需要注意session的事務性;如果是事務類型的,那麼在接收到消息之後,需要顯示的執行commit方法,commit方法將會提交事務並同時進行消息確認操作.如果不執行commit,對於JMS Provider而言,意味着此消息未能確認,將會導致此消息"重發".因此有必要對"消息接收"/"消息處理"代碼段進行try-catch操作.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章